Showing preview only (796K chars total). Download the full file or copy to clipboard to get everything.
Repository: mislav/hub
Branch: master
Commit: 5c547ed80436
Files: 211
Total size: 745.7 KB
Directory structure:
gitextract_4bom6jnl/
├── .ctags.d/
│ └── go.ctags
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── SECURITY.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── Gemfile
├── LICENSE
├── Makefile
├── README.md
├── cmd/
│ ├── cmd.go
│ └── cmd_test.go
├── commands/
│ ├── alias.go
│ ├── api.go
│ ├── apply.go
│ ├── args.go
│ ├── args_test.go
│ ├── browse.go
│ ├── checkout.go
│ ├── cherry_pick.go
│ ├── ci_status.go
│ ├── clone.go
│ ├── commands.go
│ ├── commands_test.go
│ ├── compare.go
│ ├── compare_test.go
│ ├── create.go
│ ├── delete.go
│ ├── fetch.go
│ ├── fetch_test.go
│ ├── fork.go
│ ├── gist.go
│ ├── help.go
│ ├── init.go
│ ├── init_test.go
│ ├── issue.go
│ ├── issue_test.go
│ ├── merge.go
│ ├── pr.go
│ ├── pull_request.go
│ ├── pull_request_test.go
│ ├── push.go
│ ├── push_test.go
│ ├── release.go
│ ├── remote.go
│ ├── remote_test.go
│ ├── runner.go
│ ├── runner_test.go
│ ├── submodule.go
│ ├── sync.go
│ ├── utils.go
│ ├── utils_test.go
│ └── version.go
├── coverage/
│ └── coverage.go
├── cucumber.yml
├── etc/
│ ├── README.md
│ ├── hub.bash_completion.sh
│ ├── hub.fish_completion
│ └── hub.zsh_completion
├── features/
│ ├── README.md
│ ├── alias.feature
│ ├── am.feature
│ ├── api.feature
│ ├── apply.feature
│ ├── authentication.feature
│ ├── bash_completion.feature
│ ├── browse.feature
│ ├── checkout.feature
│ ├── cherry_pick.feature
│ ├── ci_status.feature
│ ├── clone.feature
│ ├── compare.feature
│ ├── create.feature
│ ├── delete.feature
│ ├── fetch.feature
│ ├── fish_completion.feature
│ ├── fork.feature
│ ├── gist.feature
│ ├── git_compatibility.feature
│ ├── help.feature
│ ├── init.feature
│ ├── issue-transfer.feature
│ ├── issue.feature
│ ├── merge.feature
│ ├── pr-checkout.feature
│ ├── pr-list.feature
│ ├── pr-merge.feature
│ ├── pr-show.feature
│ ├── pull_request.feature
│ ├── push.feature
│ ├── release.feature
│ ├── remote_add.feature
│ ├── steps.rb
│ ├── submodule_add.feature
│ ├── support/
│ │ ├── completion.rb
│ │ ├── env.rb
│ │ ├── fakebin/
│ │ │ ├── curl
│ │ │ ├── git
│ │ │ ├── man
│ │ │ └── open
│ │ ├── local_server.rb
│ │ └── rspec_matchers.rb
│ ├── sync.feature
│ └── zsh_completion.feature
├── fixtures/
│ ├── fixtures.go
│ ├── release_dir/
│ │ ├── dir/
│ │ │ ├── file2
│ │ │ ├── file3
│ │ │ └── subdir/
│ │ │ └── file4
│ │ └── file1
│ ├── test.git/
│ │ ├── HEAD
│ │ ├── config
│ │ ├── description
│ │ ├── hooks/
│ │ │ ├── applypatch-msg.sample
│ │ │ ├── commit-msg.sample
│ │ │ ├── post-update.sample
│ │ │ ├── pre-applypatch.sample
│ │ │ ├── pre-commit.sample
│ │ │ ├── pre-push.sample
│ │ │ ├── pre-rebase.sample
│ │ │ ├── prepare-commit-msg.sample
│ │ │ └── update.sample
│ │ ├── info/
│ │ │ └── exclude
│ │ ├── objects/
│ │ │ ├── 08/
│ │ │ │ └── f4b7b6513dffc6245857e497cfd6101dc47818
│ │ │ ├── 8a/
│ │ │ │ └── 1cdac440b4a3c44b988e300758a903a9866905
│ │ │ ├── 9b/
│ │ │ │ └── 5a719a3d76ac9dc2fa635d9b1f34fd73994c06
│ │ │ ├── 9d/
│ │ │ │ └── aeafb9864cf43055ae93beb0afd6c7d144bfa4
│ │ │ ├── ca/
│ │ │ │ └── 93b49848670d03b3968c8a481eca55f5fb2150
│ │ │ └── e6/
│ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
│ │ └── refs/
│ │ └── heads/
│ │ └── master
│ ├── test_configs.go
│ └── test_repo.go
├── git/
│ ├── git.go
│ ├── git_test.go
│ ├── ssh_config.go
│ ├── ssh_config_test.go
│ ├── url.go
│ └── url_test.go
├── github/
│ ├── branch.go
│ ├── branch_test.go
│ ├── client.go
│ ├── client_test.go
│ ├── config.go
│ ├── config_decoder.go
│ ├── config_encoder.go
│ ├── config_service.go
│ ├── config_service_test.go
│ ├── crash_report.go
│ ├── crash_report_test.go
│ ├── editor.go
│ ├── editor_test.go
│ ├── hosts.go
│ ├── http.go
│ ├── http_test.go
│ ├── localrepo.go
│ ├── localrepo_test.go
│ ├── message_builder.go
│ ├── message_builder_test.go
│ ├── project.go
│ ├── project_test.go
│ ├── remote.go
│ ├── remote_test.go
│ ├── reset_console.go
│ ├── reset_console_windows.go
│ ├── template.go
│ ├── template_test.go
│ ├── url.go
│ └── url_test.go
├── go.mod
├── go.sum
├── internal/
│ └── assert/
│ └── assert.go
├── main.go
├── man-template.html
├── md2roff/
│ └── renderer.go
├── md2roff-bin/
│ └── cmd.go
├── script/
│ ├── bootstrap
│ ├── build
│ ├── build.bat
│ ├── changelog
│ ├── coverage
│ ├── cross-compile
│ ├── docker
│ ├── get
│ ├── github-release
│ ├── install.bat
│ ├── install.sh
│ ├── package
│ ├── publish-release
│ ├── ruby-test
│ ├── tag-release
│ ├── test
│ ├── version
│ └── version.bat
├── share/
│ └── vim/
│ └── vimfiles/
│ ├── ftdetect/
│ │ └── pullrequest.vim
│ └── syntax/
│ └── pullrequest.vim
├── ui/
│ ├── format.go
│ ├── format_test.go
│ └── ui.go
├── utils/
│ ├── args_parser.go
│ ├── args_parser_test.go
│ ├── color.go
│ ├── json.go
│ ├── utils.go
│ └── utils_test.go
└── version/
└── version.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .ctags.d/go.ctags
================================================
-R
--exclude=etc
--exclude=script
--exclude=site
--exclude=tmp
--exclude=vendor
--exclude=bundle
================================================
FILE: .dockerignore
================================================
*
!Gemfile
!Gemfile.lock
================================================
FILE: .gitattributes
================================================
# enforce correct line endings for shell and batch files.
*.sh text eol=lf
script/* text eol=lf
script/*.bat text eol=crlf
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Unexpected or broken behavior of "hub" command-line tool
title: ''
labels: bug
assignees: ''
---
**Command attempted:**
<!-- e.g. `hub pull-request --head mybranch` -->
**What happened:**
<!-- describe the faulty behavior you've been experiencing -->
**More info:**
<!-- output of `hub version`, your OS version, steps to reproduce, etc. -->
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest new functionality for "hub" command-line tool
title: ''
labels: feature
assignees: ''
---
**The problem I'm trying to solve:**
<!-- describe the problem that you think hub might help you with -->
**How I imagine hub could expose this functionality:**
<!-- e.g. `hub my-new-command --some-flags` -->
================================================
FILE: .github/SECURITY.md
================================================
Please report security vulnerabilities to mislav@github.com. Thank you!
Note that, unlike the [GitHub CLI](https://github.com/cli/cli), `hub` is _not_ an eligible target for the [GitHub Bug Bounty program](https://hackerone.com/github).
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "bundler"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
ignore:
- dependency-name: "*"
update-types:
- version-update:semver-major
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
env:
BUNDLE_BIN: bin
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "2.6"
bundler-cache: true
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.18"
- name: Run tests
run: make test-all
env:
CI: true
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
tags: "v*"
jobs:
release:
name: Publish release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.18"
- name: Publish release script
run: script/publish-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: mislav/bump-homebrew-formula-action@v3
if: "!contains(github.ref, '-')" # skip prereleases
with:
formula-name: hub
env:
COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
*.swp
*~
/bin
.bundle
bundle/
share/doc/*
share/man/*
!share/man/man1/hub.1.md
tmp/
*.test
target
.vagrant
tags
/site
/hub
.vscode
.DS_Store
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource+hub@github.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: CONTRIBUTING.md
================================================
Contributing to hub
===================
Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE).
This project adheres to a [Code of Conduct][code-of-conduct]. By participating, you are expected to uphold this code.
[code-of-conduct]: ./CODE_OF_CONDUCT.md
You will need:
1. Go 1.11+
1. Ruby 1.9+ with Bundler
2. git 1.8+
3. tmux & zsh (optional) - for running shell completion tests
If setting up either Go or Ruby for development proves to be a pain, you can
run the test suite in a prepared Docker container via `script/docker`.
## What makes a good hub feature
hub is a tool that wraps git to provide useful integration with GitHub. A new
feature is a good idea for hub if it improves some workflow for a GitHub user.
* A feature that encapsulates a git workflow *not specific* to GitHub is **not**
a good fit for hub, since something like that is best implemented as an
external script.
* If you're proposing to add a new custom command such as `hub foo`, please
research if there's a possibility that such a custom command could conflict
with other commands from popular 3rd party git projects.
* If your contribution fixes a security vulnerability, please refer to the [SECURITY.md](./.github/SECURITY.md) security policy file
## How to install dependencies and run tests
1. [Clone the project](./README.md#source)
2. Verify that existing tests pass:
`make test-all`
3. Create a topic branch:
`git checkout -b feature`
4. **Make your changes.**
(It helps a lot if you write tests first.)
5. Verify that the tests still pass.
6. Fork the project on GitHub:
`make && bin/hub fork --remote-name=origin`
7. Push to your fork:
`git push -u origin HEAD`
8. Open a pull request describing your changes:
`bin/hub pull-request`
Vendored Go dependencies are managed with [`go mod`](https://github.com/golang/go/wiki/Modules).
Check `go help mod` for information on how to add or update a vendored
dependency.
## How to write tests
Go unit tests are in `*_test.go` files and are runnable with `make test`. These
run really fast (under 10s).
However, most hub functionality is exercised through integration-style tests
written in Cucumber. See [Features](./features) for more info.
================================================
FILE: Dockerfile
================================================
FROM ruby:2.6
RUN apt-get update \
&& apt-get install -y sudo golang --no-install-recommends
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
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
USER app
# throw errors if Gemfile has been modified since Gemfile.lock
RUN bundle config --global frozen 1
WORKDIR /home/app/workdir
COPY Gemfile Gemfile.lock ./
RUN bundle install
ENV LANG C.UTF-8
ENV USER app
================================================
FILE: Gemfile
================================================
source 'https://rubygems.org'
gem 'aruba', '~> 1.0.4'
gem 'cucumber', '~> 3.1.2'
gem 'sinatra'
================================================
FILE: LICENSE
================================================
Copyright (c) 2009 Chris Wanstrath
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: Makefile
================================================
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)
export GO111MODULE=on
unexport GOPATH
export LDFLAGS := -extldflags '$(LDFLAGS)'
export GCFLAGS := all=-trimpath '$(PWD)'
export ASMFLAGS := all=-trimpath '$(PWD)'
MIN_COVERAGE = 90.2
HELP_CMD = \
share/man/man1/hub-alias.1 \
share/man/man1/hub-api.1 \
share/man/man1/hub-browse.1 \
share/man/man1/hub-ci-status.1 \
share/man/man1/hub-compare.1 \
share/man/man1/hub-create.1 \
share/man/man1/hub-delete.1 \
share/man/man1/hub-fork.1 \
share/man/man1/hub-gist.1 \
share/man/man1/hub-pr.1 \
share/man/man1/hub-pull-request.1 \
share/man/man1/hub-release.1 \
share/man/man1/hub-issue.1 \
share/man/man1/hub-sync.1 \
HELP_EXT = \
share/man/man1/hub-am.1 \
share/man/man1/hub-apply.1 \
share/man/man1/hub-checkout.1 \
share/man/man1/hub-cherry-pick.1 \
share/man/man1/hub-clone.1 \
share/man/man1/hub-fetch.1 \
share/man/man1/hub-help.1 \
share/man/man1/hub-init.1 \
share/man/man1/hub-merge.1 \
share/man/man1/hub-push.1 \
share/man/man1/hub-remote.1 \
share/man/man1/hub-submodule.1 \
HELP_ALL = share/man/man1/hub.1 $(HELP_CMD) $(HELP_EXT)
TEXT_WIDTH = 87
bin/hub: $(SOURCES)
script/build -o $@
bin/md2roff: $(SOURCES)
go build -o $@ github.com/github/hub/v2/md2roff-bin
test:
go test ./...
test-all: bin/cucumber
ifdef CI
script/test --coverage $(MIN_COVERAGE)
else
script/test
endif
bin/cucumber:
script/bootstrap
fmt:
go fmt ./...
man-pages: $(HELP_ALL:=.md) $(HELP_ALL) $(HELP_ALL:=.txt)
%.txt: %
groff -Wall -mtty-char -mandoc -Tutf8 -rLL=$(TEXT_WIDTH)n $< | col -b >$@
$(HELP_ALL): share/man/.man-pages.stamp
share/man/.man-pages.stamp: $(HELP_ALL:=.md) ./man-template.html bin/md2roff
bin/md2roff --manual="hub manual" \
--date="$(BUILD_DATE)" --version="$(HUB_VERSION)" \
--template=./man-template.html \
share/man/man1/*.md
mkdir -p share/doc/hub-doc
mv share/man/*/*.html share/doc/hub-doc/
touch $@
%.1.md: bin/hub
bin/hub help $(*F) --plain-text >$@
share/man/man1/hub.1.md:
true
install: bin/hub man-pages
bash < script/install.sh
clean:
git clean -fdx bin share/man
.PHONY: clean test test-all man-pages fmt install
================================================
FILE: README.md
================================================
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).
Usage
-----
``` sh
$ hub clone rtomayko/tilt
#=> git clone https://github.com/rtomayko/tilt.git
# or, if you prefer the SSH protocol:
$ git config --global hub.protocol ssh
$ hub clone rtomayko/tilt
#=> git clone git@github.com:rtomayko/tilt.git
```
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.
hub can also be used to make shell scripts that [directly interact with the
GitHub API](https://hub.github.com/#scripting).
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
------------
The `hub` executable has no dependencies, but since it was designed to wrap
`git`, it's recommended to have at least **git 1.7.3** or newer.
platform | manager | command to run
---------|---------|---------------
macOS, Linux | [Homebrew](https://docs.brew.sh/Installation) | `brew install hub`
macOS, Linux | [Nix](https://nixos.org/) | `nix-env -i hub`
Windows | [Scoop](http://scoop.sh/) | `scoop install hub`
Windows | [Chocolatey](https://chocolatey.org/) | `choco install hub`
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, Ubuntu | [apt(8)](https://manpages.debian.org/buster/apt/apt.8.en.html) | `sudo apt install hub`
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
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.11+](https://golang.org/doc/install)
Clone this repository and run `make install`:
```sh
git clone \
--config transfer.fsckobjects=false \
--config receive.fsckobjects=false \
--config fetch.fsckobjects=false \
https://github.com/github/hub.git
cd hub
make install prefix=/usr/local
```
Aliasing
--------
Some hub features feel best when it's aliased as `git`. This is not dangerous; your
_normal git commands will all work_. hub merely adds some sugar.
`hub alias` displays instructions for the current shell. With the `-s` flag, it
outputs a script suitable for `eval`.
You should place this command in your `.bash_profile` or other startup script:
``` sh
eval "$(hub alias -s)"
```
#### PowerShell
If you're using PowerShell, you can set an alias for `hub` by placing the
following in your PowerShell profile (usually
`~/Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1`):
``` sh
Set-Alias git hub
```
A simple way to do this is to run the following from the PowerShell prompt:
``` sh
Add-Content $PROFILE "`nSet-Alias git hub"
```
Note: You'll need to restart your PowerShell console in order for the changes to be picked up.
If your PowerShell profile doesn't exist, you can create it by running the following:
``` sh
New-Item -Type file -Force $PROFILE
```
### Shell tab-completion
hub repository contains [tab-completion scripts](./etc) for bash, zsh and fish.
These scripts complement existing completion scripts that ship with git.
Meta
----
* 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
================================================
FILE: cmd/cmd.go
================================================
package cmd
import (
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"syscall"
"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
Stdin *os.File
Stdout *os.File
Stderr *os.File
}
func (cmd Cmd) String() string {
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)
return cmd
}
func (cmd *Cmd) WithArgs(args ...string) *Cmd {
for _, arg := range args {
cmd.WithArg(arg)
}
return cmd
}
func (cmd *Cmd) Output() (string, error) {
verboseLog(cmd)
c := exec.Command(cmd.Name, cmd.Args...)
c.Stderr = cmd.Stderr
output, err := c.Output()
return string(output), err
}
func (cmd *Cmd) CombinedOutput() (string, error) {
verboseLog(cmd)
output, err := exec.Command(cmd.Name, cmd.Args...).CombinedOutput()
return string(output), err
}
func (cmd *Cmd) Success() bool {
verboseLog(cmd)
err := exec.Command(cmd.Name, cmd.Args...).Run()
return err == nil
}
// Run runs command with `Exec` on platforms except Windows
// which only supports `Spawn`
func (cmd *Cmd) Run() error {
if isWindows() {
return cmd.Spawn()
}
return cmd.Exec()
}
func isWindows() bool {
return runtime.GOOS == "windows" || detectWSL()
}
var detectedWSL bool
var detectedWSLContents string
// https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364
func detectWSL() bool {
if !detectedWSL {
b := make([]byte, 1024)
f, err := os.Open("/proc/version")
if err == nil {
f.Read(b)
f.Close()
detectedWSLContents = string(b)
}
detectedWSL = true
}
return strings.Contains(detectedWSLContents, "Microsoft")
}
// Spawn runs command with spawn(3)
func (cmd *Cmd) Spawn() error {
verboseLog(cmd)
c := exec.Command(cmd.Name, cmd.Args...)
c.Stdin = cmd.Stdin
c.Stdout = cmd.Stdout
c.Stderr = cmd.Stderr
return c.Run()
}
// Exec runs command with exec(3)
// Note that Windows doesn't support exec(3): http://golang.org/src/pkg/syscall/exec_windows.go#L339
func (cmd *Cmd) Exec() error {
verboseLog(cmd)
binary, err := exec.LookPath(cmd.Name)
if err != nil {
return &exec.Error{
Name: cmd.Name,
Err: fmt.Errorf("command not found"),
}
}
args := []string{binary}
args = append(args, cmd.Args...)
return syscall.Exec(binary, args, os.Environ())
}
func New(name string) *Cmd {
return &Cmd{
Name: name,
Args: []string{},
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
}
func NewWithArray(cmd []string) *Cmd {
return &Cmd{Name: cmd[0], Args: cmd[1:], Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr}
}
func verboseLog(cmd *Cmd) {
if os.Getenv("HUB_VERBOSE") != "" {
msg := fmt.Sprintf("$ %s", cmd.String())
if ui.IsTerminal(os.Stderr) {
msg = fmt.Sprintf("\033[35m%s\033[0m", msg)
}
ui.Errorln(msg)
}
}
================================================
FILE: cmd/cmd_test.go
================================================
package cmd
import (
"testing"
"github.com/github/hub/v2/internal/assert"
)
func TestNew(t *testing.T) {
execCmd := New("vim --noplugin")
assert.Equal(t, "vim --noplugin", execCmd.Name)
assert.Equal(t, 0, len(execCmd.Args))
}
func TestWithArg(t *testing.T) {
execCmd := New("git")
execCmd.WithArg("command").WithArg("--amend").WithArg("-m").WithArg(`""`)
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())
}
================================================
FILE: commands/alias.go
================================================
package commands
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/github/hub/v2/ui"
"github.com/github/hub/v2/utils"
)
var cmdAlias = &Command{
Run: alias,
Usage: "alias [-s] [<SHELL>]",
Long: `Show shell instructions for wrapping git.
## Options
-s
Output shell script suitable for ''eval''.
<SHELL>
Specify the type of shell (default: "$SHELL" environment variable).
## See also:
hub(1)
`,
}
func init() {
CmdRunner.Use(cmdAlias)
}
func alias(command *Command, args *Args) {
var shell string
if args.ParamsSize() > 0 {
shell = args.FirstParam()
} else {
shell = os.Getenv("SHELL")
}
flagAliasScript := args.Flag.Bool("-s")
if shell == "" {
cmd := "hub alias <shell>"
if flagAliasScript {
cmd = "hub alias -s <shell>"
}
utils.Check(fmt.Errorf("Error: couldn't detect shell type. Please specify your shell with `%s`", cmd))
}
shells := []string{"bash", "zsh", "sh", "ksh", "csh", "tcsh", "fish", "rc"}
shell = filepath.Base(shell)
var validShell bool
for _, s := range shells {
if s == shell {
validShell = true
break
}
}
if !validShell {
err := fmt.Errorf("hub alias: unsupported shell\nsupported shells: %s", strings.Join(shells, " "))
utils.Check(err)
}
if flagAliasScript {
var alias string
switch shell {
case "csh", "tcsh":
alias = "alias git hub"
case "rc":
alias = "fn git { builtin hub $* }"
default:
alias = "alias git=hub"
}
ui.Println(alias)
} else {
var profile string
switch shell {
case "bash":
profile = "~/.bash_profile"
case "zsh":
profile = "~/.zshrc"
case "ksh":
profile = "~/.profile"
case "fish":
profile = "~/.config/fish/functions/git.fish"
case "csh":
profile = "~/.cshrc"
case "tcsh":
profile = "~/.tcshrc"
case "rc":
profile = "$home/lib/profile"
default:
profile = "your profile"
}
msg := fmt.Sprintf("# Wrap git automatically by adding the following to %s:\n", profile)
ui.Println(msg)
var eval string
switch shell {
case "fish":
eval = `function git --wraps hub --description 'Alias for hub, which wraps git to provide extra functionality with GitHub.'
hub $argv
end`
case "rc":
eval = "eval `{hub alias -s}"
case "csh", "tcsh":
eval = "eval \"`hub alias -s`\""
default:
eval = `eval "$(hub alias -s)"`
}
indent := regexp.MustCompile(`(?m)^\t+`)
eval = indent.ReplaceAllStringFunc(eval, func(match string) string {
return strings.Repeat(" ", len(match)*4)
})
ui.Println(eval)
}
args.NoForward()
}
================================================
FILE: commands/api.go
================================================
package commands
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/ui"
"github.com/github/hub/v2/utils"
)
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.
## Options:
-X, --method <METHOD>
The HTTP method to use for the request (default: "GET"). The method is
automatically set to "POST" if ''--field'', ''--raw-field'', or ''--input''
are used.
Use ''-XGET'' to force serializing fields into the query string for the GET
request instead of JSON body of the POST request.
-F, --field <KEY>=<VALUE>
Data to serialize with the request. <VALUE> has some magic handling; use
''--raw-field'' for sending arbitrary string values.
If <VALUE> starts with "@", the rest of the value is interpreted as a
filename to read the value from. Use "@-" to read from standard input.
If <VALUE> is "true", "false", "null", or looks like a number, an
appropriate JSON type is used instead of a string.
It is not possible to serialize <VALUE> as a nested JSON array or hash.
Instead, construct the request payload externally and pass it via
''--input''.
Unless ''-XGET'' was used, all fields are sent serialized as JSON within
the request body. When <ENDPOINT> is "graphql", all fields other than
"query" are grouped under "variables". See
<https://graphql.org/learn/queries/#variables>
-f, --raw-field <KEY>=<VALUE>
Same as ''--field'', except that it allows values starting with "@", literal
strings "true", "false", and "null", as well as strings that look like
numbers.
--input <FILE>
The filename to read the raw request body from. Use "-" to read from standard
input. Use this when you want to manually construct the request payload.
-H, --header <KEY>:<VALUE>
Set an HTTP request header.
-i, --include
Include HTTP response headers in the output.
-t, --flat
Parse response JSON and output the data in a line-based key-value format
suitable for use in shell scripts.
--paginate
Automatically request and output the next page of results until all
resources have been listed. For GET requests, this follows the ''<next\>''
resource as indicated in the "Link" response header. For GraphQL queries,
this utilizes ''pageInfo'' that must be present in the query; see EXAMPLES.
Note that multiple JSON documents will be output as a result. If the API
rate limit has been reached, the final document that is output will be the
HTTP 403 notice, and the process will exit with a non-zero status. One way
this can be avoided is by enabling ''--obey-ratelimit''.
--color[=<WHEN>]
Enable colored output even if stdout is not a terminal. <WHEN> can be one
of "always" (default for ''--color''), "never", or "auto" (default).
--cache <TTL>
Cache valid responses to GET requests for <TTL> seconds.
When using "graphql" as <ENDPOINT>, caching will apply to responses to POST
requests as well. Just make sure to not use ''--cache'' for any GraphQL
mutations.
--obey-ratelimit
After exceeding the API rate limit, pause the process until the reset time
of the current rate limit window and retry the request. Note that this may
cause the process to hang for a long time (maximum of 1 hour).
<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''.
If the literal strings "{owner}" or "{repo}" appear in <ENDPOINT> or in the
GraphQL "query" field, fill in those placeholders with values read from the
git remote configuration of the current git repository.
## Examples:
# fetch information about the currently authenticated user as JSON
$ hub api user
# list user repositories as line-based output
$ hub api --flat users/octocat/repos
# post a comment to issue #23 of the current repository
$ hub api repos/{owner}/{repo}/issues/23/comments --raw-field 'body=Nice job!'
# perform a GraphQL query read from a file
$ hub api graphql -F query=@path/to/myquery.graphql
# perform pagination with GraphQL
$ hub api --paginate graphql -f query='
query($endCursor: String) {
repositoryOwner(login: "USER") {
repositories(first: 100, after: $endCursor) {
nodes {
nameWithOwner
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
'
## See also:
hub(1)
`,
}
func init() {
CmdRunner.Use(cmdAPI)
}
func apiCommand(_ *Command, args *Args) {
path := ""
if !args.IsParamsEmpty() {
path = args.GetParam(0)
}
method := "GET"
if args.Flag.HasReceived("--method") {
method = args.Flag.Value("--method")
} else if args.Flag.HasReceived("--field") || args.Flag.HasReceived("--raw-field") || args.Flag.HasReceived("--input") {
method = "POST"
}
cacheTTL := args.Flag.Int("--cache")
params := make(map[string]interface{})
for _, val := range args.Flag.AllValues("--field") {
parts := strings.SplitN(val, "=", 2)
if len(parts) >= 2 {
params[parts[0]] = magicValue(parts[1])
}
}
for _, val := range args.Flag.AllValues("--raw-field") {
parts := strings.SplitN(val, "=", 2)
if len(parts) >= 2 {
params[parts[0]] = parts[1]
}
}
headers := make(map[string]string)
for _, val := range args.Flag.AllValues("--header") {
parts := strings.SplitN(val, ":", 2)
if len(parts) >= 2 {
headers[parts[0]] = strings.TrimLeft(parts[1], " ")
}
}
host := ""
owner := ""
repo := ""
localRepo, localRepoErr := github.LocalRepo()
if localRepoErr == nil {
var project *github.Project
if project, localRepoErr = localRepo.MainProject(); localRepoErr == nil {
host = project.Host
owner = project.Owner
repo = project.Name
}
}
if host == "" {
defHost, err := github.CurrentConfig().DefaultHostNoPrompt()
utils.Check(err)
host = defHost.Host
}
isGraphQL := path == "graphql"
if isGraphQL && params["query"] != nil {
query := params["query"].(string)
query = strings.Replace(query, "{owner}", owner, -1)
query = strings.Replace(query, "{repo}", repo, -1)
variables := make(map[string]interface{})
for key, value := range params {
if key != "query" {
variables[key] = value
}
}
if len(variables) > 0 {
params = make(map[string]interface{})
params["variables"] = variables
}
params["query"] = query
} else {
path = strings.Replace(path, "{owner}", owner, -1)
path = strings.Replace(path, "{repo}", repo, -1)
}
var body interface{}
if args.Flag.HasReceived("--input") {
fn := args.Flag.Value("--input")
if fn == "-" {
body = os.Stdin
} else {
fi, err := os.Open(fn)
utils.Check(err)
body = fi
defer fi.Close()
}
} else {
body = params
}
gh := github.NewClient(host)
out := ui.Stdout
colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color"))
parseJSON := args.Flag.Bool("--flat")
includeHeaders := args.Flag.Bool("--include")
paginate := args.Flag.Bool("--paginate")
rateLimitWait := args.Flag.Bool("--obey-ratelimit")
args.NoForward()
for {
response, err := gh.GenericAPIRequest(method, path, body, headers, cacheTTL)
utils.Check(err)
if rateLimitWait && response.StatusCode == 403 && response.RateLimitRemaining() == 0 {
pauseUntil(response.RateLimitReset())
continue
}
success := response.StatusCode < 300
jsonType := true
if !success {
jsonType, _ = regexp.MatchString(`[/+]json(?:;|$)`, response.Header.Get("Content-Type"))
}
if includeHeaders {
fmt.Fprintf(out, "%s %s\r\n", response.Proto, response.Status)
response.Header.Write(out)
fmt.Fprintf(out, "\r\n")
}
endCursor := ""
hasNextPage := false
if parseJSON && jsonType {
hasNextPage, endCursor = utils.JSONPath(out, response.Body, colorize)
} else if paginate && isGraphQL {
bodyCopy := &bytes.Buffer{}
io.Copy(out, io.TeeReader(response.Body, bodyCopy))
hasNextPage, endCursor = utils.JSONPath(ioutil.Discard, bodyCopy, false)
} else {
io.Copy(out, response.Body)
}
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)
}
if paginate {
if isGraphQL && hasNextPage && endCursor != "" {
if v, ok := params["variables"]; ok {
variables := v.(map[string]interface{})
variables["endCursor"] = endCursor
} else {
variables := map[string]interface{}{"endCursor": endCursor}
params["variables"] = variables
}
goto next
} else if nextLink := response.Link("next"); nextLink != "" {
path = nextLink
goto next
}
}
break
next:
if !parseJSON {
fmt.Fprintf(out, "\n")
}
if rateLimitWait && response.RateLimitRemaining() == 0 {
pauseUntil(response.RateLimitReset())
}
}
}
func pauseUntil(timestamp int) {
rollover := time.Unix(int64(timestamp)+1, 0)
duration := time.Until(rollover)
if duration > 0 {
ui.Errorf("API rate limit exceeded; pausing until %v ...\n", rollover)
time.Sleep(duration)
}
}
const (
trueVal = "true"
falseVal = "false"
nilVal = "null"
)
func magicValue(value string) interface{} {
switch value {
case trueVal:
return true
case falseVal:
return false
case nilVal:
return nil
default:
if strings.HasPrefix(value, "@") {
return string(readFile(value[1:]))
} else if i, err := strconv.Atoi(value); err == nil {
return i
} else {
return value
}
}
}
func readFile(file string) (content []byte) {
var err error
if file == "-" {
content, err = ioutil.ReadAll(os.Stdin)
} else {
content, err = ioutil.ReadFile(file)
}
utils.Check(err)
return
}
================================================
FILE: commands/apply.go
================================================
package commands
import (
"io"
"io/ioutil"
"os"
"regexp"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/utils"
)
var cmdApply = &Command{
Run: apply,
GitExtension: true,
Usage: "apply <GITHUB-URL>",
Long: `Download a patch from GitHub and apply it locally.
## Options:
<GITHUB-URL>
A URL to a pull request or commit on GitHub.
## Examples:
$ hub apply https://github.com/jingweno/gh/pull/55
> curl https://github.com/jingweno/gh/pull/55.patch -o /tmp/55.patch
> git apply /tmp/55.patch
## See also:
hub-am(1), hub(1), git-apply(1)
`,
}
var cmdAm = &Command{
Run: apply,
GitExtension: true,
Usage: "am [-3] <GITHUB-URL>",
Long: `Replicate commits from a GitHub pull request locally.
## Options:
-3
(Recommended) See git-am(1).
<GITHUB-URL>
A URL to a pull request or commit on GitHub.
## Examples:
$ hub am -3 https://github.com/jingweno/gh/pull/55
> curl https://github.com/jingweno/gh/pull/55.patch -o /tmp/55.patch
> git am -3 /tmp/55.patch
## See also:
hub-apply(1), hub-cherry-pick(1), hub(1), git-am(1)
`,
}
func init() {
CmdRunner.Use(cmdApply)
CmdRunner.Use(cmdAm)
}
func apply(command *Command, args *Args) {
if !args.IsParamsEmpty() {
transformApplyArgs(args)
}
}
func transformApplyArgs(args *Args) {
gistRegexp := regexp.MustCompile("^https?://gist\\.github\\.com/([\\w.-]+/)?([a-f0-9]+)")
commitRegexp := regexp.MustCompile("^(commit|pull/[0-9]+/commits)/([0-9a-f]+)")
pullRegexp := regexp.MustCompile("^pull/([0-9]+)")
for idx, arg := range args.Params {
var (
patch io.ReadCloser
apiError error
)
projectURL, err := github.ParseURL(arg)
if err == nil {
gh := github.NewClient(projectURL.Project.Host)
if match := commitRegexp.FindStringSubmatch(projectURL.ProjectPath()); match != nil {
patch, apiError = gh.CommitPatch(projectURL.Project, match[2])
} else if match := pullRegexp.FindStringSubmatch(projectURL.ProjectPath()); match != nil {
patch, apiError = gh.PullRequestPatch(projectURL.Project, match[1])
}
} else {
match := gistRegexp.FindStringSubmatch(arg)
if match != nil {
// TODO: support Enterprise gist
gh := github.NewClient(github.GitHubHost)
patch, apiError = gh.GistPatch(match[2])
}
}
utils.Check(apiError)
if patch == nil {
continue
}
tempDir := os.TempDir()
err = os.MkdirAll(tempDir, 0775)
utils.Check(err)
patchFile, err := ioutil.TempFile(tempDir, "hub")
utils.Check(err)
_, err = io.Copy(patchFile, patch)
utils.Check(err)
patchFile.Close()
patch.Close()
args.ReplaceParam(idx, patchFile.Name())
}
}
================================================
FILE: commands/args.go
================================================
package commands
import (
"fmt"
"strings"
"github.com/github/hub/v2/cmd"
"github.com/github/hub/v2/utils"
)
type Args struct {
Executable string
GlobalFlags []string
Command string
ProgramPath string
Params []string
beforeChain []*cmd.Cmd
afterChain []*cmd.Cmd
Noop bool
Terminator bool
noForward bool
Callbacks []func() error
Flag *utils.ArgsParser
}
func (a *Args) Words() []string {
aa := make([]string, 0)
for _, p := range a.Params {
if !looksLikeFlag(p) {
aa = append(aa, p)
}
}
return aa
}
func (a *Args) Before(command ...string) {
a.beforeChain = append(a.beforeChain, cmd.NewWithArray(command))
}
func (a *Args) After(command ...string) {
a.afterChain = append(a.afterChain, cmd.NewWithArray(command))
}
func (a *Args) AfterFn(fn func() error) {
a.Callbacks = append(a.Callbacks, fn)
}
func (a *Args) NoForward() {
a.noForward = true
}
func (a *Args) Replace(executable, command string, params ...string) {
a.Executable = executable
a.Command = command
a.Params = params
a.GlobalFlags = []string{}
a.noForward = false
}
func (a *Args) Commands() []*cmd.Cmd {
result := []*cmd.Cmd{}
appendFromChain := func(c *cmd.Cmd) {
if c.Name == "git" {
ga := []string{c.Name}
ga = append(ga, a.GlobalFlags...)
ga = append(ga, c.Args...)
result = append(result, cmd.NewWithArray(ga))
} else {
result = append(result, c)
}
}
for _, c := range a.beforeChain {
appendFromChain(c)
}
if !a.noForward {
result = append(result, a.ToCmd())
}
for _, c := range a.afterChain {
appendFromChain(c)
}
return result
}
func (a *Args) ToCmd() *cmd.Cmd {
c := cmd.NewWithArray(append([]string{a.Executable}, a.GlobalFlags...))
if a.Command != "" {
c.WithArg(a.Command)
}
for _, arg := range a.Params {
c.WithArg(arg)
}
return c
}
func (a *Args) GetParam(i int) string {
return a.Params[i]
}
func (a *Args) FirstParam() string {
if a.ParamsSize() == 0 {
panic("Index 0 is out of bound")
}
return a.Params[0]
}
func (a *Args) LastParam() string {
if a.ParamsSize()-1 < 0 {
panic(fmt.Sprintf("Index %d is out of bound", a.ParamsSize()-1))
}
return a.Params[a.ParamsSize()-1]
}
func (a *Args) HasSubcommand() bool {
return !a.IsParamsEmpty() && a.Params[0][0] != '-'
}
func (a *Args) InsertParam(i int, items ...string) {
if i < 0 {
panic(fmt.Sprintf("Index %d is out of bound", i))
}
if i > a.ParamsSize() {
i = a.ParamsSize()
}
newParams := make([]string, 0)
newParams = append(newParams, a.Params[:i]...)
newParams = append(newParams, items...)
newParams = append(newParams, a.Params[i:]...)
a.Params = newParams
}
func (a *Args) RemoveParam(i int) string {
item := a.Params[i]
a.Params = append(a.Params[:i], a.Params[i+1:]...)
return item
}
func (a *Args) ReplaceParam(i int, item string) {
if i < 0 || i > a.ParamsSize()-1 {
panic(fmt.Sprintf("Index %d is out of bound", i))
}
a.Params[i] = item
}
func (a *Args) IndexOfParam(param string) int {
for i, p := range a.Params {
if p == param {
return i
}
}
return -1
}
func (a *Args) ParamsSize() int {
return len(a.Params)
}
func (a *Args) IsParamsEmpty() bool {
return a.ParamsSize() == 0
}
func (a *Args) PrependParams(params ...string) {
a.Params = append(params, a.Params...)
}
func (a *Args) AppendParams(params ...string) {
a.Params = append(a.Params, params...)
}
func NewArgs(args []string) *Args {
var (
command string
params []string
noop bool
)
cmdIdx := findCommandIndex(args)
globalFlags := args[:cmdIdx]
if cmdIdx > 0 {
args = args[cmdIdx:]
for i := len(globalFlags) - 1; i >= 0; i-- {
if globalFlags[i] == noopFlag {
noop = true
globalFlags = append(globalFlags[:i], globalFlags[i+1:]...)
}
}
}
if len(args) != 0 {
command = args[0]
params = args[1:]
}
return &Args{
Executable: "git",
GlobalFlags: globalFlags,
Command: command,
Params: params,
Noop: noop,
beforeChain: make([]*cmd.Cmd, 0),
afterChain: make([]*cmd.Cmd, 0),
}
}
const (
noopFlag = "--noop"
versionFlag = "--version"
listCmds = "--list-cmds="
helpFlag = "--help"
configFlag = "-c"
chdirFlag = "-C"
flagPrefix = "-"
)
func looksLikeFlag(value string) bool {
return strings.HasPrefix(value, flagPrefix)
}
func findCommandIndex(args []string) int {
slurpNextValue := false
commandIndex := 0
for i, arg := range args {
if slurpNextValue {
commandIndex = i + 1
slurpNextValue = false
} else if arg == versionFlag || arg == helpFlag || strings.HasPrefix(arg, listCmds) || !looksLikeFlag(arg) {
break
} else {
commandIndex = i + 1
if arg == configFlag || arg == chdirFlag {
slurpNextValue = true
}
}
}
return commandIndex
}
================================================
FILE: commands/args_test.go
================================================
package commands
import (
"testing"
"github.com/github/hub/v2/internal/assert"
)
func TestNewArgs(t *testing.T) {
args := NewArgs([]string{})
assert.Equal(t, "", args.Command)
assert.Equal(t, 0, args.ParamsSize())
args = NewArgs([]string{"command"})
assert.Equal(t, "command", args.Command)
assert.Equal(t, 0, args.ParamsSize())
args = NewArgs([]string{"command", "args"})
assert.Equal(t, "command", args.Command)
assert.Equal(t, 1, args.ParamsSize())
args = NewArgs([]string{"--version"})
assert.Equal(t, "--version", args.Command)
assert.Equal(t, 0, args.ParamsSize())
args = NewArgs([]string{"--help"})
assert.Equal(t, "--help", args.Command)
assert.Equal(t, 0, args.ParamsSize())
}
func TestArgs_Words(t *testing.T) {
args := NewArgs([]string{"merge", "--no-ff", "master", "-m", "message"})
a := args.Words()
assert.Equal(t, 2, len(a))
assert.Equal(t, "master", a[0])
assert.Equal(t, "message", a[1])
}
func TestArgs_Insert(t *testing.T) {
args := NewArgs([]string{"command", "1", "2", "3", "4"})
args.InsertParam(0, "foo")
assert.Equal(t, 5, args.ParamsSize())
assert.Equal(t, "foo", args.FirstParam())
args = NewArgs([]string{"command", "1", "2", "3", "4"})
args.InsertParam(3, "foo")
assert.Equal(t, 5, args.ParamsSize())
assert.Equal(t, "foo", args.Params[3])
args = NewArgs([]string{"checkout", "-b"})
args.InsertParam(1, "foo")
assert.Equal(t, 2, args.ParamsSize())
assert.Equal(t, "-b", args.Params[0])
assert.Equal(t, "foo", args.Params[1])
args = NewArgs([]string{"checkout"})
args.InsertParam(1, "foo")
assert.Equal(t, 1, args.ParamsSize())
assert.Equal(t, "foo", args.Params[0])
}
func TestArgs_Remove(t *testing.T) {
args := NewArgs([]string{"1", "2", "3", "4"})
item := args.RemoveParam(1)
assert.Equal(t, "3", item)
assert.Equal(t, 2, args.ParamsSize())
assert.Equal(t, "2", args.FirstParam())
assert.Equal(t, "4", args.GetParam(1))
}
func TestArgs_GlobalFlags(t *testing.T) {
args := NewArgs([]string{"-c", "key=value", "status", "-s", "-b"})
assert.Equal(t, "status", args.Command)
assert.Equal(t, []string{"-c", "key=value"}, args.GlobalFlags)
assert.Equal(t, []string{"-s", "-b"}, args.Params)
assert.Equal(t, false, args.Noop)
}
func TestArgs_GlobalFlags_Noop(t *testing.T) {
args := NewArgs([]string{"-c", "key=value", "--noop", "--literal-pathspecs", "status", "-s", "-b"})
assert.Equal(t, "status", args.Command)
assert.Equal(t, []string{"-c", "key=value", "--literal-pathspecs"}, args.GlobalFlags)
assert.Equal(t, []string{"-s", "-b"}, args.Params)
assert.Equal(t, true, args.Noop)
}
func TestArgs_GlobalFlags_NoopTwice(t *testing.T) {
args := NewArgs([]string{"--noop", "--bare", "--noop", "status"})
assert.Equal(t, "status", args.Command)
assert.Equal(t, []string{"--bare"}, args.GlobalFlags)
assert.Equal(t, 0, len(args.Params))
assert.Equal(t, true, args.Noop)
}
func TestArgs_GlobalFlags_Repeated(t *testing.T) {
args := NewArgs([]string{"-C", "mydir", "-c", "a=b", "--bare", "-c", "c=d", "-c", "e=f", "status"})
assert.Equal(t, "status", args.Command)
assert.Equal(t, []string{"-C", "mydir", "-c", "a=b", "--bare", "-c", "c=d", "-c", "e=f"}, args.GlobalFlags)
assert.Equal(t, 0, len(args.Params))
assert.Equal(t, false, args.Noop)
}
func TestArgs_GlobalFlags_Propagate(t *testing.T) {
args := NewArgs([]string{"-c", "key=value", "status"})
cmd := args.ToCmd()
assert.Equal(t, []string{"-c", "key=value", "status"}, cmd.Args)
}
func TestArgs_GlobalFlags_Replaced(t *testing.T) {
args := NewArgs([]string{"-c", "key=value", "status"})
args.Replace("open", "", "-a", "http://example.com")
cmd := args.ToCmd()
assert.Equal(t, "open", cmd.Name)
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")
args.After("git", "clean")
args.After("echo", "done!")
cmds := args.Commands()
assert.Equal(t, 4, len(cmds))
assert.Equal(t, "git -c key=value -C dir remote add", cmds[0].String())
assert.Equal(t, "git -c key=value -C dir status", cmds[1].String())
assert.Equal(t, "git -c key=value -C dir clean", cmds[2].String())
assert.Equal(t, "echo done!", cmds[3].String())
}
================================================
FILE: commands/browse.go
================================================
package commands
import (
"fmt"
"net/url"
"strings"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/utils"
)
var cmdBrowse = &Command{
Run: browse,
Usage: "browse [-uc] [[<USER>/]<REPOSITORY>|--] [<SUBPAGE>]",
Long: `Open a GitHub repository in a web browser.
## Options:
-u, --url
Print the URL instead of opening it.
-c, --copy
Put the URL in clipboard instead of opening it.
[<USER>/]<REPOSITORY>
Defaults to repository in the current working directory.
<SUBPAGE>
One of "wiki", "commits", "issues", or other (default: "tree").
## Examples:
$ hub browse
> open https://github.com/REPO
$ hub browse -- issues
> open https://github.com/REPO/issues
$ hub browse jingweno/gh
> open https://github.com/jingweno/gh
$ hub browse gh wiki
> open https://github.com/USER/gh/wiki
## See also:
hub-compare(1), hub(1)
`,
}
func init() {
CmdRunner.Use(cmdBrowse)
}
func browse(command *Command, args *Args) {
var (
dest string
subpage string
path string
project *github.Project
branch *github.Branch
err error
)
if !args.IsParamsEmpty() {
dest = args.RemoveParam(0)
}
if !args.IsParamsEmpty() {
subpage = args.RemoveParam(0)
}
if args.Terminator {
subpage = dest
dest = ""
}
localRepo, _ := github.LocalRepo()
if dest != "" {
project = github.NewProject("", dest, "")
branch = localRepo.MasterBranch()
} else if subpage != "" && subpage != "commits" && subpage != "tree" && subpage != "blob" && subpage != "settings" {
project, err = localRepo.MainProject()
branch = localRepo.MasterBranch()
utils.Check(err)
} else {
currentBranch, err := localRepo.CurrentBranch()
if err != nil {
currentBranch = localRepo.MasterBranch()
}
var owner string
mainProject, err := localRepo.MainProject()
if err == nil {
host, err := github.CurrentConfig().PromptForHost(mainProject.Host)
if err != nil {
utils.Check(github.FormatError("in browse", err))
} else {
owner = host.User
}
}
branch, project, _ = localRepo.RemoteBranchAndProject(owner, currentBranch.IsMaster())
if branch == nil {
branch = localRepo.MasterBranch()
}
}
if project == nil {
utils.Check(command.UsageError(""))
}
if subpage == "commits" {
path = fmt.Sprintf("commits/%s", branchInURL(branch))
} else if subpage == "tree" || subpage == "" {
if !branch.IsMaster() {
path = fmt.Sprintf("tree/%s", branchInURL(branch))
}
} else {
path = subpage
}
pageURL := project.WebURL("", "", path)
args.NoForward()
flagBrowseURLPrint := args.Flag.Bool("--url")
flagBrowseURLCopy := args.Flag.Bool("--copy")
printBrowseOrCopy(args, pageURL, !flagBrowseURLPrint && !flagBrowseURLCopy, flagBrowseURLCopy)
}
func branchInURL(branch *github.Branch) string {
parts := strings.Split(branch.ShortName(), "/")
newPath := make([]string, len(parts))
for i, s := range parts {
newPath[i] = url.QueryEscape(s)
}
return strings.Join(newPath, "/")
}
================================================
FILE: commands/checkout.go
================================================
package commands
import (
"fmt"
"regexp"
"github.com/github/hub/v2/git"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/utils"
)
var cmdCheckout = &Command{
Run: checkout,
GitExtension: true,
Usage: "checkout <PULLREQ-URL> [<BRANCH>]",
Long: `Check out the head of a pull request as a local branch.
## Examples:
$ hub checkout https://github.com/jingweno/gh/pull/73
> git fetch origin pull/73/head:jingweno-feature
> git checkout jingweno-feature
## See also:
hub-merge(1), hub-am(1), hub(1), git-checkout(1)
`,
}
func init() {
CmdRunner.Use(cmdCheckout)
}
func checkout(command *Command, args *Args) {
words := args.Words()
if len(words) == 0 {
return
}
checkoutURL := words[0]
var newBranchName string
if len(words) > 1 {
newBranchName = words[1]
}
url, err := github.ParseURL(checkoutURL)
if err != nil {
// not a valid GitHub URL
return
}
pullURLRegex := regexp.MustCompile("^pull/(\\d+)")
projectPath := url.ProjectPath()
if !pullURLRegex.MatchString(projectPath) {
// not a valid PR URL
return
}
err = sanitizeCheckoutFlags(args)
utils.Check(err)
id := pullURLRegex.FindStringSubmatch(projectPath)[1]
gh := github.NewClient(url.Project.Host)
pullRequest, err := gh.PullRequest(url.Project, id)
utils.Check(err)
newArgs, err := transformCheckoutArgs(args, pullRequest, newBranchName)
utils.Check(err)
if idx := args.IndexOfParam(newBranchName); idx >= 0 {
args.RemoveParam(idx)
}
replaceCheckoutParam(args, checkoutURL, newArgs...)
}
func transformCheckoutArgs(args *Args, pullRequest *github.PullRequest, newBranchName string) (newArgs []string, err error) {
repo, err := github.LocalRepo()
if err != nil {
return
}
baseRemote, err := repo.RemoteForRepo(pullRequest.Base.Repo)
if err != nil {
return
}
var headRemote *github.Remote
if pullRequest.IsSameRepo() {
headRemote = baseRemote
} else if pullRequest.Head.Repo != nil {
headRemote, _ = repo.RemoteForRepo(pullRequest.Head.Repo)
}
if headRemote != nil {
if newBranchName == "" {
newBranchName = pullRequest.Head.Ref
}
remoteBranch := fmt.Sprintf("%s/%s", headRemote.Name, pullRequest.Head.Ref)
refSpec := fmt.Sprintf("+refs/heads/%s:refs/remotes/%s", pullRequest.Head.Ref, remoteBranch)
if git.HasFile("refs", "heads", newBranchName) {
newArgs = append(newArgs, newBranchName)
args.After("git", "merge", "--ff-only", fmt.Sprintf("refs/remotes/%s", remoteBranch))
} else {
newArgs = append(newArgs, "-b", newBranchName, "--no-track", remoteBranch)
args.After("git", "config", fmt.Sprintf("branch.%s.remote", newBranchName), headRemote.Name)
args.After("git", "config", fmt.Sprintf("branch.%s.merge", newBranchName), "refs/heads/"+pullRequest.Head.Ref)
}
args.Before("git", "fetch", headRemote.Name, refSpec)
} else {
if newBranchName == "" {
newBranchName = pullRequest.Head.Ref
if pullRequest.Head.Repo != nil && newBranchName == pullRequest.Head.Repo.DefaultBranch {
newBranchName = fmt.Sprintf("%s-%s", pullRequest.Head.Repo.Owner.Login, newBranchName)
}
}
newArgs = append(newArgs, newBranchName)
b, errB := repo.CurrentBranch()
isCurrentBranch := errB == nil && b.ShortName() == newBranchName
ref := fmt.Sprintf("refs/pull/%d/head", pullRequest.Number)
if isCurrentBranch {
args.Before("git", "fetch", baseRemote.Name, ref)
args.After("git", "merge", "--ff-only", "FETCH_HEAD")
} else {
args.Before("git", "fetch", baseRemote.Name, fmt.Sprintf("%s:%s", ref, newBranchName))
}
remote := baseRemote.Name
mergeRef := ref
if pullRequest.MaintainerCanModify && pullRequest.Head.Repo != nil {
var project *github.Project
project, err = github.NewProjectFromRepo(pullRequest.Head.Repo)
if err != nil {
return
}
remote = project.GitURL("", "", true)
mergeRef = fmt.Sprintf("refs/heads/%s", pullRequest.Head.Ref)
}
if mc, err := git.Config(fmt.Sprintf("branch.%s.merge", newBranchName)); err != nil || mc == "" {
args.After("git", "config", fmt.Sprintf("branch.%s.remote", newBranchName), remote)
args.After("git", "config", fmt.Sprintf("branch.%s.merge", newBranchName), mergeRef)
}
}
return
}
func sanitizeCheckoutFlags(args *Args) error {
if i := args.IndexOfParam("-b"); i != -1 {
return fmt.Errorf("Unsupported flag -b when checking out pull request")
}
if i := args.IndexOfParam("--orphan"); i != -1 {
return fmt.Errorf("Unsupported flag --orphan when checking out pull request")
}
return nil
}
func replaceCheckoutParam(args *Args, checkoutURL string, replacement ...string) {
idx := args.IndexOfParam(checkoutURL)
args.RemoveParam(idx)
args.InsertParam(idx, replacement...)
}
================================================
FILE: commands/cherry_pick.go
================================================
package commands
import (
"fmt"
"regexp"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/utils"
)
var cmdCherryPick = &Command{
Run: cherryPick,
GitExtension: true,
Usage: `
cherry-pick <COMMIT-URL>
cherry-pick <USER>@<SHA>
`,
Long: `Cherry-pick a commit from a fork on GitHub.
## See also:
hub-am(1), hub(1), git-cherry-pick(1)
`,
}
func init() {
CmdRunner.Use(cmdCherryPick)
}
func cherryPick(command *Command, args *Args) {
if args.IndexOfParam("-m") == -1 && args.IndexOfParam("--mainline") == -1 {
transformCherryPickArgs(args)
}
}
func transformCherryPickArgs(args *Args) {
if args.IsParamsEmpty() {
return
}
var project *github.Project
var sha, refspec string
shaRe := "[a-f0-9]{7,40}"
var mainProject *github.Project
localRepo, mainProjectErr := github.LocalRepo()
if mainProjectErr == nil {
mainProject, mainProjectErr = localRepo.MainProject()
}
ref := args.LastParam()
if url, err := github.ParseURL(ref); err == nil {
projectPath := url.ProjectPath()
commitRegex := regexp.MustCompile(fmt.Sprintf("^commit/(%s)", shaRe))
pullRegex := regexp.MustCompile(fmt.Sprintf(`^pull/(\d+)/commits/(%s)`, shaRe))
if matches := commitRegex.FindStringSubmatch(projectPath); len(matches) > 0 {
sha = matches[1]
project = url.Project
} else if matches := pullRegex.FindStringSubmatch(projectPath); len(matches) > 0 {
pullID := matches[1]
sha = matches[2]
utils.Check(mainProjectErr)
project = mainProject
refspec = fmt.Sprintf("refs/pull/%s/head", pullID)
}
} else {
ownerWithShaRegexp := regexp.MustCompile(fmt.Sprintf("^(%s)@(%s)$", OwnerRe, shaRe))
if matches := ownerWithShaRegexp.FindStringSubmatch(ref); len(matches) > 0 {
utils.Check(mainProjectErr)
project = mainProject
project.Owner = matches[1]
sha = matches[2]
}
}
if project != nil {
args.ReplaceParam(args.IndexOfParam(ref), sha)
tmpName := "_hub-cherry-pick"
remoteName := tmpName
if remote, err := localRepo.RemoteForProject(project); err == nil {
remoteName = remote.Name
} else {
args.Before("git", "remote", "add", remoteName, project.GitURL("", "", false))
}
fetchArgs := []string{"git", "fetch", "-q", "--no-tags", remoteName}
if refspec != "" {
fetchArgs = append(fetchArgs, refspec)
}
args.Before(fetchArgs...)
if remoteName == tmpName {
args.Before("git", "remote", "rm", remoteName)
}
}
}
================================================
FILE: commands/ci_status.go
================================================
package commands
import (
"fmt"
"os"
"sort"
"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{
Run: ciStatus,
Usage: "ci-status [-v] [<COMMIT>]",
Long: `Display status of GitHub checks for a commit.
## Options:
-v, --verbose
Print detailed report of all status checks and their URLs.
-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 checks are:
%U: the URL of this status check
%S: check state (e.g. "success", "failure")
%sC: set color to red, green, or yellow, depending on state
%t: name of the status check
--color[=<WHEN>]
Enable colored output even if stdout is not a terminal. <WHEN> can be one
of "always" (default for ''--color''), "never", or "auto" (default).
<COMMIT>
A commit SHA or branch name (default: "HEAD").
Possible outputs and exit statuses:
- success, neutral: 0
- failure, error, action_required, cancelled, timed_out: 1
- pending: 2
## See also:
hub-pull-request(1), hub(1)
`,
}
var severityList []string
func init() {
CmdRunner.Use(cmdCiStatus)
severityList = []string{
"neutral",
"success",
"pending",
"cancelled",
"timed_out",
"action_required",
"failure",
"error",
}
}
func checkSeverity(targetState string) int {
for i, state := range severityList {
if state == targetState {
return i
}
}
return -1
}
func ciStatus(cmd *Command, args *Args) {
ref := "HEAD"
if !args.IsParamsEmpty() {
ref = args.RemoveParam(0)
}
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
sha, err := git.Ref(ref)
if err != nil {
err = fmt.Errorf("Aborted: no revision could be determined from '%s'", ref)
}
utils.Check(err)
if args.Noop {
ui.Printf("Would request CI status for %s\n", sha)
} else {
gh := github.NewClient(project.Host)
response, err := gh.FetchCIStatus(project, sha)
utils.Check(err)
state := ""
if len(response.Statuses) > 0 {
for _, status := range response.Statuses {
if checkSeverity(status.State) > checkSeverity(state) {
state = status.State
}
}
}
var exitCode int
switch state {
case "success", "neutral":
exitCode = 0
case "failure", "error", "action_required", "cancelled", "timed_out":
exitCode = 1
case "pending":
exitCode = 2
default:
exitCode = 3
}
verbose := args.Flag.Bool("--verbose") || args.Flag.HasReceived("--format")
if verbose && len(response.Statuses) > 0 {
colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color"))
ciVerboseFormat(response.Statuses, args.Flag.Value("--format"), colorize)
} else {
if state != "" {
ui.Println(state)
} else {
ui.Println("no status")
}
}
os.Exit(exitCode)
}
}
func ciVerboseFormat(statuses []github.CIStatus, formatString string, colorize bool) {
contextWidth := 0
for _, status := range statuses {
if len(status.Context) > contextWidth {
contextWidth = len(status.Context)
}
}
sort.SliceStable(statuses, func(a, b int) bool {
return stateRank(statuses[a].State) < stateRank(statuses[b].State)
})
for _, status := range statuses {
var color int
var stateMarker string
switch status.State {
case "success":
stateMarker = "✔︎"
color = 32
case "failure", "error", "action_required", "cancelled", "timed_out":
stateMarker = "✖︎"
color = 31
case "neutral":
stateMarker = "◦"
color = 30
case "pending":
stateMarker = "●"
color = 33
}
placeholders := map[string]string{
"S": status.State,
"sC": "",
"t": status.Context,
"U": status.TargetURL,
}
if colorize {
placeholders["sC"] = fmt.Sprintf("\033[%dm", color)
}
format := formatString
if format == "" {
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)
}
}
ui.Print(ui.Expand(format, placeholders, colorize))
}
}
func stateRank(state string) uint32 {
switch state {
case "failure", "error", "action_required", "cancelled", "timed_out":
return 1
case "success", "neutral":
return 3
default:
return 2
}
}
================================================
FILE: commands/clone.go
================================================
package commands
import (
"fmt"
"regexp"
"strings"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/utils"
)
var cmdClone = &Command{
Run: clone,
GitExtension: true,
Usage: "clone [-p] [<OPTIONS>] [<USER>/]<REPOSITORY> [<DESTINATION>]",
Long: `Clone a repository from GitHub.
## Options:
-p
(Deprecated) Clone private repositories over SSH.
[<USER>/]<REPOSITORY>
<USER> defaults to your own GitHub username.
<DESTINATION>
Directory name to clone into (default: <REPOSITORY>).
## Protocol used for cloning
HTTPS protocol is used by hub as the default. Alternatively, hub can be
configured to use SSH protocol for all git operations. See "SSH instead
of HTTPS protocol" and "HUB_PROTOCOL" of hub(1).
## Examples:
$ hub clone rtomayko/ronn
> git clone https://github.com/rtomayko/ronn.git
## See also:
hub-fork(1), hub(1), git-clone(1)
`,
}
func init() {
CmdRunner.Use(cmdClone)
}
func clone(command *Command, args *Args) {
if !args.IsParamsEmpty() {
transformCloneArgs(args)
}
}
func transformCloneArgs(args *Args) {
isPrivate := parseClonePrivateFlag(args)
// git help clone | grep -e '^ \+-.\+<'
p := utils.NewArgsParser()
p.RegisterValue("--branch", "-b")
p.RegisterValue("--depth")
p.RegisterValue("--reference")
if args.Command == "submodule" {
p.RegisterValue("--name")
} else {
p.RegisterValue("--config", "-c")
p.RegisterValue("--jobs", "-j")
p.RegisterValue("--origin", "-o")
p.RegisterValue("--reference-if-able")
p.RegisterValue("--separate-git-dir")
p.RegisterValue("--shallow-exclude")
p.RegisterValue("--shallow-since")
p.RegisterValue("--template")
p.RegisterValue("--upload-pack", "-u")
}
p.Parse(args.Params)
nameWithOwnerRegexp := regexp.MustCompile(NameWithOwnerRe)
if len(p.PositionalIndices) > 0 {
i := p.PositionalIndices[0]
a := args.Params[i]
if nameWithOwnerRegexp.MatchString(a) && !isCloneable(a) {
url := getCloneURL(a, isPrivate, args.Command != "submodule")
args.ReplaceParam(i, url)
}
}
}
func parseClonePrivateFlag(args *Args) bool {
if i := args.IndexOfParam("-p"); i != -1 {
args.RemoveParam(i)
return true
}
return false
}
func getCloneURL(nameWithOwner string, allowPush, allowPrivate bool) string {
name := nameWithOwner
owner := ""
if strings.Contains(name, "/") {
split := strings.SplitN(name, "/", 2)
owner = split[0]
name = split[1]
}
var host *github.Host
if owner == "" {
config := github.CurrentConfig()
h, err := config.DefaultHost()
if err != nil {
utils.Check(github.FormatError("cloning repository", err))
}
host = h
owner = host.User
}
var hostStr string
if host != nil {
hostStr = host.Host
}
expectWiki := strings.HasSuffix(name, ".wiki")
if expectWiki {
name = strings.TrimSuffix(name, ".wiki")
}
project := github.NewProject(owner, name, hostStr)
gh := github.NewClient(project.Host)
repo, err := gh.Repository(project)
if err != nil {
if strings.Contains(err.Error(), "HTTP 404") {
err = fmt.Errorf("Error: repository %s/%s doesn't exist", project.Owner, project.Name)
}
utils.Check(err)
}
owner = repo.Owner.Login
name = repo.Name
if expectWiki {
if !repo.HasWiki {
utils.Check(fmt.Errorf("Error: %s/%s doesn't have a wiki", owner, name))
} else {
name = name + ".wiki"
}
}
if !allowPush && allowPrivate {
allowPush = repo.Private || repo.Permissions.Push
}
return project.GitURL(name, owner, allowPush)
}
================================================
FILE: commands/commands.go
================================================
package commands
import (
"fmt"
"regexp"
"strings"
"github.com/github/hub/v2/utils"
)
var (
NameRe = `[\w.-]+`
OwnerRe = "[a-zA-Z0-9][a-zA-Z0-9-]*"
NameWithOwnerRe = fmt.Sprintf(`^(%s/)?%s$`, OwnerRe, NameRe)
CmdRunner = NewRunner()
)
type Command struct {
Run func(cmd *Command, args *Args)
Key string
Usage string
Long string
KnownFlags string
GitExtension bool
subCommands map[string]*Command
parentCommand *Command
}
func (c *Command) Call(args *Args) (err error) {
runCommand, err := c.lookupSubCommand(args)
if err != nil {
return
}
if !c.GitExtension {
err = runCommand.parseArguments(args)
if err != nil {
return
}
}
runCommand.Run(runCommand, args)
return
}
type ErrHelp struct {
err string
}
func (e ErrHelp) Error() string {
return e.err
}
func (c *Command) parseArguments(args *Args) error {
knownFlags := c.KnownFlags
if knownFlags == "" {
knownFlags = c.Long
}
args.Flag = utils.NewArgsParserWithUsage("-h, --help\n" + knownFlags)
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) {
if c.subCommands == nil {
c.subCommands = make(map[string]*Command)
}
c.subCommands[subCommand.Name()] = subCommand
subCommand.parentCommand = c
}
func (c *Command) UsageError(msg string) error {
nl := ""
if msg != "" {
nl = "\n"
}
return fmt.Errorf("%s%s%s", msg, nl, c.Synopsis())
}
func (c *Command) Synopsis() string {
lines := []string{}
usagePrefix := "Usage:"
usageStr := c.Usage
if usageStr == "" && c.parentCommand != nil {
usageStr = c.parentCommand.Usage
}
for _, line := range strings.Split(usageStr, "\n") {
if line != "" {
usage := fmt.Sprintf("%s hub %s", usagePrefix, line)
usagePrefix = " "
lines = append(lines, usage)
}
}
return strings.Join(lines, "\n")
}
func (c *Command) HelpText() string {
usage := strings.Replace(c.Usage, "-^", "`-^`", 1)
usageRe := regexp.MustCompile(`(?m)^([a-z-]+)(.*)$`)
usage = usageRe.ReplaceAllString(usage, "`hub $1`$2 ")
usage = strings.TrimSpace(usage)
var desc string
long := strings.TrimSpace(c.Long)
if lines := strings.Split(long, "\n"); len(lines) > 1 {
desc = lines[0]
long = strings.Join(lines[1:], "\n")
}
long = strings.Replace(long, "''", "`", -1)
headingRe := regexp.MustCompile(`(?m)^(## .+):$`)
long = headingRe.ReplaceAllString(long, "$1")
indentRe := regexp.MustCompile(`(?m)^\t`)
long = indentRe.ReplaceAllLiteralString(long, "")
definitionListRe := regexp.MustCompile(`(?m)^(\* )?([^#\s][^\n]*?):?\n\t`)
long = definitionListRe.ReplaceAllString(long, "$2\n:\t")
return fmt.Sprintf("hub-%s(1) -- %s\n===\n\n## Synopsis\n\n%s\n%s", c.Name(), desc, usage, long)
}
func (c *Command) Name() string {
if c.Key != "" {
return c.Key
}
usageLine := strings.Split(strings.TrimSpace(c.Usage), "\n")[0]
return strings.Split(usageLine, " ")[0]
}
func (c *Command) Runnable() bool {
return c.Run != nil
}
func (c *Command) lookupSubCommand(args *Args) (runCommand *Command, err error) {
if len(c.subCommands) > 0 && args.HasSubcommand() {
subCommandName := args.FirstParam()
if subCommand, ok := c.subCommands[subCommandName]; ok {
runCommand = subCommand
args.Params = args.Params[1:]
} else {
err = fmt.Errorf("error: Unknown subcommand: %s", subCommandName)
}
} else {
runCommand = c
}
return
}
================================================
FILE: commands/commands_test.go
================================================
package commands
import (
"io/ioutil"
"os"
"regexp"
"testing"
"github.com/github/hub/v2/internal/assert"
"github.com/github/hub/v2/ui"
)
func TestMain(m *testing.M) {
ui.Default = ui.Console{Stdout: ioutil.Discard, Stderr: ioutil.Discard}
os.Exit(m.Run())
}
func TestCommandUseSelf(t *testing.T) {
c := &Command{Usage: "foo"}
args := NewArgs([]string{"foo"})
run, err := c.lookupSubCommand(args)
assert.Equal(t, nil, err)
assert.Equal(t, c, run)
}
func TestCommandUseSubcommand(t *testing.T) {
c := &Command{Usage: "foo"}
s := &Command{Usage: "bar"}
c.Use(s)
args := NewArgs([]string{"foo", "bar"})
run, err := c.lookupSubCommand(args)
assert.Equal(t, nil, err)
assert.Equal(t, s, run)
}
func TestCommandUseErrorWhenMissingSubcommand(t *testing.T) {
c := &Command{Usage: "foo"}
s := &Command{Usage: "bar"}
c.Use(s)
args := NewArgs([]string{"foo", "baz"})
_, err := c.lookupSubCommand(args)
assert.NotEqual(t, nil, err)
}
func TestArgsForCommand(t *testing.T) {
c := &Command{Usage: "foo"}
args := NewArgs([]string{"foo", "bar", "baz"})
c.lookupSubCommand(args)
assert.Equal(t, 2, len(args.Params))
}
func TestArgsForSubCommand(t *testing.T) {
c := &Command{Usage: "foo"}
s := &Command{Usage: "bar"}
c.Use(s)
args := NewArgs([]string{"foo", "bar", "baz"})
c.lookupSubCommand(args)
assert.Equal(t, 1, len(args.Params))
}
func TestFlagsAfterArguments(t *testing.T) {
c := &Command{Long: "-m, --message MSG"}
args := NewArgs([]string{"foo", "bar", "-m", "baz"})
err := c.parseArguments(args)
assert.Equal(t, nil, err)
assert.Equal(t, "baz", args.Flag.Value("--message"))
assert.Equal(t, 1, len(args.Params))
assert.Equal(t, "bar", args.LastParam())
}
func TestCommandNameTakeKey(t *testing.T) {
c := &Command{Key: "bar", Usage: "foo -t -v --foo"}
assert.Equal(t, "bar", c.Name())
}
func TestCommandCall(t *testing.T) {
var result string
f := func(c *Command, args *Args) { result = args.FirstParam() }
c := &Command{Usage: "foo", Run: f}
args := NewArgs([]string{"foo", "bar"})
c.Call(args)
assert.Equal(t, "bar", result)
}
func TestCommandHelp(t *testing.T) {
var result string
f := func(c *Command, args *Args) { result = args.FirstParam() }
c := &Command{Usage: "foo", Run: f}
args := NewArgs([]string{"foo", "-h"})
c.Call(args)
assert.Equal(t, "", result)
}
func TestSubCommandCall(t *testing.T) {
var result string
f1 := func(c *Command, args *Args) { result = "noop" }
f2 := func(c *Command, args *Args) { result = args.LastParam() }
c := &Command{Usage: "foo", Run: f1}
s := &Command{Key: "bar", Usage: "foo bar", Run: f2}
c.Use(s)
args := NewArgs([]string{"foo", "bar", "baz"})
c.Call(args)
assert.Equal(t, "baz", result)
}
func Test_NameWithOwnerRe(t *testing.T) {
re := regexp.MustCompile(NameWithOwnerRe)
assert.Equal(t, true, re.MatchString("o/n"))
assert.Equal(t, true, re.MatchString("own-er/my-project.git"))
assert.Equal(t, true, re.MatchString("my-project.git"))
assert.Equal(t, true, re.MatchString("my_project"))
assert.Equal(t, true, re.MatchString("-dash"))
assert.Equal(t, true, re.MatchString(".dotfiles"))
assert.Equal(t, false, re.MatchString(""))
assert.Equal(t, false, re.MatchString("/"))
assert.Equal(t, false, re.MatchString(" "))
assert.Equal(t, false, re.MatchString("owner/na me"))
assert.Equal(t, false, re.MatchString("owner/na/me"))
assert.Equal(t, false, re.MatchString("own.er/name"))
assert.Equal(t, false, re.MatchString("own_er/name"))
assert.Equal(t, false, re.MatchString("-owner/name"))
}
================================================
FILE: commands/compare.go
================================================
package commands
import (
"fmt"
"net/url"
"regexp"
"strings"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/utils"
)
var cmdCompare = &Command{
Run: compare,
Usage: `
compare [-uc] [-b <BASE>]
compare [-uc] [<OWNER>] [<BASE>...]<HEAD>
`,
Long: `Open a GitHub compare page in a web browser.
## Options:
-u, --url
Print the URL instead of opening it.
-c, --copy
Put the URL to clipboard instead of opening it.
-b, --base <BASE>
Base branch to compare against in case no explicit arguments were given.
[<BASE>...]<HEAD>
Branch names, tag names, or commit SHAs specifying the range to compare.
If a range with two dots (''A..B'') is given, it will be transformed into a
range with three dots.
The <BASE> portion defaults to the default branch of the repository.
The <HEAD> argument defaults to the current branch. If the current branch
is not pushed to a remote, the command will error.
<OWNER>
Optionally specify the owner of the repository for the compare page URL.
## Examples:
$ hub compare
> open https://github.com/OWNER/REPO/compare/BRANCH
$ hub compare refactor
> open https://github.com/OWNER/REPO/compare/refactor
$ hub compare v1.0..v1.1
> open https://github.com/OWNER/REPO/compare/v1.0...v1.1
$ hub compare -u jingweno feature
https://github.com/jingweno/REPO/compare/feature
## See also:
hub-browse(1), hub(1)
`,
}
func init() {
CmdRunner.Use(cmdCompare)
}
func compare(command *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
mainProject, err := localRepo.MainProject()
utils.Check(err)
host, err := github.CurrentConfig().PromptForHost(mainProject.Host)
utils.Check(err)
var r string
flagCompareBase := args.Flag.Value("--base")
if args.IsParamsEmpty() {
currentBranch, err := localRepo.CurrentBranch()
if err != nil {
utils.Check(command.UsageError(err.Error()))
}
var remoteBranch *github.Branch
var remoteProject *github.Project
remoteBranch, remoteProject, err = findPushTarget(currentBranch)
if err != nil {
if remoteProject, err = deducePushTarget(currentBranch, host.User); err == nil {
remoteBranch = currentBranch
} else {
utils.Check(fmt.Errorf("the current branch '%s' doesn't seem pushed to a remote", currentBranch.ShortName()))
}
}
r = remoteBranch.ShortName()
if remoteProject.SameAs(mainProject) {
if flagCompareBase == "" && remoteBranch.IsMaster() {
utils.Check(fmt.Errorf("the branch to compare '%s' is the default branch", remoteBranch.ShortName()))
}
} else {
r = fmt.Sprintf("%s:%s", remoteProject.Owner, r)
}
if flagCompareBase == r {
utils.Check(fmt.Errorf("the branch to compare '%s' is the same as --base", r))
} else if flagCompareBase != "" {
r = fmt.Sprintf("%s...%s", flagCompareBase, r)
}
} else {
if flagCompareBase != "" {
utils.Check(command.UsageError(""))
} else {
r = parseCompareRange(args.RemoveParam(args.ParamsSize() - 1))
if !args.IsParamsEmpty() {
owner := args.RemoveParam(args.ParamsSize() - 1)
mainProject = github.NewProject(owner, mainProject.Name, mainProject.Host)
}
}
}
url := mainProject.WebURL("", "", "compare/"+rangeQueryEscape(r))
args.NoForward()
flagCompareURLOnly := args.Flag.Bool("--url")
flagCompareCopy := args.Flag.Bool("--copy")
printBrowseOrCopy(args, url, !flagCompareURLOnly && !flagCompareCopy, flagCompareCopy)
}
func parseCompareRange(r string) string {
shaOrTag := fmt.Sprintf("((?:%s:)?\\w(?:[\\w/.-]*\\w)?)", OwnerRe)
shaOrTagRange := fmt.Sprintf("^%s\\.\\.%s$", shaOrTag, shaOrTag)
shaOrTagRangeRegexp := regexp.MustCompile(shaOrTagRange)
return shaOrTagRangeRegexp.ReplaceAllString(r, "$1...$2")
}
// characters we want to allow unencoded in compare views
var compareUnescaper = strings.NewReplacer(
"%2F", "/",
"%3A", ":",
"%5E", "^",
"%7E", "~",
"%2A", "*",
"%21", "!",
)
func rangeQueryEscape(r string) string {
if strings.Contains(r, "..") {
return r
}
return compareUnescaper.Replace(url.QueryEscape(r))
}
================================================
FILE: commands/compare_test.go
================================================
package commands
import (
"github.com/github/hub/v2/internal/assert"
"testing"
)
func TestParseRange(t *testing.T) {
s := "1.0..2.0"
assert.Equal(t, "1.0...2.0", parseCompareRange(s))
s = "1.0...2.0"
assert.Equal(t, "1.0...2.0", parseCompareRange(s))
}
================================================
FILE: commands/create.go
================================================
package commands
import (
"fmt"
"strings"
"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{
Run: create,
Usage: "create [-poc] [-d <DESCRIPTION>] [-h <HOMEPAGE>] [[<ORGANIZATION>/]<NAME>]",
Long: `Create a new repository on GitHub and add a git remote for it.
## Options:
-p, --private
Create a private repository.
-d, --description <DESCRIPTION>
A short description of the GitHub repository.
-h, --homepage <HOMEPAGE>
A URL with more information about the repository. Use this, for example, if
your project has an external website.
--remote-name <REMOTE>
Set the name for the new git remote (default: "origin").
-o, --browse
Open the new repository in a web browser.
-c, --copy
Put the URL of the new repository to clipboard instead of printing it.
[<ORGANIZATION>/]<NAME>
The name for the repository on GitHub (default: name of the current working
directory).
Optionally, create the repository within <ORGANIZATION>.
## Examples:
$ hub create
[ repo created on GitHub ]
> git remote add -f origin git@github.com:USER/REPO.git
$ hub create sinatra/recipes
[ repo created in GitHub organization ]
> git remote add -f origin git@github.com:sinatra/recipes.git
## See also:
hub-init(1), hub(1)
`,
}
func init() {
CmdRunner.Use(cmdCreate)
}
func create(command *Command, args *Args) {
_, err := git.Dir()
if err != nil {
err = fmt.Errorf("'create' must be run from inside a git repository")
utils.Check(err)
}
var newRepoName string
if args.IsParamsEmpty() {
dirName, err := git.WorkdirName()
utils.Check(err)
newRepoName = github.SanitizeProjectName(dirName)
} else {
newRepoName = args.FirstParam()
if newRepoName == "" {
utils.Check(command.UsageError(""))
}
}
config := github.CurrentConfig()
host, err := config.DefaultHost()
if err != nil {
utils.Check(github.FormatError("creating repository", err))
}
owner := host.User
if strings.Contains(newRepoName, "/") {
split := strings.SplitN(newRepoName, "/", 2)
owner = split[0]
newRepoName = split[1]
}
project := github.NewProject(owner, newRepoName, host.Host)
gh := github.NewClient(project.Host)
flagCreatePrivate := args.Flag.Bool("--private")
repo, err := gh.Repository(project)
if err == nil {
foundProject := github.NewProject(repo.FullName, "", project.Host)
if foundProject.SameAs(project) {
if !repo.Private && flagCreatePrivate {
err = fmt.Errorf("Repository '%s' already exists and is public", repo.FullName)
utils.Check(err)
} else {
ui.Errorln("Existing repository detected")
project = foundProject
}
} else {
repo = nil
}
} else {
repo = nil
}
if repo == nil {
if !args.Noop {
flagCreateDescription := args.Flag.Value("--description")
flagCreateHomepage := args.Flag.Value("--homepage")
repo, err := gh.CreateRepository(project, flagCreateDescription, flagCreateHomepage, flagCreatePrivate)
utils.Check(err)
project = github.NewProject(repo.FullName, "", project.Host)
}
}
localRepo, err := github.LocalRepo()
utils.Check(err)
originName := args.Flag.Value("--remote-name")
if originName == "" {
originName = "origin"
}
if originRemote, err := localRepo.RemoteByName(originName); err == nil {
originProject, err := originRemote.Project()
if err != nil || !originProject.SameAs(project) {
ui.Errorf("A git remote named '%s' already exists and is set to push to '%s'.\n", originRemote.Name, originRemote.PushURL)
}
} else {
url := project.GitURL("", "", true)
args.Before("git", "remote", "add", "-f", originName, url)
}
webURL := project.WebURL("", "", "")
args.NoForward()
flagCreateBrowse := args.Flag.Bool("--browse")
flagCreateCopy := args.Flag.Bool("--copy")
printBrowseOrCopy(args, webURL, flagCreateBrowse, flagCreateCopy)
}
================================================
FILE: commands/delete.go
================================================
package commands
import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/ui"
"github.com/github/hub/v2/utils"
)
var cmdDelete = &Command{
Run: deleteRepo,
Usage: "delete [-y] [<ORGANIZATION>/]<NAME>",
Long: `Delete an existing repository on GitHub.
## Options:
-y, --yes
Skip the confirmation prompt and immediately delete the repository.
[<ORGANIZATION>/]<NAME>
The name for the repository on GitHub.
## Examples:
$ hub delete recipes
[ personal repo deleted on GitHub ]
$ hub delete sinatra/recipes
[ repo deleted in GitHub organization ]
## See also:
hub-init(1), hub(1)
`,
}
func init() {
CmdRunner.Use(cmdDelete)
}
func deleteRepo(command *Command, args *Args) {
var repoName string
if !args.IsParamsEmpty() {
repoName = args.FirstParam()
}
re := regexp.MustCompile(NameWithOwnerRe)
if !re.MatchString(repoName) {
utils.Check(command.UsageError(""))
}
config := github.CurrentConfig()
host, err := config.DefaultHost()
if err != nil {
utils.Check(github.FormatError("deleting repository", err))
}
owner := host.User
if strings.Contains(repoName, "/") {
split := strings.SplitN(repoName, "/", 2)
owner, repoName = split[0], split[1]
}
project := github.NewProject(owner, repoName, host.Host)
gh := github.NewClient(project.Host)
if !args.Flag.Bool("--yes") {
ui.Printf("Really delete repository '%s' (yes/N)? ", project)
answer := ""
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
answer = strings.TrimSpace(scanner.Text())
}
utils.Check(scanner.Err())
if answer != "yes" {
utils.Check(fmt.Errorf("Please type 'yes' for confirmation."))
}
}
if args.Noop {
ui.Printf("Would delete repository '%s'.\n", project)
} else {
err = gh.DeleteRepository(project)
if err != nil && strings.Contains(err.Error(), "HTTP 403") {
ui.Errorf("Please edit the token used for hub at https://%s/settings/tokens\n", project.Host)
ui.Errorln("and verify that the `delete_repo` scope is enabled.")
}
utils.Check(err)
ui.Printf("Deleted repository '%s'.\n", project)
}
args.NoForward()
}
================================================
FILE: commands/fetch.go
================================================
package commands
import (
"fmt"
"regexp"
"strings"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/utils"
)
var cmdFetch = &Command{
Run: fetch,
GitExtension: true,
Usage: "fetch <USER>[,<USER2>...]",
Long: `Add missing remotes prior to performing git fetch.
## Examples:
$ hub fetch --multiple jingweno mislav
> git remote add jingweno git://github.com/jingweno/REPO.git
> git remote add mislav git://github.com/mislav/REPO.git
> git fetch jingweno
> git fetch mislav
## See also:
hub-remote(1), hub(1), git-fetch(1)
`,
}
func init() {
CmdRunner.Use(cmdFetch)
}
func fetch(command *Command, args *Args) {
if !args.IsParamsEmpty() {
err := transformFetchArgs(args)
utils.Check(err)
}
}
func transformFetchArgs(args *Args) error {
names := parseRemoteNames(args)
localRepo, err := github.LocalRepo()
utils.Check(err)
currentProject, currentProjectErr := localRepo.CurrentProject()
projects := make(map[*github.Project]bool)
ownerRegexp := regexp.MustCompile(fmt.Sprintf("^%s$", OwnerRe))
for _, name := range names {
if ownerRegexp.MatchString(name) && !isCloneable(name) {
_, err := localRepo.RemoteByName(name)
if err != nil {
utils.Check(currentProjectErr)
project := github.NewProject(name, currentProject.Name, "")
gh := github.NewClient(project.Host)
repo, err := gh.Repository(project)
if err != nil {
continue
}
projects[project] = repo.Private || repo.Permissions.Push
}
}
}
for project, private := range projects {
args.Before("git", "remote", "add", project.Owner, project.GitURL("", "", private))
}
return nil
}
func parseRemoteNames(args *Args) (names []string) {
words := args.Words()
if i := args.IndexOfParam("--multiple"); i != -1 {
if args.ParamsSize() > 1 {
names = words
}
} else if len(words) > 0 {
remoteName := words[0]
commaPattern := fmt.Sprintf("^%s(,%s)+$", OwnerRe, OwnerRe)
remoteNameRegexp := regexp.MustCompile(commaPattern)
if remoteNameRegexp.MatchString(remoteName) {
i := args.IndexOfParam(remoteName)
args.RemoveParam(i)
names = strings.Split(remoteName, ",")
args.InsertParam(i, names...)
args.InsertParam(i, "--multiple")
} else {
names = append(names, remoteName)
}
}
return
}
================================================
FILE: commands/fetch_test.go
================================================
package commands
import (
"github.com/github/hub/v2/internal/assert"
"testing"
)
func TestParseRemoteNames(t *testing.T) {
args := NewArgs([]string{"fetch", "jingweno,foo"})
names := parseRemoteNames(args)
assert.Equal(t, 2, len(names))
assert.Equal(t, "jingweno", names[0])
assert.Equal(t, "foo", names[1])
cmd := args.ToCmd()
assert.Equal(t, "git fetch --multiple jingweno foo", cmd.String())
args = NewArgs([]string{"fetch", "--multiple", "jingweno", "foo"})
names = parseRemoteNames(args)
assert.Equal(t, 2, len(names))
assert.Equal(t, "jingweno", names[0])
assert.Equal(t, "foo", names[1])
args = NewArgs([]string{"fetch", "--multiple"})
names = parseRemoteNames(args)
assert.Equal(t, 0, len(names))
}
================================================
FILE: commands/fork.go
================================================
package commands
import (
"fmt"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/ui"
"github.com/github/hub/v2/utils"
)
var cmdFork = &Command{
Run: fork,
Usage: "fork [--no-remote] [--remote-name <REMOTE>] [--org <ORGANIZATION>]",
Long: `Fork the current repository on GitHub and add a git remote for it.
## Options:
--no-remote
Skip adding a git remote for the fork.
--remote-name <REMOTE>
Set the name for the new git remote.
--org <ORGANIZATION>
Fork the repository within this organization.
## Examples:
$ hub fork
[ repo forked on GitHub ]
> git remote add -f USER git@github.com:USER/REPO.git
$ hub fork --org=ORGANIZATION
[ repo forked on GitHub into the ORGANIZATION organization]
> git remote add -f ORGANIZATION git@github.com:ORGANIZATION/REPO.git
## See also:
hub-clone(1), hub(1)
`,
}
func init() {
CmdRunner.Use(cmdFork)
}
func fork(cmd *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
config := github.CurrentConfig()
host, err := config.PromptForHost(project.Host)
utils.Check(github.FormatError("forking repository", err))
params := map[string]interface{}{}
forkOwner := host.User
if flagForkOrganization := args.Flag.Value("--org"); flagForkOrganization != "" {
forkOwner = flagForkOrganization
params["organization"] = forkOwner
}
forkProject := github.NewProject(forkOwner, project.Name, project.Host)
var newRemoteName string
if flagForkRemoteName := args.Flag.Value("--remote-name"); flagForkRemoteName != "" {
newRemoteName = flagForkRemoteName
} else {
newRemoteName = forkProject.Owner
}
client := github.NewClient(project.Host)
existingRepo, err := client.Repository(forkProject)
if err == nil {
existingProject, err := github.NewProjectFromRepo(existingRepo)
if err == nil && !existingProject.SameAs(forkProject) {
existingRepo = nil
}
}
if err == nil && existingRepo != nil {
var parentURL *github.URL
if parent := existingRepo.Parent; parent != nil {
parentURL, _ = github.ParseURL(parent.HTMLURL)
}
if parentURL == nil || !project.SameAs(parentURL.Project) {
err = fmt.Errorf("Error creating fork: %s already exists on %s",
forkProject, forkProject.Host)
utils.Check(err)
}
} else {
if !args.Noop {
newRepo, err := client.ForkRepository(project, params)
utils.Check(err)
forkProject.Owner = newRepo.Owner.Login
forkProject.Name = newRepo.Name
}
}
args.NoForward()
if !args.Flag.Bool("--no-remote") {
originURL := project.GitURL("", "", false)
url := forkProject.GitURL("", "", true)
// Check to see if the remote already exists.
currentRemote, err := localRepo.RemoteByName(newRemoteName)
if err == nil {
currentProject, err := currentRemote.Project()
if err == nil {
if currentProject.SameAs(forkProject) {
ui.Printf("existing remote: %s\n", newRemoteName)
return
}
if newRemoteName == "origin" {
// Assume user wants to follow github guides for collaboration
ui.Printf("renaming existing \"origin\" remote to \"upstream\"\n")
args.Before("git", "remote", "rename", "origin", "upstream")
}
}
}
args.Before("git", "remote", "add", "-f", newRemoteName, originURL)
args.Before("git", "remote", "set-url", newRemoteName, url)
args.AfterFn(func() error {
ui.Printf("new remote: %s\n", newRemoteName)
return nil
})
}
}
================================================
FILE: commands/gist.go
================================================
package commands
import (
"fmt"
"sort"
"strings"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/ui"
"github.com/github/hub/v2/utils"
)
var (
cmdGist = &Command{
Run: printGistHelp,
Usage: `
gist create [-oc] [--public] [<FILES>...]
gist show <ID> [<FILENAME>]
`,
Long: `Create and print GitHub Gists
## Commands:
* _create_:
Create a new gist. If no <FILES> are specified, the content is read from
standard input.
* _show_:
Print the contents of a gist. If the gist contains multiple files, the
operation will error out unless <FILENAME> is specified.
## Options:
--public
Make the new gist public (default: false).
-o, --browse
Open the new gist in a web browser.
-c, --copy
Put the URL of the new gist to clipboard instead of printing it.
## Examples:
$ echo hello | hub gist create --public
$ hub gist create file1.txt file2.txt
# print a specific file within a gist:
$ hub gist show ID testfile1.txt
## See also:
hub(1), hub-api(1)
`,
}
cmdShowGist = &Command{
Key: "show",
Run: showGist,
}
cmdCreateGist = &Command{
Key: "create",
Run: createGist,
KnownFlags: `
--public
-o, --browse
-c, --copy
`,
}
)
func init() {
cmdGist.Use(cmdShowGist)
cmdGist.Use(cmdCreateGist)
CmdRunner.Use(cmdGist)
}
func getGist(gh *github.Client, id string, filename string) error {
gist, err := gh.FetchGist(id)
if err != nil {
return err
}
if len(gist.Files) > 1 && filename == "" {
filenames := []string{}
for name := range gist.Files {
filenames = append(filenames, name)
}
sort.Strings(filenames)
return fmt.Errorf("This gist contains multiple files, you must specify one:\n %s", strings.Join(filenames, "\n "))
}
if filename != "" {
if val, ok := gist.Files[filename]; ok {
ui.Println(val.Content)
} else {
return fmt.Errorf("no such file in gist")
}
} else {
for name := range gist.Files {
file := gist.Files[name]
ui.Println(file.Content)
}
}
return nil
}
func printGistHelp(command *Command, args *Args) {
utils.Check(command.UsageError(""))
}
func createGist(cmd *Command, args *Args) {
args.NoForward()
host, err := github.CurrentConfig().DefaultHostNoPrompt()
utils.Check(err)
gh := github.NewClient(host.Host)
filenames := []string{}
if args.IsParamsEmpty() {
filenames = append(filenames, "-")
} else {
filenames = args.Params
}
var gist *github.Gist
if args.Noop {
ui.Println("Would create gist")
gist = &github.Gist{
HTMLURL: fmt.Sprintf("https://gist.%s/%s", gh.Host.Host, "ID"),
}
} else {
gist, err = gh.CreateGist(filenames, args.Flag.Bool("--public"))
utils.Check(err)
}
flagIssueBrowse := args.Flag.Bool("--browse")
flagIssueCopy := args.Flag.Bool("--copy")
printBrowseOrCopy(args, gist.HTMLURL, flagIssueBrowse, flagIssueCopy)
}
func showGist(cmd *Command, args *Args) {
args.NoForward()
if args.ParamsSize() < 1 {
utils.Check(cmd.UsageError("you must specify a gist ID"))
}
host, err := github.CurrentConfig().DefaultHostNoPrompt()
utils.Check(err)
gh := github.NewClient(host.Host)
id := args.GetParam(0)
filename := ""
if args.ParamsSize() > 1 {
filename = args.GetParam(1)
}
err = getGist(gh, id, filename)
utils.Check(err)
}
================================================
FILE: commands/help.go
================================================
package commands
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"github.com/github/hub/v2/git"
"github.com/github/hub/v2/ui"
"github.com/github/hub/v2/utils"
"github.com/kballard/go-shellquote"
)
var cmdHelp = &Command{
Run: runHelp,
GitExtension: true,
Usage: `
help hub
help <COMMAND>
help hub-<COMMAND> [--plain-text]
`,
Long: `Show the help page for a command.
## Options:
hub-<COMMAND>
Use this format to view help for hub extensions to an existing git command.
--plain-text
Skip man page lookup mechanism and display raw help text.
## See also:
hub(1), git-help(1)
`,
}
var cmdListCmds = &Command{
Key: "--list-cmds",
Run: runListCmds,
GitExtension: true,
}
func init() {
CmdRunner.Use(cmdHelp, "--help")
CmdRunner.Use(cmdListCmds)
}
func runHelp(helpCmd *Command, args *Args) {
if args.IsParamsEmpty() {
args.AfterFn(func() error {
ui.Println(helpText)
return nil
})
return
}
p := utils.NewArgsParser()
p.RegisterBool("--all", "-a")
p.RegisterBool("--plain-text")
p.RegisterBool("--man", "-m")
p.RegisterBool("--web", "-w")
p.Parse(args.Params)
if p.Bool("--all") {
args.AfterFn(func() error {
ui.Printf("\nhub custom commands\n\n %s\n", strings.Join(customCommands(), " "))
return nil
})
return
}
isWeb := func() bool {
if p.Bool("--web") {
return true
}
if p.Bool("--man") {
return false
}
if f, err := git.Config("help.format"); err == nil {
return f == "web" || f == "html"
}
return false
}
cmdName := ""
if words := args.Words(); len(words) > 0 {
cmdName = words[0]
}
if cmdName == "hub" {
err := displayManPage("hub", args, isWeb())
utils.Check(err)
return
}
foundCmd := lookupCmd(cmdName)
if foundCmd == nil {
return
}
if p.Bool("--plain-text") {
ui.Println(foundCmd.HelpText())
os.Exit(0)
}
manPage := fmt.Sprintf("hub-%s", foundCmd.Name())
err := displayManPage(manPage, args, isWeb())
utils.Check(err)
}
func runListCmds(cmd *Command, args *Args) {
listOthers := false
parts := strings.SplitN(args.Command, "=", 2)
for _, kind := range strings.Split(parts[1], ",") {
if kind == "others" {
listOthers = true
break
}
}
if listOthers {
args.AfterFn(func() error {
ui.Println(strings.Join(customCommands(), "\n"))
return nil
})
}
}
// On systems where `man` was found, invoke:
// MANPATH={PREFIX}/share/man:$MANPATH man <page>
//
// otherwise:
// less -R {PREFIX}/share/man/man1/<page>.1.txt
func displayManPage(manPage string, args *Args, isWeb bool) error {
programPath, err := utils.CommandPath(args.ProgramPath)
if err != nil {
return err
}
if isWeb {
manPage += ".1.html"
manFile := filepath.Join(programPath, "..", "..", "share", "doc", "hub-doc", manPage)
args.Replace(args.Executable, "web--browse", manFile)
return nil
}
var manArgs []string
manProgram, _ := utils.CommandPath("man")
if manProgram != "" {
manArgs = []string{manProgram}
} else {
manPage += ".1.txt"
if manProgram = os.Getenv("PAGER"); manProgram != "" {
var err error
manArgs, err = shellquote.Split(manProgram)
if err != nil {
return err
}
} else {
manArgs = []string{"less", "-R"}
}
}
env := os.Environ()
if strings.HasSuffix(manPage, ".txt") {
manFile := filepath.Join(programPath, "..", "..", "share", "man", "man1", manPage)
manArgs = append(manArgs, manFile)
} else {
manArgs = append(manArgs, manPage)
manPath := filepath.Join(programPath, "..", "..", "share", "man")
env = append(env, fmt.Sprintf("MANPATH=%s:%s", manPath, os.Getenv("MANPATH")))
}
c := exec.Command(manArgs[0], manArgs[1:]...)
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
c.Env = env
if err := c.Run(); err != nil {
return err
}
os.Exit(0)
return nil
}
func lookupCmd(name string) *Command {
if strings.HasPrefix(name, "hub-") {
return CmdRunner.Lookup(strings.TrimPrefix(name, "hub-"))
}
cmd := CmdRunner.Lookup(name)
if cmd != nil && !cmd.GitExtension {
return cmd
}
return nil
}
func customCommands() []string {
cmds := []string{}
for n, c := range CmdRunner.All() {
if !c.GitExtension && !strings.HasPrefix(n, "--") {
cmds = append(cmds, n)
}
}
sort.Strings(cmds)
return cmds
}
var helpText = `
These GitHub commands are provided by hub:
api Low-level GitHub API request interface
browse Open a GitHub page in the default browser
ci-status Show the status of GitHub checks for a commit
compare Open a compare page on GitHub
create Create this repository on GitHub and add GitHub as origin
delete Delete a repository on GitHub
fork Make a fork of a remote repository on GitHub and add as remote
gist Make a gist
issue List or create GitHub issues
pr Manage GitHub pull requests
pull-request Open a pull request on GitHub
release List or create GitHub releases
sync Fetch git objects from upstream and update branches
`
================================================
FILE: commands/init.go
================================================
package commands
import (
"path/filepath"
"regexp"
"strings"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/utils"
)
var cmdInit = &Command{
Run: gitInit,
GitExtension: true,
Usage: "init -g",
Long: `Initialize a git repository and add a remote pointing to GitHub.
## Options:
-g
After initializing the repository locally, add the "origin" remote pointing
to "<USER>/<REPO>" repository on GitHub.
<USER> is your GitHub username, while <REPO> is the name of the current
working directory.
## Examples:
$ hub init -g
> git init
> git remote add origin git@github.com:USER/REPO.git
## See also:
hub-create(1), hub(1), git-init(1)
`,
}
func init() {
CmdRunner.Use(cmdInit)
}
func gitInit(command *Command, args *Args) {
err := transformInitArgs(args)
utils.Check(err)
}
func transformInitArgs(args *Args) error {
if !parseInitFlag(args) {
return nil
}
var err error
dirToInit := "."
hasValueRegexp := regexp.MustCompile("^--(template|separate-git-dir|shared)$")
// Find the first argument that isn't related to any of the init flags.
// We assume this is the optional `directory` argument to git init.
for i := 0; i < args.ParamsSize(); i++ {
arg := args.Params[i]
if hasValueRegexp.MatchString(arg) {
i++
} else if !strings.HasPrefix(arg, "-") {
dirToInit = arg
break
}
}
dirToInit, err = filepath.Abs(dirToInit)
if err != nil {
return err
}
config := github.CurrentConfig()
host, err := config.DefaultHost()
if err != nil {
utils.Check(github.FormatError("initializing repository", err))
}
// Assume that the name of the working directory is going to be the name of
// the project on GitHub.
projectName := strings.Replace(filepath.Base(dirToInit), " ", "-", -1)
project := github.NewProject(host.User, projectName, host.Host)
url := project.GitURL("", "", true)
addRemote := []string{
"git", "--git-dir", filepath.Join(dirToInit, ".git"),
"remote", "add", "origin", url,
}
args.After(addRemote...)
return nil
}
func parseInitFlag(args *Args) bool {
if i := args.IndexOfParam("-g"); i != -1 {
args.RemoveParam(i)
return true
}
return false
}
================================================
FILE: commands/init_test.go
================================================
package commands
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/internal/assert"
)
func setupInitContext() {
os.Setenv("HUB_PROTOCOL", "git")
os.Setenv("HUB_CONFIG", "")
github.CreateTestConfigs("jingweno", "123")
}
func TestEmptyParams(t *testing.T) {
setupInitContext()
args := NewArgs([]string{"init"})
err := transformInitArgs(args)
assert.Equal(t, nil, err)
assert.Equal(t, true, args.IsParamsEmpty())
}
func TestFlagToAddRemote(t *testing.T) {
setupInitContext()
args := NewArgs([]string{"init", "-g", "--quiet"})
err := transformInitArgs(args)
assert.Equal(t, nil, err)
commands := args.Commands()
assert.Equal(t, 2, len(commands))
assert.Equal(t, "git init --quiet", 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",
filepath.Join(currentDir, ".git"),
filepath.Base(currentDir),
)
assert.Equal(t, expected, commands[1].String())
}
func TestInitInAnotherDir(t *testing.T) {
setupInitContext()
args := NewArgs([]string{"init", "-g", "--template", "mytpl", "--shared=umask", "my project"})
err := transformInitArgs(args)
assert.Equal(t, nil, err)
commands := args.Commands()
assert.Equal(t, 2, len(commands))
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",
filepath.Join(currentDir, "my project", ".git"),
"my-project",
)
assert.Equal(t, expected, commands[1].String())
}
func TestSeparateGitDir(t *testing.T) {
setupInitContext()
args := NewArgs([]string{"init", "-g", "--separate-git-dir", "/tmp/where-i-play.git", "my/playground"})
err := transformInitArgs(args)
assert.Equal(t, nil, err)
commands := args.Commands()
assert.Equal(t, 2, len(commands))
assert.Equal(t, "git init --separate-git-dir /tmp/where-i-play.git my/playground", 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",
filepath.Join(currentDir, "my", "playground", ".git"),
"playground",
)
assert.Equal(t, expected, commands[1].String())
}
================================================
FILE: commands/issue.go
================================================
package commands
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"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 (
cmdIssue = &Command{
Run: listIssues,
Usage: `
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.
## Commands:
With no arguments, show a list of open issues.
* _show_:
Show an existing issue specified by <NUMBER>.
* _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>.
-a, --assign <USERS>
A comma-separated list of GitHub handles to assign to the created issue.
-c, --creator <CREATOR>
Display only issues created by <CREATOR>.
-@, --mentioned <USER>
Display only issues mentioning <USER>.
-s, --state <STATE>
Display issues with state <STATE> (default: "open").
-f, --format <FORMAT>
Pretty print the contents of the issues using format <FORMAT> (default:
"%sC%>(8)%i%Creset %t% l%n"). 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:
%I: issue number
%i: issue number prefixed with "#"
%U: the URL of this issue
%S: state (i.e. "open", "closed")
%sC: set color to red or green, depending on issue state.
%t: title
%l: colored labels
%L: raw, comma-separated labels
%b: body
%au: login name of author
%as: comma-separated list of assignees
%Mn: milestone number
%Mt: milestone title
%NC: number of comments
%Nc: number of comments wrapped in parentheses, or blank string if zero.
%cD: created date-only (no time of day)
%cr: created date, relative
%ct: created date, UNIX timestamp
%cI: created date, ISO 8601 format
%uD: updated date-only (no time of day)
%ur: updated date, relative
%ut: updated date, UNIX timestamp
%uI: updated date, ISO 8601 format
%n: newline
%%: a literal %
--color[=<WHEN>]
Enable colored output even if stdout is not a terminal. <WHEN> can be one
of "always" (default for ''--color''), "never", or "auto" (default).
-m, --message <MESSAGE>
The text up to the first blank line in <MESSAGE> is treated as the issue
title, and the rest is used as issue description in Markdown format.
When multiple ''--message'' are passed, their values are concatenated with a
blank line in-between.
When neither ''--message'' nor ''--file'' were supplied to ''issue create'', a
text editor will open to author the title and description in.
-F, --file <FILE>
Read the issue title and description from <FILE>. Pass "-" to read from
standard input instead. See ''--message'' for the formatting rules.
-e, --edit
Open the issue title and description in a text editor before submitting.
This can be used in combination with ''--message'' or ''--file''.
-o, --browse
Open the new issue in a web browser.
-c, --copy
Put the URL of the new issue to clipboard instead of printing it.
-M, --milestone <NAME>
Display only issues for a GitHub milestone with the name <NAME>.
When opening an issue, add this issue to a GitHub milestone with the name <NAME>.
Passing the milestone number is deprecated.
-l, --labels <LABELS>
Display only issues with certain labels.
When opening an issue, add a comma-separated list of labels to this issue.
-d, --since <DATE>
Display only issues updated on or after <DATE> in ISO 8601 format.
-o, --sort <KEY>
Sort displayed issues by "created" (default), "updated" or "comments".
-^ --sort-ascending
Sort by ascending dates instead of descending.
-L, --limit <LIMIT>
Display only the first <LIMIT> issues.
--include-pulls
Include pull requests as well as issues.
--color
Enable colored output for labels list.
## See also:
hub-pr(1), hub(1)
`,
KnownFlags: `
-a, --assignee USER
-s, --state STATE
-f, --format FMT
-M, --milestone NAME
-c, --creator USER
-@, --mentioned USER
-l, --labels LIST
-d, --since DATE
-o, --sort KEY
-^, --sort-ascending
--include-pulls
-L, --limit N
--color
`,
}
cmdCreateIssue = &Command{
Key: "create",
Run: createIssue,
KnownFlags: `
-m, --message MSG
-F, --file FILE
-M, --milestone NAME
-l, --labels LIST
-a, --assign USER
-o, --browse
-c, --copy
-e, --edit
`,
}
cmdShowIssue = &Command{
Key: "show",
Run: showIssue,
KnownFlags: `
-f, --format FMT
--color
`,
}
cmdLabel = &Command{
Key: "labels",
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
`,
}
)
func init() {
cmdIssue.Use(cmdShowIssue)
cmdIssue.Use(cmdCreateIssue)
cmdIssue.Use(cmdLabel)
cmdIssue.Use(cmdTransfer)
cmdIssue.Use(cmdUpdate)
CmdRunner.Use(cmdIssue)
}
func listIssues(cmd *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
gh := github.NewClient(project.Host)
if args.Noop {
ui.Printf("Would request list of issues for %s\n", project)
} else {
filters := map[string]interface{}{}
if args.Flag.HasReceived("--state") {
filters["state"] = args.Flag.Value("--state")
}
if args.Flag.HasReceived("--assignee") {
filters["assignee"] = args.Flag.Value("--assignee")
}
if args.Flag.HasReceived("--milestone") {
milestoneValue := args.Flag.Value("--milestone")
if milestoneValue == "none" {
filters["milestone"] = milestoneValue
} else {
milestoneNumber, err := milestoneValueToNumber(milestoneValue, gh, project)
utils.Check(err)
if milestoneNumber > 0 {
filters["milestone"] = milestoneNumber
}
}
}
if args.Flag.HasReceived("--creator") {
filters["creator"] = args.Flag.Value("--creator")
}
if args.Flag.HasReceived("--mentioned") {
filters["mentioned"] = args.Flag.Value("--mentioned")
}
if args.Flag.HasReceived("--labels") {
labels := commaSeparated(args.Flag.AllValues("--labels"))
filters["labels"] = strings.Join(labels, ",")
}
if args.Flag.HasReceived("--sort") {
filters["sort"] = args.Flag.Value("--sort")
}
if args.Flag.Bool("--sort-ascending") {
filters["direction"] = "asc"
} else {
filters["direction"] = "desc"
}
if args.Flag.HasReceived("--since") {
flagIssueSince := args.Flag.Value("--since")
if sinceTime, err := time.ParseInLocation("2006-01-02", flagIssueSince, time.Local); err == nil {
filters["since"] = sinceTime.Format(time.RFC3339)
} else {
filters["since"] = flagIssueSince
}
}
flagIssueLimit := args.Flag.Int("--limit")
flagIssueIncludePulls := args.Flag.Bool("--include-pulls")
flagIssueFormat := "%sC%>(8)%i%Creset %t% l%n"
if args.Flag.HasReceived("--format") {
flagIssueFormat = args.Flag.Value("--format")
}
issues, err := gh.FetchIssues(project, filters, flagIssueLimit, func(issue *github.Issue) bool {
return issue.PullRequest == nil || flagIssueIncludePulls
})
utils.Check(err)
maxNumWidth := 0
for _, issue := range issues {
if numWidth := len(strconv.Itoa(issue.Number)); numWidth > maxNumWidth {
maxNumWidth = numWidth
}
}
colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color"))
for _, issue := range issues {
ui.Print(formatIssue(issue, flagIssueFormat, colorize))
}
}
args.NoForward()
}
func formatIssuePlaceholders(issue github.Issue, colorize bool) map[string]string {
var stateColorSwitch string
if colorize {
issueColor := 32
if issue.State == "closed" {
issueColor = 31
}
stateColorSwitch = fmt.Sprintf("\033[%dm", issueColor)
}
var labelStrings []string
var rawLabels []string
for _, label := range issue.Labels {
if colorize {
color, err := utils.NewColor(label.Color)
utils.Check(err)
labelStrings = append(labelStrings, colorizeLabel(label, color))
} else {
labelStrings = append(labelStrings, fmt.Sprintf(" %s ", label.Name))
}
rawLabels = append(rawLabels, label.Name)
}
var assignees []string
for _, assignee := range issue.Assignees {
assignees = append(assignees, assignee.Login)
}
var milestoneNumber, milestoneTitle string
if issue.Milestone != nil {
milestoneNumber = fmt.Sprintf("%d", issue.Milestone.Number)
milestoneTitle = issue.Milestone.Title
}
var numCommentsWrapped string
numComments := fmt.Sprintf("%d", issue.Comments)
if issue.Comments > 0 {
numCommentsWrapped = fmt.Sprintf("(%d)", issue.Comments)
}
var createdDate, createdAtISO8601, createdAtUnix, createdAtRelative,
updatedDate, updatedAtISO8601, updatedAtUnix, updatedAtRelative string
if !issue.CreatedAt.IsZero() {
createdDate = issue.CreatedAt.Format("02 Jan 2006")
createdAtISO8601 = issue.CreatedAt.Format(time.RFC3339)
createdAtUnix = fmt.Sprintf("%d", issue.CreatedAt.Unix())
createdAtRelative = utils.TimeAgo(issue.CreatedAt)
}
if !issue.UpdatedAt.IsZero() {
updatedDate = issue.UpdatedAt.Format("02 Jan 2006")
updatedAtISO8601 = issue.UpdatedAt.Format(time.RFC3339)
updatedAtUnix = fmt.Sprintf("%d", issue.UpdatedAt.Unix())
updatedAtRelative = utils.TimeAgo(issue.UpdatedAt)
}
return map[string]string{
"I": fmt.Sprintf("%d", issue.Number),
"i": fmt.Sprintf("#%d", issue.Number),
"U": issue.HTMLURL,
"S": issue.State,
"sC": stateColorSwitch,
"t": issue.Title,
"l": strings.Join(labelStrings, " "),
"L": strings.Join(rawLabels, ", "),
"b": issue.Body,
"au": issue.User.Login,
"as": strings.Join(assignees, ", "),
"Mn": milestoneNumber,
"Mt": milestoneTitle,
"NC": numComments,
"Nc": numCommentsWrapped,
"cD": createdDate,
"cI": createdAtISO8601,
"ct": createdAtUnix,
"cr": createdAtRelative,
"uD": updatedDate,
"uI": updatedAtISO8601,
"ut": updatedAtUnix,
"ur": updatedAtRelative,
}
}
func formatPullRequestPlaceholders(pr github.PullRequest, colorize bool) map[string]string {
prState := pr.State
if prState == "open" && pr.Draft {
prState = "draft"
} else if !pr.MergedAt.IsZero() {
prState = "merged"
}
var stateColorSwitch string
var prColor int
if colorize {
switch prState {
case "draft":
prColor = 37
case "merged":
prColor = 35
case "closed":
prColor = 31
default:
prColor = 32
}
stateColorSwitch = fmt.Sprintf("\033[%dm", prColor)
}
base := pr.Base.Ref
head := pr.Head.Label
if pr.IsSameRepo() {
head = pr.Head.Ref
}
var requestedReviewers []string
for _, requestedReviewer := range pr.RequestedReviewers {
requestedReviewers = append(requestedReviewers, requestedReviewer.Login)
}
for _, requestedTeam := range pr.RequestedTeams {
teamSlug := fmt.Sprintf("%s/%s", pr.Base.Repo.Owner.Login, requestedTeam.Slug)
requestedReviewers = append(requestedReviewers, teamSlug)
}
var mergedDate, mergedAtISO8601, mergedAtUnix, mergedAtRelative string
if !pr.MergedAt.IsZero() {
mergedDate = pr.MergedAt.Format("02 Jan 2006")
mergedAtISO8601 = pr.MergedAt.Format(time.RFC3339)
mergedAtUnix = fmt.Sprintf("%d", pr.MergedAt.Unix())
mergedAtRelative = utils.TimeAgo(pr.MergedAt)
}
return map[string]string{
"pS": prState,
"pC": stateColorSwitch,
"B": base,
"H": head,
"sB": pr.Base.Sha,
"sH": pr.Head.Sha,
"sm": pr.MergeCommitSha,
"rs": strings.Join(requestedReviewers, ", "),
"mD": mergedDate,
"mI": mergedAtISO8601,
"mt": mergedAtUnix,
"mr": mergedAtRelative,
}
}
func formatIssue(issue github.Issue, format string, colorize bool) string {
placeholders := formatIssuePlaceholders(issue, colorize)
return ui.Expand(format, placeholders, colorize)
}
func showIssue(cmd *Command, args *Args) {
issueNumber := ""
if args.ParamsSize() > 0 {
issueNumber = args.GetParam(0)
}
if issueNumber == "" {
utils.Check(cmd.UsageError(""))
}
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
gh := github.NewClient(project.Host)
var issue = &github.Issue{}
issue, err = gh.FetchIssue(project, issueNumber)
utils.Check(err)
args.NoForward()
colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color"))
if args.Flag.HasReceived("--format") {
flagShowIssueFormat := args.Flag.Value("--format")
ui.Print(formatIssue(*issue, flagShowIssueFormat, colorize))
return
}
var closed = ""
if issue.State != "open" {
closed = "[CLOSED] "
}
commentsList, err := gh.FetchComments(project, issueNumber)
utils.Check(err)
ui.Printf("# %s%s\n\n", closed, issue.Title)
ui.Printf("* created by @%s on %s\n", issue.User.Login, issue.CreatedAt.String())
if len(issue.Assignees) > 0 {
var assignees []string
for _, user := range issue.Assignees {
assignees = append(assignees, user.Login)
}
ui.Printf("* assignees: %s\n", strings.Join(assignees, ", "))
}
ui.Printf("\n%s\n", issue.Body)
if issue.Comments > 0 {
ui.Printf("\n## Comments:\n")
for _, comment := range commentsList {
ui.Printf("\n### comment by @%s on %s\n\n%s\n", comment.User.Login, comment.CreatedAt.String(), comment.Body)
}
}
}
func createIssue(cmd *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
gh := github.NewClient(project.Host)
messageBuilder := &github.MessageBuilder{
Filename: "ISSUE_EDITMSG",
Title: "issue",
}
messageBuilder.AddCommentedSection(fmt.Sprintf(`Creating an issue for %s
Write a message for this issue. The first block of
text is the title and the rest is the description.`, project))
flagIssueEdit := args.Flag.Bool("--edit")
flagIssueMessage := args.Flag.AllValues("--message")
if len(flagIssueMessage) > 0 {
messageBuilder.Message = strings.Join(flagIssueMessage, "\n\n")
messageBuilder.Edit = flagIssueEdit
} else if args.Flag.HasReceived("--file") {
messageBuilder.Message, err = msgFromFile(args.Flag.Value("--file"))
utils.Check(err)
messageBuilder.Edit = flagIssueEdit
} else {
messageBuilder.Edit = true
workdir, _ := git.WorkdirName()
if workdir != "" {
template, err := github.ReadTemplate(github.IssueTemplate, workdir)
utils.Check(err)
if template != "" {
messageBuilder.Message = template
}
}
}
title, body, err := messageBuilder.Extract()
utils.Check(err)
if title == "" {
utils.Check(fmt.Errorf("Aborting creation due to empty issue title"))
}
params := map[string]interface{}{
"title": title,
"body": body,
}
setLabelsFromArgs(params, args)
setAssigneesFromArgs(params, args)
setMilestoneFromArgs(params, args, gh, project)
args.NoForward()
if args.Noop {
ui.Printf("Would create issue `%s' for %s\n", params["title"], project)
} else {
issue, err := gh.CreateIssue(project, params)
utils.Check(err)
flagIssueBrowse := args.Flag.Bool("--browse")
flagIssueCopy := args.Flag.Bool("--copy")
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)
project, err := localRepo.MainProject()
utils.Check(err)
gh := github.NewClient(project.Host)
args.NoForward()
if args.Noop {
ui.Printf("Would request list of labels for %s\n", project)
return
}
labels, err := gh.FetchLabels(project)
utils.Check(err)
flagLabelsColorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color"))
for _, label := range labels {
ui.Print(formatLabel(label, flagLabelsColorize))
}
}
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")
switch colorConfig {
case "false", "never":
return false
case "always":
return true
}
return ui.IsTerminal(os.Stdout)
} else if when == "never" {
return false
} else {
return true // "always"
}
}
func formatLabel(label github.IssueLabel, colorize bool) string {
if colorize {
if color, err := utils.NewColor(label.Color); err == nil {
return fmt.Sprintf("%s\n", colorizeLabel(label, color))
}
}
return fmt.Sprintf("%s\n", label.Name)
}
func colorizeLabel(label github.IssueLabel, color *utils.Color) string {
bgColorCode := utils.RgbToTermColorCode(color)
fgColor := pickHighContrastTextColor(color)
fgColorCode := utils.RgbToTermColorCode(fgColor)
return fmt.Sprintf("\033[38;%s;48;%sm %s \033[m",
fgColorCode, bgColorCode, label.Name)
}
type contrastCandidate struct {
color *utils.Color
contrast float64
}
func pickHighContrastTextColor(color *utils.Color) *utils.Color {
candidates := []contrastCandidate{}
appendCandidate := func(c *utils.Color) {
candidates = append(candidates, contrastCandidate{
color: c,
contrast: color.ContrastRatio(c),
})
}
appendCandidate(utils.White)
appendCandidate(utils.Black)
for _, candidate := range candidates {
if candidate.contrast >= 7.0 {
return candidate.color
}
}
for _, candidate := range candidates {
if candidate.contrast >= 4.5 {
return candidate.color
}
}
return utils.Black
}
func milestoneValueToNumber(value string, client *github.Client, project *github.Project) (int, error) {
if value == "" {
return 0, nil
}
if milestoneNumber, err := strconv.Atoi(value); err == nil {
return milestoneNumber, nil
}
milestones, err := client.FetchMilestones(project)
if err != nil {
return 0, err
}
for _, milestone := range milestones {
if strings.EqualFold(milestone.Title, value) {
return milestone.Number, nil
}
}
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()
}
================================================
FILE: commands/issue_test.go
================================================
package commands
import (
"testing"
"time"
"github.com/github/hub/v2/github"
)
type formatIssueTest struct {
name string
issue github.Issue
format string
colorize bool
expect string
}
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)
}
}
}
func TestFormatIssue(t *testing.T) {
format := "%sC%>(8)%i%Creset %t% l%n"
testFormatIssue(t, []formatIssueTest{
{
name: "standard usage",
issue: github.Issue{
Number: 42,
Title: "Just an Issue",
State: "open",
User: &github.User{Login: "pcorpet"},
Body: "Body of the\nissue",
Assignees: []github.User{{Login: "mislav"}},
},
format: format,
colorize: true,
expect: "\033[32m #42\033[m Just an Issue\n",
},
{
name: "closed issue colored differently",
issue: github.Issue{
Number: 42,
Title: "Just an Issue",
State: "closed",
User: &github.User{Login: "octocat"},
},
format: format,
colorize: true,
expect: "\033[31m #42\033[m Just an Issue\n",
},
{
name: "labels",
issue: github.Issue{
Number: 42,
Title: "An issue with labels",
State: "open",
User: &github.User{Login: "octocat"},
Labels: []github.IssueLabel{
{Name: "bug", Color: "800000"},
{Name: "reproduced", Color: "55ff55"},
},
},
format: format,
colorize: true,
expect: "\033[32m #42\033[m An issue with labels \033[38;2;255;255;255;48;2;128;0;0m bug \033[m \033[38;2;0;0;0;48;2;85;255;85m reproduced \033[m\n",
},
{
name: "not colorized",
issue: github.Issue{
Number: 42,
Title: "Just an Issue",
State: "open",
User: &github.User{Login: "octocat"},
},
format: format,
colorize: false,
expect: " #42 Just an Issue\n",
},
{
name: "labels not colorized",
issue: github.Issue{
Number: 42,
Title: "An issue with labels",
State: "open",
User: &github.User{Login: "octocat"},
Labels: []github.IssueLabel{
{Name: "bug", Color: "880000"},
{Name: "reproduced", Color: "55ff55"},
},
},
format: format,
colorize: false,
expect: " #42 An issue with labels bug reproduced \n",
},
})
}
func TestFormatIssue_customFormatString(t *testing.T) {
createdAt, err := time.Parse(time.RFC822Z, "16 Mar 15 12:34 +0000")
if err != nil {
t.Fatal(err)
}
updatedAt, err := time.Parse(time.RFC822Z, "17 Mar 15 12:34 +0900")
if err != nil {
t.Fatal(err)
}
issue := github.Issue{
Number: 42,
Title: "Just an Issue",
State: "open",
User: &github.User{Login: "pcorpet"},
Body: "Body of the\nissue",
Assignees: []github.User{
{Login: "mislav"},
{Login: "josh"},
},
Labels: []github.IssueLabel{
{Name: "bug", Color: "880000"},
{Name: "feature", Color: "008800"},
},
HTMLURL: "the://url",
Comments: 12,
Milestone: &github.Milestone{
Number: 31,
Title: "2.2-stable",
},
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
testFormatIssue(t, []formatIssueTest{
{
name: "number",
issue: issue,
format: "%I",
colorize: true,
expect: "42",
},
{
name: "hashed number",
issue: issue,
format: "%i",
colorize: true,
expect: "#42",
},
{
name: "state as text",
issue: issue,
format: "%S",
colorize: true,
expect: "open",
},
{
name: "state as color switch",
issue: issue,
format: "%sC",
colorize: true,
expect: "\033[32m",
},
{
name: "state as color switch non colorized",
issue: issue,
format: "%sC",
colorize: false,
expect: "",
},
{
name: "title",
issue: issue,
format: "%t",
colorize: true,
expect: "Just an Issue",
},
{
name: "label colorized",
issue: issue,
format: "%l",
colorize: true,
expect: "\033[38;2;255;255;255;48;2;136;0;0m bug \033[m \033[38;2;255;255;255;48;2;0;136;0m feature \033[m",
},
{
name: "label not colorized",
issue: issue,
format: "%l",
colorize: false,
expect: " bug feature ",
},
{
name: "raw labels",
issue: issue,
format: "%L",
colorize: true,
expect: "bug, feature",
},
{
name: "body",
issue: issue,
format: "%b",
colorize: true,
expect: "Body of the\nissue",
},
{
name: "user login",
issue: issue,
format: "%au",
colorize: true,
expect: "pcorpet",
},
{
name: "assignee login",
issue: issue,
format: "%as",
colorize: true,
expect: "mislav, josh",
},
{
name: "assignee login but not assigned",
issue: github.Issue{
State: "open",
User: &github.User{Login: "pcorpet"},
},
format: "%as",
colorize: true,
expect: "",
},
{
name: "milestone number",
issue: issue,
format: "%Mn",
colorize: true,
expect: "31",
},
{
name: "milestone title",
issue: issue,
format: "%Mt",
colorize: true,
expect: "2.2-stable",
},
{
name: "comments number",
issue: issue,
format: "%Nc",
colorize: true,
expect: "(12)",
},
{
name: "raw comments number",
issue: issue,
format: "%NC",
colorize: true,
expect: "12",
},
{
name: "issue URL",
issue: issue,
format: "%U",
colorize: true,
expect: "the://url",
},
{
name: "created date",
issue: issue,
format: "%cD",
colorize: true,
expect: "16 Mar 2015",
},
{
name: "created time ISO 8601",
issue: issue,
format: "%cI",
colorize: true,
expect: "2015-03-16T12:34:00Z",
},
{
name: "created time Unix",
issue: issue,
format: "%ct",
colorize: true,
expect: "1426509240",
},
{
name: "updated date",
issue: issue,
format: "%uD",
colorize: true,
expect: "17 Mar 2015",
},
{
name: "updated time ISO 8601",
issue: issue,
format: "%uI",
colorize: true,
expect: "2015-03-17T12:34:00+09:00",
},
{
name: "updated time Unix",
issue: issue,
format: "%ut",
colorize: true,
expect: "1426563240",
},
})
}
================================================
FILE: commands/merge.go
================================================
package commands
import (
"fmt"
"regexp"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/utils"
)
var cmdMerge = &Command{
Run: merge,
GitExtension: true,
Usage: "merge <PULLREQ-URL>",
Long: `Merge a pull request locally with a message like the GitHub Merge Button.
This creates a local merge commit in the current branch, but does not actually
change the state of the pull request. However, the pull request will get
auto-closed and marked as "merged" as soon as the newly created merge commit is
pushed to the default branch of the remote repository.
To merge a pull request remotely, use ''hub pr merge''.
## Examples:
$ hub merge https://github.com/jingweno/gh/pull/73
> git fetch origin refs/pull/73/head
> git merge FETCH_HEAD --no-ff -m "Merge pull request #73 from jingweno/feature..."
## See also:
hub-pr(1), hub-checkout(1), hub(1), git-merge(1)
`,
}
func init() {
CmdRunner.Use(cmdMerge)
}
func merge(command *Command, args *Args) {
if !args.IsParamsEmpty() {
err := transformMergeArgs(args)
utils.Check(err)
}
}
func transformMergeArgs(args *Args) error {
words := args.Words()
if len(words) == 0 {
return nil
}
mergeURL := words[0]
url, err := github.ParseURL(mergeURL)
if err != nil {
return nil
}
pullURLRegex := regexp.MustCompile("^pull/(\\d+)")
projectPath := url.ProjectPath()
if !pullURLRegex.MatchString(projectPath) {
return nil
}
id := pullURLRegex.FindStringSubmatch(projectPath)[1]
gh := github.NewClient(url.Project.Host)
pullRequest, err := gh.PullRequest(url.Project, id)
if err != nil {
return err
}
repo, err := github.LocalRepo()
if err != nil {
return err
}
remote, err := repo.RemoteForRepo(pullRequest.Base.Repo)
if err != nil {
return err
}
branch := pullRequest.Head.Ref
headRepo := pullRequest.Head.Repo
if headRepo == nil {
return fmt.Errorf("Error: that fork is not available anymore")
}
args.Before("git", "fetch", remote.Name, fmt.Sprintf("refs/pull/%s/head", id))
// Remove pull request URL
idx := args.IndexOfParam(mergeURL)
args.RemoveParam(idx)
mergeMsg := fmt.Sprintf("Merge pull request #%s from %s/%s\n\n%s", id, headRepo.Owner.Login, branch, pullRequest.Title)
args.AppendParams("FETCH_HEAD", "-m", mergeMsg)
if args.IndexOfParam("--ff-only") == -1 && args.IndexOfParam("--squash") == -1 && args.IndexOfParam("--ff") == -1 {
i := args.IndexOfParam("-m")
args.InsertParam(i, "--no-ff")
}
return nil
}
================================================
FILE: commands/pr.go
================================================
package commands
import (
"fmt"
"strconv"
"strings"
"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 (
cmdPr = &Command{
Run: printHelp,
Usage: `
pr list [-s <STATE>] [-h <HEAD>] [-b <BASE>] [-o <SORT_KEY> [-^]] [-f <FORMAT>] [-L <LIMIT>]
pr checkout <PR-NUMBER> [<BRANCH>]
pr show [-uc] [-f <FORMAT>] [-h <HEAD>]
pr show [-uc] [-f <FORMAT>] <PR-NUMBER>
pr merge [-d] [--squash | --rebase] <PR-NUMBER> [-m <MESSAGE> | -F <FILE>] [--head-sha <COMMIT-SHA>]
`,
Long: `Manage GitHub Pull Requests for the current repository.
## Commands:
* _list_:
List pull requests in the current repository.
* _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
the current branch name. With ''--format'', print information about the
pull request instead of opening it.
* _merge_:
Merge a pull request in the current repository remotely. Select an
alternate merge method with ''--squash'' or ''--rebase''. Change the
commit subject and body with ''--message'' or ''--file''.
## Options:
-s, --state <STATE>
Filter pull requests by <STATE>. Supported values are: "open" (default),
"closed", "merged", or "all".
-h, --head <BRANCH>
Show pull requests started from the specified head <BRANCH>. The
"OWNER:BRANCH" format must be used for pull requests from forks.
-b, --base <BRANCH>
Show pull requests based off the specified <BRANCH>.
-f, --format <FORMAT>
Pretty print the list of pull requests using format <FORMAT> (default:
"%pC%>(8)%i%Creset %t% l%n"). See the "PRETTY FORMATS" section of
git-log(1) for some additional details on how placeholders are used in
format. The available placeholders are:
%I: pull request number
%i: pull request number prefixed with "#"
%U: the URL of this pull request
%S: state ("open" or "closed")
%pS: pull request state ("open", "draft", "merged", or "closed")
%sC: set color to red or green, depending on state
%pC: set color according to pull request state
%t: title
%l: colored labels
%L: raw, comma-separated labels
%b: body
%B: base branch
%sB: base commit SHA
%H: head branch
%sH: head commit SHA
%sm: merge commit SHA
%au: login name of author
%as: comma-separated list of assignees
%rs: comma-separated list of requested reviewers
%Mn: milestone number
%Mt: milestone title
%cD: created date-only (no time of day)
%cr: created date, relative
%ct: created date, UNIX timestamp
%cI: created date, ISO 8601 format
%uD: updated date-only (no time of day)
%ur: updated date, relative
%ut: updated date, UNIX timestamp
%uI: updated date, ISO 8601 format
%mD: merged date-only (no time of day)
%mr: merged date, relative
%mt: merged date, UNIX timestamp
%mI: merged date, ISO 8601 format
%n: newline
%%: a literal %
--color[=<WHEN>]
Enable colored output even if stdout is not a terminal. <WHEN> can be one
of "always" (default for ''--color''), "never", or "auto" (default).
-o, --sort <KEY>
Sort displayed pull requests by "created" (default), "updated", "popularity", or "long-running".
-^, --sort-ascending
Sort by ascending dates instead of descending.
-L, --limit <LIMIT>
Display only the first <LIMIT> pull requests.
-u, --url
Print the pull request URL instead of opening it.
-c, --copy
Put the pull request URL to clipboard instead of opening it.
-m, --message <MESSAGE>
The text up to the first blank line in <MESSAGE> is treated as the commit
subject for the merge commit, and the rest is used as commit body.
When multiple ''--message'' are passed, their values are concatenated with a
blank line in-between.
-F, --file <FILE>
Read the subject and body for the merge commit from <FILE>. Pass "-" to read
from standard input instead. See ''--message'' for the formatting rules.
--head-sha <COMMIT-SHA>
Ensure that the head of the pull request matches the commit SHA when merging.
--squash
Squash commits instead of creating a merge commit when merging a pull request.
--rebase
Rebase commits on top of the base branch when merging a pull request.
-d, --delete-branch
Delete the head branch after successfully merging a pull request.
## See also:
hub-issue(1), hub-pull-request(1), hub(1)
`,
}
cmdCheckoutPr = &Command{
Key: "checkout",
Run: checkoutPr,
KnownFlags: "\n",
}
cmdListPulls = &Command{
Key: "list",
Run: listPulls,
Long: cmdPr.Long,
}
cmdShowPr = &Command{
Key: "show",
Run: showPr,
KnownFlags: `
-h, --head HEAD
-u, --url
-c, --copy
-f, --format FORMAT
--color
`,
}
cmdMergePr = &Command{
Key: "merge",
Run: mergePr,
KnownFlags: `
-m, --message MESSAGE
-F, --file FILE
--head-sha COMMIT
--squash
--rebase
-d, --delete-branch
`,
}
)
func init() {
cmdPr.Use(cmdListPulls)
cmdPr.Use(cmdCheckoutPr)
cmdPr.Use(cmdShowPr)
cmdPr.Use(cmdMergePr)
CmdRunner.Use(cmdPr)
}
func printHelp(command *Command, args *Args) {
utils.Check(command.UsageError(""))
}
func listPulls(cmd *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
gh := github.NewClient(project.Host)
args.NoForward()
if args.Noop {
ui.Printf("Would request list of pull requests for %s\n", project)
return
}
filters := map[string]interface{}{}
if args.Flag.HasReceived("--state") {
filters["state"] = args.Flag.Value("--state")
}
if args.Flag.HasReceived("--sort") {
filters["sort"] = args.Flag.Value("--sort")
}
if args.Flag.HasReceived("--base") {
filters["base"] = args.Flag.Value("--base")
}
if args.Flag.HasReceived("--head") {
head := args.Flag.Value("--head")
if !strings.Contains(head, ":") {
head = fmt.Sprintf("%s:%s", project.Owner, head)
}
filters["head"] = head
}
if args.Flag.Bool("--sort-ascending") {
filters["direction"] = "asc"
} else {
filters["direction"] = "desc"
}
onlyMerged := false
if filters["state"] == "merged" {
filters["state"] = "closed"
onlyMerged = true
}
flagPullRequestLimit := args.Flag.Int("--limit")
flagPullRequestFormat := args.Flag.Value("--format")
if !args.Flag.HasReceived("--format") {
flagPullRequestFormat = "%pC%>(8)%i%Creset %t% l%n"
}
pulls, err := gh.FetchPullRequests(project, filters, flagPullRequestLimit, func(pr *github.PullRequest) bool {
return !(onlyMerged && pr.MergedAt.IsZero())
})
utils.Check(err)
colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color"))
for _, pr := range pulls {
ui.Print(formatPullRequest(pr, flagPullRequestFormat, colorize))
}
}
func checkoutPr(command *Command, args *Args) {
words := args.Words()
var newBranchName string
if len(words) == 0 {
utils.Check(fmt.Errorf("Error: No pull request number given"))
} else if len(words) > 1 {
newBranchName = words[1]
}
prNumberString := words[0]
_, err := strconv.Atoi(prNumberString)
utils.Check(err)
// Figure out the PR URL
localRepo, err := github.LocalRepo()
utils.Check(err)
baseProject, err := localRepo.MainProject()
utils.Check(err)
host, err := github.CurrentConfig().PromptForHost(baseProject.Host)
utils.Check(err)
client := github.NewClientWithHost(host)
pr, err := client.PullRequest(baseProject, prNumberString)
utils.Check(err)
newArgs, err := transformCheckoutArgs(args, pr, newBranchName)
utils.Check(err)
args.Replace(args.Executable, "checkout", newArgs...)
}
func showPr(command *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
baseProject, err := localRepo.MainProject()
utils.Check(err)
host, err := github.CurrentConfig().PromptForHost(baseProject.Host)
utils.Check(err)
gh := github.NewClientWithHost(host)
words := args.Words()
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))
} 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
}
args.NoForward()
if format := args.Flag.Value("--format"); format != "" {
if pr == nil {
pr, err = gh.PullRequest(baseProject, strconv.Itoa(prNumber))
utils.Check(err)
}
colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color"))
ui.Println(formatPullRequest(*pr, format, colorize))
return
}
printURL := args.Flag.Bool("--url")
copyURL := args.Flag.Bool("--copy")
printBrowseOrCopy(args, openURL, !printURL && !copyURL, copyURL)
}
func findCurrentPullRequest(localRepo *github.GitHubRepo, gh *github.Client, baseProject *github.Project, headArg string) (*github.PullRequest, error) {
filterParams := map[string]interface{}{
"state": "open",
}
headWithOwner := ""
if headArg != "" {
headWithOwner = headArg
if !strings.Contains(headWithOwner, ":") {
headWithOwner = fmt.Sprintf("%s:%s", baseProject.Owner, headWithOwner)
}
} else {
currentBranch, err := localRepo.CurrentBranch()
utils.Check(err)
if headBranch, headProject, err := findPushTarget(currentBranch); err == nil {
headWithOwner = fmt.Sprintf("%s:%s", headProject.Owner, headBranch.ShortName())
} else if headProject, err := deducePushTarget(currentBranch, gh.Host.User); err == nil {
headWithOwner = fmt.Sprintf("%s:%s", headProject.Owner, currentBranch.ShortName())
} else {
headWithOwner = fmt.Sprintf("%s:%s", baseProject.Owner, currentBranch.ShortName())
}
}
filterParams["head"] = headWithOwner
pulls, err := gh.FetchPullRequests(baseProject, filterParams, 1, nil)
if err != nil {
return nil, err
} else if len(pulls) == 1 {
return &pulls[0], nil
} else {
return nil, fmt.Errorf("no open pull requests found for branch '%s'", headWithOwner)
}
}
func branchTrackingInformation(branch *github.Branch) (string, *github.Branch, error) {
branchRemote, err := git.Config(fmt.Sprintf("branch.%s.remote", branch.ShortName()))
if branchRemote == "." {
err = fmt.Errorf("branch is tracking another local branch")
}
if err != nil {
return "", nil, err
}
branchMerge, err := git.Config(fmt.Sprintf("branch.%s.merge", branch.ShortName()))
if err != nil {
return "", nil, err
}
trackingBranch := &github.Branch{
Repo: branch.Repo,
Name: branchMerge,
}
return branchRemote, trackingBranch, nil
}
func findPushTarget(branch *github.Branch) (*github.Branch, *github.Project, error) {
branchRemote, headBranch, err := branchTrackingInformation(branch)
if err != nil {
return nil, nil, err
}
if headRemote, err := branch.Repo.RemoteByName(branchRemote); err == nil {
headProject, err := headRemote.Project()
if err != nil {
return nil, nil, err
}
return headBranch, headProject, nil
}
remoteURL, err := git.ParseURL(branchRemote)
if err != nil {
return nil, nil, err
}
headProject, err := github.NewProjectFromURL(remoteURL)
if err != nil {
return nil, nil, err
}
return headBranch, headProject, nil
}
func deducePushTarget(branch *github.Branch, owner string) (*github.Project, error) {
remote := branch.Repo.RemoteForBranch(branch, owner)
if remote == nil {
return nil, fmt.Errorf("no remote found for branch %s", branch.ShortName())
}
return remote.Project()
}
func mergePr(command *Command, args *Args) {
words := args.Words()
if len(words) == 0 {
utils.Check(fmt.Errorf("Error: No pull request number given"))
}
prNumber, err := strconv.Atoi(words[0])
utils.Check(err)
params := map[string]interface{}{
"merge_method": "merge",
}
if args.Flag.Bool("--squash") {
params["merge_method"] = "squash"
}
if args.Flag.Bool("--rebase") {
params["merge_method"] = "rebase"
}
msgs := args.Flag.AllValues("--message")
if len(msgs) > 0 {
params["commit_title"] = msgs[0]
params["commit_message"] = strings.Join(msgs[1:], "\n\n")
} else if args.Flag.HasReceived("--file") {
content, err := msgFromFile(args.Flag.Value("--file"))
utils.Check(err)
params["commit_title"], params["commit_message"] = github.SplitTitleBody(content)
}
if headSHA := args.Flag.Value("--head-sha"); headSHA != "" {
params["sha"] = args.Flag.Value("--head-sha")
}
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
args.NoForward()
if args.Noop {
ui.Printf("Would merge pull request #%d for %s\n", prNumber, project)
return
}
gh := github.NewClient(project.Host)
_, err = gh.MergePullRequest(project, prNumber, params)
utils.Check(err)
if !args.Flag.Bool("--delete-branch") {
return
}
pr, err := gh.PullRequest(project, strconv.Itoa(prNumber))
utils.Check(err)
if !pr.IsSameRepo() {
return
}
branchName := pr.Head.Ref
err = gh.DeleteBranch(project, branchName)
utils.Check(err)
}
func formatPullRequest(pr github.PullRequest, format string, colorize bool) string {
placeholders := formatIssuePlaceholders(github.Issue(pr), colorize)
delete(placeholders, "NC")
delete(placeholders, "Nc")
for key, value := range formatPullRequestPlaceholders(pr, colorize) {
placeholders[key] = value
}
return ui.Expand(format, placeholders, colorize)
}
================================================
FILE: commands/pull_request.go
================================================
package commands
import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/github/hub/v2/git"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/utils"
)
var cmdPullRequest = &Command{
Run: pullRequest,
Usage: `
pull-request [-focpd] [-b <BASE>] [-h <HEAD>] [-r <REVIEWERS> ] [-a <ASSIGNEES>] [-M <MILESTONE>] [-l <LABELS>]
pull-request -m <MESSAGE> [--edit]
pull-request -F <FILE> [--edit]
pull-request -i <ISSUE>
`,
Long: `Create a GitHub Pull Request.
## Options:
-f, --force
Skip the check for unpushed commits.
-m, --message <MESSAGE>
The text up to the first blank line in <MESSAGE> is treated as the pull
request title, and the rest is used as pull request description in Markdown
format.
When multiple ''--message'' are passed, their values are concatenated with a
blank line in-between.
When neither ''--message'' nor ''--file'' were supplied, a text editor will open
to author the title and description in.
--no-edit
Use the message from the first commit on the branch as pull request title
and description without opening a text editor.
-F, --file <FILE>
Read the pull request title and description from <FILE>. Pass "-" to read
from standard input instead. See ''--message'' for the formatting rules.
-e, --edit
Open the pull request title and description in a text editor before
submitting. This can be used in combination with ''--message'' or ''--file''.
-i, --issue <ISSUE>
Convert <ISSUE> (referenced by its number) to a pull request.
You can only convert issues authored by you or that which you have admin
rights over. In most workflows it is not necessary to convert issues to
pull requests; you can simply reference the original issue in the body of
the new pull request.
-o, --browse
Open the new pull request in a web browser.
-c, --copy
Put the URL of the new pull request to clipboard instead of printing it.
-p, --push
Push the current branch to <HEAD> before creating the pull request.
-b, --base <BASE>
The base branch in the "[<OWNER>:]<BRANCH>" format. Defaults to the default
branch of the upstream repository (usually "master").
See the "CONVENTIONS" section of hub(1) for more information on how hub
selects the defaults in case of multiple git remotes.
-h, --head <HEAD>
The head branch in "[<OWNER>:]<BRANCH>" format. Defaults to the currently
checked out branch.
-r, --reviewer <USERS>
A comma-separated list (no spaces around the comma) of GitHub handles to
request a review from.
-a, --assign <USERS>
A comma-separated list (no spaces around the comma) of GitHub handles to
assign to this pull request.
-M, --milestone <NAME>
The milestone name to add to this pull request. Passing the milestone number
is deprecated.
-l, --labels <LABELS>
A comma-separated list (no spaces around the comma) of labels to add to
this pull request. Labels will be created if they do not already exist.
-d, --draft
Create the pull request as a draft.
--no-maintainer-edits
When creating a pull request from a fork, this disallows projects
maintainers from being able to push to the head branch of this fork.
Maintainer edits are allowed by default.
## Examples:
$ hub pull-request
[ opens a text editor for writing title and message ]
[ creates a pull request for the current branch ]
$ hub pull-request --base OWNER:master --head MYUSER:my-branch
[ creates a pull request with explicit base and head branches ]
$ hub pull-request --browse -m "My title"
[ creates a pull request with the given title and opens it in a browser ]
$ hub pull-request -F - --edit < path/to/message-template.md
[ further edit the title and message received on standard input ]
## Configuration:
* ''HUB_RETRY_TIMEOUT'':
The maximum time to keep retrying after HTTP 422 on ''--push'' (default: 9).
## See also:
hub(1), hub-merge(1), hub-checkout(1)
`,
}
func init() {
CmdRunner.Use(cmdPullRequest)
}
func pullRequest(cmd *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
currentBranch, currentBranchErr := localRepo.CurrentBranch()
baseProject, err := localRepo.MainProject()
utils.Check(err)
host, err := github.CurrentConfig().PromptForHost(baseProject.Host)
if err != nil {
utils.Check(github.FormatError("creating pull request", err))
}
client := github.NewClientWithHost(host)
trackedBranch, headProject, _ := localRepo.RemoteBranchAndProject(host.User, false)
if headProject == nil {
utils.Check(fmt.Errorf("could not determine project for head branch"))
}
var (
base, head string
)
if flagPullRequestBase := args.Flag.Value("--base"); flagPullRequestBase != "" {
baseProject, base = parsePullRequestProject(baseProject, flagPullRequestBase)
}
if flagPullRequestHead := args.Flag.Value("--head"); flagPullRequestHead != "" {
headProject, head = parsePullRequestProject(headProject, flagPullRequestHead)
}
baseRemote, _ := localRepo.RemoteForProject(baseProject)
if base == "" && baseRemote != nil {
base = localRepo.DefaultBranch(baseRemote).ShortName()
}
if head == "" && trackedBranch != nil {
if !trackedBranch.IsRemote() {
// the current branch tracking another branch
// pretend there's no upstream at all
trackedBranch = nil
} else {
if baseProject.SameAs(headProject) && base == trackedBranch.ShortName() {
e := fmt.Errorf(`Aborted: head branch is the same as base ("%s")`, base)
e = fmt.Errorf("%s\n(use `-h <branch>` to specify an explicit pull request head)", e)
utils.Check(e)
}
}
}
force := args.Flag.Bool("--force")
flagPullRequestPush := args.Flag.Bool("--push")
if head == "" {
if trackedBranch == nil {
utils.Check(currentBranchErr)
if !force && !flagPullRequestPush {
branchRemote, branchMerge, err := branchTrackingInformation(currentBranch)
if err != nil || (baseRemote != nil && branchRemote == baseRemote.Name && branchMerge.ShortName() == base) {
if localRepo.RemoteForBranch(currentBranch, host.User) == nil {
err = fmt.Errorf("Aborted: the current branch seems not yet pushed to a remote")
err = fmt.Errorf("%s\n(use `-p` to push the branch or `-f` to skip this check)", err)
utils.Check(err)
}
}
}
head = currentBranch.ShortName()
} else {
head = trackedBranch.ShortName()
}
}
if headRepo, err := client.Repository(headProject); err == nil {
headProject.Owner = headRepo.Owner.Login
headProject.Name = headRepo.Name
}
fullBase := fmt.Sprintf("%s:%s", baseProject.Owner, base)
fullHead := fmt.Sprintf("%s:%s", headProject.Owner, head)
if !force && trackedBranch != nil {
remoteCommits, err := git.RefList(trackedBranch.LongName(), "")
if err == nil && len(remoteCommits) > 0 {
err = fmt.Errorf("Aborted: %d commits are not yet pushed to %s", len(remoteCommits), trackedBranch.LongName())
err = fmt.Errorf("%s\n(use `-f` to force submit a pull request anyway)", err)
utils.Check(err)
}
}
messageBuilder := &github.MessageBuilder{
Filename: "PULLREQ_EDITMSG",
Title: "pull request",
}
baseTracking := base
headTracking := head
remote := baseRemote
if remote != nil {
baseTracking = fmt.Sprintf("%s/%s", remote.Name, base)
}
if remote == nil || !baseProject.SameAs(headProject) {
remote, _ = localRepo.RemoteForProject(headProject)
}
if remote != nil {
headTracking = fmt.Sprintf("%s/%s", remote.Name, head)
}
if flagPullRequestPush && remote == nil {
utils.Check(fmt.Errorf("Can't find remote for %s", head))
}
messageBuilder.AddCommentedSection(fmt.Sprintf(`Requesting a pull to %s from %s
Write a message for this pull request. The first block
of text is the title and the rest is the description.`, fullBase, fullHead))
flagPullRequestMessage := args.Flag.AllValues("--message")
flagPullRequestEdit := args.Flag.Bool("--edit")
flagPullRequestIssue := args.Flag.Value("--issue")
if !args.Flag.HasReceived("--issue") && args.ParamsSize() > 0 {
flagPullRequestIssue = parsePullRequestIssueNumber(args.GetParam(0))
}
if len(flagPullRequestMessage) > 0 {
messageBuilder.Message = strings.Join(flagPullRequestMessage, "\n\n")
messageBuilder.Edit = flagPullRequestEdit
} else if args.Flag.HasReceived("--file") {
messageBuilder.Message, err = msgFromFile(args.Flag.Value("--file"))
utils.Check(err)
messageBuilder.Edit = flagPullRequestEdit
} else if args.Flag.Bool("--no-edit") {
commits, _ := git.RefList(baseTracking, head)
if len(commits) == 0 {
utils.Check(fmt.Errorf("Aborted: no commits detected between %s and %s", baseTracking, head))
}
message, err := git.Show(commits[len(commits)-1])
utils.Check(err)
messageBuilder.Message = message
} else if flagPullRequestIssue == "" {
messageBuilder.Edit = true
headForMessage := headTracking
if flagPullRequestPush {
headForMessage = head
}
message := ""
commits, _ := git.RefList(baseTracking, headForMessage)
if len(commits) == 1 {
message, err = git.Show(commits[0])
utils.Check(err)
re := regexp.MustCompile(`\n(Co-authored-by|Signed-off-by):[^\n]+`)
message = re.ReplaceAllString(message, "")
} else if len(commits) > 1 {
commitLogs, err := git.Log(baseTracking, headForMessage)
utils.Check(err)
if commitLogs != "" {
messageBuilder.AddCommentedSection("\nChanges:\n\n" + strings.TrimSpace(commitLogs))
}
}
workdir, _ := git.WorkdirName()
if workdir != "" {
template, _ := github.ReadTemplate(github.PullRequestTemplate, workdir)
if template != "" {
message = message + "\n\n\n" + template
}
}
messageBuilder.Message = message
}
title, body, err := messageBuilder.Extract()
utils.Check(err)
if title == "" && flagPullRequestIssue == "" {
utils.Check(fmt.Errorf("Aborting due to empty pull request title"))
}
if flagPullRequestPush {
if args.Noop {
args.Before(fmt.Sprintf("Would push to %s/%s", remote.Name, head), "")
} else {
err = git.Spawn("push", "--set-upstream", remote.Name, fmt.Sprintf("HEAD:%s", head))
utils.Check(err)
}
}
milestoneNumber, err := milestoneValueToNumber(args.Flag.Value("--milestone"), client, baseProject)
utils.Check(err)
var pullRequestURL string
if args.Noop {
args.Before(fmt.Sprintf("Would request a pull request to %s from %s", fullBase, fullHead), "")
pullRequestURL = "PULL_REQUEST_URL"
} else {
params := map[string]interface{}{
"base": base,
"head": fullHead,
"maintainer_can_modify": !args.Flag.Bool("--no-maintainer-edits"),
}
if args.Flag.Bool("--draft") {
params["draft"] = true
}
if title != "" {
params["title"] = title
if body != "" {
params["body"] = body
}
} else {
issueNum, _ := strconv.Atoi(flagPullRequestIssue)
params["issue"] = issueNum
}
startedAt := time.Now()
numRetries := 0
retryDelay := 2
retryAllowance := 0
if flagPullRequestPush {
if allowanceFromEnv := os.Getenv("HUB_RETRY_TIMEOUT"); allowanceFromEnv != "" {
retryAllowance, err = strconv.Atoi(allowanceFromEnv)
utils.Check(err)
} else {
retryAllowance = 9
}
}
var pr *github.PullRequest
for {
pr, err = client.CreatePullRequest(baseProject, params)
if err != nil && strings.Contains(err.Error(), `Invalid value for "head"`) {
if retryAllowance > 0 {
retryAllowance -= retryDelay
time.Sleep(time.Duration(retryDelay) * time.Second)
retryDelay++
numRetries++
} else {
if numRetries > 0 {
duration := time.Since(startedAt)
err = fmt.Errorf("%s\nGiven up after retrying for %.1f seconds.", err, duration.Seconds())
}
break
}
} else {
break
}
}
if err == nil {
defer messageBuilder.Cleanup()
}
utils.Check(err)
pullRequestURL = pr.HTMLURL
params = map[string]interface{}{}
flagPullRequestLabels := commaSeparated(args.Flag.AllValues("--labels"))
if len(flagPullRequestLabels) > 0 {
params["labels"] = flagPullRequestLabels
}
flagPullRequestAssignees := commaSeparated(args.Flag.AllValues("--assign"))
if len(flagPullRequestAssignees) > 0 {
params["assignees"] = flagPullRequestAssignees
}
if milestoneNumber > 0 {
params["milestone"] = milestoneNumber
}
if len(params) > 0 {
err = client.UpdateIssue(baseProject, pr.Number, params)
utils.Check(err)
}
flagPullRequestReviewers := commaSeparated(args.Flag.AllValues("--reviewer"))
if len(flagPullRequestReviewers) > 0 {
userReviewers := []string{}
teamReviewers := []string{}
for _, reviewer := range flagPullRequestReviewers {
if strings.Contains(reviewer, "/") {
teamName := strings.SplitN(reviewer, "/", 2)[1]
if !pr.HasRequestedTeam(teamName) {
teamReviewers = append(teamReviewers, teamName)
}
} else if !pr.HasRequestedReviewer(reviewer) {
userReviewers = append(userReviewers, reviewer)
}
}
if len(userReviewers) > 0 || len(teamReviewers) > 0 {
err = client.RequestReview(baseProject, pr.Number, map[string]interface{}{
"reviewers": userReviewers,
"team_reviewers": teamReviewers,
})
utils.Check(err)
}
}
}
args.NoForward()
printBrowseOrCopy(args, pullRequestURL, args.Flag.Bool("--browse"), args.Flag.Bool("--copy"))
}
func parsePullRequestProject(context *github.Project, s string) (p *github.Project, ref string) {
p = context
ref = s
if strings.Contains(s, ":") {
split := strings.SplitN(s, ":", 2)
ref = split[1]
var name string
if !strings.Contains(split[0], "/") {
name = context.Name
}
p = github.NewProject(split[0], name, context.Host)
}
return
}
func parsePullRequestIssueNumber(url string) string {
u, e := github.ParseURL(url)
if e != nil {
return ""
}
r := regexp.MustCompile(`^issues\/(\d+)`)
p := u.ProjectPath()
if r.MatchString(p) {
return r.FindStringSubmatch(p)[1]
}
return ""
}
func commaSeparated(l []string) []string {
res := []string{}
for _, i := range l {
if i == "" {
continue
}
res = append(res, strings.Split(i, ",")...)
}
return res
}
================================================
FILE: commands/pull_request_test.go
================================================
package commands
import (
"testing"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/internal/assert"
)
func TestPullRequest_ParsePullRequestProject(t *testing.T) {
c := &github.Project{Host: "github.com", Owner: "jingweno", Name: "gh"}
s := "develop"
p, ref := parsePullRequestProject(c, s)
assert.Equal(t, "develop", ref)
assert.Equal(t, "github.com", p.Host)
assert.Equal(t, "jingweno", p.Owner)
assert.Equal(t, "gh", p.Name)
s = "mojombo:develop"
p, ref = parsePullRequestProject(c, s)
assert.Equal(t, "develop", ref)
assert.Equal(t, "github.com", p.Host)
assert.Equal(t, "mojombo", p.Owner)
assert.Equal(t, "gh", p.Name)
s = "mojombo/jekyll:develop"
p, ref = parsePullRequestProject(c, s)
assert.Equal(t, "develop", ref)
assert.Equal(t, "github.com", p.Host)
assert.Equal(t, "mojombo", p.Owner)
assert.Equal(t, "jekyll", p.Name)
}
================================================
FILE: commands/push.go
================================================
package commands
import (
"strings"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/utils"
)
var cmdPush = &Command{
Run: push,
GitExtension: true,
Usage: "push <REMOTE>[,<REMOTE2>...] [<REF>]",
Long: `Push a git branch to each of the listed remotes.
## Examples:
$ hub push origin,staging,qa bert_timeout
> git push origin bert_timeout
> git push staging bert_timeout
> git push qa bert_timeout
$ hub push origin
> git push origin HEAD
## See also:
hub(1), git-push(1)
`,
}
func init() {
CmdRunner.Use(cmdPush)
}
func push(command *Command, args *Args) {
if !args.IsParamsEmpty() && strings.Contains(args.FirstParam(), ",") {
transformPushArgs(args)
}
}
func transformPushArgs(args *Args) {
refs := []string{}
if args.ParamsSize() > 1 {
refs = args.Params[1:]
}
remotes := strings.Split(args.FirstParam(), ",")
args.ReplaceParam(0, remotes[0])
if len(refs) == 0 {
localRepo, err := github.LocalRepo()
utils.Check(err)
head, err := localRepo.CurrentBranch()
utils.Check(err)
refs = []string{head.ShortName()}
args.AppendParams(refs...)
}
for _, remote := range remotes[1:] {
afterCmd := []string{"git", "push", remote}
afterCmd = append(afterCmd, refs...)
args.After(afterCmd...)
}
}
================================================
FILE: commands/push_test.go
================================================
package commands
import (
"github.com/github/hub/v2/internal/assert"
"testing"
)
func TestTransformPushArgs(t *testing.T) {
args := NewArgs([]string{"push", "origin,staging,qa", "bert_timeout"})
transformPushArgs(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())
// TODO: travis-ci doesn't have HEAD
//args = NewArgs([]string{"push", "origin"})
//transformPushArgs(args)
//cmds = args.Commands()
//assert.Equal(t, 1, len(cmds))
//pushRegexp := regexp.MustCompile("git push origin .+")
//assert.T(t, pushRegexp.MatchString(cmds[0].String()))
}
================================================
FILE: commands/release.go
================================================
package commands
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/ui"
"github.com/github/hub/v2/utils"
)
var (
cmdRelease = &Command{
Run: listReleases,
Usage: `
release [--include-drafts] [--exclude-prereleases] [-L <LIMIT>] [-f <FORMAT>]
release show [-f <FORMAT>] <TAG>
release create [-dpoc] [-a <FILE>] [-m <MESSAGE>|-F <FILE>] [-t <TARGET>] <TAG>
release edit [<options>] <TAG>
release download <TAG> [-i <PATTERN>]
release delete <TAG>
`,
Long: `Manage GitHub Releases for the current repository.
## Commands:
With no arguments, shows a list of existing releases.
* _show_:
Show GitHub release notes for <TAG>.
With ''--show-downloads'', include the "Downloads" section.
* _create_:
Create a GitHub release for the specified <TAG> name. If git tag <TAG>
does not exist, it will be created at <TARGET> (default: current branch).
* _edit_:
Edit the GitHub release for the specified <TAG> name. Accepts the same
options as _create_ command. Publish a draft with ''--draft=false''.
Without ''--message'' or ''--file'', a text editor will open pre-populated with
the current release title and body. To re-use existing title and body
unchanged, pass ''-m ""''.
* _download_:
Download the assets attached to release for the specified <TAG>.
* _delete_:
Delete the release and associated assets for the specified <TAG>. Note that
this does **not** remove the git tag <TAG>.
## Options:
-d, --include-drafts
List drafts together with published releases.
-p, --exclude-prereleases
Exclude prereleases from the list.
-L, --limit
Display only the first <LIMIT> releases.
-d, --draft
Create a draft release.
-p, --prerelease
Create a pre-release.
-a, --attach <FILE>
Attach a file as an asset for this release.
If <FILE> is in the "<filename>#<text>" format, the text after the "#"
character is taken as asset label.
-m, --message <MESSAGE>
The text up to the first blank line in <MESSAGE> is treated as the release
title, and the rest is used as release description in Markdown format.
When multiple ''--message'' are passed, their values are concatenated with a
blank line in-between.
When neither ''--message'' nor ''--file'' were supplied to ''release create'', a
text editor will open to author the title and description in.
-F, --file <FILE>
Read the release title and description from <FILE>. Pass "-" to read from
standard input instead. See ''--message'' for the formatting rules.
-e, --edit
Open the release title and description in a text editor before submitting.
This can be used in combination with ''--message'' or ''--file''.
-o, --browse
Open the new release in a web browser.
-c, --copy
Put the URL of the new release to clipboard instead of printing it.
-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>.
-f, --format <FORMAT>
Pretty print releases using <FORMAT> (default: "%T%n"). 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:
%U: the URL of this release
%uT: tarball URL
%uZ: zipball URL
%uA: asset upload URL
%S: state (i.e. "draft", "pre-release")
%sC: set color to yellow or red, depending on state
%t: release name
%T: release tag
%b: body
%as: the list of assets attached to this release
%cD: created date-only (no time of day)
%cr: created date, relative
%ct: created date, UNIX timestamp
%cI: created date, ISO 8601 format
%pD: published date-only (no time of day)
%pr: published date, relative
%pt: published date, UNIX timestamp
%pI: published date, ISO 8601 format
%n: newline
%%: a literal %
--color[=<WHEN>]
Enable colored output even if stdout is not a terminal. <WHEN> can be one
of "always" (default for ''--color''), "never", or "auto" (default).
<TAG>
The git tag name for this release.
## See also:
hub(1), git-tag(1)
`,
KnownFlags: `
-d, --include-drafts
-p, --exclude-prereleases
-L, --limit N
-f, --format FMT
--color
`,
}
cmdShowRelease = &Command{
Key: "show",
Run: showRelease,
KnownFlags: `
-d, --show-downloads
-f, --format FMT
--color
`,
}
cmdCreateRelease = &Command{
Key: "create",
Run: createRelease,
KnownFlags: `
-e, --edit
-d, --draft
-p, --prerelease
-o, --browse
-c, --copy
-a, --attach FILE
-m, --message MSG
-F, --file FILE
-t, --commitish C
`,
}
cmdEditRelease = &Command{
Key: "edit",
Run: editRelease,
KnownFlags: `
-e, --edit
-d, --draft
-p, --prerelease
-a, --attach FILE
-m, --message MSG
-F, --file FILE
-t, --commitish C
`,
}
cmdDownloadRelease = &Command{
Key: "download",
Run: downloadRelease,
KnownFlags: `
-i, --include PATTERN
`,
}
cmdDeleteRelease = &Command{
Key: "delete",
Run: deleteRelease,
}
)
func init() {
cmdRelease.Use(cmdShowRelease)
cmdRelease.Use(cmdCreateRelease)
cmdRelease.Use(cmdEditRelease)
cmdRelease.Use(cmdDownloadRelease)
cmdRelease.Use(cmdDeleteRelease)
CmdRunner.Use(cmdRelease)
}
func listReleases(cmd *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
gh := github.NewClient(project.Host)
flagReleaseLimit := args.Flag.Int("--limit")
flagReleaseIncludeDrafts := args.Flag.Bool("--include-drafts")
flagReleaseExcludePrereleases := args.Flag.Bool("--exclude-prereleases")
if args.Noop {
ui.Printf("Would request list of releases for %s\n", project)
} else {
releases, err := gh.FetchReleases(project, flagReleaseLimit, func(release *github.Release) bool {
return (!release.Draft || flagReleaseIncludeDrafts) &&
(!release.Prerelease || !flagReleaseExcludePrereleases)
})
utils.Check(err)
colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color"))
for _, release := range releases {
flagReleaseFormat := "%T%n"
if args.Flag.HasReceived("--format") {
flagReleaseFormat = args.Flag.Value("--format")
}
ui.Print(formatRelease(release, flagReleaseFormat, colorize))
}
}
args.NoForward()
}
func formatRelease(release github.Release, format string, colorize bool) string {
state := ""
stateColorSwitch := ""
if release.Draft {
state = "draft"
stateColorSwitch = fmt.Sprintf("\033[%dm", 33)
} else if release.Prerelease {
state = "pre-release"
stateColorSwitch = fmt.Sprintf("\033[%dm", 31)
}
var createdDate, createdAtISO8601, createdAtUnix, createdAtRelative,
publishedDate, publishedAtISO8601, publishedAtUnix, publishedAtRelative string
if !release.CreatedAt.IsZero() {
createdDate = release.CreatedAt.Format("02 Jan 2006")
createdAtISO8601 = release.CreatedAt.Format(time.RFC3339)
createdAtUnix = fmt.Sprintf("%d", release.CreatedAt.Unix())
createdAtRelative = utils.TimeAgo(release.CreatedAt)
}
if !release.PublishedAt.IsZero() {
publishedDate = release.PublishedAt.Format("02 Jan 2006")
publishedAtISO8601 = release.PublishedAt.Format(time.RFC3339)
publishedAtUnix = fmt.Sprintf("%d", release.PublishedAt.Unix())
publishedAtRelative = utils.TimeAgo(release.PublishedAt)
}
assets := make([]string, len(release.Assets))
for i, asset := range release.Assets {
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,
"S": state,
"sC": stateColorSwitch,
"t": release.Name,
"T": release.TagName,
"b": release.Body,
"as": strings.Join(assets, "\n"),
"cD": createdDate,
"cI": createdAtISO8601,
"ct": createdAtUnix,
"cr": createdAtRelative,
"pD": publishedDate,
"pI": publishedAtISO8601,
"pt": publishedAtUnix,
"pr": publishedAtRelative,
}
return ui.Expand(format, placeholders, colorize)
}
func showRelease(cmd *Command, args *Args) {
tagName := ""
if args.ParamsSize() > 0 {
tagName = args.GetParam(0)
}
if tagName == "" {
utils.Check(cmd.UsageError(""))
}
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
gh := github.NewClient(project.Host)
args.NoForward()
if args.Noop {
ui.Printf("Would display information for `%s' release\n", tagName)
} else {
release, err := gh.FetchRelease(project, tagName)
utils.Check(err)
body := strings.TrimSpace(release.Body)
colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color"))
if flagShowReleaseFormat := args.Flag.Value("--format"); flagShowReleaseFormat != "" {
ui.Print(formatRelease(*release, flagShowReleaseFormat, colorize))
return
}
ui.Println(release.Name)
if body != "" {
ui.Printf("\n%s\n", body)
}
if args.Flag.Bool("--show-downloads") {
ui.Printf("\n## Downloads\n\n")
for _, asset := range release.Assets {
ui.Println(asset.DownloadURL)
}
if release.ZipballURL != "" {
ui.Println(release.ZipballURL)
ui.Println(release.TarballURL)
}
}
}
}
func downloadRelease(cmd *Command, args *Args) {
tagName := ""
if args.ParamsSize() > 0 {
tagName = args.GetParam(0)
}
if tagName == "" {
utils.Check(cmd.UsageError(""))
}
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
gh := github.NewClient(project.Host)
release, err := gh.FetchRelease(project, tagName)
utils.Check(err)
hasPattern := args.Flag.HasReceived("--include")
found := false
for _, asset := range release.Assets {
if hasPattern {
isMatch, err := filepath.Match(args.Flag.Value("--include"), asset.Name)
utils.Check(err)
if !isMatch {
continue
}
}
found = true
ui.Printf("Downloading %s ...\n", asset.Name)
err := downloadReleaseAsset(asset, gh)
utils.Check(err)
}
if !found && hasPattern {
names := []string{}
for _, asset := range release.Assets {
names = append(names, asset.Name)
}
utils.Check(fmt.Errorf("the `--include` pattern did not match any available assets:\n%s", strings.Join(names, "\n")))
}
args.NoForward()
}
func downloadReleaseAsset(asset github.ReleaseAsset, gh *github.Client) (err error) {
assetReader, err := gh.DownloadReleaseAsset(asset.APIURL)
if err != nil {
return
}
defer assetReader.Close()
assetFile, err := os.OpenFile(asset.Name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
return
}
defer assetFile.Close()
_, err = io.Copy(assetFile, assetReader)
if err != nil {
return
}
return
}
func createRelease(cmd *Command, args *Args) {
tagName := ""
if args.ParamsSize() > 0 {
tagName = args.GetParam(0)
}
if tagName == "" {
utils.Check(cmd.UsageError(""))
return
}
assetsToUpload, close, err := openAssetFiles(args.Flag.AllValues("--attach"))
utils.Check(err)
defer close()
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
gh := github.NewClient(project.Host)
messageBuilder := &github.MessageBuilder{
Filename: "RELEASE_EDITMSG",
Title: "release",
}
messageBuilder.AddCommentedSection(fmt.Sprintf(`Creating release %s for %s
Write a message for this release. The first block of
text is the title and the rest is the description.`, tagName, project))
flagReleaseMessage := args.Flag.AllValues("--message")
if len(flagReleaseMessage) > 0 {
messageBuilder.Message = strings.Join(flagReleaseMessage, "\n\n")
messageBuilder.Edit = args.Flag.Bool("--edit")
} else if args.Flag.HasReceived("--file") {
messageBuilder.Message, err = msgFromFile(args.Flag.Value("--file"))
utils.Check(err)
messageBuilder.Edit = args.Flag.Bool("--edit")
} else {
messageBuilder.Edit = true
}
title, body, err := messageBuilder.Extract()
utils.Check(err)
if title == "" {
utils.Check(fmt.Errorf("Aborting release due to empty release title"))
}
params := &github.Release{
TagName: tagName,
TargetCommitish: args.Flag.Value("--commitish"),
Name: title,
Body: body,
Draft: args.Flag.Bool("--draft"),
Prerelease: args.Flag.Bool("--prerelease"),
}
var release *github.Release
args.NoForward()
if args.Noop {
ui.Printf("Would create release `%s' for %s with tag name `%s'\n", title, project, tagName)
} else {
release, err = gh.CreateRelease(project, params)
utils.Check(err)
flagReleaseBrowse := args.Flag.Bool("--browse")
flagReleaseCopy := args.Flag.Bool("--copy")
printBrowseOrCopy(args, release.HTMLURL, flagReleaseBrowse, flagReleaseCopy)
}
messageBuilder.Cleanup()
numAssets := len(assetsToUpload)
if numAssets == 0 {
return
}
if args.Noop {
ui.Printf("Would attach %d %s\n", numAssets, pluralize(numAssets, "asset"))
} else {
ui.Errorf("Attaching %d %s...\n", numAssets, pluralize(numAssets, "asset"))
uploaded, err := gh.UploadReleaseAssets(release, assetsToUpload)
if err != nil {
failed := []string{}
for _, a := range assetsToUpload[len(uploaded):] {
failed = append(failed, fmt.Sprintf("-a %s", a.Name))
}
ui.Errorf("The release was created, but attaching %d %s failed. ", len(failed), pluralize(len(failed), "asset"))
ui.Errorf("You can retry with:\n%s release edit %s -m '' %s\n\n", "hub", release.TagName, strings.Join(failed, " "))
utils.Check(err)
}
}
}
func editRelease(cmd *Command, args *Args) {
tagName := ""
if args.ParamsSize() > 0 {
tagName = args.GetParam(0)
}
if tagName == "" {
utils.Check(cmd.UsageError(""))
return
}
assetsToUpload, close, err := openAssetFiles(args.Flag.AllValues("--attach"))
utils.Check(err)
defer close()
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
gh := github.NewClient(project.Host)
release, err := gh.FetchRelease(project, tagName)
utils.Check(err)
params := map[string]interface{}{}
if args.Flag.HasReceived("--commitish") {
params["target_commitish"] = args.Flag.Value("--commitish")
}
if args.Flag.HasReceived("--draft") {
params["draft"] = args.Flag.Bool("--draft")
}
if args.Flag.HasReceived("--prerelease") {
params["prerelease"] = args.Flag.Bool("--prerelease")
}
messageBuilder := &github.MessageBuilder{
Filename: "RELEASE_EDITMSG",
Title: "release",
}
messageBuilder.AddCommentedSection(fmt.Sprintf(`Editing release %s for %s
Write a message for this release. The first block of
text is the title and the rest is the description.`, tagName, project))
flagReleaseMessage := args.Flag.AllValues("--message")
if len(flagReleaseMessage) > 0 {
messageBuilder.Message = strings.Join(flagReleaseMessage, "\n\n")
messageBuilder.Edit = args.Flag.Bool("--edit")
} else if args.Flag.HasReceived("--file") {
messageBuilder.Message, err = msgFromFile(args.Flag.Value("--file"))
utils.Check(err)
messageBuilder.Edit = args.Flag.Bool("--edit")
} else {
messageBuilder.Edit = true
messageBuilder.Message = strings.Replace(fmt.Sprintf("%s\n\n%s", release.Name, release.Body), "\r\n", "\n", -1)
}
title, body, err := messageBuilder.Extract()
utils.Check(err)
if title == "" && len(flagReleaseMessage) == 0 {
utils.Check(fmt.Errorf("Aborting editing due to empty release title"))
}
if title != "" {
params["name"] = title
}
if body != "" {
params["body"] = body
}
args.NoForward()
if len(params) > 0 {
if args.Noop {
ui.Printf("Would edit release `%s'\n", tagName)
} else {
release, err = gh.EditRelease(release, params)
utils.Check(err)
}
messageBuilder.Cleanup()
}
numAssets := len(assetsToUpload)
if numAssets == 0 {
return
}
if args.Noop {
ui.Printf("Would attach %d %s\n", numAssets, pluralize(numAssets, "asset"))
} else {
ui.Errorf("Attaching %d %s...\n", numAssets, pluralize(numAssets, "asset"))
uploaded, err := gh.UploadReleaseAssets(release, assetsToUpload)
if err != nil {
failed := []string{}
for _, a := range assetsToUpload[len(uploaded):] {
failed = append(failed, a.Name)
}
ui.Errorf("Attaching these assets failed:\n%s\n\n", strings.Join(failed, "\n"))
utils.Check(err)
}
}
}
func deleteRelease(cmd *Command, args *Args) {
tagName := ""
if args.ParamsSize() > 0 {
tagName = args.GetParam(0)
}
if tagName == "" {
utils.Check(cmd.UsageError(""))
return
}
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
gh := github.NewClient(project.Host)
release, err := gh.FetchRelease(project, tagName)
utils.Check(err)
if args.Noop {
message := fmt.Sprintf("Deleting release related to %s...", tagName)
ui.Println(message)
} else {
err = gh.DeleteRelease(release)
utils.Check(err)
}
args.NoForward()
}
func openAssetFiles(args []string) ([]github.LocalAsset, func(), error) {
assets := []github.LocalAsset{}
files := []*os.File{}
for _, arg := range args {
var label string
parts := strings.SplitN(arg, "#", 2)
path := parts[0]
if len(parts) > 1 {
label = parts[1]
}
file, err := os.Open(path)
if err != nil {
return nil, nil, err
}
stat, err := file.Stat()
if err != nil {
return nil, nil, err
}
files = append(files, file)
assets = append(assets, github.LocalAsset{
Name: path,
Label: label,
Size: stat.Size(),
Contents: file,
})
}
close := func() {
for _, f := range files {
f.Close()
}
}
return assets, close, nil
}
func pluralize(count int, label string) string {
if count == 1 {
return label
}
return fmt.Sprintf("%ss", label)
}
================================================
FILE: commands/remote.go
================================================
package commands
import (
"fmt"
"regexp"
"strings"
"github.com/github/hub/v2/git"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/utils"
)
var cmdRemote = &Command{
Run: remote,
GitExtension: true,
Usage: `
remote add [-p] [<OPTIONS>] <USER>[/<REPOSITORY>]
remote set-url [-p] [<OPTIONS>] <NAME> <USER>[/<REPOSITORY>]
`,
Long: `Add a git remote for a GitHub repository.
## Options:
-p
(Deprecated) Use the ''ssh:'' protocol instead of ''git:'' for the remote URL.
The writeable ''ssh:'' protocol is automatically used for own repos, GitHub
Enterprise remotes, and private or pushable repositories.
<USER>[/<REPOSITORY>]
If <USER> is "origin", that value will be substituted for your GitHub
username. <REPOSITORY> defaults to the name of the current working directory.
## Examples:
$ hub remote add jingweno
> git remote add jingweno git://github.com/jingweno/REPO.git
$ hub remote add origin
> git remote add origin git@github.com:USER/REPO.git
## See also:
hub-fork(1), hub(1), git-remote(1)
`,
}
func init() {
CmdRunner.Use(cmdRemote)
}
/*
*/
func remote(command *Command, args *Args) {
if !args.IsParamsEmpty() && (args.FirstParam() == "add" || args.FirstParam() == "set-url") {
transformRemoteArgs(args)
}
}
func transformRemoteArgs(args *Args) {
ownerWithName := args.LastParam()
re := regexp.MustCompile(fmt.Sprintf(`^%s(/%s)?$`, OwnerRe, NameRe))
if !re.MatchString(ownerWithName) {
return
}
owner := ownerWithName
name := ""
if strings.Contains(ownerWithName, "/") {
parts := strings.SplitN(ownerWithName, "/", 2)
owner, name = parts[0], parts[1]
}
localRepo, err := github.LocalRepo()
utils.Check(err)
var host string
mainProject, err := localRepo.MainProject()
if err == nil {
host = mainProject.Host
}
if name == "" {
if mainProject != nil {
name = mainProject.Name
} else {
dirName, err := git.WorkdirName()
utils.Check(err)
name = github.SanitizeProjectName(dirName)
}
}
var hostConfig *github.Host
if host == "" {
hostConfig, err = github.CurrentConfig().DefaultHost()
} else {
hostConfig, err = github.CurrentConfig().PromptForHost(host)
}
if err != nil {
utils.Check(github.FormatError("adding remote", err))
}
host = hostConfig.Host
p := utils.NewArgsParser()
p.RegisterValue("-t")
p.RegisterValue("-m")
params, _ := p.Parse(args.Params)
if len(params) > 3 {
return
}
for i, pi := range p.PositionalIndices {
if i == 1 && strings.Contains(params[i], "/") {
args.ReplaceParam(pi, owner)
} else if i == 2 {
args.RemoveParam(pi)
}
}
if len(params) == 2 && owner == "origin" {
owner = hostConfig.User
}
if strings.EqualFold(owner, hostConfig.User) {
owner = hostConfig.User
}
project := github.NewProject(owner, name, host)
isPrivate := parseRemotePrivateFlag(args) || owner == hostConfig.User || project.Host != github.GitHubHost
if !isPrivate {
gh := github.NewClient(project.Host)
repo, err := gh.Repository(project)
if err != nil {
if strings.Contains(err.Error(), "HTTP 404") {
err = fmt.Errorf("Error: repository %s/%s doesn't exist", project.Owner, project.Name)
}
utils.Check(err)
}
isPrivate = repo.Private || repo.Permissions.Push
}
url := project.GitURL("", "", isPrivate)
args.AppendParams(url)
}
func parseRemotePrivateFlag(args *Args) bool {
if i := args.IndexOfParam("-p"); i != -1 {
args.RemoveParam(i)
return true
}
return false
}
================================================
FILE: commands/remote_test.go
================================================
package commands
import (
"os"
"regexp"
"testing"
"github.com/github/hub/v2/fixtures"
"github.com/github/hub/v2/github"
"github.com/github/hub/v2/internal/assert"
)
func TestTransformRemoteArgs(t *testing.T) {
repo := fixtures.SetupTestRepo()
defer repo.TearDown()
os.Setenv("HUB_PROTOCOL", "git")
github.CreateTestConfigs("jingweno", "123")
args := NewArgs([]string{"remote", "add", "jingweno"})
transformRemoteArgs(args)
assert.Equal(t, 3, args.ParamsSize())
assert.Equal(t, "add", args.FirstParam())
assert.Equal(t, "jingweno", args.GetParam(1))
reg := regexp.MustCompile("^git@github\\.com:jingweno/.+\\.git$")
assert.T(t, reg.MatchString(args.GetParam(2)))
args = NewArgs([]string{"remote", "add", "-p", "mislav"})
transformRemoteArgs(args)
assert.Equal(t, 3, args.ParamsSize())
assert.Equal(t, "add", args.FirstParam())
assert.Equal(t, "mislav", args.GetParam(1))
reg = regexp.MustCompile("^git@github\\.com:mislav/.+\\.git$")
assert.T(t, reg.MatchString(args.GetParam(2)))
args = NewArgs([]string{"remote", "add", "origin"})
transformRemoteArgs(args)
assert.Equal(t, 3, args.ParamsSize())
assert.Equal(t, "add", args.FirstParam())
assert.Equal(t, "origin", args.GetParam(1))
reg = regexp.MustCompile("^git@github\\.com:jingweno/.+\\.git$")
assert.T(t, reg.MatchString(args.GetParam(2)))
args = NewArgs([]string{"remote", "add", "jingweno", "git@github.com:jingweno/gh.git"})
transformRemoteArgs(args)
assert.Equal(t, 3, args.ParamsSize())
assert.Equal(t, "jingweno", args.GetParam(1))
assert.Equal(t, "add", args.FirstParam())
assert.Equal(t, "git@github.com:jingweno/gh.git", args.GetParam(2))
}
================================================
FILE: commands/runner.go
================================================
package commands
import (
"fmt"
"strings"
"github.com/github/hub/v2/cmd"
"github.com/github/hub/v2/git"
"github.com/github/hub/v2/ui"
"github.com/kballard/go-shellquote"
)
type Runner struct {
commands map[string]*Command
}
func NewRunner() *Runner {
return &Runner{
commands: make(map[string]*Command),
}
}
func (r *Runner) All() map[string]*Command {
return r.commands
}
func (r *Runner) Use(command *Command, aliases ...string) {
r.commands[command.Name()] = command
if len(aliases) > 0 {
r.commands[aliases[0]] = command
}
}
func (r *Runner) Lookup(name string) *Command {
return r.commands[name]
}
func (r *Runner) Execute(cliArgs []string) error {
args := NewArgs(cliArgs[1:])
args.ProgramPath = cliArgs[0]
forceFail := false
if args.Command == "" && len(args.GlobalFlags) == 0 {
args.Command = "help"
forceFail = true
}
cmdName := args.Command
if strings.Contains(cmdName, "=") {
cmdName = strings.SplitN(cmdName, "=", 2)[0]
}
git.GlobalFlags = args.GlobalFlags // preserve git global flags
if !isBuiltInHubCommand(cmdName) {
expandAlias(args)
cmdName = args.Command
}
// make `<cmd> --help` equivalent to `help <cmd>`
if args.ParamsSize() == 1 && args.GetParam(0) == helpFlag {
if c := r.Lookup(cmdName); c != nil && !c.GitExtension {
args.ReplaceParam(0, cmdName)
args.Command = "help"
cmdName = args.Command
}
}
cmd := r.Lookup(cmdName)
if cmd != nil && cmd.Runnable() {
err := callRunnableCommand(cmd, args)
if err == nil && forceFail {
err = fmt.Errorf("")
}
return err
}
gitArgs := []string{}
if args.Command != "" {
gitArgs = append(gitArgs, args.Command)
}
gitArgs = append(gitArgs, args.Params...)
return git.Run(gitArgs...)
}
func callRunnableCommand(cmd *Command, args *Args) error {
err := cmd.Call(args)
if err != nil {
return err
}
cmds := args.Commands()
if args.Noop {
printCommands(cmds)
} else if err = executeCommands(cmds, len(args.Callbacks) == 0); err != nil {
return err
}
for _, fn := range args.Callbacks {
if err = fn(); err != nil {
return err
}
}
return nil
}
func printCommands(cmds []*cmd.Cmd) {
for _, c := range cmds {
ui.Println(c)
}
}
func executeCommands(cmds []*cmd.Cmd, execFinal bool) error {
for i, c := range cmds {
var err error
// Run with `Exec` for the last command in chain
if execFinal && i == len(cmds)-1 {
err = c.Run()
} else {
err = c.Spawn()
}
if err != nil {
return err
}
}
return nil
}
func expandAlias(args *Args) {
cmd := args.Command
if cmd == "" {
return
}
expandedCmd, err := git.Alias(cmd)
if err == nil && expandedCmd != "" && !git.IsBuiltInGitCommand(cmd) {
words, e := splitAliasCmd(expandedCmd)
if e == nil {
args.Command = words[0]
args.PrependParams(words[1:]...)
}
}
}
func isBuiltInHubCommand(command string) bool {
for hubCommand := range CmdRunner.All() {
if hubCommand == command {
return true
}
}
return false
}
func splitAliasCmd(cmd string) ([]string, error) {
if cmd == "" {
return nil, fmt.Errorf("alias can't be empty")
}
if strings.HasPrefix(cmd, "!") {
return nil, fmt.Errorf("alias starting with ! can't be split")
}
words, err := shellquote.Split(cmd)
if err != nil {
return nil, err
}
return words, nil
}
================================================
FILE: commands/runner_test.go
================================================
package commands
import (
"testing"
"github.com/github/hub/v2/internal/assert"
)
func TestRunner_splitAliasCmd(t *testing.T) {
_, err := splitAliasCmd("!source ~/.zshrc")
assert.NotEqual(t, nil, err)
words, err := splitAliasCmd("log --pretty=oneline --abbrev-commit --graph --decorate")
assert.Equal(t, nil, err)
assert.Equal(t, 5, len(words))
_, err = splitAliasCmd("")
assert.NotEqual(t, nil, err)
}
================================================
FILE: commands/submodule.go
================================================
package commands
var cmdSubmodule = &Command{
Run: submodule,
GitExtension: true,
Usage: "submodule add [-p] [<OPTIONS>] [<USER>/]<REPOSITORY> <DESTINATION>",
Long: `Add a git submodule for a GitHub repository.
## Examples:
$ hub submodule add jingweno/gh vendor/gh
> git submodule add git://github.com/jingweno/gh.git vendor/gh
## See also:
hub-remote(1), hub(1), git-submodule(1)
`,
}
func init() {
CmdRunner.Use(cmdSubmodule)
}
func submodule(command *Command, args *Args) {
if !args.IsParamsEmpty() {
transformSubmoduleArgs(args)
}
}
func transformSubmoduleArgs(args *Args) {
var idx int
if idx = args.IndexOfParam("add"); idx == -1 {
return
}
args.RemoveParam(idx)
transformCloneArgs(args)
args.InsertParam(idx, "add")
}
================================================
FILE: commands/sync.go
================================================
package commands
import (
"fmt"
"regexp"
"strings"
"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{
Run: sync,
Usage: "sync [--color]",
Long: `Fetch git objects from upstream and update local branches.
- If the local branch is outdated, fast-forward it;
- If the local branch contains unpushed work, warn about it;
- If the branch seems merged and its upstream branch was deleted, delete it.
If a local branch does not have any upstream configuration, but has a
same-named branch on the remote, treat that as its upstream branch.
## Options:
--color[=<WHEN>]
Enable colored output even if stdout is not a terminal. <WHEN> can be one
of "always" (default for ''--color''), "never", or "auto" (default).
## See also:
hub(1), git-fetch(1)
`,
}
func init() {
CmdRunner.Use(cmdSync)
}
func sync(cmd *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
remote, err := localRepo.MainRemote()
utils.Check(err)
defaultB
gitextract_4bom6jnl/
├── .ctags.d/
│ └── go.ctags
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── SECURITY.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── Gemfile
├── LICENSE
├── Makefile
├── README.md
├── cmd/
│ ├── cmd.go
│ └── cmd_test.go
├── commands/
│ ├── alias.go
│ ├── api.go
│ ├── apply.go
│ ├── args.go
│ ├── args_test.go
│ ├── browse.go
│ ├── checkout.go
│ ├── cherry_pick.go
│ ├── ci_status.go
│ ├── clone.go
│ ├── commands.go
│ ├── commands_test.go
│ ├── compare.go
│ ├── compare_test.go
│ ├── create.go
│ ├── delete.go
│ ├── fetch.go
│ ├── fetch_test.go
│ ├── fork.go
│ ├── gist.go
│ ├── help.go
│ ├── init.go
│ ├── init_test.go
│ ├── issue.go
│ ├── issue_test.go
│ ├── merge.go
│ ├── pr.go
│ ├── pull_request.go
│ ├── pull_request_test.go
│ ├── push.go
│ ├── push_test.go
│ ├── release.go
│ ├── remote.go
│ ├── remote_test.go
│ ├── runner.go
│ ├── runner_test.go
│ ├── submodule.go
│ ├── sync.go
│ ├── utils.go
│ ├── utils_test.go
│ └── version.go
├── coverage/
│ └── coverage.go
├── cucumber.yml
├── etc/
│ ├── README.md
│ ├── hub.bash_completion.sh
│ ├── hub.fish_completion
│ └── hub.zsh_completion
├── features/
│ ├── README.md
│ ├── alias.feature
│ ├── am.feature
│ ├── api.feature
│ ├── apply.feature
│ ├── authentication.feature
│ ├── bash_completion.feature
│ ├── browse.feature
│ ├── checkout.feature
│ ├── cherry_pick.feature
│ ├── ci_status.feature
│ ├── clone.feature
│ ├── compare.feature
│ ├── create.feature
│ ├── delete.feature
│ ├── fetch.feature
│ ├── fish_completion.feature
│ ├── fork.feature
│ ├── gist.feature
│ ├── git_compatibility.feature
│ ├── help.feature
│ ├── init.feature
│ ├── issue-transfer.feature
│ ├── issue.feature
│ ├── merge.feature
│ ├── pr-checkout.feature
│ ├── pr-list.feature
│ ├── pr-merge.feature
│ ├── pr-show.feature
│ ├── pull_request.feature
│ ├── push.feature
│ ├── release.feature
│ ├── remote_add.feature
│ ├── steps.rb
│ ├── submodule_add.feature
│ ├── support/
│ │ ├── completion.rb
│ │ ├── env.rb
│ │ ├── fakebin/
│ │ │ ├── curl
│ │ │ ├── git
│ │ │ ├── man
│ │ │ └── open
│ │ ├── local_server.rb
│ │ └── rspec_matchers.rb
│ ├── sync.feature
│ └── zsh_completion.feature
├── fixtures/
│ ├── fixtures.go
│ ├── release_dir/
│ │ ├── dir/
│ │ │ ├── file2
│ │ │ ├── file3
│ │ │ └── subdir/
│ │ │ └── file4
│ │ └── file1
│ ├── test.git/
│ │ ├── HEAD
│ │ ├── config
│ │ ├── description
│ │ ├── hooks/
│ │ │ ├── applypatch-msg.sample
│ │ │ ├── commit-msg.sample
│ │ │ ├── post-update.sample
│ │ │ ├── pre-applypatch.sample
│ │ │ ├── pre-commit.sample
│ │ │ ├── pre-push.sample
│ │ │ ├── pre-rebase.sample
│ │ │ ├── prepare-commit-msg.sample
│ │ │ └── update.sample
│ │ ├── info/
│ │ │ └── exclude
│ │ ├── objects/
│ │ │ ├── 08/
│ │ │ │ └── f4b7b6513dffc6245857e497cfd6101dc47818
│ │ │ ├── 8a/
│ │ │ │ └── 1cdac440b4a3c44b988e300758a903a9866905
│ │ │ ├── 9b/
│ │ │ │ └── 5a719a3d76ac9dc2fa635d9b1f34fd73994c06
│ │ │ ├── 9d/
│ │ │ │ └── aeafb9864cf43055ae93beb0afd6c7d144bfa4
│ │ │ ├── ca/
│ │ │ │ └── 93b49848670d03b3968c8a481eca55f5fb2150
│ │ │ └── e6/
│ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
│ │ └── refs/
│ │ └── heads/
│ │ └── master
│ ├── test_configs.go
│ └── test_repo.go
├── git/
│ ├── git.go
│ ├── git_test.go
│ ├── ssh_config.go
│ ├── ssh_config_test.go
│ ├── url.go
│ └── url_test.go
├── github/
│ ├── branch.go
│ ├── branch_test.go
│ ├── client.go
│ ├── client_test.go
│ ├── config.go
│ ├── config_decoder.go
│ ├── config_encoder.go
│ ├── config_service.go
│ ├── config_service_test.go
│ ├── crash_report.go
│ ├── crash_report_test.go
│ ├── editor.go
│ ├── editor_test.go
│ ├── hosts.go
│ ├── http.go
│ ├── http_test.go
│ ├── localrepo.go
│ ├── localrepo_test.go
│ ├── message_builder.go
│ ├── message_builder_test.go
│ ├── project.go
│ ├── project_test.go
│ ├── remote.go
│ ├── remote_test.go
│ ├── reset_console.go
│ ├── reset_console_windows.go
│ ├── template.go
│ ├── template_test.go
│ ├── url.go
│ └── url_test.go
├── go.mod
├── go.sum
├── internal/
│ └── assert/
│ └── assert.go
├── main.go
├── man-template.html
├── md2roff/
│ └── renderer.go
├── md2roff-bin/
│ └── cmd.go
├── script/
│ ├── bootstrap
│ ├── build
│ ├── build.bat
│ ├── changelog
│ ├── coverage
│ ├── cross-compile
│ ├── docker
│ ├── get
│ ├── github-release
│ ├── install.bat
│ ├── install.sh
│ ├── package
│ ├── publish-release
│ ├── ruby-test
│ ├── tag-release
│ ├── test
│ ├── version
│ └── version.bat
├── share/
│ └── vim/
│ └── vimfiles/
│ ├── ftdetect/
│ │ └── pullrequest.vim
│ └── syntax/
│ └── pullrequest.vim
├── ui/
│ ├── format.go
│ ├── format_test.go
│ └── ui.go
├── utils/
│ ├── args_parser.go
│ ├── args_parser_test.go
│ ├── color.go
│ ├── json.go
│ ├── utils.go
│ └── utils_test.go
└── version/
└── version.go
SYMBOL INDEX (792 symbols across 99 files)
FILE: cmd/cmd.go
type Cmd (line 15) | type Cmd struct
method String (line 23) | func (cmd Cmd) String() string {
method WithArg (line 38) | func (cmd *Cmd) WithArg(arg string) *Cmd {
method WithArgs (line 44) | func (cmd *Cmd) WithArgs(args ...string) *Cmd {
method Output (line 52) | func (cmd *Cmd) Output() (string, error) {
method CombinedOutput (line 61) | func (cmd *Cmd) CombinedOutput() (string, error) {
method Success (line 68) | func (cmd *Cmd) Success() bool {
method Run (line 76) | func (cmd *Cmd) Run() error {
method Spawn (line 106) | func (cmd *Cmd) Spawn() error {
method Exec (line 118) | func (cmd *Cmd) Exec() error {
function isWindows (line 83) | func isWindows() bool {
function detectWSL (line 91) | func detectWSL() bool {
function New (line 135) | func New(name string) *Cmd {
function NewWithArray (line 145) | func NewWithArray(cmd []string) *Cmd {
function verboseLog (line 149) | func verboseLog(cmd *Cmd) {
FILE: cmd/cmd_test.go
function TestNew (line 9) | func TestNew(t *testing.T) {
function TestWithArg (line 15) | func TestWithArg(t *testing.T) {
function Test_String (line 22) | func Test_String(t *testing.T) {
FILE: commands/alias.go
function init (line 32) | func init() {
function alias (line 36) | func alias(command *Command, args *Args) {
FILE: commands/api.go
function init (line 145) | func init() {
function apiCommand (line 149) | func apiCommand(_ *Command, args *Args) {
function pauseUntil (line 327) | func pauseUntil(timestamp int) {
constant trueVal (line 337) | trueVal = "true"
constant falseVal (line 338) | falseVal = "false"
constant nilVal (line 339) | nilVal = "null"
function magicValue (line 342) | func magicValue(value string) interface{} {
function readFile (line 361) | func readFile(file string) (content []byte) {
FILE: commands/apply.go
function init (line 58) | func init() {
function apply (line 63) | func apply(command *Command, args *Args) {
function transformApplyArgs (line 69) | func transformApplyArgs(args *Args) {
FILE: commands/args.go
type Args (line 11) | type Args struct
method Words (line 26) | func (a *Args) Words() []string {
method Before (line 37) | func (a *Args) Before(command ...string) {
method After (line 41) | func (a *Args) After(command ...string) {
method AfterFn (line 45) | func (a *Args) AfterFn(fn func() error) {
method NoForward (line 49) | func (a *Args) NoForward() {
method Replace (line 53) | func (a *Args) Replace(executable, command string, params ...string) {
method Commands (line 61) | func (a *Args) Commands() []*cmd.Cmd {
method ToCmd (line 87) | func (a *Args) ToCmd() *cmd.Cmd {
method GetParam (line 101) | func (a *Args) GetParam(i int) string {
method FirstParam (line 105) | func (a *Args) FirstParam() string {
method LastParam (line 113) | func (a *Args) LastParam() string {
method HasSubcommand (line 121) | func (a *Args) HasSubcommand() bool {
method InsertParam (line 125) | func (a *Args) InsertParam(i int, items ...string) {
method RemoveParam (line 142) | func (a *Args) RemoveParam(i int) string {
method ReplaceParam (line 148) | func (a *Args) ReplaceParam(i int, item string) {
method IndexOfParam (line 156) | func (a *Args) IndexOfParam(param string) int {
method ParamsSize (line 166) | func (a *Args) ParamsSize() int {
method IsParamsEmpty (line 170) | func (a *Args) IsParamsEmpty() bool {
method PrependParams (line 174) | func (a *Args) PrependParams(params ...string) {
method AppendParams (line 178) | func (a *Args) AppendParams(params ...string) {
function NewArgs (line 182) | func NewArgs(args []string) *Args {
constant noopFlag (line 218) | noopFlag = "--noop"
constant versionFlag (line 219) | versionFlag = "--version"
constant listCmds (line 220) | listCmds = "--list-cmds="
constant helpFlag (line 221) | helpFlag = "--help"
constant configFlag (line 222) | configFlag = "-c"
constant chdirFlag (line 223) | chdirFlag = "-C"
constant flagPrefix (line 224) | flagPrefix = "-"
function looksLikeFlag (line 227) | func looksLikeFlag(value string) bool {
function findCommandIndex (line 231) | func findCommandIndex(args []string) int {
FILE: commands/args_test.go
function TestNewArgs (line 9) | func TestNewArgs(t *testing.T) {
function TestArgs_Words (line 31) | func TestArgs_Words(t *testing.T) {
function TestArgs_Insert (line 40) | func TestArgs_Insert(t *testing.T) {
function TestArgs_Remove (line 67) | func TestArgs_Remove(t *testing.T) {
function TestArgs_GlobalFlags (line 77) | func TestArgs_GlobalFlags(t *testing.T) {
function TestArgs_GlobalFlags_Noop (line 85) | func TestArgs_GlobalFlags_Noop(t *testing.T) {
function TestArgs_GlobalFlags_NoopTwice (line 93) | func TestArgs_GlobalFlags_NoopTwice(t *testing.T) {
function TestArgs_GlobalFlags_Repeated (line 101) | func TestArgs_GlobalFlags_Repeated(t *testing.T) {
function TestArgs_GlobalFlags_Propagate (line 109) | func TestArgs_GlobalFlags_Propagate(t *testing.T) {
function TestArgs_GlobalFlags_Replaced (line 115) | func TestArgs_GlobalFlags_Replaced(t *testing.T) {
function TestArgs_ToCmd (line 123) | func TestArgs_ToCmd(t *testing.T) {
function TestArgs_GlobalFlags_BeforeAfterChain (line 129) | func TestArgs_GlobalFlags_BeforeAfterChain(t *testing.T) {
FILE: commands/browse.go
function init (line 49) | func init() {
function browse (line 53) | func browse(command *Command, args *Args) {
function branchInURL (line 129) | func branchInURL(branch *github.Branch) string {
FILE: commands/checkout.go
function init (line 29) | func init() {
function checkout (line 33) | func checkout(command *Command, args *Args) {
function transformCheckoutArgs (line 76) | func transformCheckoutArgs(args *Args, pullRequest *github.PullRequest, ...
function sanitizeCheckoutFlags (line 150) | func sanitizeCheckoutFlags(args *Args) error {
function replaceCheckoutParam (line 162) | func replaceCheckoutParam(args *Args, checkoutURL string, replacement .....
FILE: commands/cherry_pick.go
function init (line 26) | func init() {
function cherryPick (line 30) | func cherryPick(command *Command, args *Args) {
function transformCherryPickArgs (line 36) | func transformCherryPickArgs(args *Args) {
FILE: commands/ci_status.go
function init (line 57) | func init() {
function checkSeverity (line 72) | func checkSeverity(targetState string) int {
function ciStatus (line 81) | func ciStatus(cmd *Command, args *Args) {
function ciVerboseFormat (line 143) | func ciVerboseFormat(statuses []github.CIStatus, formatString string, co...
function stateRank (line 196) | func stateRank(state string) uint32 {
FILE: commands/clone.go
function init (line 44) | func init() {
function clone (line 48) | func clone(command *Command, args *Args) {
function transformCloneArgs (line 54) | func transformCloneArgs(args *Args) {
function parseClonePrivateFlag (line 88) | func parseClonePrivateFlag(args *Args) bool {
function getCloneURL (line 97) | func getCloneURL(nameWithOwner string, allowPush, allowPrivate bool) str...
FILE: commands/commands.go
type Command (line 19) | type Command struct
method Call (line 32) | func (c *Command) Call(args *Args) (err error) {
method parseArguments (line 58) | func (c *Command) parseArguments(args *Args) error {
method Use (line 77) | func (c *Command) Use(subCommand *Command) {
method UsageError (line 85) | func (c *Command) UsageError(msg string) error {
method Synopsis (line 93) | func (c *Command) Synopsis() string {
method HelpText (line 111) | func (c *Command) HelpText() string {
method Name (line 136) | func (c *Command) Name() string {
method Runnable (line 144) | func (c *Command) Runnable() bool {
method lookupSubCommand (line 148) | func (c *Command) lookupSubCommand(args *Args) (runCommand *Command, e...
type ErrHelp (line 50) | type ErrHelp struct
method Error (line 54) | func (e ErrHelp) Error() string {
FILE: commands/commands_test.go
function TestMain (line 13) | func TestMain(m *testing.M) {
function TestCommandUseSelf (line 18) | func TestCommandUseSelf(t *testing.T) {
function TestCommandUseSubcommand (line 29) | func TestCommandUseSubcommand(t *testing.T) {
function TestCommandUseErrorWhenMissingSubcommand (line 42) | func TestCommandUseErrorWhenMissingSubcommand(t *testing.T) {
function TestArgsForCommand (line 54) | func TestArgsForCommand(t *testing.T) {
function TestArgsForSubCommand (line 64) | func TestArgsForSubCommand(t *testing.T) {
function TestFlagsAfterArguments (line 76) | func TestFlagsAfterArguments(t *testing.T) {
function TestCommandNameTakeKey (line 87) | func TestCommandNameTakeKey(t *testing.T) {
function TestCommandCall (line 92) | func TestCommandCall(t *testing.T) {
function TestCommandHelp (line 103) | func TestCommandHelp(t *testing.T) {
function TestSubCommandCall (line 113) | func TestSubCommandCall(t *testing.T) {
function Test_NameWithOwnerRe (line 128) | func Test_NameWithOwnerRe(t *testing.T) {
FILE: commands/compare.go
function init (line 63) | func init() {
function compare (line 67) | func compare(command *Command, args *Args) {
function parseCompareRange (line 132) | func parseCompareRange(r string) string {
function rangeQueryEscape (line 149) | func rangeQueryEscape(r string) string {
FILE: commands/compare_test.go
function TestParseRange (line 8) | func TestParseRange(t *testing.T) {
FILE: commands/create.go
function init (line 59) | func init() {
function create (line 63) | func create(command *Command, args *Args) {
FILE: commands/delete.go
function init (line 41) | func init() {
function deleteRepo (line 45) | func deleteRepo(command *Command, args *Args) {
FILE: commands/fetch.go
function init (line 31) | func init() {
function fetch (line 35) | func fetch(command *Command, args *Args) {
function transformFetchArgs (line 42) | func transformFetchArgs(args *Args) error {
function parseRemoteNames (line 76) | func parseRemoteNames(args *Args) (names []string) {
FILE: commands/fetch_test.go
function TestParseRemoteNames (line 8) | func TestParseRemoteNames(t *testing.T) {
FILE: commands/fork.go
function init (line 41) | func init() {
function fork (line 45) | func fork(cmd *Command, args *Args) {
FILE: commands/gist.go
function init (line 74) | func init() {
function getGist (line 80) | func getGist(gh *github.Client, id string, filename string) error {
function printGistHelp (line 110) | func printGistHelp(command *Command, args *Args) {
function createGist (line 114) | func createGist(cmd *Command, args *Args) {
function showGist (line 144) | func showGist(cmd *Command, args *Args) {
FILE: commands/help.go
function init (line 46) | func init() {
function runHelp (line 51) | func runHelp(helpCmd *Command, args *Args) {
function runListCmds (line 114) | func runListCmds(cmd *Command, args *Args) {
function displayManPage (line 137) | func displayManPage(manPage string, args *Args, isWeb bool) error {
function lookupCmd (line 189) | func lookupCmd(name string) *Command {
function customCommands (line 200) | func customCommands() []string {
FILE: commands/init.go
function init (line 37) | func init() {
function gitInit (line 41) | func gitInit(command *Command, args *Args) {
function transformInitArgs (line 46) | func transformInitArgs(args *Args) error {
function parseInitFlag (line 93) | func parseInitFlag(args *Args) bool {
FILE: commands/init_test.go
function setupInitContext (line 13) | func setupInitContext() {
function TestEmptyParams (line 19) | func TestEmptyParams(t *testing.T) {
function TestFlagToAddRemote (line 29) | func TestFlagToAddRemote(t *testing.T) {
function TestInitInAnotherDir (line 51) | func TestInitInAnotherDir(t *testing.T) {
function TestSeparateGitDir (line 73) | func TestSeparateGitDir(t *testing.T) {
FILE: commands/issue.go
function init (line 251) | func init() {
function listIssues (line 260) | func listIssues(cmd *Command, args *Args) {
function formatIssuePlaceholders (line 348) | func formatIssuePlaceholders(issue github.Issue, colorize bool) map[stri...
function formatPullRequestPlaceholders (line 430) | func formatPullRequestPlaceholders(pr github.PullRequest, colorize bool)...
function formatIssue (line 493) | func formatIssue(issue github.Issue, format string, colorize bool) string {
function showIssue (line 498) | func showIssue(cmd *Command, args *Args) {
function createIssue (line 556) | func createIssue(cmd *Command, args *Args) {
function updateIssue (line 631) | func updateIssue(cmd *Command, args *Args) {
function listLabels (line 704) | func listLabels(cmd *Command, args *Args) {
function hasField (line 728) | func hasField(args *Args, names ...string) bool {
function setLabelsFromArgs (line 738) | func setLabelsFromArgs(params map[string]interface{}, args *Args) {
function setAssigneesFromArgs (line 745) | func setAssigneesFromArgs(params map[string]interface{}, args *Args) {
function setMilestoneFromArgs (line 752) | func setMilestoneFromArgs(params map[string]interface{}, args *Args, gh ...
function colorizeOutput (line 765) | func colorizeOutput(colorSet bool, when string) bool {
function formatLabel (line 782) | func formatLabel(label github.IssueLabel, colorize bool) string {
function colorizeLabel (line 791) | func colorizeLabel(label github.IssueLabel, color *utils.Color) string {
type contrastCandidate (line 799) | type contrastCandidate struct
function pickHighContrastTextColor (line 804) | func pickHighContrastTextColor(color *utils.Color) *utils.Color {
function milestoneValueToNumber (line 829) | func milestoneValueToNumber(value string, client *github.Client, project...
function transferIssue (line 851) | func transferIssue(cmd *Command, args *Args) {
FILE: commands/issue_test.go
type formatIssueTest (line 10) | type formatIssueTest struct
function testFormatIssue (line 18) | func testFormatIssue(t *testing.T, tests []formatIssueTest) {
function TestFormatIssue (line 27) | func TestFormatIssue(t *testing.T) {
function TestFormatIssue_customFormatString (line 103) | func TestFormatIssue_customFormatString(t *testing.T) {
FILE: commands/merge.go
function init (line 35) | func init() {
function merge (line 39) | func merge(command *Command, args *Args) {
function transformMergeArgs (line 46) | func transformMergeArgs(args *Args) error {
FILE: commands/pr.go
function init (line 222) | func init() {
function printHelp (line 230) | func printHelp(command *Command, args *Args) {
function listPulls (line 234) | func listPulls(cmd *Command, args *Args) {
function checkoutPr (line 296) | func checkoutPr(command *Command, args *Args) {
function showPr (line 327) | func showPr(command *Command, args *Args) {
function findCurrentPullRequest (line 372) | func findCurrentPullRequest(localRepo *github.GitHubRepo, gh *github.Cli...
function branchTrackingInformation (line 407) | func branchTrackingInformation(branch *github.Branch) (string, *github.B...
function findPushTarget (line 426) | func findPushTarget(branch *github.Branch) (*github.Branch, *github.Proj...
function deducePushTarget (line 451) | func deducePushTarget(branch *github.Branch, owner string) (*github.Proj...
function mergePr (line 459) | func mergePr(command *Command, args *Args) {
function formatPullRequest (line 523) | func formatPullRequest(pr github.PullRequest, format string, colorize bo...
FILE: commands/pull_request.go
function init (line 130) | func init() {
function pullRequest (line 134) | func pullRequest(cmd *Command, args *Args) {
function parsePullRequestProject (line 443) | func parsePullRequestProject(context *github.Project, s string) (p *gith...
function parsePullRequestIssueNumber (line 460) | func parsePullRequestIssueNumber(url string) string {
function commaSeparated (line 475) | func commaSeparated(l []string) []string {
FILE: commands/pull_request_test.go
function TestPullRequest_ParsePullRequestProject (line 10) | func TestPullRequest_ParsePullRequestProject(t *testing.T) {
FILE: commands/push.go
function init (line 31) | func init() {
function push (line 35) | func push(command *Command, args *Args) {
function transformPushArgs (line 41) | func transformPushArgs(args *Args) {
FILE: commands/push_test.go
function TestTransformPushArgs (line 8) | func TestTransformPushArgs(t *testing.T) {
FILE: commands/release.go
function init (line 229) | func init() {
function listReleases (line 238) | func listReleases(cmd *Command, args *Args) {
function formatRelease (line 273) | func formatRelease(release github.Release, format string, colorize bool)...
function showRelease (line 328) | func showRelease(cmd *Command, args *Args) {
function downloadRelease (line 378) | func downloadRelease(cmd *Command, args *Args) {
function downloadReleaseAsset (line 426) | func downloadReleaseAsset(asset github.ReleaseAsset, gh *github.Client) ...
function createRelease (line 446) | func createRelease(cmd *Command, args *Args) {
function editRelease (line 543) | func editRelease(cmd *Command, args *Args) {
function deleteRelease (line 648) | func deleteRelease(cmd *Command, args *Args) {
function openAssetFiles (line 680) | func openAssetFiles(args []string) ([]github.LocalAsset, func(), error) {
function pluralize (line 719) | func pluralize(count int, label string) string {
FILE: commands/remote.go
function init (line 45) | func init() {
function remote (line 51) | func remote(command *Command, args *Args) {
function transformRemoteArgs (line 57) | func transformRemoteArgs(args *Args) {
function parseRemotePrivateFlag (line 143) | func parseRemotePrivateFlag(args *Args) bool {
FILE: commands/remote_test.go
function TestTransformRemoteArgs (line 13) | func TestTransformRemoteArgs(t *testing.T) {
FILE: commands/runner.go
type Runner (line 13) | type Runner struct
method All (line 23) | func (r *Runner) All() map[string]*Command {
method Use (line 27) | func (r *Runner) Use(command *Command, aliases ...string) {
method Lookup (line 34) | func (r *Runner) Lookup(name string) *Command {
method Execute (line 38) | func (r *Runner) Execute(cliArgs []string) error {
function NewRunner (line 17) | func NewRunner() *Runner {
function callRunnableCommand (line 86) | func callRunnableCommand(cmd *Command, args *Args) error {
function printCommands (line 108) | func printCommands(cmds []*cmd.Cmd) {
function executeCommands (line 114) | func executeCommands(cmds []*cmd.Cmd, execFinal bool) error {
function expandAlias (line 132) | func expandAlias(args *Args) {
function isBuiltInHubCommand (line 148) | func isBuiltInHubCommand(command string) bool {
function splitAliasCmd (line 157) | func splitAliasCmd(cmd string) ([]string, error) {
FILE: commands/runner_test.go
function TestRunner_splitAliasCmd (line 9) | func TestRunner_splitAliasCmd(t *testing.T) {
FILE: commands/submodule.go
function init (line 19) | func init() {
function submodule (line 23) | func submodule(command *Command, args *Args) {
function transformSubmoduleArgs (line 29) | func transformSubmoduleArgs(args *Args) {
FILE: commands/sync.go
function init (line 37) | func init() {
function sync (line 41) | func sync(cmd *Command, args *Args) {
FILE: commands/utils.go
type stringSliceValue (line 17) | type stringSliceValue
method Set (line 19) | func (s *stringSliceValue) Set(val string) error {
method String (line 24) | func (s *stringSliceValue) String() string {
type listFlag (line 28) | type listFlag
method String (line 30) | func (l *listFlag) String() string {
method Set (line 34) | func (l *listFlag) Set(value string) error {
type messageBlocks (line 41) | type messageBlocks
method String (line 43) | func (m *messageBlocks) String() string {
method Set (line 47) | func (m *messageBlocks) Set(value string) error {
function isCloneable (line 52) | func isCloneable(file string) bool {
function isEmptyDir (line 80) | func isEmptyDir(path string) bool {
function msgFromFile (line 86) | func msgFromFile(filename string) (string, error) {
function printBrowseOrCopy (line 102) | func printBrowseOrCopy(args *Args, msg string, openBrowser bool, perform...
FILE: commands/utils_test.go
function TestDirIsNotEmpty (line 11) | func TestDirIsNotEmpty(t *testing.T) {
function TestDirIsEmpty (line 19) | func TestDirIsEmpty(t *testing.T) {
function createTempDir (line 26) | func createTempDir(t *testing.T) string {
FILE: commands/version.go
function init (line 15) | func init() {
function runVersion (line 19) | func runVersion(cmd *Command, args *Args) {
FILE: coverage/coverage.go
function init (line 14) | func init() {
function Record (line 23) | func Record(data interface{}, i int) {
function write (line 37) | func write(data interface{}, i, count int, filename string) {
FILE: features/support/completion.rb
function set_shell (line 132) | def set_shell(shell)
function tmux_pane? (line 148) | def tmux_pane?
function tmux_pane_contents (line 152) | def tmux_pane_contents
function tmux_send_keys (line 157) | def tmux_send_keys(*keys)
function tmux_send_tab (line 161) | def tmux_send_tab
function tmux_kill_pane (line 166) | def tmux_kill_pane
function tmux_wait_for_prompt (line 170) | def tmux_wait_for_prompt
function tmux_wait_for_completion (line 179) | def tmux_wait_for_completion
function tmux_output_lines (line 194) | def tmux_output_lines
function tmux_completion_menu (line 200) | def tmux_completion_menu
function tmux_completion_menu_basic (line 220) | def tmux_completion_menu_basic
FILE: features/support/env.rb
function type (line 99) | def type(*args)
function history (line 103) | def history
function assert_command_run (line 112) | def assert_command_run cmd
function edit_hub_config (line 117) | def edit_hub_config
function empty_commit (line 138) | def empty_commit(message = nil)
function shell_escape (line 146) | def shell_escape(message)
function run_ignored_command (line 151) | def run_ignored_command(cmd_string)
FILE: features/support/local_server.rb
type Hub (line 7) | module Hub
class LocalServer (line 8) | class LocalServer
class Identify (line 9) | class Identify < Struct.new(:app)
method call (line 10) | def call(env)
method ports (line 19) | def self.ports
class JsonParamsParser (line 23) | class JsonParamsParser < Struct.new(:app)
method call (line 24) | def call(env)
method input_parsed? (line 33) | def input_parsed? env
method type_match? (line 37) | def type_match? env
class App (line 43) | class App < Sinatra::Base
method invoke (line 44) | def invoke
method blank_response? (line 52) | def blank_response?(obj)
method start_sinatra (line 57) | def self.start_sinatra(&block)
method initialize (line 123) | def initialize(app, host = '127.0.0.1')
method responsive? (line 130) | def responsive?
method start (line 140) | def start
method start_handler (line 162) | def start_handler(app)
method server_options (line 170) | def server_options
method stop (line 180) | def stop
FILE: fixtures/fixtures.go
function Path (line 8) | func Path(segment ...string) string {
FILE: fixtures/test_configs.go
type TestConfigs (line 8) | type TestConfigs struct
method TearDown (line 12) | func (c *TestConfigs) TearDown() {
function SetupTomlTestConfig (line 17) | func SetupTomlTestConfig() *TestConfigs {
function SetupTomlTestConfigWithUnixSocket (line 31) | func SetupTomlTestConfigWithUnixSocket() *TestConfigs {
function SetupTestConfigs (line 46) | func SetupTestConfigs() *TestConfigs {
function SetupTestConfigsWithUnixSocket (line 60) | func SetupTestConfigsWithUnixSocket() *TestConfigs {
function SetupTestConfigsInvalidHostName (line 75) | func SetupTestConfigsInvalidHostName() *TestConfigs {
function SetupTestConfigsInvalidHostEntry (line 90) | func SetupTestConfigsInvalidHostEntry() *TestConfigs {
function SetupTestConfigsInvalidPropertyValue (line 101) | func SetupTestConfigsInvalidPropertyValue() *TestConfigs {
FILE: fixtures/test_repo.go
type TestRepo (line 12) | type TestRepo struct
method AddRemote (line 17) | func (r *TestRepo) AddRemote(name, url, pushURL string) {
method AddFile (line 30) | func (r *TestRepo) AddFile(filePath string, content string) {
function SetupTestRepo (line 40) | func SetupTestRepo() *TestRepo {
FILE: git/git.go
function Version (line 14) | func Version() (string, error) {
function Dir (line 25) | func Dir() (string, error) {
function WorkdirName (line 68) | func WorkdirName() (string, error) {
function HasFile (line 79) | func HasFile(segments ...string) bool {
function Editor (line 107) | func Editor() (string, error) {
function Head (line 118) | func Head() (string, error) {
function SymbolicRef (line 123) | func SymbolicRef(ref string) (string, error) {
function SymbolicFullName (line 131) | func SymbolicFullName(name string) (string, error) {
function Ref (line 142) | func Ref(ref string) (string, error) {
function RefList (line 153) | func RefList(a, b string) ([]string, error) {
function NewRange (line 165) | func NewRange(a, b string) (*Range, error) {
type Range (line 180) | type Range struct
method IsIdentical (line 185) | func (r *Range) IsIdentical() bool {
method IsAncestor (line 189) | func (r *Range) IsAncestor() bool {
function CommentChar (line 194) | func CommentChar(text string) (string, error) {
function Show (line 216) | func Show(sha string) (string, error) {
function Log (line 226) | func Log(sha1, sha2 string) (string, error) {
function Remotes (line 242) | func Remotes() ([]string, error) {
function Config (line 249) | func Config(name string) (string, error) {
function ConfigAll (line 253) | func ConfigAll(name string) ([]string, error) {
function GlobalConfig (line 267) | func GlobalConfig(name string) (string, error) {
function SetGlobalConfig (line 271) | func SetGlobalConfig(name, value string) error {
function gitGetConfig (line 276) | func gitGetConfig(args ...string) (string, error) {
function gitConfig (line 286) | func gitConfig(args ...string) ([]string, error) {
function gitConfigCommand (line 292) | func gitConfigCommand(args []string) []string {
function Alias (line 297) | func Alias(name string) (string, error) {
function Run (line 301) | func Run(args ...string) error {
function Spawn (line 306) | func Spawn(args ...string) error {
function Quiet (line 311) | func Quiet(args ...string) bool {
function IsGitDir (line 316) | func IsGitDir(dir string) bool {
function LocalBranches (line 322) | func LocalBranches() ([]string, error) {
function outputLines (line 336) | func outputLines(output string) []string {
function firstLine (line 344) | func firstLine(output string) string {
function gitCmd (line 351) | func gitCmd(args ...string) *cmd.Cmd {
function IsBuiltInGitCommand (line 365) | func IsBuiltInGitCommand(command string) bool {
FILE: git/git_test.go
function TestGitDir (line 13) | func TestGitDir(t *testing.T) {
function TestGitEditor (line 21) | func TestGitEditor(t *testing.T) {
function TestGitLog (line 45) | func TestGitLog(t *testing.T) {
function TestGitRef (line 54) | func TestGitRef(t *testing.T) {
function TestGitRefList (line 64) | func TestGitRefList(t *testing.T) {
function TestGitShow (line 75) | func TestGitShow(t *testing.T) {
function TestGitConfig (line 84) | func TestGitConfig(t *testing.T) {
function TestRemotes (line 102) | func TestRemotes(t *testing.T) {
function TestCommentChar (line 158) | func TestCommentChar(t *testing.T) {
FILE: git/ssh_config.go
constant hostReStr (line 14) | hostReStr = "(?i)^[ \t]*(host|hostname)[ \t]+(.+)$"
type SSHConfig (line 17) | type SSHConfig
function newSSHConfigReader (line 19) | func newSSHConfigReader() *SSHConfigReader {
type SSHConfigReader (line 33) | type SSHConfigReader struct
method Read (line 37) | func (r *SSHConfigReader) Read() SSHConfig {
method readFile (line 48) | func (r *SSHConfigReader) readFile(c SSHConfig, re *regexp.Regexp, f s...
function expandTokens (line 79) | func expandTokens(text, host string) string {
FILE: git/ssh_config_test.go
function TestSSHConfigReader_Read (line 11) | func TestSSHConfigReader_Read(t *testing.T) {
function TestSSHConfigReader_ExpandTokens (line 28) | func TestSSHConfigReader_ExpandTokens(t *testing.T) {
FILE: git/url.go
type URLParser (line 14) | type URLParser struct
method Parse (line 18) | func (p *URLParser) Parse(rawURL string) (u *url.URL, err error) {
function ParseURL (line 58) | func ParseURL(rawURL string) (u *url.URL, err error) {
FILE: git/url_test.go
function createURLParser (line 9) | func createURLParser() *URLParser {
function TestURLParser_ParseURL_HTTPURL (line 18) | func TestURLParser_ParseURL_HTTPURL(t *testing.T) {
function TestURLParser_ParseURL_GitURL (line 28) | func TestURLParser_ParseURL_GitURL(t *testing.T) {
function TestURLParser_ParseURL_SSHURL (line 50) | func TestURLParser_ParseURL_SSHURL(t *testing.T) {
function TestURLParser_ParseURL_LocalPath (line 73) | func TestURLParser_ParseURL_LocalPath(t *testing.T) {
FILE: github/branch.go
type Branch (line 11) | type Branch struct
method ShortName (line 16) | func (b *Branch) ShortName() string {
method LongName (line 21) | func (b *Branch) LongName() string {
method RemoteName (line 26) | func (b *Branch) RemoteName() string {
method Upstream (line 35) | func (b *Branch) Upstream() (u *Branch, err error) {
method IsMaster (line 46) | func (b *Branch) IsMaster() bool {
method IsRemote (line 51) | func (b *Branch) IsRemote() bool {
FILE: github/branch_test.go
function TestBranch_ShortName (line 9) | func TestBranch_ShortName(t *testing.T) {
function TestBranch_LongName (line 15) | func TestBranch_LongName(t *testing.T) {
function TestBranch_RemoteName (line 25) | func TestBranch_RemoteName(t *testing.T) {
function TestBranch_IsRemote (line 35) | func TestBranch_IsRemote(t *testing.T) {
FILE: github/client.go
constant GitHubHost (line 24) | GitHubHost string = "github.com"
constant OAuthAppURL (line 25) | OAuthAppURL string = "https://hub.github.com/"
function NewClient (line 30) | func NewClient(h string) *Client {
function NewClientWithHost (line 34) | func NewClientWithHost(host *Host) *Client {
type Client (line 38) | type Client struct
method FetchPullRequests (line 58) | func (client *Client) FetchPullRequests(project *Project, filterParams...
method PullRequest (line 97) | func (client *Client) PullRequest(project *Project, id string) (pr *Pu...
method PullRequestPatch (line 114) | func (client *Client) PullRequestPatch(project *Project, id string) (p...
method CreatePullRequest (line 128) | func (client *Client) CreatePullRequest(project *Project, params map[s...
method MergePullRequest (line 155) | func (client *Client) MergePullRequest(project *Project, prNumber int,...
method DeleteBranch (line 171) | func (client *Client) DeleteBranch(project *Project, branchName string...
method RequestReview (line 191) | func (client *Client) RequestReview(project *Project, prNumber int, pa...
method CommitPatch (line 206) | func (client *Client) CommitPatch(project *Project, sha string) (patch...
method GistPatch (line 220) | func (client *Client) GistPatch(id string) (patch io.ReadCloser, err e...
method Repository (line 249) | func (client *Client) Repository(project *Project) (repo *Repository, ...
method CreateRepository (line 265) | func (client *Client) CreateRepository(project *Project, description, ...
method DeleteRepository (line 293) | func (client *Client) DeleteRepository(project *Project) error {
method FetchReleases (line 328) | func (client *Client) FetchReleases(project *Project, limit int, filte...
method FetchRelease (line 364) | func (client *Client) FetchRelease(project *Project, tagName string) (...
method CreateRelease (line 378) | func (client *Client) CreateRelease(project *Project, releaseParams *R...
method EditRelease (line 394) | func (client *Client) EditRelease(release *Release, releaseParams map[...
method DeleteRelease (line 410) | func (client *Client) DeleteRelease(release *Release) (err error) {
method UploadReleaseAssets (line 431) | func (client *Client) UploadReleaseAssets(release *Release, assets []L...
method DeleteReleaseAsset (line 491) | func (client *Client) DeleteReleaseAsset(asset *ReleaseAsset) (err err...
method DownloadReleaseAsset (line 503) | func (client *Client) DownloadReleaseAsset(url string) (asset io.ReadC...
method FetchCIStatus (line 539) | func (client *Client) FetchCIStatus(project *Project, sha string) (sta...
method ForkRepository (line 617) | func (client *Client) ForkRepository(project *Project, params map[stri...
method FetchIssues (line 725) | func (client *Client) FetchIssues(project *Project, filterParams map[s...
method FetchIssue (line 764) | func (client *Client) FetchIssue(project *Project, number string) (iss...
method FetchComments (line 780) | func (client *Client) FetchComments(project *Project, number string) (...
method CreateIssue (line 796) | func (client *Client) CreateIssue(project *Project, params interface{}...
method UpdateIssue (line 812) | func (client *Client) UpdateIssue(project *Project, issueNumber int, p...
method FetchLabels (line 839) | func (client *Client) FetchLabels(project *Project) (labels []IssueLab...
method FetchMilestones (line 869) | func (client *Client) FetchMilestones(project *Project) (milestones []...
method GenericAPIRequest (line 897) | func (client *Client) GenericAPIRequest(method, path string, data inte...
method GraphQL (line 931) | func (client *Client) GraphQL(query string, variables interface{}, dat...
method CurrentUser (line 969) | func (client *Client) CurrentUser() (user *User, err error) {
method FindOrCreateToken (line 1001) | func (client *Client) FindOrCreateToken(user, password, twoFactorCode ...
method ensureAccessToken (line 1057) | func (client *Client) ensureAccessToken() error {
method simpleAPI (line 1068) | func (client *Client) simpleAPI() (c *simpleClient, err error) {
method apiClient (line 1095) | func (client *Client) apiClient() *simpleClient {
method absolute (line 1109) | func (client *Client) absolute(host string) *url.URL {
method FetchGist (line 1119) | func (client *Client) FetchGist(id string) (gist *Gist, err error) {
method CreateGist (line 1134) | func (client *Client) CreateGist(filenames []string, public bool) (gis...
type Gist (line 43) | type Gist struct
type GistFile (line 51) | type GistFile struct
type PullRequestMergeResponse (line 149) | type PullRequestMergeResponse struct
type Release (line 304) | type Release struct
type ReleaseAsset (line 321) | type ReleaseAsset struct
type LocalAsset (line 424) | type LocalAsset struct
type CIStatusResponse (line 517) | type CIStatusResponse struct
type CIStatus (line 522) | type CIStatus struct
type CheckRunsResponse (line 528) | type CheckRunsResponse struct
type CheckRun (line 532) | type CheckRun struct
type Repository (line 599) | type Repository struct
type RepositoryPermissions (line 611) | type RepositoryPermissions struct
type Comment (line 634) | type Comment struct
type Issue (line 641) | type Issue struct
type PullRequest (line 673) | type PullRequest
method IsSameRepo (line 682) | func (pr *PullRequest) IsSameRepo() bool {
method HasRequestedReviewer (line 688) | func (pr *PullRequest) HasRequestedReviewer(name string) bool {
method HasRequestedTeam (line 697) | func (pr *PullRequest) HasRequestedTeam(name string) bool {
type PullRequestSpec (line 675) | type PullRequestSpec struct
type IssueLabel (line 706) | type IssueLabel struct
type User (line 711) | type User struct
type Team (line 715) | type Team struct
type Milestone (line 720) | type Milestone struct
type sortedLabels (line 827) | type sortedLabels
method Len (line 829) | func (s sortedLabels) Len() int {
method Swap (line 832) | func (s sortedLabels) Swap(i, j int) {
method Less (line 835) | func (s sortedLabels) Less(i, j int) bool {
type AuthorizationEntry (line 985) | type AuthorizationEntry struct
function isToken (line 989) | func isToken(api *simpleClient, password string) bool {
function normalizeHost (line 1173) | func normalizeHost(host string) string {
function reverseNormalizeHost (line 1185) | func reverseNormalizeHost(host string) string {
function checkStatus (line 1196) | func checkStatus(expectedStatus int, action string, response *simpleResp...
function FormatError (line 1214) | func FormatError(action string, err error) error {
function formatError (line 1221) | func formatError(action string, e *errorInfo) error {
function ValidateGitHubSSO (line 1270) | func ValidateGitHubSSO(res *http.Response) error {
function ValidateSufficientOAuthScopes (line 1285) | func ValidateSufficientOAuthScopes(res *http.Response) error {
function isGistWrite (line 1308) | func isGistWrite(req *http.Request) bool {
type scopeSet (line 1316) | type scopeSet
method String (line 1318) | func (s scopeSet) String() string {
method Intersects (line 1327) | func (s scopeSet) Intersects(other scopeSet) bool {
function newScopeSet (line 1336) | func newScopeSet(s string) scopeSet {
function authTokenNote (line 1346) | func authTokenNote(num int) (string, error) {
function perPage (line 1374) | func perPage(limit, max int) int {
function addQuery (line 1384) | func addQuery(path string, params map[string]interface{}) string {
FILE: github/client_test.go
function TestClient_FormatError (line 12) | func TestClient_FormatError(t *testing.T) {
function TestAuthTokenNote (line 34) | func TestAuthTokenNote(t *testing.T) {
FILE: github/config.go
type yamlHost (line 21) | type yamlHost struct
type Host (line 28) | type Host struct
type Config (line 36) | type Config struct
method PromptForHost (line 42) | func (c *Config) PromptForHost(host string) (h *Host, err error) {
method authorizeClient (line 118) | func (c *Config) authorizeClient(client *Client, host string) (err err...
method DetectToken (line 146) | func (c *Config) DetectToken() string {
method PromptForUser (line 150) | func (c *Config) PromptForUser(host string) (user string) {
method PromptForPassword (line 162) | func (c *Config) PromptForPassword(host, user string) (pass string) {
method PromptForOTP (line 180) | func (c *Config) PromptForOTP() string {
method scanLine (line 185) | func (c *Config) scanLine() string {
method Find (line 230) | func (c *Config) Find(host string) *Host {
method selectHost (line 240) | func (c *Config) selectHost() *Host {
method DefaultHost (line 338) | func (c *Config) DefaultHost() (host *Host, err error) {
method DefaultHostNoPrompt (line 352) | func (c *Config) DefaultHostNoPrompt() (*Host, error) {
function getPassword (line 199) | func getPassword() (string, error) {
function configsFile (line 265) | func configsFile() string {
function homeConfig (line 277) | func homeConfig() (string, error) {
function determineConfigLocation (line 285) | func determineConfigLocation() (string, error) {
function CurrentConfig (line 327) | func CurrentConfig() *Config {
function CheckWriteable (line 367) | func CheckWriteable(filename string) error {
function CreateTestConfigs (line 397) | func CreateTestConfigs(user, token string) *Config {
FILE: github/config_decoder.go
type configDecoder (line 12) | type configDecoder interface
type tomlConfigDecoder (line 16) | type tomlConfigDecoder struct
method Decode (line 19) | func (t *tomlConfigDecoder) Decode(r io.Reader, c *Config) error {
type yamlConfigDecoder (line 24) | type yamlConfigDecoder struct
method Decode (line 27) | func (y *yamlConfigDecoder) Decode(r io.Reader, c *Config) error {
FILE: github/config_encoder.go
type configEncoder (line 10) | type configEncoder interface
type tomlConfigEncoder (line 14) | type tomlConfigEncoder struct
method Encode (line 17) | func (t *tomlConfigEncoder) Encode(w io.Writer, c *Config) error {
type yamlConfigEncoder (line 22) | type yamlConfigEncoder struct
method Encode (line 25) | func (y *yamlConfigEncoder) Encode(w io.Writer, c *Config) error {
FILE: github/config_service.go
function newConfigService (line 8) | func newConfigService() *configService {
type configService (line 15) | type configService struct
method Save (line 20) | func (s *configService) Save(filename string, c *Config) error {
method Load (line 35) | func (s *configService) Load(filename string, c *Config) error {
FILE: github/config_service_test.go
function TestConfigService_TomlLoad (line 13) | func TestConfigService_TomlLoad(t *testing.T) {
function TestConfigService_TomlLoad_UnixSocket (line 33) | func TestConfigService_TomlLoad_UnixSocket(t *testing.T) {
function TestConfigService_YamlLoad (line 55) | func TestConfigService_YamlLoad(t *testing.T) {
function TestConfigService_YamlLoad_Unix_Socket (line 75) | func TestConfigService_YamlLoad_Unix_Socket(t *testing.T) {
function TestConfigService_YamlLoad_Invalid_HostName (line 97) | func TestConfigService_YamlLoad_Invalid_HostName(t *testing.T) {
function TestConfigService_YamlLoad_Invalid_HostEntry (line 112) | func TestConfigService_YamlLoad_Invalid_HostEntry(t *testing.T) {
function TestConfigService_YamlLoad_Invalid_PropertyValue (line 127) | func TestConfigService_YamlLoad_Invalid_PropertyValue(t *testing.T) {
function TestConfigService_TomlSave (line 142) | func TestConfigService_TomlSave(t *testing.T) {
function TestConfigService_TomlSave_UnixSocket (line 170) | func TestConfigService_TomlSave_UnixSocket(t *testing.T) {
function TestConfigService_YamlSave (line 200) | func TestConfigService_YamlSave(t *testing.T) {
function TestConfigService_YamlSave_UnixSocket (line 227) | func TestConfigService_YamlSave_UnixSocket(t *testing.T) {
FILE: github/crash_report.go
constant hubReportCrashConfig (line 20) | hubReportCrashConfig = "hub.reportCrash"
constant hubProjectOwner (line 21) | hubProjectOwner = "github"
constant hubProjectName (line 22) | hubProjectName = "hub"
function CaptureCrash (line 25) | func CaptureCrash() {
function reportCrash (line 39) | func reportCrash(err error) {
function isOption (line 69) | func isOption(confirm, short, long string) bool {
function report (line 73) | func report(reportedError error, stack string) {
constant crashReportTmpl (line 93) | crashReportTmpl = "Crash report - %v\n\n" +
function reportTitleAndBody (line 99) | func reportTitleAndBody(reportedError error, stack string) (title, body ...
function runtimeInfo (line 138) | func runtimeInfo() string {
function formatStack (line 142) | func formatStack(buf []byte) string {
function reportCrashConfig (line 151) | func reportCrashConfig() (opt string) {
FILE: github/crash_report_test.go
function TestStackRemoveSelfAndPanic (line 9) | func TestStackRemoveSelfAndPanic(t *testing.T) {
FILE: github/editor.go
constant Scissors (line 18) | Scissors = "------------------------ >8 ------------------------"
function NewEditor (line 20) | func NewEditor(filename, topic, message string) (editor *Editor, err err...
type Editor (line 49) | type Editor struct
method AddCommentedSection (line 59) | func (e *Editor) AddCommentedSection(text string) {
method DeleteFile (line 71) | func (e *Editor) DeleteFile() error {
method EditContent (line 75) | func (e *Editor) EditContent() (content string, err error) {
method openAndEdit (line 102) | func (e *Editor) openAndEdit() (content []byte, err error) {
method writeContent (line 120) | func (e *Editor) writeContent() (err error) {
method isFileExist (line 131) | func (e *Editor) isFileExist() bool {
method readContent (line 136) | func (e *Editor) readContent() (content []byte, err error) {
function openTextEditor (line 140) | func openTextEditor(program, file string) error {
FILE: github/editor_test.go
function TestEditor_openAndEdit_deleteFileWhenOpeningEditorFails (line 12) | func TestEditor_openAndEdit_deleteFileWhenOpeningEditorFails(t *testing....
function TestEditor_openAndEdit_readFileIfExist (line 39) | func TestEditor_openAndEdit_readFileIfExist(t *testing.T) {
function TestEditor_openAndEdit_writeFileIfNotExist (line 60) | func TestEditor_openAndEdit_writeFileIfNotExist(t *testing.T) {
FILE: github/hosts.go
type HostError (line 17) | type HostError struct
method Error (line 21) | func (e *HostError) Error() string {
function knownGitHubHostsInclude (line 25) | func knownGitHubHostsInclude(host string) bool {
function knownGitHubHosts (line 35) | func knownGitHubHosts() []string {
function DefaultGitHubHost (line 57) | func DefaultGitHubHost() string {
FILE: github/http.go
constant apiPayloadVersion (line 29) | apiPayloadVersion = "application/vnd.github.v3+json;charset=utf-8"
constant patchMediaType (line 30) | patchMediaType = "application/vnd.github.v3.patch;charset=utf-8"
constant textMediaType (line 31) | textMediaType = "text/plain;charset=utf-8"
constant checksType (line 32) | checksType = "application/vnd.github.antiope-preview+json;charset=utf-8"
constant draftsType (line 33) | draftsType = "application/vnd.github.shadow-cat-preview+json;charset=utf-8"
constant cacheVersion (line 34) | cacheVersion = 2
constant rateLimitRemainingHeader (line 37) | rateLimitRemainingHeader = "X-Ratelimit-Remaining"
constant rateLimitResetHeader (line 38) | rateLimitResetHeader = "X-Ratelimit-Reset"
type verboseTransport (line 54) | type verboseTransport struct
method RoundTrip (line 62) | func (t *verboseTransport) RoundTrip(req *http.Request) (resp *http.Re...
method dumpRequest (line 90) | func (t *verboseTransport) dumpRequest(req *http.Request) {
method dumpResponse (line 103) | func (t *verboseTransport) dumpResponse(resp *http.Response) {
method dumpHeaders (line 116) | func (t *verboseTransport) dumpHeaders(header http.Header, indent stri...
method dumpBody (line 137) | func (t *verboseTransport) dumpBody(body io.ReadCloser) io.ReadCloser {
method verbosePrintln (line 154) | func (t *verboseTransport) verbosePrintln(msg string) {
function inspectableType (line 164) | func inspectableType(ct string) bool {
function newHTTPClient (line 168) | func newHTTPClient(testHost string, verbose bool, unixSocket string) *ht...
function checkRedirect (line 212) | func checkRedirect(req *http.Request, via []*http.Request) error {
function cloneRequest (line 236) | func cloneRequest(req *http.Request) *http.Request {
function proxyFromEnvironment (line 249) | func proxyFromEnvironment(req *http.Request) (*url.URL, error) {
type simpleClient (line 256) | type simpleClient struct
method performRequest (line 263) | func (c *simpleClient) performRequest(method, path string, body io.Rea...
method performRequestURL (line 277) | func (c *simpleClient) performRequestURL(method string, url *url.URL, ...
method cacheRead (line 317) | func (c *simpleClient) cacheRead(key string, req *http.Request) (res *...
method cacheWrite (line 368) | func (c *simpleClient) cacheWrite(key string, res *http.Response) {
method jsonRequest (line 440) | func (c *simpleClient) jsonRequest(method, path string, body interface...
method Get (line 455) | func (c *simpleClient) Get(path string) (*simpleResponse, error) {
method GetFile (line 459) | func (c *simpleClient) GetFile(path string, mimeType string) (*simpleR...
method Delete (line 465) | func (c *simpleClient) Delete(path string) (*simpleResponse, error) {
method PostJSON (line 469) | func (c *simpleClient) PostJSON(path string, payload interface{}) (*si...
method PostJSONPreview (line 473) | func (c *simpleClient) PostJSONPreview(path string, payload interface{...
method PutJSON (line 479) | func (c *simpleClient) PutJSON(path string, payload interface{}) (*sim...
method PatchJSON (line 483) | func (c *simpleClient) PatchJSON(path string, payload interface{}) (*s...
method PostFile (line 487) | func (c *simpleClient) PostFile(path string, contents io.Reader, fileS...
function isGraphQL (line 309) | func isGraphQL(req *http.Request) bool {
function canCache (line 313) | func canCache(req *http.Request) bool {
type readCloserCallback (line 395) | type readCloserCallback struct
method Close (line 401) | func (rc *readCloserCallback) Close() error {
function cacheKey (line 409) | func cacheKey(req *http.Request) string {
function cacheFile (line 436) | func cacheFile(key string) string {
type simpleResponse (line 496) | type simpleResponse struct
method Unmarshal (line 520) | func (res *simpleResponse) Unmarshal(dest interface{}) (err error) {
method ErrorInfo (line 531) | func (res *simpleResponse) ErrorInfo() (msg *errorInfo, err error) {
method Link (line 560) | func (res *simpleResponse) Link(name string) string {
method RateLimitRemaining (line 571) | func (res *simpleResponse) RateLimitRemaining() int {
method RateLimitReset (line 580) | func (res *simpleResponse) RateLimitReset() int {
type errorInfo (line 500) | type errorInfo struct
method Error (line 516) | func (e *errorInfo) Error() string {
type errorInfoSimple (line 505) | type errorInfoSimple struct
type fieldError (line 509) | type fieldError struct
FILE: github/http_test.go
function setupTestServer (line 18) | func setupTestServer(unixSocket string) *testServer {
type testServer (line 39) | type testServer struct
method Close (line 45) | func (s *testServer) Close() {
function TestNewHttpClient_OverrideURL (line 49) | func TestNewHttpClient_OverrideURL(t *testing.T) {
function TestNewHttpClient_UnixSocket (line 70) | func TestNewHttpClient_UnixSocket(t *testing.T) {
function TestVerboseTransport_VerbosePrintln (line 85) | func TestVerboseTransport_VerbosePrintln(t *testing.T) {
FILE: github/localrepo.go
function LocalRepo (line 11) | func LocalRepo() (repo *GitHubRepo, err error) {
type GitHubRepo (line 23) | type GitHubRepo struct
method loadRemotes (line 27) | func (r *GitHubRepo) loadRemotes() error {
method RemoteByName (line 41) | func (r *GitHubRepo) RemoteByName(name string) (*Remote, error) {
method remotesForPublish (line 55) | func (r *GitHubRepo) remotesForPublish(owner string) (remotes []Remote) {
method CurrentBranch (line 96) | func (r *GitHubRepo) CurrentBranch() (branch *Branch, err error) {
method MasterBranch (line 107) | func (r *GitHubRepo) MasterBranch() *Branch {
method DefaultBranch (line 114) | func (r *GitHubRepo) DefaultBranch(remote *Remote) *Branch {
method RemoteBranchAndProject (line 127) | func (r *GitHubRepo) RemoteBranchAndProject(owner string, preferUpstre...
method RemoteForBranch (line 193) | func (r *GitHubRepo) RemoteForBranch(branch *Branch, owner string) *Re...
method RemoteForRepo (line 203) | func (r *GitHubRepo) RemoteForRepo(repo *Repository) (*Remote, error) {
method RemoteForProject (line 225) | func (r *GitHubRepo) RemoteForProject(project *Project) (*Remote, erro...
method MainRemote (line 239) | func (r *GitHubRepo) MainRemote() (*Remote, error) {
method MainProject (line 248) | func (r *GitHubRepo) MainProject() (*Project, error) {
method CurrentProject (line 259) | func (r *GitHubRepo) CurrentProject() (project *Project, err error) {
method UpstreamProject (line 268) | func (r *GitHubRepo) UpstreamProject() (project *Project, err error) {
FILE: github/localrepo_test.go
function TestGitHubRepo_remotesForPublish (line 10) | func TestGitHubRepo_remotesForPublish(t *testing.T) {
FILE: github/message_builder.go
type MessageBuilder (line 8) | type MessageBuilder struct
method AddCommentedSection (line 17) | func (b *MessageBuilder) AddCommentedSection(section string) {
method Extract (line 21) | func (b *MessageBuilder) Extract() (title, body string, err error) {
method Cleanup (line 49) | func (b *MessageBuilder) Cleanup() {
function SplitTitleBody (line 55) | func SplitTitleBody(content string) (title string, body string) {
FILE: github/message_builder_test.go
function TestMessageBuilder_multiline_title (line 9) | func TestMessageBuilder_multiline_title(t *testing.T) {
FILE: github/project.go
type Project (line 14) | type Project struct
method String (line 21) | func (p Project) String() string {
method SameAs (line 25) | func (p *Project) SameAs(other *Project) bool {
method WebURL (line 31) | func (p *Project) WebURL(name, owner, path string) string {
method GitURL (line 65) | func (p *Project) GitURL(name, owner string, allowPush bool) string {
function rawHost (line 89) | func rawHost(host string) string {
function preferredProtocol (line 99) | func preferredProtocol() string {
function NewProjectFromRepo (line 107) | func NewProjectFromRepo(repo *Repository) (p *Project, err error) {
function NewProjectFromURL (line 117) | func NewProjectFromURL(url *url.URL) (p *Project, err error) {
function NewProject (line 135) | func NewProject(owner, name, host string) *Project {
function newProject (line 139) | func newProject(owner, name, host, protocol string) *Project {
function SanitizeProjectName (line 189) | func SanitizeProjectName(name string) string {
FILE: github/project_test.go
function TestSameAs (line 12) | func TestSameAs(t *testing.T) {
function TestProject_WebURL (line 59) | func TestProject_WebURL(t *testing.T) {
function TestProject_GitURL (line 83) | func TestProject_GitURL(t *testing.T) {
function TestProject_GitURLEnterprise (line 108) | func TestProject_GitURLEnterprise(t *testing.T) {
function TestProject_NewProjectFromURL (line 133) | func TestProject_NewProjectFromURL(t *testing.T) {
FILE: github/remote.go
type Remote (line 16) | type Remote struct
method String (line 22) | func (remote *Remote) String() string {
method Project (line 26) | func (remote *Remote) Project() (*Project, error) {
function Remotes (line 34) | func Remotes() (remotes []Remote, err error) {
function newRemote (line 83) | func newRemote(name string, urlMap map[string]string) (Remote, error) {
FILE: github/remote_test.go
function TestGithubRemote_NoPush (line 10) | func TestGithubRemote_NoPush(t *testing.T) {
function TestGithubRemote_GitPlusSsh (line 28) | func TestGithubRemote_GitPlusSsh(t *testing.T) {
function TestGithubRemote_SshPort (line 46) | func TestGithubRemote_SshPort(t *testing.T) {
function TestGithubRemote_ColonSlash (line 64) | func TestGithubRemote_ColonSlash(t *testing.T) {
FILE: github/reset_console.go
function setConsole (line 12) | func setConsole(cmd *cmd.Cmd) {
FILE: github/reset_console_windows.go
function setConsole (line 9) | func setConsole(cmd *cmd.Cmd) {
FILE: github/template.go
constant PullRequestTemplate (line 12) | PullRequestTemplate = "pull_request_template"
constant IssueTemplate (line 13) | IssueTemplate = "issue_template"
constant githubTemplateDir (line 14) | githubTemplateDir = ".github"
constant docsDir (line 15) | docsDir = "docs"
function ReadTemplate (line 18) | func ReadTemplate(kind, workdir string) (body string, err error) {
type sortedFiles (line 36) | type sortedFiles
method Len (line 38) | func (s sortedFiles) Len() int {
method Swap (line 41) | func (s sortedFiles) Swap(i, j int) {
method Less (line 44) | func (s sortedFiles) Less(i, j int) bool {
function getFilePath (line 48) | func getFilePath(dir, pattern string) (found string, err error) {
function readContentsFromFile (line 69) | func readContentsFromFile(filename string) (contents string, err error) {
FILE: github/template_test.go
function TestGithubTemplate_withoutTemplate (line 20) | func TestGithubTemplate_withoutTemplate(t *testing.T) {
function TestGithubTemplate_withInvalidTemplate (line 34) | func TestGithubTemplate_withInvalidTemplate(t *testing.T) {
function TestGithubTemplate_WithMarkdown (line 50) | func TestGithubTemplate_WithMarkdown(t *testing.T) {
function TestGithubTemplate_WithTemplateInHome (line 70) | func TestGithubTemplate_WithTemplateInHome(t *testing.T) {
function TestGithubTemplate_WithTemplateInGithubDir (line 86) | func TestGithubTemplate_WithTemplateInGithubDir(t *testing.T) {
function TestGithubTemplate_WithTemplateInGithubDirAndMarkdown (line 102) | func TestGithubTemplate_WithTemplateInGithubDirAndMarkdown(t *testing.T) {
function TestGithubTemplate_WithTemplateInDocsDir (line 123) | func TestGithubTemplate_WithTemplateInDocsDir(t *testing.T) {
function addGithubTemplates (line 139) | func addGithubTemplates(r *fixtures.TestRepo, config map[string]string) {
FILE: github/url.go
type URL (line 8) | type URL struct
method ProjectPath (line 13) | func (url URL) ProjectPath() (projectPath string) {
function ParseURL (line 22) | func ParseURL(rawurl string) (*URL, error) {
FILE: github/url_test.go
function TestParseURL (line 10) | func TestParseURL(t *testing.T) {
FILE: internal/assert/assert.go
function Equal (line 13) | func Equal(t testing.TB, want, got interface{}, args ...interface{}) {
function NotEqual (line 22) | func NotEqual(t testing.TB, want, got interface{}, args ...interface{}) {
function T (line 31) | func T(t testing.TB, ok bool, args ...interface{}) {
FILE: main.go
function main (line 16) | func main() {
function handleError (line 23) | func handleError(err error) int {
FILE: md2roff-bin/cmd.go
function init (line 30) | func init() {
type templateData (line 34) | type templateData struct
function generateFromFile (line 44) | func generateFromFile(mdFile string) error {
function main (line 134) | func main() {
FILE: md2roff/renderer.go
constant ParserExtensions (line 16) | ParserExtensions = blackfriday.NoIntraEmphasis |
function escape (line 34) | func escape(src []byte, re *regexp.Regexp) []byte {
function roffText (line 40) | func roffText(src []byte) []byte {
type RoffRenderer (line 44) | type RoffRenderer struct
method RenderHeader (line 55) | func (r *RoffRenderer) RenderHeader(buf io.Writer, ast *blackfriday.No...
method RenderFooter (line 58) | func (r *RoffRenderer) RenderFooter(buf io.Writer, ast *blackfriday.No...
method RenderNode (line 61) | func (r *RoffRenderer) RenderNode(buf io.Writer, node *blackfriday.Nod...
method renderHeading (line 161) | func (r *RoffRenderer) renderHeading(buf io.Writer, node *blackfriday....
function textContent (line 150) | func textContent(node *blackfriday.Node) []byte {
function sanitizeInput (line 196) | func sanitizeInput(src []byte) []byte {
type renderOption (line 203) | type renderOption struct
function Opt (line 208) | func Opt(buffer io.Writer, renderer blackfriday.Renderer) *renderOption {
function Generate (line 212) | func Generate(src []byte, opts ...*renderOption) {
FILE: ui/format.go
function Expand (line 11) | func Expand(format string, values map[string]string, colorize bool) stri...
type expander (line 17) | type expander struct
method Expand (line 35) | func (f *expander) Expand(format string) string {
method append (line 46) | func (f *expander) append(formattedText ...string) {
method crush (line 50) | func (f *expander) crush() string {
method expandOneVar (line 68) | func (f *expander) expandOneVar(format string) (expand string, untouch...
method expandSpecialChar (line 100) | func (f *expander) expandSpecialChar(firstChar byte, format string) (e...
method pad (line 151) | func (f *expander) pad(s string, p *padder) string {
type paddingOrientation (line 188) | type paddingOrientation
constant padRight (line 191) | padRight paddingOrientation = iota
constant padLeft (line 192) | padLeft
constant padMiddle (line 193) | padMiddle
type truncingMethod (line 196) | type truncingMethod
constant truncLeft (line 199) | truncLeft truncingMethod = iota
constant truncRight (line 200) | truncRight
constant truncMiddle (line 201) | truncMiddle
type padder (line 204) | type padder struct
method truncate (line 246) | func (p *padder) truncate(s string, numReduce int) string {
function padderFromConfig (line 214) | func padderFromConfig(alsoLeft, orientation, asColumn, size, trunc strin...
FILE: ui/format_test.go
type expanderTest (line 7) | type expanderTest struct
function testExpander (line 15) | func testExpander(t *testing.T, tests []expanderTest) {
function TestExpand (line 23) | func TestExpand(t *testing.T) {
function TestExpand_Modifiers (line 64) | func TestExpand_Modifiers(t *testing.T) {
function TestExpand_Padding (line 87) | func TestExpand_Padding(t *testing.T) {
function TestExpand_Truncing (line 134) | func TestExpand_Truncing(t *testing.T) {
FILE: ui/ui.go
type UI (line 12) | type UI interface
function Print (line 26) | func Print(a ...interface{}) (n int) {
function Printf (line 35) | func Printf(format string, a ...interface{}) (n int) {
function Println (line 44) | func Println(a ...interface{}) (n int) {
function Errorf (line 53) | func Errorf(format string, a ...interface{}) (n int) {
function Errorln (line 62) | func Errorln(a ...interface{}) (n int) {
function IsTerminal (line 71) | func IsTerminal(f *os.File) bool {
type Console (line 75) | type Console struct
method Print (line 80) | func (c Console) Print(a ...interface{}) (n int, err error) {
method Printf (line 84) | func (c Console) Printf(format string, a ...interface{}) (n int, err e...
method Println (line 88) | func (c Console) Println(a ...interface{}) (n int, err error) {
method Errorf (line 92) | func (c Console) Errorf(format string, a ...interface{}) (n int, err e...
method Errorln (line 96) | func (c Console) Errorln(a ...interface{}) (n int, err error) {
FILE: utils/args_parser.go
type argsFlag (line 10) | type argsFlag struct
method addValue (line 15) | func (f *argsFlag) addValue(v string) {
method lastValue (line 19) | func (f *argsFlag) lastValue() string {
method reset (line 27) | func (f *argsFlag) reset() {
type ArgsParser (line 33) | type ArgsParser struct
method Parse (line 40) | func (p *ArgsParser) Parse(args []string) ([]string, error) {
method RegisterValue (line 136) | func (p *ArgsParser) RegisterValue(name string, aliases ...string) {
method RegisterBool (line 144) | func (p *ArgsParser) RegisterBool(name string, aliases ...string) {
method Value (line 152) | func (p *ArgsParser) Value(name string) string {
method AllValues (line 159) | func (p *ArgsParser) AllValues(name string) []string {
method Bool (line 166) | func (p *ArgsParser) Bool(name string) bool {
method Int (line 173) | func (p *ArgsParser) Int(name string) int {
method HasReceived (line 178) | func (p *ArgsParser) HasReceived(name string) bool {
function NewArgsParser (line 183) | func NewArgsParser() *ArgsParser {
function NewArgsParserWithUsage (line 190) | func NewArgsParserWithUsage(usage string) *ArgsParser {
FILE: utils/args_parser_test.go
function equal (line 9) | func equal(t *testing.T, expected, got interface{}) {
function TestArgsParser (line 16) | func TestArgsParser(t *testing.T) {
function TestArgsParser_RepeatedInvocation (line 33) | func TestArgsParser_RepeatedInvocation(t *testing.T) {
function TestArgsParser_UnknownFlag (line 57) | func TestArgsParser_UnknownFlag(t *testing.T) {
function TestArgsParser_BlankArgs (line 73) | func TestArgsParser_BlankArgs(t *testing.T) {
function TestArgsParser_Values (line 81) | func TestArgsParser_Values(t *testing.T) {
function TestArgsParser_Bool (line 91) | func TestArgsParser_Bool(t *testing.T) {
function TestArgsParser_BoolValue (line 109) | func TestArgsParser_BoolValue(t *testing.T) {
function TestArgsParser_BoolValue_multiple (line 121) | func TestArgsParser_BoolValue_multiple(t *testing.T) {
function TestArgsParser_Shorthand (line 133) | func TestArgsParser_Shorthand(t *testing.T) {
function TestArgsParser_ShorthandEdgeCase (line 147) | func TestArgsParser_ShorthandEdgeCase(t *testing.T) {
function TestArgsParser_Dashes (line 162) | func TestArgsParser_Dashes(t *testing.T) {
function TestArgsParser_RepeatedArg (line 172) | func TestArgsParser_RepeatedArg(t *testing.T) {
function TestArgsParser_Int (line 183) | func TestArgsParser_Int(t *testing.T) {
function TestArgsParser_WithUsage (line 197) | func TestArgsParser_WithUsage(t *testing.T) {
FILE: utils/color.go
function init (line 15) | func init() {
type Color (line 21) | type Color struct
method Distance (line 48) | func (c *Color) Distance(other *Color) float64 {
method Luminance (line 62) | func (c *Color) Luminance() float64 {
method ContrastRatio (line 68) | func (c *Color) ContrastRatio(other *Color) float64 {
function NewColor (line 27) | func NewColor(hex string) (*Color, error) {
function rgbComponentToBoldValue (line 54) | func rgbComponentToBoldValue(component uint8) float64 {
function initColorCube (line 84) | func initColorCube() {
function ditherTo256ColorCode (line 100) | func ditherTo256ColorCode(color *Color) (code int) {
function isTerm24bitColorCapable (line 119) | func isTerm24bitColorCapable() bool {
function RgbToTermColorCode (line 134) | func RgbToTermColorCode(color *Color) string {
FILE: utils/json.go
type state (line 10) | type state struct
function stateKey (line 18) | func stateKey(s *state) string {
function JSONPath (line 32) | func JSONPath(out io.Writer, src io.Reader, colorize bool) (hasNextPage ...
FILE: utils/utils.go
function Check (line 19) | func Check(err error) {
function ConcatPaths (line 26) | func ConcatPaths(paths ...string) string {
function BrowserLauncher (line 30) | func BrowserLauncher() ([]string, error) {
function searchBrowserLauncher (line 45) | func searchBrowserLauncher(goos string) (browser string) {
function CommandPath (line 66) | func CommandPath(cmd string) (string, error) {
function TimeAgo (line 84) | func TimeAgo(t time.Time) string {
FILE: utils/utils_test.go
function TestSearchBrowserLauncher (line 9) | func TestSearchBrowserLauncher(t *testing.T) {
function TestConcatPaths (line 17) | func TestConcatPaths(t *testing.T) {
function TestTimeAgo (line 21) | func TestTimeAgo(t *testing.T) {
Condensed preview — 211 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (839K chars).
[
{
"path": ".ctags.d/go.ctags",
"chars": 97,
"preview": "-R\n--exclude=etc\n--exclude=script\n--exclude=site\n--exclude=tmp\n--exclude=vendor\n--exclude=bundle\n"
},
{
"path": ".dockerignore",
"chars": 24,
"preview": "*\n!Gemfile\n!Gemfile.lock"
},
{
"path": ".gitattributes",
"chars": 124,
"preview": "\n# enforce correct line endings for shell and batch files.\n*.sh text eol=lf\nscript/* text eol=lf\nscript/*.bat text eol=c"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 374,
"preview": "---\nname: Bug report\nabout: Unexpected or broken behavior of \"hub\" command-line tool\ntitle: ''\nlabels: bug\nassignees: ''"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 343,
"preview": "---\nname: Feature request\nabout: Suggest new functionality for \"hub\" command-line tool\ntitle: ''\nlabels: feature\nassigne"
},
{
"path": ".github/SECURITY.md",
"chars": 238,
"preview": "Please report security vulnerabilities to mislav@github.com. Thank you!\n\nNote that, unlike the [GitHub CLI](https://gith"
},
{
"path": ".github/dependabot.yml",
"chars": 400,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"github-actions\"\n directory: \"/\"\n schedule:\n interval: \"weekly\"\n "
},
{
"path": ".github/workflows/ci.yml",
"chars": 475,
"preview": "name: CI\n\non: [push, pull_request]\n\njobs:\n build:\n runs-on: ubuntu-latest\n env:\n BUNDLE_BIN: bin\n\n steps:"
},
{
"path": ".github/workflows/release.yml",
"chars": 678,
"preview": "name: Release\non:\n push:\n tags: \"v*\"\n\njobs:\n release:\n name: Publish release\n runs-on: ubuntu-latest\n\n ste"
},
{
"path": ".gitignore",
"chars": 141,
"preview": "*.swp\n*~\n/bin\n.bundle\nbundle/\nshare/doc/*\nshare/man/*\n!share/man/man1/hub.1.md\ntmp/\n*.test\ntarget\n.vagrant\ntags\n/site\n/h"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3222,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "CONTRIBUTING.md",
"chars": 2375,
"preview": "Contributing to hub\n===================\n\nContributions to this project are [released](https://help.github.com/articles/g"
},
{
"path": "Dockerfile",
"chars": 551,
"preview": "FROM ruby:2.6\n\nRUN apt-get update \\\n\t&& apt-get install -y sudo golang --no-install-recommends\nRUN apt-get purge --auto-"
},
{
"path": "Gemfile",
"chars": 96,
"preview": "source 'https://rubygems.org'\n\ngem 'aruba', '~> 1.0.4'\ngem 'cucumber', '~> 3.1.2'\ngem 'sinatra'\n"
},
{
"path": "LICENSE",
"chars": 1059,
"preview": "Copyright (c) 2009 Chris Wanstrath\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this"
},
{
"path": "Makefile",
"chars": 2397,
"preview": "SOURCES = $(shell go list -f '{{range .GoFiles}}{{$$.Dir}}/{{.}}\\\n{{end}}' ./...)\nSOURCE_DATE_EPOCH ?= $(shell date +%s)"
},
{
"path": "README.md",
"chars": 5564,
"preview": "hub is a command line tool that wraps `git` in order to extend it with extra\nfeatures and commands that make working wit"
},
{
"path": "cmd/cmd.go",
"chars": 3241,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/github/hub/v2/ui\"\n)\n\n// Cmd"
},
{
"path": "cmd/cmd_test.go",
"chars": 650,
"preview": "package cmd\n\nimport (\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n\nfunc TestNew(t *testing.T) {\n\texecCmd :"
},
{
"path": "commands/alias.go",
"chars": 2541,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/ui\"\n\t\"github.c"
},
{
"path": "commands/api.go",
"chars": 10084,
"preview": "package commands\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.co"
},
{
"path": "commands/apply.go",
"chars": 2640,
"preview": "package commands\n\nimport (\n\t\"io\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"regexp\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/github/hu"
},
{
"path": "commands/args.go",
"chars": 4764,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/cmd\"\n\t\"github.com/github/hub/v2/utils\"\n)\n\ntype "
},
{
"path": "commands/args_test.go",
"chars": 4438,
"preview": "package commands\n\nimport (\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n\nfunc TestNewArgs(t *testing.T) {\n\t"
},
{
"path": "commands/browse.go",
"chars": 2973,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/github/hub/v2/u"
},
{
"path": "commands/checkout.go",
"chars": 4701,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\n\t\"github.com/github/hub/v2/git\"\n\t\"github.com/github/hub/v2/github\"\n\t\"github"
},
{
"path": "commands/cherry_pick.go",
"chars": 2416,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/github/hub/v2/utils\"\n)\n\nvar"
},
{
"path": "commands/ci_status.go",
"chars": 4473,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"sort\"\n\n\t\"github.com/github/hub/v2/git\"\n\t\"github.com/github/hub/v2/github\"\n\t\"gi"
},
{
"path": "commands/clone.go",
"chars": 3467,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/github/hub/v2/ut"
},
{
"path": "commands/commands.go",
"chars": 3610,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/utils\"\n)\n\nvar (\n\tNameRe = `["
},
{
"path": "commands/commands_test.go",
"chars": 3548,
"preview": "package commands\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/internal/assert\"\n\t\"github"
},
{
"path": "commands/compare.go",
"chars": 4049,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/githu"
},
{
"path": "commands/compare_test.go",
"chars": 262,
"preview": "package commands\n\nimport (\n\t\"github.com/github/hub/v2/internal/assert\"\n\t\"testing\"\n)\n\nfunc TestParseRange(t *testing.T) {"
},
{
"path": "commands/create.go",
"chars": 3916,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/git\"\n\t\"github.com/github/hub/v2/github\"\n\t\"githu"
},
{
"path": "commands/delete.go",
"chars": 2148,
"preview": "package commands\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/g"
},
{
"path": "commands/fetch.go",
"chars": 2287,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/github/hub/v2/ut"
},
{
"path": "commands/fetch_test.go",
"chars": 729,
"preview": "package commands\n\nimport (\n\t\"github.com/github/hub/v2/internal/assert\"\n\t\"testing\"\n)\n\nfunc TestParseRemoteNames(t *testin"
},
{
"path": "commands/fork.go",
"chars": 3452,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/github/hub/v2/ui\"\n\t\"github.com/github"
},
{
"path": "commands/gist.go",
"chars": 3251,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/github/hub/v2/ui\"\n"
},
{
"path": "commands/help.go",
"chars": 5061,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/git\"\n"
},
{
"path": "commands/init.go",
"chars": 2175,
"preview": "package commands\n\nimport (\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/github"
},
{
"path": "commands/init_test.go",
"chars": 2408,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/git"
},
{
"path": "commands/issue.go",
"chars": 23384,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/github/hub/v2/git\"\n\t\"github.com/gith"
},
{
"path": "commands/issue_test.go",
"chars": 6484,
"preview": "package commands\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/github/hub/v2/github\"\n)\n\ntype formatIssueTest struct {\n\tname"
},
{
"path": "commands/merge.go",
"chars": 2473,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/github/hub/v2/utils\"\n)\n\nvar"
},
{
"path": "commands/pr.go",
"chars": 13597,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/git\"\n\t\"github.com/github/hub/v2/gith"
},
{
"path": "commands/pull_request.go",
"chars": 14074,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/github/hub/v2/git\"\n\t\"githu"
},
{
"path": "commands/pull_request_test.go",
"chars": 875,
"preview": "package commands\n\nimport (\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n"
},
{
"path": "commands/push.go",
"chars": 1280,
"preview": "package commands\n\nimport (\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\"github.com/github/hub/v2/utils\"\n)\n\nvar cmdPu"
},
{
"path": "commands/push_test.go",
"chars": 706,
"preview": "package commands\n\nimport (\n\t\"github.com/github/hub/v2/internal/assert\"\n\t\"testing\"\n)\n\nfunc TestTransformPushArgs(t *testi"
},
{
"path": "commands/release.go",
"chars": 17859,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/github/hub/v2/github\"\n\t\""
},
{
"path": "commands/remote.go",
"chars": 3464,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/git\"\n\t\"github.com/github/hub/v2/githu"
},
{
"path": "commands/remote_test.go",
"chars": 1653,
"preview": "package commands\n\nimport (\n\t\"os\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/fixtures\"\n\t\"github.com/github/hub/v2/g"
},
{
"path": "commands/runner.go",
"chars": 3296,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/cmd\"\n\t\"github.com/github/hub/v2/git\"\n\t\"github.c"
},
{
"path": "commands/runner_test.go",
"chars": 416,
"preview": "package commands\n\nimport (\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n\nfunc TestRunner_splitAliasCmd(t *t"
},
{
"path": "commands/submodule.go",
"chars": 770,
"preview": "package commands\n\nvar cmdSubmodule = &Command{\n\tRun: submodule,\n\tGitExtension: true,\n\tUsage: \"submodule "
},
{
"path": "commands/sync.go",
"chars": 3703,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/git\"\n\t\"github.com/github/hub/v2/githu"
},
{
"path": "commands/utils.go",
"chars": 2289,
"preview": "package commands\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/atotto/clipboard"
},
{
"path": "commands/utils_test.go",
"chars": 530,
"preview": "package commands\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n\nfunc TestDirIsN"
},
{
"path": "commands/version.go",
"chars": 478,
"preview": "package commands\n\nimport (\n\t\"github.com/github/hub/v2/ui\"\n\t\"github.com/github/hub/v2/version\"\n)\n\nvar cmdVersion = &Comma"
},
{
"path": "coverage/coverage.go",
"chars": 1086,
"preview": "package coverage\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"reflect\"\n\t\"runtime\"\n)\n\nvar out io.Writer\nvar seen map[string]bool\n\nfunc "
},
{
"path": "cucumber.yml",
"chars": 114,
"preview": "default: --format progress -t 'not @completion'\ncompletion: --format pretty -t @completion\nall: --format progress\n"
},
{
"path": "etc/README.md",
"chars": 1216,
"preview": "# Installation instructions\n\n## Homebrew\n\nIf you're using Homebrew, just run `brew install hub` and you should be all se"
},
{
"path": "etc/hub.bash_completion.sh",
"chars": 10076,
"preview": "# hub tab-completion script for bash.\n# This script complements the completion script that ships with git.\n\n# If there i"
},
{
"path": "etc/hub.fish_completion",
"chars": 8495,
"preview": "complete -c hub --wraps git\n\nfunction __fish_hub_needs_command\n set cmd (commandline -opc)\n if [ (count $cmd) -eq 1 ]\n"
},
{
"path": "etc/hub.zsh_completion",
"chars": 5603,
"preview": "#compdef hub\n\n# Zsh will source this file when attempting to autoload the \"_hub\" function,\n# typically on the first atte"
},
{
"path": "features/README.md",
"chars": 1300,
"preview": "# Cucumber features for hub\n\nHow to run all features:\n\n```sh\nmake bin/cucumber\nbin/cucumber\n```\n\nBecause this can take a"
},
{
"path": "features/alias.feature",
"chars": 3772,
"preview": "Feature: hub alias\n\n Scenario: bash instructions\n Given $SHELL is \"/bin/bash\"\n When I successfully run `hub alias"
},
{
"path": "features/am.feature",
"chars": 3506,
"preview": "Feature: hub am\n Background:\n Given I am in \"git://github.com/mislav/dotfiles.git\" git repo\n And I am \"mislav\" on"
},
{
"path": "features/api.feature",
"chars": 15087,
"preview": "@cache_clear\nFeature: hub api\n Background:\n Given I am \"octokitten\" on github.com with OAuth token \"OTOKEN\"\n\n Scena"
},
{
"path": "features/apply.feature",
"chars": 2963,
"preview": "Feature: hub apply\n Background:\n Given I am in \"git://github.com/mislav/dotfiles.git\" git repo\n And I am \"mislav\""
},
{
"path": "features/authentication.feature",
"chars": 19190,
"preview": "Feature: OAuth authentication\n Background:\n Given I am in \"dotfiles\" git repo\n\n Scenario: Ask for username & passwo"
},
{
"path": "features/bash_completion.feature",
"chars": 1639,
"preview": "@completion\nFeature: bash tab-completion\n\n Background:\n Given my shell is bash\n And I'm using git-distributed bas"
},
{
"path": "features/browse.feature",
"chars": 9945,
"preview": "Feature: hub browse\n Background:\n Given I am \"mislav\" on github.com with OAuth token \"OTOKEN\"\n\n Scenario: No repo\n "
},
{
"path": "features/checkout.feature",
"chars": 12062,
"preview": "Feature: hub checkout <PULLREQ-URL>\n Background:\n Given I am in \"git://github.com/mojombo/jekyll.git\" git repo\n A"
},
{
"path": "features/cherry_pick.feature",
"chars": 2659,
"preview": "Feature: hub cherry-pick\n Background:\n Given I am in \"git://github.com/rtomayko/ronn.git\" git repo\n And I am \"mis"
},
{
"path": "features/ci_status.feature",
"chars": 8550,
"preview": "Feature: hub ci-status\n\n Background:\n Given I am in \"git://github.com/michiels/pencilbox.git\" git repo\n And I am "
},
{
"path": "features/clone.feature",
"chars": 11182,
"preview": "Feature: hub clone\n Background:\n Given I am \"mislav\" on github.com with OAuth token \"OTOKEN\"\n\n Scenario: Clone a pu"
},
{
"path": "features/compare.feature",
"chars": 8419,
"preview": "Feature: hub compare\n Background:\n Given I am in \"git://github.com/mislav/dotfiles.git\" git repo\n And I am \"misla"
},
{
"path": "features/create.feature",
"chars": 9892,
"preview": "Feature: hub create\n Background:\n Given I am in \"dotfiles\" git repo\n And I am \"mislav\" on github.com with OAuth t"
},
{
"path": "features/delete.feature",
"chars": 2714,
"preview": "Feature: hub delete\n Background:\n Given I am \"andreasbaumann\" on github.com with OAuth token \"OTOKEN\"\n\n Scenario: N"
},
{
"path": "features/fetch.feature",
"chars": 6156,
"preview": "Feature: hub fetch\n Background:\n Given I am in \"dotfiles\" git repo\n And the \"origin\" remote has url \"git://github"
},
{
"path": "features/fish_completion.feature",
"chars": 836,
"preview": "@completion\nFeature: fish tab-completion\n\n Background:\n Given my shell is fish\n\n Scenario: \"pu\" matches multiple co"
},
{
"path": "features/fork.feature",
"chars": 11768,
"preview": "Feature: hub fork\n Background:\n Given I am in \"dotfiles\" git repo\n And the \"origin\" remote has url \"git://github."
},
{
"path": "features/gist.feature",
"chars": 5967,
"preview": "Feature: hub gist\n Background:\n Given I am \"octokitten\" on github.com with OAuth token \"OTOKEN\"\n\n Scenario: Fetch a"
},
{
"path": "features/git_compatibility.feature",
"chars": 1046,
"preview": "Feature: git-hub compatibility\n Scenario: If alias named branch exists, it should not be expanded.\n Given I am in \"g"
},
{
"path": "features/help.feature",
"chars": 1864,
"preview": "Feature: hub help\n Scenario: Appends hub help to regular help text\n When I successfully run `hub help`\n Then the "
},
{
"path": "features/init.feature",
"chars": 865,
"preview": "Feature: hub init\n Background:\n Given I am \"mislav\" on github.com with OAuth token \"OTOKEN\"\n Given a directory na"
},
{
"path": "features/issue-transfer.feature",
"chars": 2086,
"preview": "Feature: hub issue transfer\n Background:\n Given I am in \"git://github.com/octocat/hello-world.git\" git repo\n And "
},
{
"path": "features/issue.feature",
"chars": 25866,
"preview": "Feature: hub issue\n Background:\n Given I am in \"git://github.com/github/hub.git\" git repo\n And I am \"cornwe19\" on"
},
{
"path": "features/merge.feature",
"chars": 2929,
"preview": "Feature: hub merge\n Background:\n Given I am in \"hub\" git repo\n And the \"origin\" remote has url \"git://github.com/"
},
{
"path": "features/pr-checkout.feature",
"chars": 2843,
"preview": "Feature: hub pr checkout <PULLREQ-NUMBER>\n Background:\n Given I am in \"git://github.com/mojombo/jekyll.git\" git repo"
},
{
"path": "features/pr-list.feature",
"chars": 7958,
"preview": "Feature: hub pr list\n Background:\n Given I am in \"git://github.com/github/hub.git\" git repo\n And I am \"defunkt\" o"
},
{
"path": "features/pr-merge.feature",
"chars": 6130,
"preview": "Feature: hub pr merge\n Background:\n Given I am in \"git://github.com/friederbluemle/hub.git\" git repo\n And I am \"f"
},
{
"path": "features/pr-show.feature",
"chars": 7311,
"preview": "Feature: hub pr show\n Background:\n Given I am in \"git://github.com/ashemesh/hub.git\" git repo\n And I am \"ashemesh"
},
{
"path": "features/pull_request.feature",
"chars": 48992,
"preview": "Feature: hub pull-request\n Background:\n Given I am in \"git://github.com/mislav/coral.git\" git repo\n And I am \"mis"
},
{
"path": "features/push.feature",
"chars": 993,
"preview": "Feature: hub push\n Background:\n Given I am in \"git://github.com/mislav/coral.git\" git repo\n\n Scenario: Normal push\n"
},
{
"path": "features/release.feature",
"chars": 27731,
"preview": "Feature: hub release\n\n Background:\n Given I am in \"git://github.com/mislav/will_paginate.git\" git repo\n And I am "
},
{
"path": "features/remote_add.feature",
"chars": 8808,
"preview": "Feature: hub remote add\n Background:\n Given I am \"EvilChelu\" on GitHub.com\n And I am in \"dotfiles\" git repo\n\n Sc"
},
{
"path": "features/steps.rb",
"chars": 9731,
"preview": "require 'fileutils'\n\nGiven(/^git protocol is preferred$/) do\n set_environment_variable \"HUB_PROTOCOL\", \"git\"\nend\n\nGiven"
},
{
"path": "features/submodule_add.feature",
"chars": 2739,
"preview": "Feature: hub submodule add\n Background:\n Given I am \"mislav\" on github.com with OAuth token \"OTOKEN\"\n Given I am "
},
{
"path": "features/support/completion.rb",
"chars": 7857,
"preview": "# Driver for completion tests executed via a separate tmux pane in which we\n# spawn an interactive shell, send keystroke"
},
{
"path": "features/support/env.rb",
"chars": 4557,
"preview": "require 'aruba/cucumber'\nrequire 'fileutils'\nrequire 'forwardable'\nrequire 'tmpdir'\nrequire 'open3'\n\nsystem_git = `which"
},
{
"path": "features/support/fakebin/curl",
"chars": 51,
"preview": "#!/bin/bash\n\necho \"curl is not allowed\" >&2\nexit 1\n"
},
{
"path": "features/support/fakebin/git",
"chars": 1471,
"preview": "#!/bin/bash\n# A wrapper for system git that prevents commands such as `clone` or `fetch` to be\n# executed in testing. It"
},
{
"path": "features/support/fakebin/man",
"chars": 44,
"preview": "#!/bin/sh\necho man \"$@\" >> \"$HOME\"/.history\n"
},
{
"path": "features/support/fakebin/open",
"chars": 45,
"preview": "#!/bin/sh\necho open \"$@\" >> \"$HOME\"/.history\n"
},
{
"path": "features/support/local_server.rb",
"chars": 5009,
"preview": "# based on <github.com/jnicklas/capybara/blob/ab62b27/lib/capybara/server.rb>\nrequire 'net/http'\nrequire 'rack/handler/w"
},
{
"path": "features/support/rspec_matchers.rb",
"chars": 1503,
"preview": "# Avoids over-zealous sanitize_text\n# https://github.com/cucumber/aruba/blob/v1.0.4/lib/aruba/matchers/string/output_str"
},
{
"path": "features/sync.feature",
"chars": 2633,
"preview": "Feature: hub sync\n Background:\n Given I am in \"dotfiles\" git repo\n And I make a commit\n And the \"origin\" remot"
},
{
"path": "features/zsh_completion.feature",
"chars": 1830,
"preview": "@completion\nFeature: zsh tab-completion\n\n Background:\n Given my shell is zsh\n And I'm using zsh-distributed base "
},
{
"path": "fixtures/fixtures.go",
"chars": 209,
"preview": "package fixtures\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc Path(segment ...string) string {\n\tpwd, _ := os.Getwd()\n\tp := ["
},
{
"path": "fixtures/release_dir/dir/file2",
"chars": 0,
"preview": ""
},
{
"path": "fixtures/release_dir/dir/file3",
"chars": 0,
"preview": ""
},
{
"path": "fixtures/release_dir/dir/subdir/file4",
"chars": 0,
"preview": ""
},
{
"path": "fixtures/release_dir/file1",
"chars": 0,
"preview": ""
},
{
"path": "fixtures/test.git/HEAD",
"chars": 23,
"preview": "ref: refs/heads/master\n"
},
{
"path": "fixtures/test.git/config",
"chars": 112,
"preview": "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = true\n\tignorecase = true\n\tprecomposeunicode = false\n"
},
{
"path": "fixtures/test.git/description",
"chars": 73,
"preview": "Unnamed repository; edit this file 'description' to name the repository.\n"
},
{
"path": "fixtures/test.git/hooks/applypatch-msg.sample",
"chars": 452,
"preview": "#!/bin/sh\n#\n# An example hook script to check the commit log message taken by\n# applypatch from an e-mail message.\n#\n# T"
},
{
"path": "fixtures/test.git/hooks/commit-msg.sample",
"chars": 896,
"preview": "#!/bin/sh\n#\n# An example hook script to check the commit log message.\n# Called by \"git commit\" with one argument, the na"
},
{
"path": "fixtures/test.git/hooks/post-update.sample",
"chars": 189,
"preview": "#!/bin/sh\n#\n# An example hook script to prepare a packed repository for use over\n# dumb transports.\n#\n# To enable this h"
},
{
"path": "fixtures/test.git/hooks/pre-applypatch.sample",
"chars": 398,
"preview": "#!/bin/sh\n#\n# An example hook script to verify what is about to be committed\n# by applypatch from an e-mail message.\n#\n#"
},
{
"path": "fixtures/test.git/hooks/pre-commit.sample",
"chars": 1704,
"preview": "#!/bin/sh\n#\n# An example hook script to verify what is about to be committed.\n# Called by \"git commit\" with no arguments"
},
{
"path": "fixtures/test.git/hooks/pre-push.sample",
"chars": 1348,
"preview": "#!/bin/sh\n\n# An example hook script to verify what is about to be pushed. Called by \"git\n# push\" after it has checked t"
},
{
"path": "fixtures/test.git/hooks/pre-rebase.sample",
"chars": 4951,
"preview": "#!/bin/sh\n#\n# Copyright (c) 2006, 2008 Junio C Hamano\n#\n# The \"pre-rebase\" hook is run just before \"git rebase\" starts d"
},
{
"path": "fixtures/test.git/hooks/prepare-commit-msg.sample",
"chars": 1239,
"preview": "#!/bin/sh\n#\n# An example hook script to prepare the commit log message.\n# Called by \"git commit\" with the name of the fi"
},
{
"path": "fixtures/test.git/hooks/update.sample",
"chars": 3611,
"preview": "#!/bin/sh\n#\n# An example hook script to blocks unannotated tags from entering.\n# Called by \"git receive-pack\" with argum"
},
{
"path": "fixtures/test.git/info/exclude",
"chars": 240,
"preview": "# git ls-files --others --exclude-from=.git/info/exclude\n# Lines that start with '#' are comments.\n# For a project mostl"
},
{
"path": "fixtures/test.git/objects/08/f4b7b6513dffc6245857e497cfd6101dc47818",
"chars": 76,
"preview": "x\u0001M\n0\u0010F]\u0014s\u0001eb~nx\u0004cMi\u001a)^ߠ7p>xb9U7u\u0011\u0001b\u001d{bgٴ\u0002\u0018ă#\u000eh8\u0001>\u00024\u000fo\n'yȜ],\u0004ڄFk-\u0012jUW*zI\u0007);"
},
{
"path": "fixtures/test.git/objects/9b/5a719a3d76ac9dc2fa635d9b1f34fd73994c06",
"chars": 109,
"preview": "x\u0001K\n\u00021\u0010D]\u0014}\u00013$\u001d\u0010qB\u0010ϐd\u0012\u001dq&\u0012#^s\u00027\u0005UPūXqh\u0011/ZM\tw*cbcG\u00153\u001c\u0013\u0014YIk\u001a g\n6\u0018-Us4\u001di61F#Y,K0iG\u001e|=~b\u00197 $b\rKdD13eKn\u0006\u0019ZC՟{\u0001\u000f\u001eNm"
},
{
"path": "fixtures/test.git/refs/heads/master",
"chars": 41,
"preview": "9b5a719a3d76ac9dc2fa635d9b1f34fd73994c06\n"
},
{
"path": "fixtures/test_configs.go",
"chars": 2553,
"preview": "package fixtures\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n)\n\ntype TestConfigs struct {\n\tPath string\n}\n\nfunc (c *TestConfigs) TearDow"
},
{
"path": "fixtures/test_repo.go",
"chars": 1782,
"preview": "package fixtures\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/github/hub/v2/cmd\"\n)\n\ntype TestRepo "
},
{
"path": "git/git.go",
"chars": 8780,
"preview": "package git\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/cmd\"\n)\n\nvar GlobalFlags []str"
},
{
"path": "git/git_test.go",
"chars": 4624,
"preview": "package git\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/fixtures\"\n\t\"github.com/github/hub/v"
},
{
"path": "git/ssh_config.go",
"chars": 1658,
"preview": "package git\n\nimport (\n\t\"bufio\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/mitchellh/go-homedir\"\n)\n\nconst "
},
{
"path": "git/ssh_config_test.go",
"chars": 878,
"preview": "package git\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n\nfunc TestSSHConfigRe"
},
{
"path": "git/url.go",
"chars": 1266,
"preview": "package git\n\nimport (\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nvar (\n\tcachedSSHConfig SSHConfig\n\tprotocolRe = regexp.Must"
},
{
"path": "git/url_test.go",
"chars": 2437,
"preview": "package git\n\nimport (\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n\nfunc createURLParser() *URLParser {\n\tc "
},
{
"path": "github/branch.go",
"chars": 1001,
"preview": "package github\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/git\"\n)\n\ntype Branch struct {\n\tRepo *Git"
},
{
"path": "github/branch_test.go",
"chars": 851,
"preview": "package github\n\nimport (\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n\nfunc TestBranch_ShortName(t *testing"
},
{
"path": "github/client.go",
"chars": 33831,
"preview": "package github\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os"
},
{
"path": "github/client_test.go",
"chars": 990,
"preview": "package github\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n\nfunc Te"
},
{
"path": "github/config.go",
"chars": 8738,
"preview": "package github\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"string"
},
{
"path": "github/config_decoder.go",
"chars": 1580,
"preview": "package github\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\n\t\"github.com/BurntSushi/toml\"\n\t\"gopkg.in/yaml.v2\"\n)\n\ntype configDeco"
},
{
"path": "github/config_encoder.go",
"chars": 838,
"preview": "package github\n\nimport (\n\t\"io\"\n\n\t\"github.com/BurntSushi/toml\"\n\t\"gopkg.in/yaml.v2\"\n)\n\ntype configEncoder interface {\n\tEnc"
},
{
"path": "github/config_service.go",
"chars": 750,
"preview": "package github\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc newConfigService() *configService {\n\treturn &configService{\n\t\tEn"
},
{
"path": "github/config_service_test.go",
"chars": 6523,
"preview": "package github\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/fixtures\"\n\t\"github.com/git"
},
{
"path": "github/crash_report.go",
"chars": 3441,
"preview": "package github\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/github/"
},
{
"path": "github/crash_report_test.go",
"chars": 1332,
"preview": "package github\n\nimport (\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n\nfunc TestStackRemoveSelfAndPanic(t *"
},
{
"path": "github/editor.go",
"chars": 3129,
"preview": "package github\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com"
},
{
"path": "github/editor_test.go",
"chars": 1903,
"preview": "package github\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n\nfunc TestE"
},
{
"path": "github/hosts.go",
"chars": 1025,
"preview": "package github\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/git\"\n)\n\nvar (\n\tGitHubHostEnv = o"
},
{
"path": "github/http.go",
"chars": 14204,
"preview": "package github\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net\"\n\t"
},
{
"path": "github/http_test.go",
"chars": 2066,
"preview": "package github\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\""
},
{
"path": "github/localrepo.go",
"chars": 5893,
"preview": "package github\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/git\"\n)\n\nfunc LocalRepo() (repo *GitHub"
},
{
"path": "github/localrepo_test.go",
"chars": 524,
"preview": "package github\n\nimport (\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n\nfunc TestGitHubRepo_remot"
},
{
"path": "github/message_builder.go",
"chars": 1280,
"preview": "package github\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n)\n\ntype MessageBuilder struct {\n\tTitle string\n\tFilename "
},
{
"path": "github/message_builder_test.go",
"chars": 400,
"preview": "package github\n\nimport (\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n\nfunc TestMessageBuilder_multiline_ti"
},
{
"path": "github/project.go",
"chars": 3752,
"preview": "package github\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/git\"\n\t\"github.c"
},
{
"path": "github/project_test.go",
"chars": 4374,
"preview": "package github\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/fixtures\"\n\t\"github.com/github/hub/v2/in"
},
{
"path": "github/remote.go",
"chars": 1947,
"preview": "package github\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/github/hub/v2/git\"\n)\n\nvar (\n\tOriginNamesIn"
},
{
"path": "github/remote_test.go",
"chars": 2555,
"preview": "package github\n\nimport (\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/fixtures\"\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n"
},
{
"path": "github/reset_console.go",
"chars": 240,
"preview": "//go:build !windows\n// +build !windows\n\npackage github\n\nimport (\n\t\"os\"\n\n\t\"github.com/github/hub/v2/cmd\"\n)\n\nfunc setConso"
},
{
"path": "github/reset_console_windows.go",
"chars": 159,
"preview": "//go:build windows\n// +build windows\n\npackage github\n\nimport \"github.com/github/hub/v2/cmd\"\n\n// This does nothing on win"
},
{
"path": "github/template.go",
"chars": 1721,
"preview": "package github\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n)\n\nconst (\n\tPullRequestTemplate = \"pull_"
},
{
"path": "github/template_test.go",
"chars": 4049,
"preview": "package github\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/fixtures\"\n\t\"github.com/github/hub"
},
{
"path": "github/url.go",
"chars": 489,
"preview": "package github\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n)\n\ntype URL struct {\n\turl.URL\n\t*Project\n}\n\nfunc (url URL) ProjectPath() ("
},
{
"path": "github/url_test.go",
"chars": 844,
"preview": "package github\n\nimport (\n\t\"testing\"\n\n\t\"github.com/github/hub/v2/fixtures\"\n\t\"github.com/github/hub/v2/internal/assert\"\n)\n"
},
{
"path": "go.mod",
"chars": 661,
"preview": "module github.com/github/hub/v2\n\ngo 1.11\n\nrequire (\n\tgithub.com/BurntSushi/toml v0.3.0\n\tgithub.com/atotto/clipboard v0.0"
},
{
"path": "go.sum",
"chars": 5937,
"preview": "github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY=\ngithub.com/BurntSushi/toml v0.3.0/go.m"
},
{
"path": "internal/assert/assert.go",
"chars": 930,
"preview": "// Package assert provides functions for testing.\npackage assert\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/go"
},
{
"path": "main.go",
"chars": 697,
"preview": "//go:build go1.8\n// +build go1.8\n\npackage main\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"syscall\"\n\n\t\"github.com/github/hub/v2/command"
},
{
"path": "man-template.html",
"chars": 1666,
"preview": "<!doctype html>\n<title>{{.Name}}({{.Section}}) - {{.Title}}</title>\n<meta charset=\"utf-8\">\n\n<style>\nbody {\n margin: 0;\n"
},
{
"path": "md2roff/renderer.go",
"chars": 5874,
"preview": "package md2roff\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/russross/blackfriday\"\n)\n\n/"
},
{
"path": "md2roff-bin/cmd.go",
"chars": 3460,
"preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path\"\n\t\"regexp\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github."
},
{
"path": "script/bootstrap",
"chars": 268,
"preview": "#!/usr/bin/env bash\nset -e\n\nSTATUS=0\n\n{ ruby --version\n bundle install\n bundle binstub cucumber --path bin\n} || {\n ec"
},
{
"path": "script/build",
"chars": 666,
"preview": "#!/usr/bin/env bash\n# Usage: script/build [-o <BIN>]\n# script/build files\n\nset -e\n\nwindows=\n[[ $OS == Windows* ]]"
},
{
"path": "script/build.bat",
"chars": 35,
"preview": "@echo off\r\n\r\nbash script\\build %*\r\n"
},
{
"path": "script/changelog",
"chars": 651,
"preview": "#!/bin/bash\n# vi:ft=sh:\n# Usage: script/changelog [HEAD]\n#\n# Show changes to runtime files between HEAD and previous rel"
},
{
"path": "script/coverage",
"chars": 1933,
"preview": "#!/bin/bash\nset -e\n\nsource_files() {\n script/build files | grep -vE '^\\./(coverage|fixtures|version)/'\n}\n\nprepare() {\n "
},
{
"path": "script/cross-compile",
"chars": 820,
"preview": "#!/usr/bin/env bash\n# Usage: script/cross-compile <version>\n#\n# Packages the project over a matrix of supported OS and a"
},
{
"path": "script/docker",
"chars": 501,
"preview": "#!/bin/bash\n# Usage: script/docker [<cucumber-args>]\nset -e\n\ncontainer=hub-test\nworkdir=/home/app/workdir\n\ndocker build "
},
{
"path": "script/get",
"chars": 1344,
"preview": "#!/bin/bash\n# Usage: curl -fsSL https://github.com/github/hub/raw/master/script/get | bash -s <HUB_VERSION>\n#\n# Download"
},
{
"path": "script/github-release",
"chars": 752,
"preview": "#!/bin/bash\n# Usage: script/cross-compile | script/github-release <tag>\n#\n# Takes in a list of asset filenames + labels "
},
{
"path": "script/install.bat",
"chars": 2107,
"preview": "@echo off\r\nCLS\r\ngoto checkPrivileges\r\n\r\n:: Unfortunately, Windows doesn't have a decent built-in way to append a string "
},
{
"path": "script/install.sh",
"chars": 529,
"preview": "#!/usr/bin/env bash\n# Usage: [sudo] [prefix=/usr/local] ./install\nset -e\n\ncase \"$1\" in\n'-h' | '--help' )\n sed -ne '/^#/"
},
{
"path": "script/package",
"chars": 1442,
"preview": "#!/usr/bin/env bash\n# Usage: script/package <os> <arch> <version>\n#\n# Packages the project as a release asset and prints"
},
{
"path": "script/publish-release",
"chars": 927,
"preview": "#!/usr/bin/env bash\nset -e\n\npublish_documentation() {\n local version=\"$1\"\n local doc_dir=\"site\"\n local doc_branch=\"gh"
},
{
"path": "script/ruby-test",
"chars": 332,
"preview": "#!/usr/bin/env bash\nset -e\n\nif [ -z \"$GITHUB_ACTIONS\" ] && tmux -V; then\n if [ -n \"$CI\" ]; then\n git --version\n b"
},
{
"path": "script/tag-release",
"chars": 543,
"preview": "#!/bin/bash\nset -e\n\nversion_file=\"version/version.go\"\n\nif git diff --exit-code >/dev/null -- \"$version_file\"; then\n ech"
},
{
"path": "script/test",
"chars": 1565,
"preview": "#!/usr/bin/env bash\n# Usage: script/test [--coverage [<MIN>]] [<FEATURES>...]\n#\n# Run Go and Cucumber test suites for hu"
},
{
"path": "script/version",
"chars": 472,
"preview": "#!/usr/bin/env bash\n# Displays hub's release version\nset -e\n\nif [ -n \"$GITHUB_REF\" ]; then\n echo \"${GITHUB_REF#refs/tag"
},
{
"path": "script/version.bat",
"chars": 37,
"preview": "@echo off\r\n\r\nbash script\\version %*\r\n"
},
{
"path": "share/vim/vimfiles/ftdetect/pullrequest.vim",
"chars": 68,
"preview": "autocmd BufNewFile,BufRead PULLREQ_EDITMSG set filetype=pullrequest\n"
},
{
"path": "share/vim/vimfiles/syntax/pullrequest.vim",
"chars": 1992,
"preview": "\" Vim syntax file\n\" Language: Hub Pull Request\n\" Maintainer: Derek Sifford <dereksifford@gmail.com>\n\" Filenames: *.git/P"
},
{
"path": "ui/format.go",
"chars": 5592,
"preview": "package ui\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Expand expands a format string using `git log` message"
},
{
"path": "ui/format_test.go",
"chars": 4296,
"preview": "package ui\n\nimport (\n\t\"testing\"\n)\n\ntype expanderTest struct {\n\tname string\n\tformat string\n\tvalues map[string]str"
},
{
"path": "ui/ui.go",
"chars": 2237,
"preview": "package ui\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/mattn/go-colorable\"\n\t\"github.com/mattn/go-isatty\"\n)\n\ntype UI inter"
}
]
// ... and 11 more files (download for full content)
About this extraction
This page contains the full source code of the mislav/hub GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 211 files (745.7 KB), approximately 216.9k tokens, and a symbol index with 792 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.