2 Commits

Author SHA1 Message Date
cc77d5e17b Add Changelog for v0.3.1 (#139)
Add Changelog for v0.3.1

Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/139
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2020-06-15 15:46:49 +00:00
413b9d6985 --ssh-key should be string not bool (#135) (#137)
--ssh-key should be string not bool (#135)

--ssh-key should be string not bool

Fix #134

Reviewed-on: https://gitea.com/gitea/tea/pulls/135
Reviewed-by: 6543 <6543@noreply.gitea.io>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>

Co-authored-by: techknowlogick <techknowlogick@gitea.io>
Reviewed-on: https://gitea.com/gitea/tea/pulls/137
Reviewed-by: John Olheiser <john.olheiser@gmail.com>
2020-06-15 15:27:15 +00:00
1612 changed files with 108672 additions and 177930 deletions

View File

@ -25,6 +25,8 @@ groups:
name: ENHANCEMENTS
labels:
- kind/enhancement
- kind/refactor
- kind/ui
-
name: SECURITY
labels:

View File

@ -6,10 +6,14 @@ platform:
os: linux
arch: amd64
workspace:
base: /go
path: src/code.gitea.io/tea
steps:
- name: build
pull: always
image: golang:1.16
image: golang:1.13
environment:
GOPROXY: https://goproxy.cn
commands:
@ -27,7 +31,8 @@ steps:
- pull_request
- name: unit-test
image: golang:1.16
pull: always
image: golang:1.13
commands:
- make unit-test-coverage
settings:
@ -40,7 +45,8 @@ steps:
- pull_request
- name: release-test
image: golang:1.16
pull: always
image: golang:1.13
commands:
- make test
settings:
@ -54,7 +60,7 @@ steps:
- name: tag-test
pull: always
image: golang:1.16
image: golang:1.13
commands:
- make test
settings:
@ -64,7 +70,8 @@ steps:
- tag
- name: static
image: golang:1.16
pull: always
image: techknowlogick/xgo:latest
environment:
GOPROXY: https://goproxy.cn
commands:
@ -99,7 +106,7 @@ steps:
image: plugins/s3:1
settings:
acl: public-read
bucket: gitea-artifacts
bucket: releases
endpoint: https://storage.gitea.io
path_style: true
source: "dist/release/*"
@ -119,7 +126,7 @@ steps:
image: plugins/s3:1
settings:
acl: public-read
bucket: gitea-artifacts
bucket: releases
endpoint: https://storage.gitea.io
path_style: true
source: "dist/release/*"
@ -141,7 +148,7 @@ steps:
image: plugins/s3:1
settings:
acl: public-read
bucket: gitea-artifacts
bucket: releases
endpoint: https://storage.gitea.io
path_style: true
source: "dist/release/*"

4
.gitignore vendored
View File

@ -1,8 +1,4 @@
tea
/gitea-vet
.idea/
.history/
dist/
.vscode/

View File

@ -1,116 +1,5 @@
# Changelog
## [v0.7.0](https://gitea.com/gitea/tea/releases/tag/v0.7.0) - 2021-03-12
* BREAKING
* `tea issue create`: move `-b` flag to `-d` (#331)
* Drop `tea notif` shorthand in favor of `tea n` (#307)
* FEATURES
* Add commands for reviews (#315)
* Add `tea comment` and show comments of issues/pulls (#313)
* Add interactive mode for `tea milestone create` (#310)
* Add command to install shell completion (#309)
* Implement PR closing and reopening (#304)
* Add interactive mode for `tea issue create` (#302)
* BUGFIXES
* Introduce workaround for missing pull head sha (#340)
* Don't exit if we can't find a local repo with a remote matching to a login (#336)
* Don't push before creating a pull (#334)
* InitCommand() robustness (#327)
* `tea comment`: handle piped stdin (#322)
* ENHANCEMENTS
* Allow checking out PRs with deleted head branch (#341)
* Markdown renderer: detect terminal width, resolve relative URLs (#332)
* Add more issue / pr creation parameters (#331)
* Improve `tea time` (#319)
* `tea pr checkout`: dont create local branches (#314)
* Add `tea issues --fields`, allow printing labels (#312)
* Add more command shorthands (#307)
* Show PR CI status (#306)
* Make PR workflow helpers more robust (#300)
## [v0.6.0](https://gitea.com/gitea/tea/releases/tag/v0.6.0) - 2020-12-11
* BREAKING
* Add `tea repos search`, improve repo listing (#215)
* Add Detail View for Login (#212)
* FEATURES
* Add interactive mode for `tea pr create` (#279)
* Add organization delete command (#270)
* Add organization list command (#264)
* BUGFIXES
* Forces needed arguments to `tea ms issues` (#297)
* Subcommands work outside of git repos (#285)
* Fix repo flag ignores local repo for login detection (#285)
* Improve ssh handling (#277)
* Issue create return web url (#257)
* Support prerelease gitea instances (#252)
* Fix `tea pr create` within same repo (#248)
* Handle login name case-insensitive on all comands (#227)
* ENHANCEMENTS
* Add `tea login delete` (#296)
* Release delete: add --delete-tag & --confirm (#286)
* Sorted milestones list (#281)
* Pull clean & checkout use token for http(s) auth (#275)
* Show more infos in pull detail view (#271)
* Specify fields to print on `tea repos list` (#223)
* Print times in local timezone (#217)
* Issue create/edit print details (#214)
* Improve `tea logout` (#213)
* Added a shorthand for notifications (#209)
* Common subcommand naming scheme (#208)
* `tea pr checkout`: fetch via ssh if available (#192)
* Major refactor of codebase
* BUILD
* Use gox to cross-compile (#274)
* DOCS
* Update Docs to new code structure (#247)
## [v0.5.0](https://gitea.com/gitea/tea/releases/tag/v0.5.0) - 2020-09-27
* BREAKING
* Add Login Manage Functions (#182)
* FEATURES
* Add Release Subcomands (#195)
* Render Markdown and colorize labels table (#181)
* Add BasicAuth & Interactive for Login (#174)
* Add milestones subcomands (#149)
* BUGFIXES
* Fix Pulls Create (#202)
* Pulls create: detect head branch repo owner (#193)
* Fix Labels Delete (#180)
* ENHANCEMENTS
* Add Pagination Options for List Subcomands (#204)
* Issues/Pulls: Details show State (#196)
* Make issues & pulls subcommands consistent (#188)
* Update SDK to v0.13.0 (#179)
* More Options To Specify Repo (#178)
* Add Repo Create subcomand & enhancements (#173)
* Times: format duration as seconds for machine-readable outputs (#168)
* Add user message to login list view (#166)
## [v0.4.1](https://gitea.com/gitea/tea/releases/tag/v0.4.1) - 2020-09-13
* BUGFIXES
* Notification don't relay on a repo (#159)
## [v0.4.0](https://gitea.com/gitea/tea/pulls?q=&type=all&state=closed&milestone=1264) - 2020-07-18
* FEATURES
* Add notifications subcomand (#148)
* Add subcomand 'pulls create' (#144)
* BUGFIXES
* Fix Login Detection By Repo Param (#151)
* Fix Login List Output (#150)
* Fix --ssh-key Option (#135)
* ENHANCEMENTS
* Subcomand Login Show List By Default (#152)
* BUILD
* Migrate src-d/go-git to go-git/go-git (#128)
* Migrate gitea-sdk to v0.12.0 (#133)
* Migrate yaml lib (#130)
* Add gitea-vet (#121)
## [v0.3.1](https://gitea.com/gitea/tea/pulls?q=&type=all&state=closed&milestone=1265) - 2020-06-15
* BUGFIXES

View File

@ -8,6 +8,7 @@
- [Discuss your design](#discuss-your-design)
- [Testing redux](#testing-redux)
- [Vendoring](#vendoring)
- [Translation](#translation)
- [Code review](#code-review)
- [Styleguide](#styleguide)
- [Sign-off your work](#sign-off-your-work)
@ -19,31 +20,36 @@
## Introduction
This document explains how to contribute changes to TEA.
This document explains how to contribute changes to the Gitea project.
It assumes you have followed the
[installation instructions](https://docs.gitea.io/en-us/).
Sensitive security-related issues should be reported to
[security@gitea.io](mailto:security@gitea.io).
For configuring IDE or code editor to develop Gitea see [IDE and code editor configuration](https://github.com/go-gitea/gitea/tree/master/contrib/ide)
For configuring IDE or code editor to develop Gitea see [IDE and code editor configuration](contrib/ide/)
## Bug reports
Please search the issues on the issue tracker with a variety of keywords
to ensure your bug is not already reported.
If unique, [open an issue](https://gitea.com/gitea/tea/issues/new).
If unique, [open an issue](https://github.com/go-gitea/gitea/issues/new)
and answer the questions so we can understand and reproduce the
problematic behavior.
Please write clear, concise instructions so we can reproduce the behavior—
To show us that the issue you are having is in Gitea itself, please
write clear, concise instructions so we can reproduce the behavior—
even if it seems obvious. The more detailed and specific you are,
the faster we can fix the issue. Check out [How to Report Bugs
Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
Please be kind, remember that TEA comes at no cost to you, and you're
Please be kind, remember that Gitea comes at no cost to you, and you're
getting free help.
## Discuss your design
The project welcomes submissions. If you want to change or add something,
please let everyone know what you're working on—[file an issue](https://gitea.com/gitea/tea/issues/new)!
please let everyone know what you're working on—[file an issue](https://github.com/go-gitea/gitea/issues/new)!
Significant changes must go through the change proposal process
before they can be accepted. To create a proposal, file an issue with
your proposed changes documented, and make sure to note in the title
@ -57,8 +63,15 @@ high-level discussions.
## Testing redux
Before sending code out for review, run all the test by execting: `make test`
Since TEA is an cli tool it should be obvious to test your feature localy first.
Before sending code out for review, run all the tests for the
whole tree to make sure the changes don't break other usage
and keep the compatibility on upgrade. To make sure you are
running the test suite exactly like we do, you should install
the CLI for [Drone CI](https://github.com/drone/drone), as
we are using the server for continous testing, following [these
instructions](http://docs.drone.io/cli-installation/). After that,
you can simply call `drone exec --local --build-event "pull_request"` within
your working directory and it will try to run the test suite locally.
## Vendoring
@ -74,11 +87,31 @@ an existing upstream commit.
You can find more information on how to get started with it on the [dep project website](https://golang.github.io/dep/docs/introduction.html).
## Translation
We do all translation work inside [Crowdin](https://crowdin.com/project/gitea).
The only translation that is maintained in this git repository is
[`en_US.ini`](https://github.com/go-gitea/gitea/blob/master/options/locale/locale_en-US.ini)
and is synced regularily to Crowdin. Once a translation has reached
A SATISFACTORY PERCENTAGE it will be synced back into this repo and
included in the next released version.
## Building Gitea
Generally, the go build tools are installed as-needed in the `Makefile`.
An exception are the tools to build the CSS and images.
- To build CSS: Install [Node.js](https://nodejs.org/en/download/package-manager)
with `npm` and then run `npm install` and `make generate-stylesheets`.
- To build Images: ImageMagick, inkscape and zopflipng binaries must be
available in your `PATH` to run `make generate-images`.
## Code review
Changes to TEA must be reviewed before they are accepted—no matter who
makes the change, even if they are an owner or a maintainer. We use Giteas's
pull request & review workflow to do that. Gitea ensure every PR is reviewed by at least 2 maintainers.
Changes to Gitea must be reviewed before they are accepted—no matter who
makes the change, even if they are an owner or a maintainer. We use GitHub's
pull request workflow to do that. And, we also use [LGTM](http://lgtm.co)
to ensure every PR is reviewed by at least 2 maintainers.
Please try to make your pull request easy to review for us. And, please read
the *[How to get faster PR reviews](https://github.com/kubernetes/community/blob/261cb0fd089b64002c91e8eddceebf032462ccd6/contributors/guide/pull-requests.md#best-practices-for-faster-reviews)* guide;
@ -95,41 +128,6 @@ Some of the key points:
## Styleguide
### Commands
- Subcommands should follow the following structure:
```
tea <noun> <verb> [<noun>] [<flags>]
```
for example:
```
tea issues list
tea pulls create
tea teams add user --team x --user y
```
- Commands should accept nouns as singular & plural by making use of the `Aliases` field.
- The default action without a verb is `list`.
- There is a standard set of verbs: `list/ls`, `create`, `edit`, `delete`
- `ls` lists objects with filter options, and applies pagination where available.
- `delete` should show info what is deleted and ask user again, if force flag`-y` is not set
- Verbs that accept large numbers of flags provide an interactive mode when called without any arguments or flags.
- Try to reuse as many flag definitions as possible, see `cmd/flags/flags.go`.
- Always make sure that the help texts are properly set, and as concise as possible.
### Internal Module Structure
- `cmd`: only contains comand/flag options for `urfave/cli`
- subcomands are in a subpackage named after its parent comand
- `modules/task`: contain func for doing something with gitea
(e.g. create token by user/passwd)
- `modules/print`: contain all functions that print to stdout
- `modules/config`: config tea & login things
- `modules/interact`: contain functions to interact with user by prompts
- `modules/git`: do git related stuff (get info/push/pull/checkout/...)
- `modules/utils`: helper functions used by other functions
### Code Style
Use `make fmt`, check with `make lint`.
For imports you should use the following format (_without_ the comments)
```go
import (
@ -166,7 +164,25 @@ commit automatically with `git commit -s`.
## Release Cycle
Before we reach v1 there is no fixed release cycle.
We adopted a release schedule to streamline the process of working
on, finishing, and issuing releases. The overall goal is to make a
minor release every two months, which breaks down into one month of
general development followed by one month of testing and polishing
known as the release freeze. All the feature pull requests should be
merged in the first month of one release period. And, during the frozen
period, a corresponding release branch is open for fixes backported from
master. Release candidates are made during this period for user testing to
obtain a final version that is maintained in this branch. A release is
maintained by issuing patch releases to only correct critical problems
such as crashes or security issues.
Major release cycles are bimonthly. They always begin on the 25th and end on
the 24th (i.e., the 25th of December to February 24th).
During a development cycle, we may also publish any necessary minor releases
for the previous version. For example, if the latest, published release is
v1.2, then minor changes for the previous release—e.g., v1.1.0 -> v1.1.1—are
still possible.
## Maintainers
@ -192,9 +208,6 @@ https://help.github.com/articles/signing-commits-with-gpg/
## Owners
This repo is part of the Gitea project and as such part of that project's
governance.
Since Gitea is a pure community organization without any company support,
to keep the development healthy we will elect three owners every year. All
contributors may vote to elect up to three candidates, one of which will
@ -217,9 +230,22 @@ I'm honored to having been elected an owner of Gitea, I agree with
and lead the development of Gitea.
```
To honor the past owners, here's the history of the owners and the time
they served:
* 2016-11-04 ~ 2017-12-31
* [Lunny Xiao](https://github.com/lunny) <xiaolunwen@gmail.com>
* [Thomas Boerger](https://github.com/tboerger) <thomas@webhippie.de>
* [Kim Carlbäcker](https://github.com/bkcsoft) <kim.carlbacker@gmail.com>
* 2018-01-01 ~ 2018-12-31
* [Lunny Xiao](https://github.com/lunny) <xiaolunwen@gmail.com>
* [Lauris Bukšis-Haberkorns](https://github.com/lafriks) <lauris@nix.lv>
* [Kim Carlbäcker](https://github.com/bkcsoft) <kim.carlbacker@gmail.com>
## Versions
tea has the `master` branch as a tip branch and has version branches
Gitea has the `master` branch as a tip branch and has version branches
such as `release/v0.9`. `release/v0.9` is a release branch and we will
tag `v0.9.0` for binary download. If `v0.9.0` has bugs, we will accept
pull requests on the `release/v0.9` branch and publish a `v0.9.1` tag,
@ -235,7 +261,7 @@ be reviewed by two maintainers and must pass the automatic tests.
Code that you contribute should use the standard copyright header:
```
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
```

View File

@ -1,63 +0,0 @@
# comparing git forge commandline interfaces
[tea]: https://gitea.com/gitea/tea
[sip]: https://gitea.com/jolheiser/sip
[gitlab]: https://github.com/makkes/gitlab-cli
[glab]: https://github.com/profclems/glab
[gh]: https://cli.github.com
last update: 2020-12-11
## general
/ | [tea][tea] | [sip][sip] | [gitlab][gitlab] | [gh][gh]
-----------------------|:-----:|:-----:|:-----:|:-----:
forge|gitea|gitea|gitlab|github
official forge support|✓|✘|✘|✓
dev status|adding features|maintenance||
platform|any|any|any|any
## philosophy
/ | [tea][tea] | [sip][sip] | [gitlab][gitlab] | [gh][gh]
-----------------------|:-----:|:-----:|:-----:|:-----:
aims to replace git cli|✘|||✓
works with decentralization in mind|✓|✓|✓|✘
per-repo setup needed|✘||✓|✘
workflow helpers|✓|||
interactive mode |[(✓)](https://gitea.com/gitea/tea/issues?type=all&state=open&labels=&milestone=0&assignee=0&q=interactive)|✘| |✓
programmatic mode|✓|||✓
machine readable output|✓|||
follows XDG spec|✓|||
## features
/ | [tea][tea] | [sip][sip] | [gitlab][gitlab] | [gh][gh]
-----------------------|:-----:|:-----:|:-----:|:-----:
open web UI|✓|||
search repos|✓|||
search issues|✘|✓||
textual item search filter syntax|✘|✓||
CRUD repos|[(✓)](https://gitea.com/gitea/tea/issues/239)|||
CRUD issues|[(✓)](https://gitea.com/gitea/tea/issues/229)|||
CRUD milestones|[(✓)](https://gitea.com/gitea/tea/issues/246)|||
CRUD releases|✓|||
CRUD labels|✓|||
CRUD PRs|✓|||
CRUD time tracking|✓|||x
CRUD orgs|[(✓)](https://gitea.com/gitea/tea/issues/287)|||
create PRs from local repo|✓|||
create PRs from remote repo|✓|||
code review|[u](https://gitea.com/gitea/tea/issues/131)|||
merge PRs||||
read comments|[u](https://gitea.com/gitea/tea/issues/172)|||
post comments||||
manage CI|✘|✘|✓|
manage notifications|[(✓)]()|||
administration|[u](https://gitea.com/gitea/tea/issues/161)|✘||✘
markdown rendering|✓|||✓
issue import/export|[u](https://gitea.com/gitea/tea/issues/132)|||
checkout PRs|✓|||
- ✓: supported
- (✓): partial support
- u: upcoming
- ✘: not supported
- ?: unknown

View File

@ -68,11 +68,7 @@ fmt:
.PHONY: vet
vet:
# Default vet
$(GO) vet -mod=vendor $(PACKAGES)
# Custom vet
$(GO) build -mod=vendor code.gitea.io/gitea-vet
$(GO) vet -vettool=gitea-vet $(PACKAGES)
.PHONY: lint
lint:
@ -131,7 +127,7 @@ check: test
.PHONY: install
install: $(wildcard *.go)
$(GO) install -mod=vendor -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)'
$(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)'
.PHONY: build
build: $(EXECUTABLE)
@ -140,18 +136,45 @@ $(EXECUTABLE): $(SOURCES)
$(GO) build -mod=vendor $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
.PHONY: release
release: release-dirs release-os release-compress release-check
release: release-dirs release-windows release-linux release-darwin release-copy release-compress release-check
.PHONY: release-dirs
release-dirs:
mkdir -p $(DIST)/binaries $(DIST)/release
.PHONY: release-os
release-os:
@hash gox > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
cd /tmp && $(GO) get -u github.com/mitchellh/gox; \
.PHONY: release-windows
release-windows:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
cd /tmp && $(GO) get -u src.techknowlogick.com/xgo; \
fi
CGO_ENABLED=0 gox -verbose -cgo=false -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -osarch='!darwin/386 !darwin/arm' -os="windows linux darwin" -arch="386 amd64 arm arm64" -output="$(DIST)/release/tea-$(VERSION)-{{.OS}}-{{.Arch}}"
GO111MODULE=off xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out tea-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-linux
release-linux:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
cd /tmp && $(GO) get -u src.techknowlogick.com/xgo; \
fi
GO111MODULE=off xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/mips64le,linux/mips,linux/mipsle' -out tea-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-darwin
release-darwin:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
cd /tmp && $(GO) get -u src.techknowlogick.com/xgo; \
fi
GO111MODULE=off xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin/*' -out tea-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-copy
release-copy:
cd $(DIST); for file in `find /build -type f -name "*"`; do cp $${file} ./release/; done;
.PHONY: release-compress
release-compress:

140
README.md
View File

@ -1,106 +1,64 @@
# <img alt='' src='https://gitea.com/repo-avatars/550-80a3a8c2ab0e2c2d69f296b7f8582485' height="40"/> *T E A*
# Gitea Command Line Tool for Go
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Release](https://raster.shields.io/badge/dynamic/json.svg?label=release&url=https://gitea.com/api/v1/repos/gitea/tea/releases&query=$[0].tag_name)](https://gitea.com/gitea/tea/releases) [![Build Status](https://drone.gitea.com/api/badges/gitea/tea/status.svg)](https://drone.gitea.com/gitea/tea) [![Join the chat at https://img.shields.io/discord/322538954119184384.svg](https://img.shields.io/discord/322538954119184384.svg)](https://discord.gg/Gitea) [![Go Report Card](https://goreportcard.com/badge/code.gitea.io/tea)](https://goreportcard.com/report/code.gitea.io/tea) [![GoDoc](https://godoc.org/code.gitea.io/tea?status.svg)](https://godoc.org/code.gitea.io/tea)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Release](https://raster.shields.io/badge/dynamic/json.svg?label=release&url=https://gitea.com/api/v1/repos/gitea/tea/releases&query=$[0].tag_name)](https://gitea.com/gitea/tea/releases)
[![Build Status](https://drone.gitea.com/api/badges/gitea/tea/status.svg)](https://drone.gitea.com/gitea/tea)
[![Join the chat at https://img.shields.io/discord/322538954119184384.svg](https://img.shields.io/discord/322538954119184384.svg)](https://discord.gg/Gitea)
[![Go Report Card](https://goreportcard.com/badge/code.gitea.io/tea)](https://goreportcard.com/report/code.gitea.io/tea)
[![GoDoc](https://godoc.org/code.gitea.io/tea?status.svg)](https://godoc.org/code.gitea.io/tea)
### The official CLI for Gitea
![demo gif](./demo.gif)
```
tea - command line tool to interact with Gitea
version 0.7.0-preview
USAGE
tea command [subcommand] [command options] [arguments...]
DESCRIPTION
tea is a productivity helper for Gitea. It can be used to manage most entities on one
or multiple Gitea instances and provides local helpers like 'tea pull checkout'.
tea makes use of context provided by the repository in $PWD if available, but is still
usable independently of $PWD. Configuration is persisted in $XDG_CONFIG_HOME/tea.
COMMANDS
help, h Shows a list of commands or help for one command
ENTITIES:
issues, issue, i List, create and update issues
pulls, pull, pr Manage and checkout pull requests
labels, label Manage issue labels
milestones, milestone, ms List and create milestones
releases, release, r Manage releases
times, time, t Operate on tracked times of a repository's issues & pulls
organizations, organization, org List, create, delete organizations
repos, repo Show repository details
HELPERS:
open, o Open something of the repository in web browser
notifications, notification, n Show notifications
SETUP:
logins, login Log in to a Gitea server
logout Log out from a Gitea server
shellcompletion, autocomplete Install shell completion for tea
OPTIONS
--help, -h show help (default: false)
--version, -v print the version (default: false)
EXAMPLES
tea login add # add a login once to get started
tea pulls # list open pulls for the repo in $PWD
tea pulls --repo $HOME/foo # list open pulls for the repo in $HOME/foo
tea pulls --remote upstream # list open pulls for the repo pointed at by
# your local "upstream" git remote
# list open pulls for any gitea repo at the given login instance
tea pulls --repo gitea/tea --login gitea.com
tea milestone issues 0.7.0 # view open issues for milestone '0.7.0'
tea issue 189 # view contents of issue 189
tea open 189 # open web ui for issue 189
tea open milestones # open web ui for milestones
# send gitea desktop notifications every 5 minutes (bash + libnotify)
while :; do tea notifications --all -o simple | xargs -i notify-send {}; sleep 300; done
ABOUT
Written & maintained by The Gitea Authors.
If you find a bug or want to contribute, we'll welcome you at https://gitea.com/gitea/tea.
More info about Gitea itself on https://gitea.io.
```
- [Compare features with other git forge CLIs](./FEATURE-COMPARISON.md)
- tea uses [code.gitea.io/sdk](https://code.gitea.io/sdk) and interacts with the Gitea API.
This project acts as a command line tool for operating one or multiple Gitea instances. It depends on [code.gitea.io/sdk](https://code.gitea.io/sdk) client SDK implementation written in Go to interact with
the Gitea API implementation.
## Installation
There are different ways to get `tea`:
Currently no prebuilt binaries are provided.
To install, a Go installation is needed.
1. Install via your system package manager:
- macOS via `brew` (gitea-maintained):
```sh
brew tap gitea/tap https://gitea.com/gitea/homebrew-gitea
brew install tea
```
- arch linux ([gitea-tea](https://aur.archlinux.org/packages/gitea-tea), thirdparty)
- alpine linux ([tea](https://pkgs.alpinelinux.org/packages?name=tea&branch=edge), thirdparty)
```sh
go get code.gitea.io/tea
go install code.gitea.io/tea
```
2. Use the prebuilt binaries from [dl.gitea.io](https://dl.gitea.io/tea/)
If the `tea` executable is not found, you might need to set up your `$GOPATH` and `$PATH` variables first:
3. Install from source (go 1.13 or newer is required):
```sh
go get code.gitea.io/tea
go install code.gitea.io/tea
```
```sh
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
```
4. Docker (thirdparty): [tgerczei/tea](https://hub.docker.com/r/tgerczei/tea)
If you have `brew` installed, you can install tea version via:
```sh
brew tap gitea/tap https://gitea.com/gitea/homebrew-gitea
brew install --devel tea
```
## Usage
First of all, you have to create a token on your `personal settings -> application` page of your gitea instance.
Use this token to login with `tea`:
```sh
tea login add --name=try --url=https://try.gitea.io --token=xxxxxx
```
Now you can use the `tea` commands:
```sh
tea issues
tea releases
```
To fetch issues from different repos, use the `--remote` flag (when inside a gitea repository directory) or `--login` & `--repo` flags.
## Compilation
Make sure you have installed a current go version.
To compile the sources yourself run the following:
```sh
git clone https://gitea.com/gitea/tea.git
cd tea
make
go get code.gitea.io/tea
cd "${GOPATH}/src/code.gitea.io/tea"
go build
```
## Contributing
@ -108,11 +66,13 @@ make
Fork -> Patch -> Push -> Pull Request
- `make test` run testsuite
- `make vet` run checks (check the order of imports; preventing failure on CI pipeline beforehand)
- `make vendor` when adding new dependencies
- ... (for other development tasks, check the `Makefile`)
**Please** read the [CONTRIBUTING](CONTRIBUTING.md) documentation, it will tell you about internal structures and concepts.
## Authors
* [Maintainers](https://github.com/orgs/go-gitea/people)
* [Contributors](https://github.com/go-gitea/tea/graphs/contributors)
## License

View File

@ -1,14 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
//+build vendor
package main
// Libraries that are included to vendor utilities used during build.
// These libraries will not be included in a normal compilation.
import (
// for vet
_ "code.gitea.io/gitea-vet"
)

View File

@ -1,106 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"github.com/adrg/xdg"
"github.com/urfave/cli/v2"
)
// CmdAutocomplete manages autocompletion
var CmdAutocomplete = cli.Command{
Name: "shellcompletion",
Aliases: []string{"autocomplete"},
Category: catSetup,
Usage: "Install shell completion for tea",
Description: "Install shell completion for tea",
ArgsUsage: "<shell type> (bash, zsh, powershell)",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "install",
Usage: "Persist in shell config instead of printing commands",
},
},
Action: runAutocompleteAdd,
}
func runAutocompleteAdd(ctx *cli.Context) error {
var remoteFile, localFile, cmds string
shell := ctx.Args().First()
switch shell {
case "zsh":
remoteFile = "contrib/autocomplete.zsh"
localFile = "autocomplete.zsh"
cmds = "echo 'PROG=tea _CLI_ZSH_AUTOCOMPLETE_HACK=1 source %s' >> ~/.zshrc && source ~/.zshrc"
case "bash":
remoteFile = "contrib/autocomplete.sh"
localFile = "autocomplete.sh"
cmds = "echo 'PROG=tea source %s' >> ~/.bashrc && source ~/.bashrc"
case "powershell":
remoteFile = "contrib/autocomplete.ps1"
localFile = "tea.ps1"
cmds = "\"& %s\" >> $profile"
default:
return fmt.Errorf("Must specify valid shell type")
}
localPath, err := xdg.ConfigFile("tea/" + localFile)
if err != nil {
return err
}
cmds = fmt.Sprintf(cmds, localPath)
if err := saveAutoCompleteFile(remoteFile, localPath); err != nil {
return err
}
if ctx.Bool("install") {
fmt.Println("Installing in your shellrc")
installer := exec.Command(shell, "-c", cmds)
if shell == "powershell" {
installer = exec.Command("powershell.exe", "-Command", cmds)
}
out, err := installer.CombinedOutput()
if err != nil {
return fmt.Errorf("Couldn't run the commands: %s %s", err, out)
}
} else {
fmt.Println("\n# Run the following commands to install autocompletion (or use --install)")
fmt.Println(cmds)
}
return nil
}
func saveAutoCompleteFile(file, destPath string) error {
url := fmt.Sprintf("https://gitea.com/gitea/tea/raw/branch/master/%s", file)
fmt.Println("Fetching " + url)
res, err := http.Get(url)
if err != nil {
return err
}
defer res.Body.Close()
writer, err := os.Create(destPath)
if err != nil {
return err
}
defer writer.Close()
_, err = io.Copy(writer, res.Body)
return err
}

View File

@ -1,11 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
var (
catSetup = "SETUP"
catEntities = "ENTITIES"
catHelpers = "HELPERS"
)

View File

@ -1,77 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"fmt"
"io/ioutil"
"strings"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2"
)
// CmdAddComment is the main command to operate with notifications
var CmdAddComment = cli.Command{
Name: "comment",
Aliases: []string{"c"},
Category: catEntities,
Usage: "Add a comment to an issue / pr",
Description: "Add a comment to an issue / pr",
ArgsUsage: "<issue / pr index> [<comment body>]",
Action: runAddComment,
Flags: flags.AllDefaultFlags,
}
func runAddComment(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
args := ctx.Args()
if args.Len() == 0 {
return fmt.Errorf("Please specify issue / pr index")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
body := strings.Join(ctx.Args().Tail(), " ")
if interact.IsStdinPiped() {
// custom solution until https://github.com/AlecAivazis/survey/issues/328 is fixed
if bodyStdin, err := ioutil.ReadAll(ctx.App.Reader); err != nil {
return err
} else if len(bodyStdin) != 0 {
body = strings.Join([]string{body, string(bodyStdin)}, "\n\n")
}
} else if len(body) == 0 {
if body, err = interact.PromptMultiline("Content"); err != nil {
return err
}
}
if len(body) == 0 {
return fmt.Errorf("No comment body provided")
}
client := ctx.Login.Client()
comment, _, err := client.CreateIssueComment(ctx.Owner, ctx.Repo, idx, gitea.CreateIssueCommentOption{
Body: body,
})
if err != nil {
return err
}
print.Comment(comment)
return nil
}

249
cmd/config.go Normal file
View File

@ -0,0 +1,249 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"path/filepath"
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/utils"
"github.com/go-gitea/yaml"
)
// Login represents a login to a gitea server, you even could add multiple logins for one gitea server
type Login struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
Token string `yaml:"token"`
Active bool `yaml:"active"`
SSHHost string `yaml:"ssh_host"`
// optional path to the private key
SSHKey string `yaml:"ssh_key"`
Insecure bool `yaml:"insecure"`
// optional gitea username
User string `yaml:"user"`
}
// Client returns a client to operate Gitea API
func (l *Login) Client() *gitea.Client {
client := gitea.NewClient(l.URL, l.Token)
if l.Insecure {
cookieJar, _ := cookiejar.New(nil)
client.SetHTTPClient(&http.Client{
Jar: cookieJar,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
})
}
return client
}
// GetSSHHost returns SSH host name
func (l *Login) GetSSHHost() string {
if l.SSHHost != "" {
return l.SSHHost
}
u, err := url.Parse(l.URL)
if err != nil {
return ""
}
return u.Hostname()
}
// Config reprensents local configurations
type Config struct {
Logins []Login `yaml:"logins"`
}
var (
config Config
yamlConfigPath string
)
func init() {
homeDir, err := utils.Home()
if err != nil {
log.Fatal("Retrieve home dir failed")
}
dir := filepath.Join(homeDir, ".tea")
err = os.MkdirAll(dir, os.ModePerm)
if err != nil {
log.Fatal("Init tea config dir " + dir + " failed")
}
yamlConfigPath = filepath.Join(dir, "tea.yml")
}
func splitRepo(repoPath string) (string, string) {
p := strings.Split(repoPath, "/")
if len(p) >= 2 {
return p[0], p[1]
}
return repoPath, ""
}
func getActiveLogin() (*Login, error) {
if len(config.Logins) == 0 {
return nil, errors.New("No available login")
}
for _, l := range config.Logins {
if l.Active {
return &l, nil
}
}
return &config.Logins[0], nil
}
func getLoginByName(name string) *Login {
for _, l := range config.Logins {
if l.Name == name {
return &l
}
}
return nil
}
func addLogin(login Login) error {
for _, l := range config.Logins {
if l.Name == login.Name {
if l.URL == login.URL && l.Token == login.Token {
return nil
}
return errors.New("Login name has already been used")
}
if l.URL == login.URL && l.Token == login.Token {
return errors.New("URL has been added")
}
}
u, err := url.Parse(login.URL)
if err != nil {
return err
}
if login.SSHHost == "" {
login.SSHHost = u.Hostname()
}
config.Logins = append(config.Logins, login)
return nil
}
func isFileExist(fileName string) (bool, error) {
f, err := os.Stat(fileName)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
if f.IsDir() {
return false, errors.New("A directory with the same name exists")
}
return true, nil
}
func loadConfig(ymlPath string) error {
exist, _ := isFileExist(ymlPath)
if exist {
Println("Found config file", ymlPath)
bs, err := ioutil.ReadFile(ymlPath)
if err != nil {
return err
}
err = yaml.Unmarshal(bs, &config)
if err != nil {
return err
}
}
return nil
}
func saveConfig(ymlPath string) error {
bs, err := yaml.Marshal(&config)
if err != nil {
return err
}
return ioutil.WriteFile(ymlPath, bs, 0660)
}
func curGitRepoPath() (*Login, string, error) {
repo, err := git.RepoForWorkdir()
if err != nil {
return nil, "", errors.New("No Gitea login found")
}
gitConfig, err := repo.Config()
if err != nil {
return nil, "", err
}
// if no remote
if len(gitConfig.Remotes) == 0 {
return nil, "", errors.New("No remote(s) found in this Git repository")
}
// if only one remote exists
if len(gitConfig.Remotes) >= 1 && len(remoteValue) == 0 {
for remote := range gitConfig.Remotes {
remoteValue = remote
}
if len(gitConfig.Remotes) > 1 {
// if master branch is present, use it as the default remote
masterBranch, ok := gitConfig.Branches["master"]
if ok {
if len(masterBranch.Remote) > 0 {
remoteValue = masterBranch.Remote
}
}
}
}
remoteConfig, ok := gitConfig.Remotes[remoteValue]
if !ok || remoteConfig == nil {
return nil, "", errors.New("Remote " + remoteValue + " not found in this Git repository")
}
for _, l := range config.Logins {
for _, u := range remoteConfig.URLs {
p, err := git.ParseURL(strings.TrimSpace(u))
if err != nil {
return nil, "", fmt.Errorf("Git remote URL parse failed: %s", err.Error())
}
if strings.EqualFold(p.Scheme, "http") || strings.EqualFold(p.Scheme, "https") {
if strings.HasPrefix(u, l.URL) {
ps := strings.Split(p.Path, "/")
path := strings.Join(ps[len(ps)-2:], "/")
return &l, strings.TrimSuffix(path, ".git"), nil
}
} else if strings.EqualFold(p.Scheme, "ssh") {
if l.GetSSHHost() == strings.Split(p.Host, ":")[0] {
return &l, strings.TrimLeft(strings.TrimSuffix(p.Path, ".git"), "/"), nil
}
}
}
}
return nil, "", errors.New("No Gitea login found. You might want to specify --repo (and --login) to work outside of a repository")
}

120
cmd/flags.go Normal file
View File

@ -0,0 +1,120 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"log"
"github.com/urfave/cli/v2"
)
// create global variables for global Flags to simplify
// access to the options without requiring cli.Context
var (
loginValue string
repoValue string
outputValue string
remoteValue string
)
// LoginFlag provides flag to specify tea login profile
var LoginFlag = cli.StringFlag{
Name: "login",
Aliases: []string{"l"},
Usage: "Use a different Gitea login. Optional",
Destination: &loginValue,
}
// RepoFlag provides flag to specify repository
var RepoFlag = cli.StringFlag{
Name: "repo",
Aliases: []string{"r"},
Usage: "Repository to interact with. Optional",
Destination: &repoValue,
}
// RemoteFlag provides flag to specify remote repository
var RemoteFlag = cli.StringFlag{
Name: "remote",
Aliases: []string{"R"},
Usage: "Discover Gitea login from remote. Optional",
Destination: &remoteValue,
}
// OutputFlag provides flag to specify output type
var OutputFlag = cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Output format. (csv, simple, table, tsv, yaml)",
Destination: &outputValue,
}
// LoginOutputFlags defines login and output flags that should
// added to all subcommands and appended to the flags of the
// subcommand to work around issue and provide --login and --output:
// https://github.com/urfave/cli/issues/585
var LoginOutputFlags = []cli.Flag{
&LoginFlag,
&OutputFlag,
}
// LoginRepoFlags defines login and repo flags that should
// be used for all subcommands and appended to the flags of
// the subcommand to work around issue and provide --login and --repo:
// https://github.com/urfave/cli/issues/585
var LoginRepoFlags = []cli.Flag{
&LoginFlag,
&RepoFlag,
&RemoteFlag,
}
// AllDefaultFlags defines flags that should be available
// for all subcommands working with dedicated repositories
// to work around issue and provide --login, --repo and --output:
// https://github.com/urfave/cli/issues/585
var AllDefaultFlags = append([]cli.Flag{
&RepoFlag,
&RemoteFlag,
}, LoginOutputFlags...)
// initCommand returns repository and *Login based on flags
func initCommand() (*Login, string, string) {
login := initCommandLoginOnly()
var err error
repoPath := repoValue
if repoPath == "" {
login, repoPath, err = curGitRepoPath()
if err != nil {
log.Fatal(err.Error())
}
}
owner, repo := splitRepo(repoPath)
return login, owner, repo
}
// initCommandLoginOnly return *Login based on flags
func initCommandLoginOnly() *Login {
err := loadConfig(yamlConfigPath)
if err != nil {
log.Fatal("load config file failed ", yamlConfigPath)
}
var login *Login
if loginValue == "" {
login, err = getActiveLogin()
if err != nil {
log.Fatal(err)
}
} else {
login = getLoginByName(loginValue)
if login == nil {
log.Fatal("Login name " + loginValue + " does not exist")
}
}
return login
}

View File

@ -1,205 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package flags
import (
"fmt"
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v2"
)
// LoginFlag provides flag to specify tea login profile
var LoginFlag = cli.StringFlag{
Name: "login",
Aliases: []string{"l"},
Usage: "Use a different Gitea Login. Optional",
}
// RepoFlag provides flag to specify repository
var RepoFlag = cli.StringFlag{
Name: "repo",
Aliases: []string{"r"},
Usage: "Override local repository path or gitea repository slug to interact with. Optional",
}
// RemoteFlag provides flag to specify remote repository
var RemoteFlag = cli.StringFlag{
Name: "remote",
Aliases: []string{"R"},
Usage: "Discover Gitea login from remote. Optional",
}
// OutputFlag provides flag to specify output type
var OutputFlag = cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Output format. (csv, simple, table, tsv, yaml)",
}
// StateFlag provides flag to specify issue/pr state, defaulting to "open"
var StateFlag = cli.StringFlag{
Name: "state",
Usage: "Filter by state (all|open|closed)",
DefaultText: "open",
}
// PaginationPageFlag provides flag for pagination options
var PaginationPageFlag = cli.StringFlag{
Name: "page",
Aliases: []string{"p"},
Usage: "specify page, default is 1",
}
// PaginationLimitFlag provides flag for pagination options
var PaginationLimitFlag = cli.StringFlag{
Name: "limit",
Aliases: []string{"lm"},
Usage: "specify limit of items per page",
}
// LoginOutputFlags defines login and output flags that should
// added to all subcommands and appended to the flags of the
// subcommand to work around issue and provide --login and --output:
// https://github.com/urfave/cli/issues/585
var LoginOutputFlags = []cli.Flag{
&LoginFlag,
&OutputFlag,
}
// LoginRepoFlags defines login and repo flags that should
// be used for all subcommands and appended to the flags of
// the subcommand to work around issue and provide --login and --repo:
// https://github.com/urfave/cli/issues/585
var LoginRepoFlags = []cli.Flag{
&LoginFlag,
&RepoFlag,
&RemoteFlag,
}
// AllDefaultFlags defines flags that should be available
// for all subcommands working with dedicated repositories
// to work around issue and provide --login, --repo and --output:
// https://github.com/urfave/cli/issues/585
var AllDefaultFlags = append([]cli.Flag{
&RepoFlag,
&RemoteFlag,
}, LoginOutputFlags...)
// IssuePRFlags defines flags that should be available on issue & pr listing flags.
var IssuePRFlags = append([]cli.Flag{
&StateFlag,
&PaginationPageFlag,
&PaginationLimitFlag,
}, AllDefaultFlags...)
// IssuePREditFlags defines flags for properties of issues and PRs
var IssuePREditFlags = append([]cli.Flag{
&cli.StringFlag{
Name: "title",
Aliases: []string{"t"},
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
},
&cli.StringFlag{
Name: "assignees",
Aliases: []string{"a"},
Usage: "Comma-separated list of usernames to assign",
},
&cli.StringFlag{
Name: "labels",
Aliases: []string{"L"},
Usage: "Comma-separated list of labels to assign",
},
&cli.StringFlag{
Name: "deadline",
Aliases: []string{"D"},
Usage: "Deadline timestamp to assign",
},
&cli.StringFlag{
Name: "milestone",
Aliases: []string{"m"},
Usage: "Milestone to assign",
},
}, LoginRepoFlags...)
// GetIssuePREditFlags parses all IssuePREditFlags
func GetIssuePREditFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, error) {
opts := gitea.CreateIssueOption{
Title: ctx.String("title"),
Body: ctx.String("body"),
Assignees: strings.Split(ctx.String("assignees"), ","),
}
var err error
date := ctx.String("deadline")
if date != "" {
t, err := dateparse.ParseAny(date)
if err != nil {
return nil, err
}
opts.Deadline = &t
}
client := ctx.Login.Client()
labelNames := strings.Split(ctx.String("labels"), ",")
if len(labelNames) != 0 {
if client == nil {
client = ctx.Login.Client()
}
if opts.Labels, err = task.ResolveLabelNames(client, ctx.Owner, ctx.Repo, labelNames); err != nil {
return nil, err
}
}
if milestoneName := ctx.String("milestone"); len(milestoneName) != 0 {
if client == nil {
client = ctx.Login.Client()
}
ms, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestoneName)
if err != nil {
return nil, fmt.Errorf("Milestone '%s' not found", milestoneName)
}
opts.Milestone = ms.ID
}
return &opts, nil
}
// FieldsFlag generates a flag selecting printable fields.
// To retrieve the value, use GetFields()
func FieldsFlag(availableFields, defaultFields []string) *cli.StringFlag {
return &cli.StringFlag{
Name: "fields",
Aliases: []string{"f"},
Usage: fmt.Sprintf(`Comma-separated list of fields to print. Available values:
%s
`, strings.Join(availableFields, ",")),
Value: strings.Join(defaultFields, ","),
}
}
// GetFields parses the values provided in a fields flag, and
// optionally validates against valid values.
func GetFields(ctx *cli.Context, validFields []string) ([]string, error) {
selection := strings.Split(ctx.String("fields"), ",")
if validFields != nil {
for _, field := range selection {
if !utils.Contains(validFields, field) {
return nil, fmt.Errorf("Invalid field '%s'", field)
}
}
}
return selection, nil
}

View File

@ -6,12 +6,10 @@ package cmd
import (
"fmt"
"log"
"strconv"
"code.gitea.io/tea/cmd/issues"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
@ -19,53 +17,202 @@ import (
// CmdIssues represents to login a gitea server.
var CmdIssues = cli.Command{
Name: "issues",
Aliases: []string{"issue", "i"},
Category: catEntities,
Usage: "List, create and update issues",
Description: `Lists issues when called without argument. If issue index is provided, will show it in detail.`,
Usage: "List and create issues",
Description: `List and create issues`,
ArgsUsage: "[<issue index>]",
Action: runIssues,
Subcommands: []*cli.Command{
&issues.CmdIssuesList,
&issues.CmdIssuesCreate,
&issues.CmdIssuesReopen,
&issues.CmdIssuesClose,
&CmdIssuesList,
&CmdIssuesCreate,
&CmdIssuesReopen,
&CmdIssuesClose,
},
Flags: AllDefaultFlags,
}
// CmdIssuesList represents a sub command of issues to list issues
var CmdIssuesList = cli.Command{
Name: "ls",
Usage: "List issues of the repository",
Description: `List issues of the repository`,
Action: runIssuesList,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "comments",
Usage: "Wether to display comments (will prompt if not provided & run interactively)",
&cli.StringFlag{
Name: "state",
Usage: "Filter by issue state (all|open|closed)",
DefaultText: "open",
},
}, issues.CmdIssuesList.Flags...),
}, AllDefaultFlags...),
}
func runIssues(ctx *cli.Context) error {
if ctx.Args().Len() == 1 {
return runIssueDetail(ctx, ctx.Args().First())
}
return issues.RunIssuesList(ctx)
return runIssuesList(ctx)
}
func runIssueDetail(cmd *cli.Context, index string) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func runIssueDetail(ctx *cli.Context, index string) error {
login, owner, repo := initCommand()
idx, err := utils.ArgToIndex(index)
idx, err := argToIndex(index)
if err != nil {
return err
}
issue, _, err := ctx.Login.Client().GetIssue(ctx.Owner, ctx.Repo, idx)
issue, err := login.Client().GetIssue(owner, repo, idx)
if err != nil {
return err
}
print.IssueDetails(issue)
if issue.Comments > 0 {
err = interact.ShowCommentsMaybeInteractive(ctx, idx, issue.Comments)
if err != nil {
return fmt.Errorf("error loading comments: %v", err)
fmt.Printf("#%d %s\n%s created %s\n\n%s\n", issue.Index,
issue.Title,
issue.Poster.UserName,
issue.Created.Format("2006-01-02 15:04:05"),
issue.Body,
)
return nil
}
func runIssuesList(ctx *cli.Context) error {
login, owner, repo := initCommand()
state := gitea.StateOpen
switch ctx.String("state") {
case "all":
state = gitea.StateAll
case "open":
state = gitea.StateOpen
case "closed":
state = gitea.StateClosed
}
issues, err := login.Client().ListRepoIssues(owner, repo, gitea.ListIssueOption{
Page: 0,
State: string(state),
Type: gitea.IssueTypeIssue,
})
if err != nil {
log.Fatal(err)
}
headers := []string{
"Index",
"State",
"Author",
"Updated",
"Title",
}
var values [][]string
if len(issues) == 0 {
Output(outputValue, headers, values)
return nil
}
for _, issue := range issues {
name := issue.Poster.FullName
if len(name) == 0 {
name = issue.Poster.UserName
}
values = append(
values,
[]string{
strconv.FormatInt(issue.Index, 10),
string(issue.State),
name,
issue.Updated.Format("2006-01-02 15:04:05"),
issue.Title,
},
)
}
Output(outputValue, headers, values)
return nil
}
// CmdIssuesCreate represents a sub command of issues to create issue
var CmdIssuesCreate = cli.Command{
Name: "create",
Usage: "Create an issue on repository",
Description: `Create an issue on repository`,
Action: runIssuesCreate,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "title",
Aliases: []string{"t"},
Usage: "issue title to create",
},
&cli.StringFlag{
Name: "body",
Aliases: []string{"b"},
Usage: "issue body to create",
},
}, LoginRepoFlags...),
}
func runIssuesCreate(ctx *cli.Context) error {
login, owner, repo := initCommand()
_, err := login.Client().CreateIssue(owner, repo, gitea.CreateIssueOption{
Title: ctx.String("title"),
Body: ctx.String("body"),
// TODO:
//Assignee string `json:"assignee"`
//Assignees []string `json:"assignees"`
//Deadline *time.Time `json:"due_date"`
//Milestone int64 `json:"milestone"`
//Labels []int64 `json:"labels"`
//Closed bool `json:"closed"`
})
if err != nil {
log.Fatal(err)
}
return nil
}
// CmdIssuesReopen represents a sub command of issues to open an issue
var CmdIssuesReopen = cli.Command{
Name: "reopen",
Aliases: []string{"open"},
Usage: "Change state of an issue to 'open'",
Description: `Change state of an issue to 'open'`,
ArgsUsage: "<issue index>",
Action: func(ctx *cli.Context) error {
var s = string(gitea.StateOpen)
return editIssueState(ctx, gitea.EditIssueOption{State: &s})
},
Flags: AllDefaultFlags,
}
// CmdIssuesClose represents a sub command of issues to close an issue
var CmdIssuesClose = cli.Command{
Name: "close",
Usage: "Change state of an issue to 'closed'",
Description: `Change state of an issue to 'closed'`,
ArgsUsage: "<issue index>",
Action: func(ctx *cli.Context) error {
var s = string(gitea.StateClosed)
return editIssueState(ctx, gitea.EditIssueOption{State: &s})
},
Flags: AllDefaultFlags,
}
// editIssueState abstracts the arg parsing to edit the given issue
func editIssueState(ctx *cli.Context, opts gitea.EditIssueOption) error {
login, owner, repo := initCommand()
if ctx.Args().Len() == 0 {
log.Fatal(ctx.Command.ArgsUsage)
}
index, err := argToIndex(ctx.Args().First())
if err != nil {
return err
}
_, err = login.Client().EditIssue(owner, repo, index, opts)
return err
}

View File

@ -1,52 +0,0 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package issues
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdIssuesClose represents a sub command of issues to close an issue
var CmdIssuesClose = cli.Command{
Name: "close",
Usage: "Change state of an issue to 'closed'",
Description: `Change state of an issue to 'closed'`,
ArgsUsage: "<issue index>",
Action: func(ctx *cli.Context) error {
var s = gitea.StateClosed
return editIssueState(ctx, gitea.EditIssueOption{State: &s})
},
Flags: flags.AllDefaultFlags,
}
// editIssueState abstracts the arg parsing to edit the given issue
func editIssueState(cmd *cli.Context, opts gitea.EditIssueOption) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() == 0 {
return fmt.Errorf(ctx.Command.ArgsUsage)
}
index, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
issue, _, err := ctx.Login.Client().EditIssue(ctx.Owner, ctx.Repo, index, opts)
if err != nil {
return err
}
print.IssueDetails(issue)
return nil
}

View File

@ -1,45 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package issues
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"github.com/urfave/cli/v2"
)
// CmdIssuesCreate represents a sub command of issues to create issue
var CmdIssuesCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create an issue on repository",
Description: `Create an issue on repository`,
Action: runIssuesCreate,
Flags: flags.IssuePREditFlags,
}
func runIssuesCreate(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.NumFlags() == 0 {
return interact.CreateIssue(ctx.Login, ctx.Owner, ctx.Repo)
}
opts, err := flags.GetIssuePREditFlags(ctx)
if err != nil {
return err
}
return task.CreateIssue(
ctx.Login,
ctx.Owner,
ctx.Repo,
*opts,
)
}

View File

@ -1,62 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package issues
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdIssuesList represents a sub command of issues to list issues
var CmdIssuesList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List issues of the repository",
Description: `List issues of the repository`,
Action: RunIssuesList,
Flags: append([]cli.Flag{
flags.FieldsFlag(print.IssueFields, []string{
"index", "title", "state", "author", "milestone", "labels",
}),
}, flags.IssuePRFlags...),
}
// RunIssuesList list issues
func RunIssuesList(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
state := gitea.StateOpen
switch ctx.String("state") {
case "all":
state = gitea.StateAll
case "open":
state = gitea.StateOpen
case "closed":
state = gitea.StateClosed
}
issues, _, err := ctx.Login.Client().ListRepoIssues(ctx.Owner, ctx.Repo, gitea.ListIssueOption{
ListOptions: ctx.GetListOptions(),
State: state,
Type: gitea.IssueTypeIssue,
})
if err != nil {
return err
}
fields, err := flags.GetFields(cmd, print.IssueFields)
if err != nil {
return err
}
print.IssuesPullsList(issues, ctx.Output, fields)
return nil
}

View File

@ -1,26 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package issues
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdIssuesReopen represents a sub command of issues to open an issue
var CmdIssuesReopen = cli.Command{
Name: "reopen",
Aliases: []string{"open"},
Usage: "Change state of an issue to 'open'",
Description: `Change state of an issue to 'open'`,
ArgsUsage: "<issue index>",
Action: func(ctx *cli.Context) error {
var s = gitea.StateOpen
return editIssueState(ctx, gitea.EditIssueOption{State: &s})
},
Flags: flags.AllDefaultFlags,
}

View File

@ -5,35 +5,262 @@
package cmd
import (
"bufio"
"fmt"
"log"
"os"
"strconv"
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/cmd/labels"
"github.com/urfave/cli/v2"
)
// CmdLabels represents to operate repositories' labels.
var CmdLabels = cli.Command{
Name: "labels",
Aliases: []string{"label"},
Category: catEntities,
Usage: "Manage issue labels",
Description: `Manage issue labels`,
Action: runLabels,
Subcommands: []*cli.Command{
&labels.CmdLabelsList,
&labels.CmdLabelCreate,
&labels.CmdLabelUpdate,
&labels.CmdLabelDelete,
&CmdLabelCreate,
&CmdLabelUpdate,
&CmdLabelDelete,
},
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "save",
Aliases: []string{"s"},
Usage: "Save all the labels as a file",
},
}, AllDefaultFlags...),
}
func runLabels(ctx *cli.Context) error {
if ctx.Args().Len() == 1 {
return runLabelsDetails(ctx)
login, owner, repo := initCommand()
headers := []string{
"Index",
"Color",
"Name",
"Description",
}
return labels.RunLabelsList(ctx)
var values [][]string
labels, err := login.Client().ListRepoLabels(owner, repo)
if err != nil {
log.Fatal(err)
}
if len(labels) == 0 {
Output(outputValue, headers, values)
return nil
}
fPath := ctx.String("save")
if len(fPath) > 0 {
f, err := os.Create(fPath)
if err != nil {
return err
}
defer f.Close()
for _, label := range labels {
fmt.Fprintf(f, "#%s %s\n", label.Color, label.Name)
}
} else {
for _, label := range labels {
values = append(
values,
[]string{
strconv.FormatInt(label.ID, 10),
label.Color,
label.Name,
label.Description,
},
)
}
Output(outputValue, headers, values)
}
return nil
}
func runLabelsDetails(ctx *cli.Context) error {
return fmt.Errorf("Not yet implemented")
// CmdLabelCreate represents a sub command of labels to create label.
var CmdLabelCreate = cli.Command{
Name: "create",
Usage: "Create a label",
Description: `Create a label`,
Action: runLabelCreate,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "label name",
},
&cli.StringFlag{
Name: "color",
Usage: "label color value",
},
&cli.StringFlag{
Name: "description",
Usage: "label description",
},
&cli.StringFlag{
Name: "file",
Usage: "indicate a label file",
},
},
}
func splitLabelLine(line string) (string, string, string) {
fields := strings.SplitN(line, ";", 2)
var color, name, description string
if len(fields) < 1 {
return "", "", ""
} else if len(fields) >= 2 {
description = strings.TrimSpace(fields[1])
}
fields = strings.Fields(fields[0])
if len(fields) <= 0 {
return "", "", ""
}
color = fields[0]
if len(fields) == 2 {
name = fields[1]
} else if len(fields) > 2 {
name = strings.Join(fields[1:], " ")
}
return color, name, description
}
func runLabelCreate(ctx *cli.Context) error {
login, owner, repo := initCommand()
labelFile := ctx.String("file")
var err error
if len(labelFile) == 0 {
_, err = login.Client().CreateLabel(owner, repo, gitea.CreateLabelOption{
Name: ctx.String("name"),
Color: ctx.String("color"),
Description: ctx.String("description"),
})
} else {
f, err := os.Open(labelFile)
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
var i = 1
// FIXME: if Gitea's API support create multiple labels once, we should move to that API.
for scanner.Scan() {
line := scanner.Text()
color, name, description := splitLabelLine(line)
if color == "" || name == "" {
log.Printf("Line %d ignored because lack of enough fields: %s\n", i, line)
} else {
_, err = login.Client().CreateLabel(owner, repo, gitea.CreateLabelOption{
Name: name,
Color: color,
Description: description,
})
}
i++
}
}
if err != nil {
log.Fatal(err)
}
return nil
}
// CmdLabelUpdate represents a sub command of labels to update label.
var CmdLabelUpdate = cli.Command{
Name: "update",
Usage: "Update a label",
Description: `Update a label`,
Action: runLabelUpdate,
Flags: []cli.Flag{
&cli.IntFlag{
Name: "id",
Usage: "label id",
},
&cli.StringFlag{
Name: "name",
Usage: "label name",
},
&cli.StringFlag{
Name: "color",
Usage: "label color value",
},
&cli.StringFlag{
Name: "description",
Usage: "label description",
},
},
}
func runLabelUpdate(ctx *cli.Context) error {
login, owner, repo := initCommand()
id := ctx.Int64("id")
var pName, pColor, pDescription *string
name := ctx.String("name")
if name != "" {
pName = &name
}
color := ctx.String("color")
if color != "" {
pColor = &color
}
description := ctx.String("description")
if description != "" {
pDescription = &description
}
var err error
_, err = login.Client().EditLabel(owner, repo, id, gitea.EditLabelOption{
Name: pName,
Color: pColor,
Description: pDescription,
})
if err != nil {
log.Fatal(err)
}
return nil
}
// CmdLabelDelete represents a sub command of labels to delete label.
var CmdLabelDelete = cli.Command{
Name: "delete",
Usage: "Delete a label",
Description: `Delete a label`,
Action: runLabelCreate,
Flags: []cli.Flag{
&cli.IntFlag{
Name: "id",
Usage: "label id",
},
},
}
func runLabelDelete(ctx *cli.Context) error {
login, owner, repo := initCommand()
err := login.Client().DeleteLabel(owner, repo, ctx.Int64("id"))
if err != nil {
log.Fatal(err)
}
return nil
}

View File

@ -1,106 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package labels
import (
"bufio"
"log"
"os"
"strings"
"code.gitea.io/tea/modules/context"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdLabelCreate represents a sub command of labels to create label.
var CmdLabelCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create a label",
Description: `Create a label`,
Action: runLabelCreate,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "label name",
},
&cli.StringFlag{
Name: "color",
Usage: "label color value",
},
&cli.StringFlag{
Name: "description",
Usage: "label description",
},
&cli.StringFlag{
Name: "file",
Usage: "indicate a label file",
},
},
}
func runLabelCreate(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
labelFile := ctx.String("file")
var err error
if len(labelFile) == 0 {
_, _, err = ctx.Login.Client().CreateLabel(ctx.Owner, ctx.Repo, gitea.CreateLabelOption{
Name: ctx.String("name"),
Color: ctx.String("color"),
Description: ctx.String("description"),
})
} else {
f, err := os.Open(labelFile)
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
var i = 1
for scanner.Scan() {
line := scanner.Text()
color, name, description := splitLabelLine(line)
if color == "" || name == "" {
log.Printf("Line %d ignored because lack of enough fields: %s\n", i, line)
} else {
_, _, err = ctx.Login.Client().CreateLabel(ctx.Owner, ctx.Repo, gitea.CreateLabelOption{
Name: name,
Color: color,
Description: description,
})
}
i++
}
}
return err
}
func splitLabelLine(line string) (string, string, string) {
fields := strings.SplitN(line, ";", 2)
var color, name, description string
if len(fields) < 1 {
return "", "", ""
} else if len(fields) >= 2 {
description = strings.TrimSpace(fields[1])
}
fields = strings.Fields(fields[0])
if len(fields) <= 0 {
return "", "", ""
}
color = fields[0]
if len(fields) == 2 {
name = fields[1]
} else if len(fields) > 2 {
name = strings.Join(fields[1:], " ")
}
return color, name, description
}

View File

@ -1,34 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package labels
import (
"code.gitea.io/tea/modules/context"
"github.com/urfave/cli/v2"
)
// CmdLabelDelete represents a sub command of labels to delete label.
var CmdLabelDelete = cli.Command{
Name: "delete",
Aliases: []string{"rm"},
Usage: "Delete a label",
Description: `Delete a label`,
Action: runLabelDelete,
Flags: []cli.Flag{
&cli.IntFlag{
Name: "id",
Usage: "label id",
},
},
}
func runLabelDelete(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
_, err := ctx.Login.Client().DeleteLabel(ctx.Owner, ctx.Repo, ctx.Int64("id"))
return err
}

View File

@ -1,54 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package labels
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/task"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdLabelsList represents a sub command of labels to list labels
var CmdLabelsList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List labels",
Description: "List labels",
Action: RunLabelsList,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "save",
Aliases: []string{"s"},
Usage: "Save all the labels as a file",
},
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...),
}
// RunLabelsList list labels.
func RunLabelsList(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
labels, _, err := client.ListRepoLabels(ctx.Owner, ctx.Repo, gitea.ListLabelsOptions{
ListOptions: ctx.GetListOptions(),
})
if err != nil {
return err
}
if ctx.IsSet("save") {
return task.LabelsExport(labels, ctx.String("save"))
}
print.LabelsList(labels, ctx.Output)
return nil
}

View File

@ -1,73 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package labels
import (
"code.gitea.io/tea/modules/context"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdLabelUpdate represents a sub command of labels to update label.
var CmdLabelUpdate = cli.Command{
Name: "update",
Usage: "Update a label",
Description: `Update a label`,
Action: runLabelUpdate,
Flags: []cli.Flag{
&cli.IntFlag{
Name: "id",
Usage: "label id",
},
&cli.StringFlag{
Name: "name",
Usage: "label name",
},
&cli.StringFlag{
Name: "color",
Usage: "label color value",
},
&cli.StringFlag{
Name: "description",
Usage: "label description",
},
},
}
func runLabelUpdate(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
id := ctx.Int64("id")
var pName, pColor, pDescription *string
name := ctx.String("name")
if name != "" {
pName = &name
}
color := ctx.String("color")
if color != "" {
pColor = &color
}
description := ctx.String("description")
if description != "" {
pDescription = &description
}
var err error
_, _, err = ctx.Login.Client().EditLabel(ctx.Owner, ctx.Repo, id, gitea.EditLabelOption{
Name: pName,
Color: pColor,
Description: pDescription,
})
if err != nil {
return err
}
return nil
}

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package labels
package cmd
import (
"bufio"

111
cmd/log.go Normal file
View File

@ -0,0 +1,111 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/olekukonko/tablewriter"
)
var (
showLog bool
)
// Println println content according the flag
func Println(a ...interface{}) {
if showLog {
fmt.Println(a...)
}
}
// Printf printf content according the flag
func Printf(format string, a ...interface{}) {
if showLog {
fmt.Printf(format, a...)
}
}
// Error println content as an error information
func Error(a ...interface{}) {
fmt.Println(a...)
}
// Errorf printf content as an error information
func Errorf(format string, a ...interface{}) {
fmt.Printf(format, a...)
}
// outputtable prints structured data as table
func outputtable(headers []string, values [][]string) {
table := tablewriter.NewWriter(os.Stdout)
if len(headers) > 0 {
table.SetHeader(headers)
}
for _, value := range values {
table.Append(value)
}
table.Render()
}
// outputsimple prints structured data as space delimited value
func outputsimple(headers []string, values [][]string) {
for _, value := range values {
fmt.Printf(strings.Join(value, " "))
fmt.Printf("\n")
}
}
// outputdsv prints structured data as delimiter separated value format
func outputdsv(headers []string, values [][]string, delimiterOpt ...string) {
delimiter := ","
if len(delimiterOpt) > 0 {
delimiter = delimiterOpt[0]
}
fmt.Println("\"" + strings.Join(headers, "\""+delimiter+"\"") + "\"")
for _, value := range values {
fmt.Printf("\"")
fmt.Printf(strings.Join(value, "\""+delimiter+"\""))
fmt.Printf("\"")
fmt.Printf("\n")
}
}
// outputyaml prints structured data as yaml
func outputyaml(headers []string, values [][]string) {
for _, value := range values {
fmt.Println("-")
for j, val := range value {
intVal, _ := strconv.Atoi(val)
if strconv.Itoa(intVal) == val {
fmt.Printf(" %s: %s\n", headers[j], val)
} else {
fmt.Printf(" %s: '%s'\n", headers[j], val)
}
}
}
}
// Output provides general function to convert given information
// into several outputs
func Output(output string, headers []string, values [][]string) {
switch {
case output == "" || output == "table":
outputtable(headers, values)
case output == "csv":
outputdsv(headers, values, ",")
case output == "simple":
outputsimple(headers, values)
case output == "tsv":
outputdsv(headers, values, "\t")
case output == "yaml":
outputyaml(headers, values)
default:
Errorf("unknown output type '" + output + "', available types are:\n- csv: comma-separated values\n- simple: space-separated values\n- table: auto-aligned table format (default)\n- tsv: tab-separated values\n- yaml: YAML format\n")
}
}

View File

@ -1,51 +1,146 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"crypto/tls"
"fmt"
"log"
"net/http"
"net/http/cookiejar"
"code.gitea.io/tea/cmd/login"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdLogin represents to login a gitea server.
var CmdLogin = cli.Command{
Name: "logins",
Aliases: []string{"login"},
Category: catSetup,
Name: "login",
Usage: "Log in to a Gitea server",
Description: `Log in to a Gitea server`,
ArgsUsage: "[<login name>]",
Action: runLogins,
Subcommands: []*cli.Command{
&login.CmdLoginList,
&login.CmdLoginAdd,
&login.CmdLoginEdit,
&login.CmdLoginDelete,
&login.CmdLoginSetDefault,
&cmdLoginList,
&cmdLoginAdd,
},
}
func runLogins(ctx *cli.Context) error {
if ctx.Args().Len() == 1 {
return runLoginDetail(ctx.Args().First())
}
return login.RunLoginList(ctx)
// CmdLogin represents to login a gitea server.
var cmdLoginAdd = cli.Command{
Name: "add",
Usage: "Add a Gitea login",
Description: `Add a Gitea login`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "Login name",
Required: true,
},
&cli.StringFlag{
Name: "url",
Aliases: []string{"u"},
Value: "https://try.gitea.io",
EnvVars: []string{"GITEA_SERVER_URL"},
Usage: "Server URL",
Required: true,
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"t"},
Value: "",
EnvVars: []string{"GITEA_SERVER_TOKEN"},
Usage: "Access token. Can be obtained from Settings > Applications",
Required: true,
},
&cli.StringFlag{
Name: "ssh-key",
Aliases: []string{"s"},
Usage: "Path to a SSH key to use for pull/push operations",
},
&cli.BoolFlag{
Name: "insecure",
Aliases: []string{"i"},
Usage: "Disable TLS verification",
},
},
Action: runLoginAdd,
}
func runLoginDetail(name string) error {
l := config.GetLoginByName(name)
if l == nil {
fmt.Printf("Login '%s' do not exist\n\n", name)
return nil
func runLoginAdd(ctx *cli.Context) error {
if !ctx.IsSet("url") {
log.Fatal("You have to input Gitea server URL")
}
if !ctx.IsSet("token") {
log.Fatal("No token found")
}
if !ctx.IsSet("name") {
log.Fatal("You have to set a name for the login")
}
err := loadConfig(yamlConfigPath)
if err != nil {
log.Fatal("Unable to load config file " + yamlConfigPath)
}
client := gitea.NewClient(ctx.String("url"), ctx.String("token"))
if ctx.Bool("insecure") {
cookieJar, _ := cookiejar.New(nil)
client.SetHTTPClient(&http.Client{
Jar: cookieJar,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
})
}
u, err := client.GetMyUserInfo()
if err != nil {
log.Fatal(err)
}
fmt.Println("Login successful! Login name " + u.UserName)
err = addLogin(Login{
Name: ctx.String("name"),
URL: ctx.String("url"),
Token: ctx.String("token"),
Insecure: ctx.Bool("insecure"),
SSHKey: ctx.String("ssh-key"),
User: u.UserName,
})
if err != nil {
log.Fatal(err)
}
err = saveConfig(yamlConfigPath)
if err != nil {
log.Fatal(err)
}
return nil
}
// CmdLogin represents to login a gitea server.
var cmdLoginList = cli.Command{
Name: "ls",
Usage: "List Gitea logins",
Description: `List Gitea logins`,
Action: runLoginList,
}
func runLoginList(ctx *cli.Context) error {
err := loadConfig(yamlConfigPath)
if err != nil {
log.Fatal("Unable to load config file " + yamlConfigPath)
}
fmt.Printf("Name\tURL\tSSHHost\n")
for _, l := range config.Logins {
fmt.Printf("%s\t%s\t%s\n", l.Name, l.URL, l.GetSSHHost())
}
print.LoginDetails(l)
return nil
}

View File

@ -1,81 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package login
import (
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"github.com/urfave/cli/v2"
)
// CmdLoginAdd represents to login a gitea server.
var CmdLoginAdd = cli.Command{
Name: "add",
Usage: "Add a Gitea login",
Description: `Add a Gitea login, without args it will create one interactively`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "Login name",
},
&cli.StringFlag{
Name: "url",
Aliases: []string{"u"},
Value: "https://gitea.com",
EnvVars: []string{"GITEA_SERVER_URL"},
Usage: "Server URL",
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"t"},
Value: "",
EnvVars: []string{"GITEA_SERVER_TOKEN"},
Usage: "Access token. Can be obtained from Settings > Applications",
},
&cli.StringFlag{
Name: "user",
Value: "",
EnvVars: []string{"GITEA_SERVER_USER"},
Usage: "User for basic auth (will create token)",
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"pwd"},
Value: "",
EnvVars: []string{"GITEA_SERVER_PASSWORD"},
Usage: "Password for basic auth (will create token)",
},
&cli.StringFlag{
Name: "ssh-key",
Aliases: []string{"s"},
Usage: "Path to a SSH key to use, overrides auto-discovery",
},
&cli.BoolFlag{
Name: "insecure",
Aliases: []string{"i"},
Usage: "Disable TLS verification",
},
},
Action: runLoginAdd,
}
func runLoginAdd(ctx *cli.Context) error {
// if no args create login interactive
if ctx.NumFlags() == 0 {
return interact.CreateLogin()
}
// else use args to add login
return task.CreateLogin(
ctx.String("name"),
ctx.String("token"),
ctx.String("user"),
ctx.String("password"),
ctx.String("ssh-key"),
ctx.String("url"),
ctx.Bool("insecure"))
}

View File

@ -1,38 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package login
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"github.com/urfave/cli/v2"
)
// CmdLoginSetDefault represents to login a gitea server.
var CmdLoginSetDefault = cli.Command{
Name: "default",
Usage: "Get or Set Default Login",
Description: `Get or Set Default Login`,
ArgsUsage: "<Login>",
Action: runLoginSetDefault,
Flags: []cli.Flag{&flags.OutputFlag},
}
func runLoginSetDefault(ctx *cli.Context) error {
if ctx.Args().Len() == 0 {
l, err := config.GetDefaultLogin()
if err != nil {
return err
}
fmt.Printf("Default Login: %s\n", l.Name)
return nil
}
name := ctx.Args().First()
return config.SetDefaultLogin(name)
}

View File

@ -1,44 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package login
import (
"errors"
"log"
"code.gitea.io/tea/modules/config"
"github.com/urfave/cli/v2"
)
// CmdLoginDelete is a command to delete a login
var CmdLoginDelete = cli.Command{
Name: "delete",
Aliases: []string{"rm"},
Usage: "Remove a Gitea login",
Description: `Remove a Gitea login`,
ArgsUsage: "<login name>",
Action: RunLoginDelete,
}
// RunLoginDelete runs the action of a login delete command
func RunLoginDelete(ctx *cli.Context) error {
logins, err := config.GetLogins()
if err != nil {
log.Fatal(err)
}
var name string
if len(ctx.Args().First()) != 0 {
name = ctx.Args().First()
} else if len(logins) == 1 {
name = logins[0].Name
} else {
return errors.New("Please specify a login name")
}
return config.DeleteLogin(name)
}

View File

@ -1,27 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package login
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"github.com/skratchdot/open-golang/open"
"github.com/urfave/cli/v2"
)
// CmdLoginEdit represents to login a gitea server.
var CmdLoginEdit = cli.Command{
Name: "edit",
Aliases: []string{"e"},
Usage: "Edit Gitea logins",
Description: `Edit Gitea logins`,
Action: runLoginEdit,
Flags: []cli.Flag{&flags.OutputFlag},
}
func runLoginEdit(_ *cli.Context) error {
return open.Start(config.GetConfigPath())
}

View File

@ -1,33 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package login
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/print"
"github.com/urfave/cli/v2"
)
// CmdLoginList represents to login a gitea server.
var CmdLoginList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List Gitea logins",
Description: `List Gitea logins`,
Action: RunLoginList,
Flags: []cli.Flag{&flags.OutputFlag},
}
// RunLoginList list all logins
func RunLoginList(cmd *cli.Context) error {
logins, err := config.GetLogins()
if err != nil {
return err
}
print.LoginsList(logins, cmd.String("output"))
return nil
}

View File

@ -1,11 +1,13 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"code.gitea.io/tea/cmd/login"
"errors"
"log"
"os"
"github.com/urfave/cli/v2"
)
@ -13,9 +15,47 @@ import (
// CmdLogout represents to logout a gitea server.
var CmdLogout = cli.Command{
Name: "logout",
Category: catSetup,
Usage: "Log out from a Gitea server",
Description: `Log out from a Gitea server`,
ArgsUsage: "<login name>",
Action: login.RunLoginDelete,
Action: runLogout,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "Login name to remove",
},
},
}
func runLogout(ctx *cli.Context) error {
var name string
if len(os.Args) == 3 {
name = os.Args[2]
} else if ctx.IsSet("name") {
name = ctx.String("name")
} else {
return errors.New("Please specify a login name")
}
err := loadConfig(yamlConfigPath)
if err != nil {
log.Fatal("Unable to load config file " + yamlConfigPath)
}
var idx = -1
for i, l := range config.Logins {
if l.Name == name {
idx = i
break
}
}
if idx > -1 {
config.Logins = append(config.Logins[:idx], config.Logins[idx+1:]...)
err = saveConfig(yamlConfigPath)
if err != nil {
log.Fatal("Unable to save config file " + yamlConfigPath)
}
}
return nil
}

View File

@ -1,54 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"code.gitea.io/tea/cmd/milestones"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"github.com/urfave/cli/v2"
)
// CmdMilestones represents to operate repositories milestones.
var CmdMilestones = cli.Command{
Name: "milestones",
Aliases: []string{"milestone", "ms"},
Category: catEntities,
Usage: "List and create milestones",
Description: `List and create milestones`,
ArgsUsage: "[<milestone name>]",
Action: runMilestones,
Subcommands: []*cli.Command{
&milestones.CmdMilestonesList,
&milestones.CmdMilestonesCreate,
&milestones.CmdMilestonesClose,
&milestones.CmdMilestonesDelete,
&milestones.CmdMilestonesReopen,
&milestones.CmdMilestonesIssues,
},
Flags: milestones.CmdMilestonesList.Flags,
}
func runMilestones(ctx *cli.Context) error {
if ctx.Args().Len() == 1 {
return runMilestoneDetail(ctx, ctx.Args().First())
}
return milestones.RunMilestonesList(ctx)
}
func runMilestoneDetail(cmd *cli.Context, name string) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
milestone, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, name)
if err != nil {
return err
}
print.MilestoneDetails(milestone)
return nil
}

View File

@ -1,32 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package milestones
import (
"code.gitea.io/tea/cmd/flags"
"github.com/urfave/cli/v2"
)
// CmdMilestonesClose represents a sub command of milestones to close an milestone
var CmdMilestonesClose = cli.Command{
Name: "close",
Usage: "Change state of an milestone to 'closed'",
Description: `Change state of an milestone to 'closed'`,
ArgsUsage: "<milestone name>",
Action: func(ctx *cli.Context) error {
if ctx.Bool("force") {
return deleteMilestone(ctx)
}
return editMilestoneStatus(ctx, true)
},
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "delete milestone",
},
}, flags.AllDefaultFlags...),
}

View File

@ -1,82 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package milestones
import (
"time"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"code.gitea.io/sdk/gitea"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v2"
)
// CmdMilestonesCreate represents a sub command of milestones to create milestone
var CmdMilestonesCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create an milestone on repository",
Description: `Create an milestone on repository`,
Action: runMilestonesCreate,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "title",
Aliases: []string{"t"},
Usage: "milestone title to create",
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
Usage: "milestone description to create",
},
&cli.StringFlag{
Name: "deadline",
Aliases: []string{"expires", "x"},
Usage: "set milestone deadline (default is no due date)",
},
&cli.StringFlag{
Name: "state",
Usage: "set milestone state (default is open)",
DefaultText: "open",
},
}, flags.AllDefaultFlags...),
}
func runMilestonesCreate(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
date := ctx.String("deadline")
deadline := &time.Time{}
if date != "" {
t, err := dateparse.ParseAny(date)
if err == nil {
return err
}
deadline = &t
}
state := gitea.StateOpen
if ctx.String("state") == "closed" {
state = gitea.StateClosed
}
if ctx.NumFlags() == 0 {
return interact.CreateMilestone(ctx.Login, ctx.Owner, ctx.Repo)
}
return task.CreateMilestone(
ctx.Login,
ctx.Owner,
ctx.Repo,
ctx.String("title"),
ctx.String("description"),
deadline,
state,
)
}

View File

@ -1,32 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package milestones
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"github.com/urfave/cli/v2"
)
// CmdMilestonesDelete represents a sub command of milestones to delete an milestone
var CmdMilestonesDelete = cli.Command{
Name: "delete",
Aliases: []string{"rm"},
Usage: "delete a milestone",
Description: "delete a milestone",
ArgsUsage: "<milestone name>",
Action: deleteMilestone,
Flags: flags.AllDefaultFlags,
}
func deleteMilestone(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
_, err := client.DeleteMilestoneByName(ctx.Owner, ctx.Repo, ctx.Args().First())
return err
}

View File

@ -1,181 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package milestones
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdMilestonesIssues represents a sub command of milestones to manage issue/pull of an milestone
var CmdMilestonesIssues = cli.Command{
Name: "issues",
Aliases: []string{"i"},
Usage: "manage issue/pull of an milestone",
Description: "manage issue/pull of an milestone",
ArgsUsage: "<milestone name>",
Action: runMilestoneIssueList,
Subcommands: []*cli.Command{
&CmdMilestoneAddIssue,
&CmdMilestoneRemoveIssue,
},
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "state",
Usage: "Filter by issue state (all|open|closed)",
DefaultText: "open",
},
&cli.StringFlag{
Name: "kind",
Usage: "Filter by kind (issue|pull)",
},
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
flags.FieldsFlag(print.IssueFields, []string{
"index", "kind", "title", "state", "updated", "labels",
}),
}, flags.AllDefaultFlags...),
}
// CmdMilestoneAddIssue represents a sub command of milestone issues to add an issue/pull to an milestone
var CmdMilestoneAddIssue = cli.Command{
Name: "add",
Aliases: []string{"a"},
Usage: "Add an issue/pull to an milestone",
Description: "Add an issue/pull to an milestone",
ArgsUsage: "<milestone name> <issue/pull index>",
Action: runMilestoneIssueAdd,
Flags: flags.AllDefaultFlags,
}
// CmdMilestoneRemoveIssue represents a sub command of milestones to remove an issue/pull from an milestone
var CmdMilestoneRemoveIssue = cli.Command{
Name: "remove",
Aliases: []string{"r"},
Usage: "Remove an issue/pull to an milestone",
Description: "Remove an issue/pull to an milestone",
ArgsUsage: "<milestone name> <issue/pull index>",
Action: runMilestoneIssueRemove,
Flags: flags.AllDefaultFlags,
}
func runMilestoneIssueList(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
state := gitea.StateOpen
switch ctx.String("state") {
case "all":
state = gitea.StateAll
case "closed":
state = gitea.StateClosed
}
kind := gitea.IssueTypeAll
switch ctx.String("kind") {
case "issue":
kind = gitea.IssueTypeIssue
case "pull":
kind = gitea.IssueTypePull
}
if ctx.Args().Len() != 1 {
return fmt.Errorf("Must specify milestone name")
}
milestone := ctx.Args().First()
// make sure milestone exist
_, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestone)
if err != nil {
return err
}
issues, _, err := client.ListRepoIssues(ctx.Owner, ctx.Repo, gitea.ListIssueOption{
ListOptions: ctx.GetListOptions(),
Milestones: []string{milestone},
Type: kind,
State: state,
})
if err != nil {
return err
}
fields, err := flags.GetFields(cmd, print.IssueFields)
if err != nil {
return err
}
print.IssuesPullsList(issues, ctx.Output, fields)
return nil
}
func runMilestoneIssueAdd(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
if ctx.Args().Len() != 2 {
return fmt.Errorf("need two arguments")
}
mileName := ctx.Args().Get(0)
issueIndex := ctx.Args().Get(1)
idx, err := utils.ArgToIndex(issueIndex)
if err != nil {
return err
}
// make sure milestone exist
mile, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, mileName)
if err != nil {
return err
}
_, _, err = client.EditIssue(ctx.Owner, ctx.Repo, idx, gitea.EditIssueOption{
Milestone: &mile.ID,
})
return err
}
func runMilestoneIssueRemove(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
if ctx.Args().Len() != 2 {
return fmt.Errorf("need two arguments")
}
mileName := ctx.Args().Get(0)
issueIndex := ctx.Args().Get(1)
idx, err := utils.ArgToIndex(issueIndex)
if err != nil {
return err
}
issue, _, err := client.GetIssue(ctx.Owner, ctx.Repo, idx)
if err != nil {
return err
}
if issue.Milestone == nil {
return fmt.Errorf("issue is not assigned to a milestone")
}
if issue.Milestone.Title != mileName {
return fmt.Errorf("issue is not assigned to this milestone")
}
zero := int64(0)
_, _, err = client.EditIssue(ctx.Owner, ctx.Repo, idx, gitea.EditIssueOption{
Milestone: &zero,
})
return err
}

View File

@ -1,59 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package milestones
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdMilestonesList represents a sub command of milestones to list milestones
var CmdMilestonesList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List milestones of the repository",
Description: `List milestones of the repository`,
Action: RunMilestonesList,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "state",
Usage: "Filter by milestone state (all|open|closed)",
DefaultText: "open",
},
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...),
}
// RunMilestonesList list milestones
func RunMilestonesList(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
state := gitea.StateOpen
switch ctx.String("state") {
case "all":
state = gitea.StateAll
case "closed":
state = gitea.StateClosed
}
client := ctx.Login.Client()
milestones, _, err := client.ListRepoMilestones(ctx.Owner, ctx.Repo, gitea.ListMilestoneOption{
ListOptions: ctx.GetListOptions(),
State: state,
})
if err != nil {
return err
}
print.MilestonesList(milestones, ctx.Output, state)
return nil
}

View File

@ -1,43 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package milestones
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdMilestonesReopen represents a sub command of milestones to open an milestone
var CmdMilestonesReopen = cli.Command{
Name: "reopen",
Aliases: []string{"open"},
Usage: "Change state of an milestone to 'open'",
Description: `Change state of an milestone to 'open'`,
ArgsUsage: "<milestone name>",
Action: func(ctx *cli.Context) error {
return editMilestoneStatus(ctx, false)
},
Flags: flags.AllDefaultFlags,
}
func editMilestoneStatus(cmd *cli.Context, close bool) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
state := gitea.StateOpen
if close {
state = gitea.StateClosed
}
_, _, err := client.EditMilestoneByName(ctx.Owner, ctx.Repo, ctx.Args().First(), gitea.EditMilestoneOption{
State: &state,
Title: ctx.Args().First(),
})
return err
}

View File

@ -1,83 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdNotifications is the main command to operate with notifications
var CmdNotifications = cli.Command{
Name: "notifications",
Aliases: []string{"notification", "n"},
Category: catHelpers,
Usage: "Show notifications",
Description: "Show notifications, by default based of the current repo and unread one",
Action: runNotifications,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "all",
Aliases: []string{"a"},
Usage: "show all notifications of related gitea instance",
},
&cli.BoolFlag{
Name: "read",
Aliases: []string{"rd"},
Usage: "show read notifications instead unread",
},
&cli.BoolFlag{
Name: "pinned",
Aliases: []string{"pd"},
Usage: "show pinned notifications instead unread",
},
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...),
}
func runNotifications(cmd *cli.Context) error {
var news []*gitea.NotificationThread
var err error
ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
listOpts := ctx.GetListOptions()
if listOpts.Page == 0 {
listOpts.Page = 1
}
var status []gitea.NotifyStatus
if ctx.Bool("read") {
status = []gitea.NotifyStatus{gitea.NotifyStatusRead}
}
if ctx.Bool("pinned") {
status = append(status, gitea.NotifyStatusPinned)
}
if ctx.Bool("all") {
news, _, err = client.ListNotifications(gitea.ListNotificationOptions{
ListOptions: listOpts,
Status: status,
})
} else {
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
news, _, err = client.ListRepoNotifications(ctx.Owner, ctx.Repo, gitea.ListNotificationOptions{
ListOptions: listOpts,
Status: status,
})
}
if err != nil {
return err
}
print.NotificationsList(news, ctx.Output, ctx.Bool("all"))
return nil
}

View File

@ -5,11 +5,10 @@
package cmd
import (
"log"
"path"
"strings"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
local_git "code.gitea.io/tea/modules/git"
"github.com/skratchdot/open-golang/open"
@ -19,17 +18,14 @@ import (
// CmdOpen represents a sub command of issues to open issue on the web browser
var CmdOpen = cli.Command{
Name: "open",
Aliases: []string{"o"},
Category: catHelpers,
Usage: "Open something of the repository in web browser",
Description: `Open something of the repository in web browser`,
Usage: "Open something of the repository on web browser",
Description: `Open something of the repository on web browser`,
Action: runOpen,
Flags: append([]cli.Flag{}, flags.LoginRepoFlags...),
Flags: append([]cli.Flag{}, LoginRepoFlags...),
}
func runOpen(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func runOpen(ctx *cli.Context) error {
login, owner, repo := initCommand()
var suffix string
number := ctx.Args().Get(0)
@ -43,11 +39,12 @@ func runOpen(cmd *cli.Context) error {
case strings.EqualFold(number, "commits"):
repo, err := local_git.RepoForWorkdir()
if err != nil {
return err
log.Fatal(err)
}
b, err := repo.Head()
if err != nil {
return err
log.Fatal(err)
return nil
}
name := b.Name()
switch {
@ -74,6 +71,11 @@ func runOpen(cmd *cli.Context) error {
suffix = number
}
u := path.Join(ctx.Login.URL, ctx.Owner, ctx.Repo, suffix)
return open.Run(u)
u := path.Join(login.URL, owner, repo, suffix)
err := open.Run(u)
if err != nil {
log.Fatal(err)
}
return nil
}

View File

@ -1,39 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"fmt"
"code.gitea.io/tea/cmd/organizations"
"github.com/urfave/cli/v2"
)
// CmdOrgs represents handle organization
var CmdOrgs = cli.Command{
Name: "organizations",
Aliases: []string{"organization", "org"},
Category: catEntities,
Usage: "List, create, delete organizations",
Description: "Show organization details",
ArgsUsage: "[<organization>]",
Action: runOrganizations,
Subcommands: []*cli.Command{
&organizations.CmdOrganizationList,
&organizations.CmdOrganizationDelete,
},
}
func runOrganizations(ctx *cli.Context) error {
if ctx.Args().Len() == 1 {
return runOrganizationDetail(ctx.Args().First())
}
return organizations.RunOrganizationList(ctx)
}
func runOrganizationDetail(path string) error {
return fmt.Errorf("Not yet implemented")
}

View File

@ -1,40 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package organizations
import (
"fmt"
"code.gitea.io/tea/modules/context"
"github.com/urfave/cli/v2"
)
// CmdOrganizationDelete represents a sub command of organizations to delete a given user organization
var CmdOrganizationDelete = cli.Command{
Name: "delete",
Aliases: []string{"rm"},
Usage: "Delete users Organizations",
Description: "Delete users organizations",
ArgsUsage: "<organization name>",
Action: RunOrganizationDelete,
}
// RunOrganizationDelete delete user organization
func RunOrganizationDelete(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
if ctx.Args().Len() < 1 {
return fmt.Errorf("You have to specify the organization name you want to delete")
}
response, err := client.DeleteOrg(ctx.Args().First())
if response != nil && response.StatusCode == 404 {
return fmt.Errorf("The given organization does not exist")
}
return err
}

View File

@ -1,44 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package organizations
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdOrganizationList represents a sub command of organizations to list users organizations
var CmdOrganizationList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List Organizations",
Description: "List users organizations",
Action: RunOrganizationList,
Flags: append([]cli.Flag{
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...),
}
// RunOrganizationList list user organizations
func RunOrganizationList(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
userOrganizations, _, err := client.ListUserOrgs(ctx.Login.User, gitea.ListOrgsOptions{
ListOptions: ctx.GetListOptions(),
})
if err != nil {
return err
}
print.OrganizationsList(userOrganizations, ctx.Output)
return nil
}

View File

@ -6,88 +6,268 @@ package cmd
import (
"fmt"
"log"
"strconv"
"strings"
"code.gitea.io/tea/cmd/pulls"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/tea/modules/workaround"
local_git "code.gitea.io/tea/modules/git"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
"gopkg.in/src-d/go-git.v4"
git_config "gopkg.in/src-d/go-git.v4/config"
)
// CmdPulls is the main command to operate on PRs
var CmdPulls = cli.Command{
Name: "pulls",
Aliases: []string{"pull", "pr"},
Category: catEntities,
Usage: "Manage and checkout pull requests",
Description: `Lists PRs when called without argument. If PR index is provided, will show it in detail.`,
ArgsUsage: "[<pull index>]",
Usage: "List open pull requests",
Description: `List open pull requests`,
Action: runPulls,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "comments",
Usage: "Wether to display comments (will prompt if not provided & run interactively)",
&cli.StringFlag{
Name: "state",
Usage: "Filter by PR state (all|open|closed)",
DefaultText: "open",
},
}, pulls.CmdPullsList.Flags...),
}, AllDefaultFlags...),
Subcommands: []*cli.Command{
&pulls.CmdPullsList,
&pulls.CmdPullsCheckout,
&pulls.CmdPullsClean,
&pulls.CmdPullsCreate,
&pulls.CmdPullsClose,
&pulls.CmdPullsReopen,
&pulls.CmdPullsReview,
&pulls.CmdPullsApprove,
&pulls.CmdPullsReject,
&CmdPullsCheckout,
&CmdPullsClean,
},
}
func runPulls(ctx *cli.Context) error {
if ctx.Args().Len() == 1 {
return runPullDetail(ctx, ctx.Args().First())
}
return pulls.RunPullsList(ctx)
}
login, owner, repo := initCommand()
state := gitea.StateOpen
switch ctx.String("state") {
case "all":
state = gitea.StateAll
case "open":
state = gitea.StateOpen
case "closed":
state = gitea.StateClosed
}
prs, err := login.Client().ListRepoPullRequests(owner, repo, gitea.ListPullRequestsOptions{
Page: 0,
State: string(state),
})
func runPullDetail(cmd *cli.Context, index string) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
idx, err := utils.ArgToIndex(index)
if err != nil {
return err
log.Fatal(err)
}
client := ctx.Login.Client()
pr, _, err := client.GetPullRequest(ctx.Owner, ctx.Repo, idx)
if err != nil {
return err
}
if err := workaround.FixPullHeadSha(client, pr); err != nil {
return err
headers := []string{
"Index",
"State",
"Author",
"Updated",
"Title",
}
reviews, _, err := client.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{})
if err != nil {
fmt.Printf("error while loading reviews: %v\n", err)
var values [][]string
if len(prs) == 0 {
Output(outputValue, headers, values)
return nil
}
ci, _, err := client.GetCombinedStatus(ctx.Owner, ctx.Repo, pr.Head.Sha)
if err != nil {
fmt.Printf("error while loading CI: %v\n", err)
for _, pr := range prs {
if pr == nil {
continue
}
print.PullDetails(pr, reviews, ci)
if pr.Comments > 0 {
err = interact.ShowCommentsMaybeInteractive(ctx, idx, pr.Comments)
if err != nil {
fmt.Printf("error loading comments: %v\n", err)
name := pr.Poster.FullName
if len(name) == 0 {
name = pr.Poster.UserName
}
values = append(
values,
[]string{
strconv.FormatInt(pr.Index, 10),
string(pr.State),
name,
pr.Updated.Format("2006-01-02 15:04:05"),
pr.Title,
},
)
}
Output(outputValue, headers, values)
return nil
}
// CmdPullsCheckout is a command to locally checkout the given PR
var CmdPullsCheckout = cli.Command{
Name: "checkout",
Usage: "Locally check out the given PR",
Description: `Locally check out the given PR`,
Action: runPullsCheckout,
ArgsUsage: "<pull index>",
Flags: AllDefaultFlags,
}
func runPullsCheckout(ctx *cli.Context) error {
login, owner, repo := initCommand()
if ctx.Args().Len() != 1 {
log.Fatal("Must specify a PR index")
}
// fetch PR source-repo & -branch from gitea
idx, err := argToIndex(ctx.Args().First())
if err != nil {
return err
}
pr, err := login.Client().GetPullRequest(owner, repo, idx)
if err != nil {
return err
}
remoteURL := pr.Head.Repository.CloneURL
remoteBranchName := pr.Head.Ref
// open local git repo
localRepo, err := local_git.RepoForWorkdir()
if err != nil {
return nil
}
// verify related remote is in local repo, otherwise add it
newRemoteName := fmt.Sprintf("pulls/%v", pr.Head.Repository.Owner.UserName)
localRemote, err := localRepo.GetOrCreateRemote(remoteURL, newRemoteName)
if err != nil {
return err
}
localRemoteName := localRemote.Config().Name
localBranchName := fmt.Sprintf("pulls/%v-%v", idx, remoteBranchName)
// fetch remote
fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n",
idx, remoteURL, remoteBranchName, localRemoteName)
url, err := local_git.ParseURL(localRemote.Config().URLs[0])
if err != nil {
return err
}
auth, err := local_git.GetAuthForURL(url, login.User, login.SSHKey)
if err != nil {
return err
}
err = localRemote.Fetch(&git.FetchOptions{Auth: auth})
if err == git.NoErrAlreadyUpToDate {
fmt.Println(err)
} else if err != nil {
return err
}
// checkout local branch
fmt.Printf("Creating branch '%s'\n", localBranchName)
err = localRepo.TeaCreateBranch(localBranchName, remoteBranchName, localRemoteName)
if err == git.ErrBranchExists {
fmt.Println(err)
} else if err != nil {
return err
}
fmt.Printf("Checking out PR %v\n", idx)
err = localRepo.TeaCheckout(localBranchName)
return err
}
// CmdPullsClean removes the remote and local feature branches, if a PR is merged.
var CmdPullsClean = cli.Command{
Name: "clean",
Usage: "Deletes local & remote feature-branches for a closed pull request",
Description: `Deletes local & remote feature-branches for a closed pull request`,
ArgsUsage: "<pull index>",
Action: runPullsClean,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "ignore-sha",
Usage: "Find the local branch by name instead of commit hash (less precise)",
},
}, AllDefaultFlags...),
}
func runPullsClean(ctx *cli.Context) error {
login, owner, repo := initCommand()
if ctx.Args().Len() != 1 {
return fmt.Errorf("Must specify a PR index")
}
// fetch PR source-repo & -branch from gitea
idx, err := argToIndex(ctx.Args().First())
if err != nil {
return err
}
pr, err := login.Client().GetPullRequest(owner, repo, idx)
if err != nil {
return err
}
if pr.State == gitea.StateOpen {
return fmt.Errorf("PR is still open, won't delete branches")
}
// IDEA: abort if PR.Head.Repository.CloneURL does not match login.URL?
r, err := local_git.RepoForWorkdir()
if err != nil {
return err
}
// find a branch with matching sha or name, that has a remote matching the repo url
var branch *git_config.Branch
if ctx.Bool("ignore-sha") {
branch, err = r.TeaFindBranchByName(pr.Head.Ref, pr.Head.Repository.CloneURL)
} else {
branch, err = r.TeaFindBranchBySha(pr.Head.Sha, pr.Head.Repository.CloneURL)
}
if err != nil {
return err
}
if branch == nil {
if ctx.Bool("ignore-sha") {
return fmt.Errorf("Remote branch %s not found in local repo", pr.Head.Ref)
}
return fmt.Errorf(`Remote branch %s not found in local repo.
Either you don't track this PR, or the local branch has diverged from the remote.
If you still want to continue & are sure you don't loose any important commits,
call me again with the --ignore-sha flag`, pr.Head.Ref)
}
// prepare deletion of local branch:
headRef, err := r.Head()
if err != nil {
return err
}
if headRef.Name().Short() == branch.Name {
fmt.Printf("Checking out 'master' to delete local branch '%s'\n", branch.Name)
err = r.TeaCheckout("master")
if err != nil {
return err
}
}
// remove local & remote branch
fmt.Printf("Deleting local branch %s and remote branch %s\n", branch.Name, pr.Head.Ref)
url, err := r.TeaRemoteURL(branch.Remote)
if err != nil {
return err
}
auth, err := local_git.GetAuthForURL(url, login.User, login.SSHKey)
if err != nil {
return err
}
return r.TeaDeleteBranch(branch, pr.Head.Ref, auth)
}
func argToIndex(arg string) (int64, error) {
if strings.HasPrefix(arg, "#") {
arg = arg[1:]
}
return strconv.ParseInt(arg, 10, 64)
}

View File

@ -1,45 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package pulls
import (
"fmt"
"strings"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdPullsApprove approves a PR
var CmdPullsApprove = cli.Command{
Name: "approve",
Aliases: []string{"lgtm", "a"},
Usage: "Approve a pull request",
Description: "Approve a pull request",
ArgsUsage: "<pull index> [<comment>]",
Action: func(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() == 0 {
return fmt.Errorf("Must specify a PR index")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
comment := strings.Join(ctx.Args().Tail(), " ")
return task.CreatePullReview(ctx, idx, gitea.ReviewStateApproved, comment, nil)
},
Flags: flags.AllDefaultFlags,
}

View File

@ -1,48 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package pulls
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2"
)
// CmdPullsCheckout is a command to locally checkout the given PR
var CmdPullsCheckout = cli.Command{
Name: "checkout",
Aliases: []string{"co"},
Usage: "Locally check out the given PR",
Description: `Locally check out the given PR`,
Action: runPullsCheckout,
ArgsUsage: "<pull index>",
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "branch",
Aliases: []string{"b"},
Usage: "Create a local branch if it doesn't exist yet",
},
}, flags.AllDefaultFlags...),
}
func runPullsCheckout(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{LocalRepo: true})
if ctx.Args().Len() != 1 {
return fmt.Errorf("Must specify a PR index")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
return task.PullCheckout(ctx.Login, ctx.Owner, ctx.Repo, ctx.Bool("branch"), idx, interact.PromptPassword)
}

View File

@ -1,47 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package pulls
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2"
)
// CmdPullsClean removes the remote and local feature branches, if a PR is merged.
var CmdPullsClean = cli.Command{
Name: "clean",
Usage: "Deletes local & remote feature-branches for a closed pull request",
Description: `Deletes local & remote feature-branches for a closed pull request`,
ArgsUsage: "<pull index>",
Action: runPullsClean,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "ignore-sha",
Usage: "Find the local branch by name instead of commit hash (less precise)",
},
}, flags.AllDefaultFlags...),
}
func runPullsClean(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{LocalRepo: true})
if ctx.Args().Len() != 1 {
return fmt.Errorf("Must specify a PR index")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
return task.PullClean(ctx.Login, ctx.Owner, ctx.Repo, idx, ctx.Bool("ignore-sha"), interact.PromptPassword)
}

View File

@ -1,25 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package pulls
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdPullsClose closes a given open pull request
var CmdPullsClose = cli.Command{
Name: "close",
Usage: "Change state of a pull request to 'closed'",
Description: `Change state of a pull request to 'closed'`,
ArgsUsage: "<pull index>",
Action: func(ctx *cli.Context) error {
var s = gitea.StateClosed
return editPullState(ctx, gitea.EditPullRequestOption{State: &s})
},
Flags: flags.AllDefaultFlags,
}

View File

@ -1,59 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package pulls
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"github.com/urfave/cli/v2"
)
// CmdPullsCreate creates a pull request
var CmdPullsCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create a pull-request",
Description: "Create a pull-request",
Action: runPullsCreate,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "head",
Usage: "Set head branch (default is current one)",
},
&cli.StringFlag{
Name: "base",
Aliases: []string{"b"},
Usage: "Set base branch (default is default branch)",
},
}, flags.IssuePREditFlags...),
}
func runPullsCreate(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{LocalRepo: true})
// no args -> interactive mode
if ctx.NumFlags() == 0 {
return interact.CreatePull(ctx.Login, ctx.Owner, ctx.Repo)
}
// else use args to create PR
opts, err := flags.GetIssuePREditFlags(ctx)
if err != nil {
return err
}
return task.CreatePull(
ctx.Login,
ctx.Owner,
ctx.Repo,
ctx.String("base"),
ctx.String("head"),
opts,
)
}

View File

@ -1,38 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package pulls
import (
"fmt"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// editPullState abstracts the arg parsing to edit the given pull request
func editPullState(cmd *cli.Context, opts gitea.EditPullRequestOption) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() == 0 {
return fmt.Errorf("Please provide a Pull Request index")
}
index, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
pr, _, err := ctx.Login.Client().EditPullRequest(ctx.Owner, ctx.Repo, index, opts)
if err != nil {
return err
}
print.PullDetails(pr, nil, nil)
return nil
}

View File

@ -1,51 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package pulls
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdPullsList represents a sub command of issues to list pulls
var CmdPullsList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List pull requests of the repository",
Description: `List pull requests of the repository`,
Action: RunPullsList,
Flags: flags.IssuePRFlags,
}
// RunPullsList return list of pulls
func RunPullsList(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
state := gitea.StateOpen
switch ctx.String("state") {
case "all":
state = gitea.StateAll
case "open":
state = gitea.StateOpen
case "closed":
state = gitea.StateClosed
}
prs, _, err := ctx.Login.Client().ListRepoPullRequests(ctx.Owner, ctx.Repo, gitea.ListPullRequestsOptions{
State: state,
})
if err != nil {
return err
}
print.PullsList(prs, ctx.Output)
return nil
}

View File

@ -1,44 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package pulls
import (
"fmt"
"strings"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdPullsReject requests changes to a PR
var CmdPullsReject = cli.Command{
Name: "reject",
Usage: "Request changes to a pull request",
Description: "Request changes to a pull request",
ArgsUsage: "<pull index> <reason>",
Action: func(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() < 2 {
return fmt.Errorf("Must specify a PR index and comment")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
comment := strings.Join(ctx.Args().Tail(), " ")
return task.CreatePullReview(ctx, idx, gitea.ReviewStateRequestChanges, comment, nil)
},
Flags: flags.AllDefaultFlags,
}

View File

@ -1,26 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package pulls
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdPullsReopen reopens a given closed pull request
var CmdPullsReopen = cli.Command{
Name: "reopen",
Aliases: []string{"open"},
Usage: "Change state of a pull request to 'open'",
Description: `Change state of a pull request to 'open'`,
ArgsUsage: "<pull index>",
Action: func(ctx *cli.Context) error {
var s = gitea.StateOpen
return editPullState(ctx, gitea.EditPullRequestOption{State: &s})
},
Flags: flags.AllDefaultFlags,
}

View File

@ -1,40 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package pulls
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2"
)
// CmdPullsReview starts an interactive review session
var CmdPullsReview = cli.Command{
Name: "review",
Usage: "Interactively review a pull request",
Description: "Interactively review a pull request",
ArgsUsage: "<pull index>",
Action: func(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() != 1 {
return fmt.Errorf("Must specify a PR index")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
return interact.ReviewPull(ctx, idx)
},
Flags: flags.AllDefaultFlags,
}

View File

@ -5,26 +5,144 @@
package cmd
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/cmd/releases"
"log"
"os"
"path/filepath"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdReleases represents to login a gitea server.
// ToDo: ReleaseDetails
var CmdReleases = cli.Command{
Name: "releases",
Aliases: []string{"release", "r"},
Category: catEntities,
Usage: "Manage releases",
Description: "Manage releases",
Action: releases.RunReleasesList,
Usage: "Create releases",
Description: `Create releases`,
Action: runReleases,
Subcommands: []*cli.Command{
&releases.CmdReleaseList,
&releases.CmdReleaseCreate,
&releases.CmdReleaseDelete,
&releases.CmdReleaseEdit,
&CmdReleaseCreate,
},
Flags: flags.AllDefaultFlags,
Flags: AllDefaultFlags,
}
func runReleases(ctx *cli.Context) error {
login, owner, repo := initCommand()
releases, err := login.Client().ListReleases(owner, repo)
if err != nil {
log.Fatal(err)
}
headers := []string{
"Tag-Name",
"Title",
"Published At",
"Tar URL",
}
var values [][]string
if len(releases) == 0 {
Output(outputValue, headers, values)
return nil
}
for _, release := range releases {
values = append(
values,
[]string{
release.TagName,
release.Title,
release.PublishedAt.Format("2006-01-02 15:04:05"),
release.TarURL,
},
)
}
Output(outputValue, headers, values)
return nil
}
// CmdReleaseCreate represents a sub command of Release to create release.
var CmdReleaseCreate = cli.Command{
Name: "create",
Usage: "Create a release",
Description: `Create a release`,
Action: runReleaseCreate,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "tag",
Usage: "Tag name",
},
&cli.StringFlag{
Name: "target",
Usage: "Target refs, branch name or commit id",
},
&cli.StringFlag{
Name: "title",
Aliases: []string{"t"},
Usage: "Release title",
},
&cli.StringFlag{
Name: "note",
Aliases: []string{"n"},
Usage: "Release notes",
},
&cli.BoolFlag{
Name: "draft",
Aliases: []string{"d"},
Usage: "Is a draft",
},
&cli.BoolFlag{
Name: "prerelease",
Aliases: []string{"p"},
Usage: "Is a pre-release",
},
&cli.StringSliceFlag{
Name: "asset",
Aliases: []string{"a"},
Usage: "List of files to attach",
},
}, LoginRepoFlags...),
}
func runReleaseCreate(ctx *cli.Context) error {
login, owner, repo := initCommand()
release, err := login.Client().CreateRelease(owner, repo, gitea.CreateReleaseOption{
TagName: ctx.String("tag"),
Target: ctx.String("target"),
Title: ctx.String("title"),
Note: ctx.String("note"),
IsDraft: ctx.Bool("draft"),
IsPrerelease: ctx.Bool("prerelease"),
})
if err != nil {
if err.Error() == "409 Conflict" {
log.Fatal("error: There already is a release for this tag")
}
log.Fatal(err)
}
for _, asset := range ctx.StringSlice("asset") {
var file *os.File
if file, err = os.Open(asset); err != nil {
log.Fatal(err)
}
filePath := filepath.Base(asset)
if _, err = login.Client().CreateReleaseAttachment(owner, repo, release.ID, file, filePath); err != nil {
file.Close()
log.Fatal(err)
}
file.Close()
}
return nil
}

View File

@ -1,102 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package releases
import (
"fmt"
"net/http"
"os"
"path/filepath"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdReleaseCreate represents a sub command of Release to create release
var CmdReleaseCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create a release",
Description: `Create a release`,
Action: runReleaseCreate,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "tag",
Usage: "Tag name",
},
&cli.StringFlag{
Name: "target",
Usage: "Target refs, branch name or commit id",
},
&cli.StringFlag{
Name: "title",
Aliases: []string{"t"},
Usage: "Release title",
},
&cli.StringFlag{
Name: "note",
Aliases: []string{"n"},
Usage: "Release notes",
},
&cli.BoolFlag{
Name: "draft",
Aliases: []string{"d"},
Usage: "Is a draft",
},
&cli.BoolFlag{
Name: "prerelease",
Aliases: []string{"p"},
Usage: "Is a pre-release",
},
&cli.StringSliceFlag{
Name: "asset",
Aliases: []string{"a"},
Usage: "List of files to attach",
},
}, flags.AllDefaultFlags...),
}
func runReleaseCreate(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
release, resp, err := ctx.Login.Client().CreateRelease(ctx.Owner, ctx.Repo, gitea.CreateReleaseOption{
TagName: ctx.String("tag"),
Target: ctx.String("target"),
Title: ctx.String("title"),
Note: ctx.String("note"),
IsDraft: ctx.Bool("draft"),
IsPrerelease: ctx.Bool("prerelease"),
})
if err != nil {
if resp != nil && resp.StatusCode == http.StatusConflict {
return fmt.Errorf("There already is a release for this tag")
}
return err
}
for _, asset := range ctx.StringSlice("asset") {
var file *os.File
if file, err = os.Open(asset); err != nil {
return err
}
filePath := filepath.Base(asset)
if _, _, err = ctx.Login.Client().CreateReleaseAttachment(ctx.Owner, ctx.Repo, release.ID, file, filePath); err != nil {
file.Close()
return err
}
file.Close()
}
return nil
}

View File

@ -1,72 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package releases
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"github.com/urfave/cli/v2"
)
// CmdReleaseDelete represents a sub command of Release to delete a release
var CmdReleaseDelete = cli.Command{
Name: "delete",
Aliases: []string{"rm"},
Usage: "Delete a release",
Description: `Delete a release`,
ArgsUsage: "<release tag>",
Action: runReleaseDelete,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "confirm",
Aliases: []string{"y"},
Usage: "Confirm deletion (required)",
},
&cli.BoolFlag{
Name: "delete-tag",
Usage: "Also delete the git tag for this release",
},
}, flags.AllDefaultFlags...),
}
func runReleaseDelete(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
tag := ctx.Args().First()
if len(tag) == 0 {
fmt.Println("Release tag needed to delete")
return nil
}
if !ctx.Bool("confirm") {
fmt.Println("Are you sure? Please confirm with -y or --confirm.")
return nil
}
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
if err != nil {
return err
}
if release == nil {
return nil
}
_, err = client.DeleteRelease(ctx.Owner, ctx.Repo, release.ID)
if err != nil {
return err
}
if ctx.Bool("delete-tag") {
_, err = client.DeleteTag(ctx.Owner, ctx.Repo, tag)
return err
}
return nil
}

View File

@ -1,96 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package releases
import (
"fmt"
"strings"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdReleaseEdit represents a sub command of Release to edit releases
var CmdReleaseEdit = cli.Command{
Name: "edit",
Aliases: []string{"e"},
Usage: "Edit a release",
Description: `Edit a release`,
ArgsUsage: "<release tag>",
Action: runReleaseEdit,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "tag",
Usage: "Change Tag",
},
&cli.StringFlag{
Name: "target",
Usage: "Change Target",
},
&cli.StringFlag{
Name: "title",
Aliases: []string{"t"},
Usage: "Change Title",
},
&cli.StringFlag{
Name: "note",
Aliases: []string{"n"},
Usage: "Change Notes",
},
&cli.StringFlag{
Name: "draft",
Aliases: []string{"d"},
Usage: "Mark as Draft [True/false]",
DefaultText: "true",
},
&cli.StringFlag{
Name: "prerelease",
Aliases: []string{"p"},
Usage: "Mark as Pre-Release [True/false]",
DefaultText: "true",
},
}, flags.AllDefaultFlags...),
}
func runReleaseEdit(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
tag := ctx.Args().First()
if len(tag) == 0 {
fmt.Println("Release tag needed to edit")
return nil
}
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
if err != nil {
return err
}
if release == nil {
return nil
}
var isDraft, isPre *bool
if ctx.IsSet("draft") {
isDraft = gitea.OptionalBool(strings.ToLower(ctx.String("draft"))[:1] == "t")
}
if ctx.IsSet("prerelease") {
isPre = gitea.OptionalBool(strings.ToLower(ctx.String("prerelease"))[:1] == "t")
}
_, _, err = client.EditRelease(ctx.Owner, ctx.Repo, release.ID, gitea.EditReleaseOption{
TagName: ctx.String("tag"),
Target: ctx.String("target"),
Title: ctx.String("title"),
Note: ctx.String("note"),
IsDraft: isDraft,
IsPrerelease: isPre,
})
return err
}

View File

@ -1,63 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package releases
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdReleaseList represents a sub command of Release to list releases
var CmdReleaseList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List Releases",
Description: "List Releases",
Action: RunReleasesList,
Flags: append([]cli.Flag{
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...),
}
// RunReleasesList list releases
func RunReleasesList(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
releases, _, err := ctx.Login.Client().ListReleases(ctx.Owner, ctx.Repo, gitea.ListReleasesOptions{
ListOptions: ctx.GetListOptions(),
})
if err != nil {
return err
}
print.ReleasesList(releases, ctx.Output)
return nil
}
func getReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) {
rl, _, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{})
if err != nil {
return nil, err
}
if len(rl) == 0 {
fmt.Println("Repo does not have any release")
return nil, nil
}
for _, r := range rl {
if r.TagName == tag {
return r, nil
}
}
fmt.Println("Release tag does not exist")
return nil, nil
}

View File

@ -5,52 +5,128 @@
package cmd
import (
"code.gitea.io/tea/cmd/repos"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"log"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdRepos represents to login a gitea server.
var CmdRepos = cli.Command{
Name: "repos",
Aliases: []string{"repo"},
Category: catEntities,
Usage: "Show repository details",
Description: "Show repository details",
ArgsUsage: "[<repo owner>/<repo name>]",
Action: runRepos,
Usage: "Operate with repositories",
Description: `Operate with repositories`,
Action: runReposList,
Subcommands: []*cli.Command{
&repos.CmdReposList,
&repos.CmdReposSearch,
&repos.CmdRepoCreate,
&CmdReposList,
},
Flags: repos.CmdReposListFlags,
Flags: LoginOutputFlags,
}
func runRepos(ctx *cli.Context) error {
if ctx.Args().Len() == 1 {
return runRepoDetail(ctx, ctx.Args().First())
}
return repos.RunReposList(ctx)
// CmdReposList represents a sub command of issues to list issues
var CmdReposList = cli.Command{
Name: "ls",
Usage: "List available repositories",
Description: `List available repositories`,
Action: runReposList,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "mode",
Usage: "Filter listed repositories based on mode, optional - fork, mirror, source",
},
&cli.StringFlag{
Name: "org",
Usage: "Filter listed repositories based on organization, optional",
},
&cli.StringFlag{
Name: "user",
Usage: "Filter listed repositories absed on user, optional",
},
}, LoginOutputFlags...),
}
func runRepoDetail(cmd *cli.Context, path string) error {
ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
repoOwner, repoName := utils.GetOwnerAndRepo(path, ctx.Owner)
repo, _, err := client.GetRepo(repoOwner, repoName)
if err != nil {
return err
// runReposList list repositories
func runReposList(ctx *cli.Context) error {
login := initCommandLoginOnly()
mode := ctx.String("mode")
org := ctx.String("org")
user := ctx.String("user")
var rps []*gitea.Repository
var err error
if org != "" {
rps, err = login.Client().ListOrgRepos(org)
} else if user != "" {
rps, err = login.Client().ListUserRepos(user)
} else {
rps, err = login.Client().ListMyRepos()
}
topics, _, err := client.ListRepoTopics(repoOwner, repoName, gitea.ListRepoTopicsOptions{})
if err != nil {
return err
log.Fatal(err)
}
print.RepoDetails(repo, topics)
var repos []*gitea.Repository
if mode == "" {
repos = rps
} else if mode == "fork" {
for _, rp := range rps {
if rp.Fork == true {
repos = append(repos, rp)
}
}
} else if mode == "mirror" {
for _, rp := range rps {
if rp.Mirror == true {
repos = append(repos, rp)
}
}
} else if mode == "source" {
for _, rp := range rps {
if rp.Mirror != true && rp.Fork != true {
repos = append(repos, rp)
}
}
} else {
log.Fatal("Unknown mode: ", mode, "\nUse one of the following:\n- fork\n- mirror\n- source\n")
return nil
}
if len(rps) == 0 {
log.Fatal("No repositories found", rps)
return nil
}
headers := []string{
"Name",
"Type",
"SSH",
"Owner",
}
var values [][]string
for _, rp := range repos {
var mode = "source"
if rp.Fork {
mode = "fork"
}
if rp.Mirror {
mode = "mirror"
}
values = append(
values,
[]string{
rp.FullName,
mode,
rp.SSHURL,
rp.Owner.UserName,
},
)
}
Output(outputValue, headers, values)
return nil
}

View File

@ -1,120 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repos
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdRepoCreate represents a sub command of repos to create one
var CmdRepoCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create a repository",
Description: "Create a repository",
Action: runRepoCreate,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "name",
Aliases: []string{""},
Required: true,
Usage: "name of new repo",
},
&cli.StringFlag{
Name: "owner",
Aliases: []string{"O"},
Required: false,
Usage: "name of repo owner",
},
&cli.BoolFlag{
Name: "private",
Required: false,
Value: false,
Usage: "make repo private",
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"desc"},
Required: false,
Usage: "add description to repo",
},
&cli.BoolFlag{
Name: "init",
Required: false,
Value: false,
Usage: "initialize repo",
},
&cli.StringFlag{
Name: "labels",
Required: false,
Usage: "name of label set to add",
},
&cli.StringFlag{
Name: "gitignores",
Aliases: []string{"git"},
Required: false,
Usage: "list of gitignore templates (need --init)",
},
&cli.StringFlag{
Name: "license",
Required: false,
Usage: "add license (need --init)",
},
&cli.StringFlag{
Name: "readme",
Required: false,
Usage: "use readme template (need --init)",
},
&cli.StringFlag{
Name: "branch",
Required: false,
Usage: "use custom default branch (need --init)",
},
}, flags.LoginOutputFlags...),
}
func runRepoCreate(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
var (
repo *gitea.Repository
err error
)
opts := gitea.CreateRepoOption{
Name: ctx.String("name"),
Description: ctx.String("description"),
Private: ctx.Bool("private"),
AutoInit: ctx.Bool("init"),
IssueLabels: ctx.String("labels"),
Gitignores: ctx.String("gitignores"),
License: ctx.String("license"),
Readme: ctx.String("readme"),
DefaultBranch: ctx.String("branch"),
}
if len(ctx.String("owner")) != 0 {
repo, _, err = client.CreateOrgRepo(ctx.String("owner"), opts)
} else {
repo, _, err = client.CreateRepo(opts)
}
if err != nil {
return err
}
topics, _, err := client.ListRepoTopics(repo.Owner.UserName, repo.Name, gitea.ListRepoTopicsOptions{})
if err != nil {
return err
}
print.RepoDetails(repo, topics)
fmt.Printf("%s\n", repo.HTMLURL)
return nil
}

View File

@ -1,37 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repos
import (
"fmt"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
var typeFilterFlag = cli.StringFlag{
Name: "type",
Aliases: []string{"T"},
Required: false,
Usage: "Filter by type: fork, mirror, source",
}
func getTypeFilter(ctx *cli.Context) (filter gitea.RepoType, err error) {
t := ctx.String("type")
filter = gitea.RepoTypeNone
switch t {
case "":
filter = gitea.RepoTypeNone
case "fork":
filter = gitea.RepoTypeFork
case "mirror":
filter = gitea.RepoTypeMirror
case "source":
filter = gitea.RepoTypeSource
default:
err = fmt.Errorf("invalid repo type '%s'. valid: fork, mirror, source", t)
}
return
}

View File

@ -1,115 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repos
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdReposListFlags contains all flags needed for repo listing
var CmdReposListFlags = append([]cli.Flag{
&cli.BoolFlag{
Name: "watched",
Aliases: []string{"w"},
Required: false,
Usage: "List your watched repos instead",
},
&cli.BoolFlag{
Name: "starred",
Aliases: []string{"s"},
Required: false,
Usage: "List your starred repos instead",
},
flags.FieldsFlag(print.RepoFields, []string{
"owner", "name", "type", "ssh",
}),
&typeFilterFlag,
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.LoginOutputFlags...)
// CmdReposList represents a sub command of repos to list them
var CmdReposList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List repositories you have access to",
Description: "List repositories you have access to",
Action: RunReposList,
Flags: CmdReposListFlags,
}
// RunReposList list repositories
func RunReposList(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
typeFilter, err := getTypeFilter(cmd)
if err != nil {
return err
}
var rps []*gitea.Repository
if ctx.Bool("starred") {
user, _, err := client.GetMyUserInfo()
if err != nil {
return err
}
rps, _, err = client.SearchRepos(gitea.SearchRepoOptions{
ListOptions: ctx.GetListOptions(),
StarredByUserID: user.ID,
})
} else if ctx.Bool("watched") {
rps, _, err = client.GetMyWatchedRepos() // TODO: this does not expose pagination..
} else {
rps, _, err = client.ListMyRepos(gitea.ListReposOptions{
ListOptions: ctx.GetListOptions(),
})
}
if err != nil {
return err
}
reposFiltered := rps
if typeFilter != gitea.RepoTypeNone {
reposFiltered = filterReposByType(rps, typeFilter)
}
fields, err := flags.GetFields(cmd, print.RepoFields)
if err != nil {
return err
}
print.ReposList(reposFiltered, ctx.Output, fields)
return nil
}
func filterReposByType(repos []*gitea.Repository, t gitea.RepoType) []*gitea.Repository {
var filtered []*gitea.Repository
for _, r := range repos {
switch t {
case gitea.RepoTypeFork:
if !r.Fork {
continue
}
case gitea.RepoTypeMirror:
if !r.Mirror {
continue
}
case gitea.RepoTypeSource:
if r.Fork || r.Mirror {
continue
}
}
filtered = append(filtered, r)
}
return filtered
}

View File

@ -1,134 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repos
import (
"fmt"
"strings"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdReposSearch represents a sub command of repos to find them
var CmdReposSearch = cli.Command{
Name: "search",
Aliases: []string{"s"},
Usage: "Find any repo on an Gitea instance",
Description: "Find any repo on an Gitea instance",
ArgsUsage: "[<search term>]",
Action: runReposSearch,
Flags: append([]cli.Flag{
&cli.BoolFlag{
// TODO: it might be nice to search for topics as an ADDITIONAL filter.
// for that, we'd probably need to make multiple queries and UNION the results.
Name: "topic",
Aliases: []string{"t"},
Required: false,
Usage: "Search for term in repo topics instead of name",
},
&typeFilterFlag,
&cli.StringFlag{
Name: "owner",
Aliases: []string{"O"},
Required: false,
Usage: "Filter by owner",
},
&cli.StringFlag{
Name: "private",
Required: false,
Usage: "Filter private repos (true|false)",
},
&cli.StringFlag{
Name: "archived",
Required: false,
Usage: "Filter archived repos (true|false)",
},
flags.FieldsFlag(print.RepoFields, []string{
"owner", "name", "type", "ssh",
}),
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.LoginOutputFlags...),
}
func runReposSearch(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
var ownerID int64
if ctx.IsSet("owner") {
// test if owner is a organisation
org, _, err := client.GetOrg(ctx.String("owner"))
if err != nil {
// HACK: the client does not return a response on 404, so we can't check res.StatusCode
if err.Error() != "404 Not Found" {
return fmt.Errorf("Could not find owner: %s", err)
}
// if owner is no org, its a user
user, _, err := client.GetUserInfo(ctx.String("owner"))
if err != nil {
return err
}
ownerID = user.ID
} else {
ownerID = org.ID
}
}
var isArchived *bool
if ctx.IsSet("archived") {
archived := strings.ToLower(ctx.String("archived"))[:1] == "t"
isArchived = &archived
}
var isPrivate *bool
if ctx.IsSet("private") {
private := strings.ToLower(ctx.String("private"))[:1] == "t"
isPrivate = &private
}
mode, err := getTypeFilter(cmd)
if err != nil {
return err
}
var keyword string
if ctx.Args().Present() {
keyword = strings.Join(ctx.Args().Slice(), " ")
}
user, _, err := client.GetMyUserInfo()
if err != nil {
return err
}
rps, _, err := client.SearchRepos(gitea.SearchRepoOptions{
ListOptions: ctx.GetListOptions(),
OwnerID: ownerID,
IsPrivate: isPrivate,
IsArchived: isArchived,
Type: mode,
Keyword: keyword,
KeywordInDescription: true,
KeywordIsTopic: ctx.Bool("topic"),
PrioritizedByOwnerID: user.ID,
})
if err != nil {
return err
}
fields, err := flags.GetFields(cmd, nil)
if err != nil {
return err
}
print.ReposList(rps, ctx.Output, fields)
return nil
}

View File

@ -5,26 +5,258 @@
package cmd
import (
"code.gitea.io/tea/cmd/times"
"fmt"
"log"
"strconv"
"strings"
"time"
"code.gitea.io/sdk/gitea"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v2"
)
// CmdTrackedTimes represents the command to operate repositories' times.
var CmdTrackedTimes = cli.Command{
Name: "times",
Aliases: []string{"time", "t"},
Category: catEntities,
Aliases: []string{"time"},
Usage: "Operate on tracked times of a repository's issues & pulls",
Description: `Operate on tracked times of a repository's issues & pulls.
Depending on your permissions on the repository, only your own tracked
times might be listed.`,
ArgsUsage: "[username | #issue]",
Action: times.RunTimesList,
Action: runTrackedTimes,
Subcommands: []*cli.Command{
&times.CmdTrackedTimesAdd,
&times.CmdTrackedTimesDelete,
&times.CmdTrackedTimesReset,
&times.CmdTrackedTimesList,
&CmdTrackedTimesAdd,
&CmdTrackedTimesDelete,
&CmdTrackedTimesReset,
},
Flags: times.CmdTrackedTimesList.Flags,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "from",
Aliases: []string{"f"},
Usage: "Show only times tracked after this date",
},
&cli.StringFlag{
Name: "until",
Aliases: []string{"u"},
Usage: "Show only times tracked before this date",
},
&cli.BoolFlag{
Name: "total",
Aliases: []string{"t"},
Usage: "Print the total duration at the end",
},
}, AllDefaultFlags...),
}
func runTrackedTimes(ctx *cli.Context) error {
login, owner, repo := initCommand()
client := login.Client()
if err := client.CheckServerVersionConstraint(">= 1.11"); err != nil {
return err
}
var times []*gitea.TrackedTime
var err error
user := ctx.Args().First()
fmt.Println(ctx.Command.ArgsUsage)
if user == "" {
// get all tracked times on the repo
times, err = client.GetRepoTrackedTimes(owner, repo)
} else if strings.HasPrefix(user, "#") {
// get all tracked times on the specified issue
issue, err := argToIndex(user)
if err != nil {
return err
}
times, err = client.ListTrackedTimes(owner, repo, issue)
} else {
// get all tracked times by the specified user
times, err = client.GetUserTrackedTimes(owner, repo, user)
}
if err != nil {
return err
}
var from, until time.Time
if ctx.String("from") != "" {
from, err = dateparse.ParseLocal(ctx.String("from"))
if err != nil {
return err
}
}
if ctx.String("until") != "" {
until, err = dateparse.ParseLocal(ctx.String("until"))
if err != nil {
return err
}
}
printTrackedTimes(times, outputValue, from, until, ctx.Bool("total"))
return nil
}
func printTrackedTimes(times []*gitea.TrackedTime, outputType string, from, until time.Time, printTotal bool) {
var outputValues [][]string
var totalDuration int64
localLoc, err := time.LoadLocation("Local") // local timezone for time formatting
if err != nil {
log.Fatal(err)
}
for _, t := range times {
if !from.IsZero() && from.After(t.Created) {
continue
}
if !until.IsZero() && until.Before(t.Created) {
continue
}
totalDuration += t.Time
outputValues = append(
outputValues,
[]string{
t.Created.In(localLoc).Format("2006-01-02 15:04:05"),
"#" + strconv.FormatInt(t.Issue.Index, 10),
t.UserName,
time.Duration(1e9 * t.Time).String(),
},
)
}
if printTotal {
outputValues = append(outputValues, []string{
"TOTAL", "", "", time.Duration(1e9 * totalDuration).String(),
})
}
headers := []string{
"Created",
"Issue",
"User",
"Duration",
}
Output(outputType, headers, outputValues)
}
// CmdTrackedTimesAdd represents a sub command of times to add time to an issue
var CmdTrackedTimesAdd = cli.Command{
Name: "add",
Usage: "Track spent time on an issue",
UsageText: "tea times add <issue> <duration>",
Description: `Track spent time on an issue
Example:
tea times add 1 1h25m
`,
Action: runTrackedTimesAdd,
Flags: LoginRepoFlags,
}
func runTrackedTimesAdd(ctx *cli.Context) error {
login, owner, repo := initCommand()
if ctx.Args().Len() < 2 {
return fmt.Errorf("No issue or duration specified.\nUsage:\t%s", ctx.Command.UsageText)
}
issue, err := argToIndex(ctx.Args().First())
if err != nil {
log.Fatal(err)
}
duration, err := time.ParseDuration(strings.Join(ctx.Args().Tail(), ""))
if err != nil {
log.Fatal(err)
}
_, err = login.Client().AddTime(owner, repo, issue, gitea.AddTimeOption{
Time: int64(duration.Seconds()),
})
if err != nil {
log.Fatal(err)
}
return nil
}
// CmdTrackedTimesDelete is a sub command of CmdTrackedTimes, and removes time from an issue
var CmdTrackedTimesDelete = cli.Command{
Name: "delete",
Aliases: []string{"rm"},
Usage: "Delete a single tracked time on an issue",
UsageText: "tea times delete <issue> <time ID>",
Action: runTrackedTimesDelete,
Flags: LoginRepoFlags,
}
func runTrackedTimesDelete(ctx *cli.Context) error {
login, owner, repo := initCommand()
client := login.Client()
if err := client.CheckServerVersionConstraint(">= 1.11"); err != nil {
return err
}
if ctx.Args().Len() < 2 {
return fmt.Errorf("No issue or time ID specified.\nUsage:\t%s", ctx.Command.UsageText)
}
issue, err := argToIndex(ctx.Args().First())
if err != nil {
log.Fatal(err)
}
timeID, err := strconv.ParseInt(ctx.Args().Get(1), 10, 64)
if err != nil {
log.Fatal(err)
}
err = client.DeleteTime(owner, repo, issue, timeID)
if err != nil {
log.Fatal(err)
}
return nil
}
// CmdTrackedTimesReset is a subcommand of CmdTrackedTimes, and
// clears all tracked times on an issue.
var CmdTrackedTimesReset = cli.Command{
Name: "reset",
Usage: "Reset tracked time on an issue",
UsageText: "tea times reset <issue>",
Action: runTrackedTimesReset,
Flags: LoginRepoFlags,
}
func runTrackedTimesReset(ctx *cli.Context) error {
login, owner, repo := initCommand()
client := login.Client()
if err := client.CheckServerVersionConstraint(">= 1.11"); err != nil {
return err
}
if ctx.Args().Len() != 1 {
return fmt.Errorf("No issue specified.\nUsage:\t%s", ctx.Command.UsageText)
}
issue, err := argToIndex(ctx.Args().First())
if err != nil {
log.Fatal(err)
}
err = client.ResetIssueTime(owner, repo, issue)
if err != nil {
log.Fatal(err)
}
return nil
}

View File

@ -1,56 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package times
import (
"fmt"
"strings"
"time"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdTrackedTimesAdd represents a sub command of times to add time to an issue
var CmdTrackedTimesAdd = cli.Command{
Name: "add",
Aliases: []string{"a"},
Usage: "Track spent time on an issue",
UsageText: "tea times add <issue> <duration>",
Description: `Track spent time on an issue
Example:
tea times add 1 1h25m
`,
Action: runTrackedTimesAdd,
Flags: flags.LoginRepoFlags,
}
func runTrackedTimesAdd(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() < 2 {
return fmt.Errorf("No issue or duration specified.\nUsage:\t%s", ctx.Command.UsageText)
}
issue, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
duration, err := time.ParseDuration(strings.Join(ctx.Args().Tail(), ""))
if err != nil {
return err
}
_, _, err = ctx.Login.Client().AddTime(ctx.Owner, ctx.Repo, issue, gitea.AddTimeOption{
Time: int64(duration.Seconds()),
})
return err
}

View File

@ -1,49 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package times
import (
"fmt"
"strconv"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2"
)
// CmdTrackedTimesDelete is a sub command of CmdTrackedTimes, and removes time from an issue
var CmdTrackedTimesDelete = cli.Command{
Name: "delete",
Aliases: []string{"rm"},
Usage: "Delete a single tracked time on an issue",
UsageText: "tea times delete <issue> <time ID>",
Action: runTrackedTimesDelete,
Flags: flags.LoginRepoFlags,
}
func runTrackedTimesDelete(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
if ctx.Args().Len() < 2 {
return fmt.Errorf("No issue or time ID specified.\nUsage:\t%s", ctx.Command.UsageText)
}
issue, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
timeID, err := strconv.ParseInt(ctx.Args().Get(1), 10, 64)
if err != nil {
return err
}
_, err = client.DeleteTime(ctx.Owner, ctx.Repo, issue, timeID)
return err
}

View File

@ -1,126 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package times
import (
"fmt"
"strings"
"time"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v2"
)
// CmdTrackedTimesList represents a sub command of times to list them
var CmdTrackedTimesList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Action: RunTimesList,
Usage: "List tracked times on issues & pulls",
Description: `List tracked times, across repos, or on a single repo or issue:
- given a username all times on a repo by that user are shown,
- given a issue index with '#' prefix, all times on that issue are listed,
- given --mine, your times are listed across all repositories.
Depending on your permissions on the repository, only your own tracked times might be listed.`,
ArgsUsage: "[username | #issue]",
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "from",
Aliases: []string{"f"},
Usage: "Show only times tracked after this date",
},
&cli.StringFlag{
Name: "until",
Aliases: []string{"u"},
Usage: "Show only times tracked before this date",
},
&cli.BoolFlag{
Name: "total",
Aliases: []string{"t"},
Usage: "Print the total duration at the end",
},
&cli.BoolFlag{
Name: "mine",
Aliases: []string{"m"},
Usage: "Show all times tracked by you across all repositories (overrides command arguments)",
},
&cli.StringFlag{
Name: "fields",
Usage: fmt.Sprintf(`Comma-separated list of fields to print. Available values:
%s
`, strings.Join(print.TrackedTimeFields, ",")),
},
}, flags.AllDefaultFlags...),
}
// RunTimesList list repositories
func RunTimesList(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
var times []*gitea.TrackedTime
var err error
var from, until time.Time
var fields []string
if ctx.IsSet("from") {
from, err = dateparse.ParseLocal(ctx.String("from"))
if err != nil {
return err
}
}
if ctx.IsSet("until") {
until, err = dateparse.ParseLocal(ctx.String("until"))
if err != nil {
return err
}
}
opts := gitea.ListTrackedTimesOptions{Since: from, Before: until}
user := ctx.Args().First()
if ctx.Bool("mine") {
times, _, err = client.GetMyTrackedTimes()
fields = []string{"created", "repo", "issue", "duration"}
} else if user == "" {
// get all tracked times on the repo
times, _, err = client.ListRepoTrackedTimes(ctx.Owner, ctx.Repo, opts)
fields = []string{"created", "issue", "user", "duration"}
} else if strings.HasPrefix(user, "#") {
// get all tracked times on the specified issue
issue, err := utils.ArgToIndex(user)
if err != nil {
return err
}
times, _, err = client.ListIssueTrackedTimes(ctx.Owner, ctx.Repo, issue, opts)
fields = []string{"created", "user", "duration"}
} else {
// get all tracked times by the specified user
opts.User = user
times, _, err = client.ListRepoTrackedTimes(ctx.Owner, ctx.Repo, opts)
fields = []string{"created", "issue", "duration"}
}
if err != nil {
return err
}
if ctx.IsSet("fields") {
if fields, err = flags.GetFields(cmd, print.TrackedTimeFields); err != nil {
return err
}
}
print.TrackedTimesList(times, ctx.Output, fields, ctx.Bool("total"))
return nil
}

View File

@ -1,43 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package times
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2"
)
// CmdTrackedTimesReset is a subcommand of CmdTrackedTimes, and
// clears all tracked times on an issue.
var CmdTrackedTimesReset = cli.Command{
Name: "reset",
Usage: "Reset tracked time on an issue",
UsageText: "tea times reset <issue>",
Action: runTrackedTimesReset,
Flags: flags.LoginRepoFlags,
}
func runTrackedTimesReset(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
if ctx.Args().Len() != 1 {
return fmt.Errorf("No issue specified.\nUsage:\t%s", ctx.Command.UsageText)
}
issue, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
_, err = client.ResetIssueTime(ctx.Owner, ctx.Repo, issue)
return err
}

View File

@ -1,9 +0,0 @@
$fn = $($MyInvocation.MyCommand.Name)
$name = $fn -replace "(.*)\.ps1$", '$1'
Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock {
param($commandName, $wordToComplete, $cursorPosition)
$other = "$wordToComplete --generate-bash-completion"
Invoke-Expression $other | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}

View File

@ -1,21 +0,0 @@
#! /bin/bash
: ${PROG:=$(basename ${BASH_SOURCE})}
_cli_bash_autocomplete() {
if [[ "${COMP_WORDS[0]}" != "source" ]]; then
local cur opts base
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == "-"* ]]; then
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
else
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
}
complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG
unset PROG

View File

@ -1,23 +0,0 @@
#compdef $PROG
_cli_zsh_autocomplete() {
local -a opts
local cur
cur=${words[-1]}
if [[ "$cur" == "-"* ]]; then
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
else
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}")
fi
if [[ "${opts[1]}" != "" ]]; then
_describe 'values' opts
else
_files
fi
return
}
compdef _cli_zsh_autocomplete $PROG

BIN
demo.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

42
go.mod
View File

@ -1,38 +1,18 @@
module code.gitea.io/tea
go 1.13
go 1.12
require (
code.gitea.io/gitea-vet v0.2.1
code.gitea.io/sdk/gitea v0.13.1-0.20210304201955-ff82113459b5
gitea.com/noerw/unidiff-comments v0.0.0-20201219085024-64aec5658f2b
github.com/AlecAivazis/survey/v2 v2.2.8
github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/adrg/xdg v0.3.1
github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e
github.com/charmbracelet/glamour v0.2.0
code.gitea.io/sdk/gitea v0.11.2
github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/go-git/go-git/v5 v5.2.0
github.com/imdario/mergo v0.3.11 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/muesli/termenv v0.7.4
github.com/olekukonko/tablewriter v0.0.5
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/go-gitea/yaml v0.0.0-20170812160011-eb3733d160e7
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/olekukonko/tablewriter v0.0.1
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.3.0
github.com/xanzy/ssh-agent v0.3.0 // indirect
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
golang.org/x/sys v0.0.0-20210305034016-7844c3c200c3 // indirect
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
golang.org/x/text v0.3.5 // indirect
golang.org/x/tools v0.1.0 // indirect
gopkg.in/yaml.v2 v2.4.0
github.com/stretchr/testify v1.4.0
github.com/urfave/cli/v2 v2.1.1
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/yaml.v2 v2.2.7 // indirect
)
replace github.com/charmbracelet/glamour => github.com/noerw/glamour v0.2.1-0.20210305125354-f0a29f1de0c2

229
go.sum
View File

@ -1,235 +1,108 @@
code.gitea.io/gitea-vet v0.2.1 h1:b30by7+3SkmiftK0RjuXqFvZg2q4p68uoPGuxhzBN0s=
code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
code.gitea.io/sdk/gitea v0.13.1-0.20210304201955-ff82113459b5 h1:va0KddYHN8bH6MCUaWf5e4p+il55blUw5J0ha5vTMaQ=
code.gitea.io/sdk/gitea v0.13.1-0.20210304201955-ff82113459b5/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs=
gitea.com/noerw/unidiff-comments v0.0.0-20201219085024-64aec5658f2b h1:CLYsMGcGLohESQDMth+RgJ4cB3CCHToxnj0zBbvB3sE=
gitea.com/noerw/unidiff-comments v0.0.0-20201219085024-64aec5658f2b/go.mod h1:Fc8iyPm4NINRWujeIk2bTfcbGc4ZYY29/oMAAGcr4qI=
github.com/AlecAivazis/survey/v2 v2.2.8 h1:TgxCwybKdBckmC+/P9/5h49rw/nAHe/itZL0dgHs+Q0=
github.com/AlecAivazis/survey/v2 v2.2.8/go.mod h1:9DYvHgXtiXm6nCn+jXnOXLKbH+Yo9u8fAS/SduGdoPk=
code.gitea.io/sdk/gitea v0.11.2 h1:D0xIRlHv3IckzdYOWzHK1bPvlkXdA4LD909UYyBdi1o=
code.gitea.io/sdk/gitea v0.11.2/go.mod h1:z3uwDV/b9Ls47NGukYM9XhnHtqPh/J+t40lsUrR6JDY=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/adrg/xdg v0.3.1 h1:uIyL9BYfXaFgDyVRKE8wjtm6ETQULweQqTofphEFJYY=
github.com/adrg/xdg v0.3.1/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.8.1 h1:ym20sbvyC6RXz45u4qDglcgr8E313oPROshcuCHqiEE=
github.com/alecthomas/chroma v0.8.1/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e h1:OjdSMCht0ZVX7IH0nTdf00xEustvbtUGRgMh3gbdmOg=
github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 h1:c4mLfegoDw6OhSJXTd2jUEQgZUQuJWtocudb97Qn9EM=
github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU=
github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.2.0 h1:YPBLG/3UK1we1ohRkncLjaXWLW+HKp5QNM/jTli2JgI=
github.com/go-git/go-git/v5 v5.2.0/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs=
github.com/go-gitea/yaml v0.0.0-20170812160011-eb3733d160e7 h1:/FEVbfrJ50yBk73Lyq1oCZ4VaCc0g1xd9xLHjz+Znf8=
github.com/go-gitea/yaml v0.0.0-20170812160011-eb3733d160e7/go.mod h1:WjJPyqjAk/UMv+Fk/ZRjEOh5SXszSALnSzKqICd7pNg=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDEq1axMbGg=
github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/muesli/reflow v0.2.0 h1:2o0UBJPHHH4fa2GCXU4Rg4DwOtWPMekCeyc5EWbAQp0=
github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8=
github.com/muesli/termenv v0.7.4 h1:/pBqvU5CpkY53tU0vVn+xgs2ZTX63aH5nY+SSps5Xa8=
github.com/muesli/termenv v0.7.4/go.mod h1:pZ7qY9l3F7e5xsAOS0zCew2tME+p7bWeBkotCEcIIcc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/noerw/glamour v0.2.1-0.20210305125354-f0a29f1de0c2 h1:ACjOTGUGi7rt3JQU9GIFFs8sueFGShy6GcGjQhMmKjs=
github.com/noerw/glamour v0.2.1-0.20210305125354-f0a29f1de0c2/go.mod h1:WIVFX8Y2VIK1Y/1qtXYL/Vvzqlcbo3VgVop9i2piPkE=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/seletskiy/tplutil v0.0.0-20200921103632-f880f6245597 h1:nZY1S2jo+VtDrUfjO9XYI137O41hhRkxZNV5Fb5ixCA=
github.com/seletskiy/tplutil v0.0.0-20200921103632-f880f6245597/go.mod h1:F8CBHSOjnzjx9EeXyWJTAzJyVxN+Y8JH2WjLMn4utiw=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.1 h1:eVwehsLsZlCJCwXyGLgg+Q4iFWE/eTIMG0e8waCmm/I=
github.com/yuin/goldmark v1.3.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305034016-7844c3c200c3 h1:RdE7htvBru4I4VZQofQjCZk5W9+aLNlSF5n0zgVwm8s=
golang.org/x/sys v0.0.0-20210305034016-7844c3c200c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

87
main.go
View File

@ -1,4 +1,4 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
@ -6,11 +6,12 @@
package main // import "code.gitea.io/tea"
import (
"fmt"
"log"
"os"
"strings"
"code.gitea.io/tea/cmd"
"code.gitea.io/tea/modules/setting"
"github.com/urfave/cli/v2"
)
@ -21,41 +22,32 @@ var Version = "development"
// Tags holds the build tags used
var Tags = ""
func main() {
// make parsing tea --version easier, by printing /just/ the version string
cli.VersionPrinter = func(c *cli.Context) { fmt.Fprintln(c.App.Writer, c.App.Version) }
func init() {
setting.AppVer = Version
setting.AppBuiltWith = formatBuiltWith(Tags)
}
func main() {
app := cli.NewApp()
app.Name = "tea"
app.Usage = "command line tool to interact with Gitea"
app.Description = appDescription
app.CustomAppHelpTemplate = helpTemplate
app.Usage = "Command line tool to interact with Gitea"
app.Description = ``
app.Version = Version + formatBuiltWith(Tags)
app.Commands = []*cli.Command{
&cmd.CmdLogin,
&cmd.CmdLogout,
&cmd.CmdAutocomplete,
&cmd.CmdIssues,
&cmd.CmdPulls,
&cmd.CmdLabels,
&cmd.CmdMilestones,
&cmd.CmdReleases,
&cmd.CmdTrackedTimes,
&cmd.CmdOrgs,
&cmd.CmdRepos,
&cmd.CmdAddComment,
&cmd.CmdLabels,
&cmd.CmdTrackedTimes,
&cmd.CmdOpen,
&cmd.CmdNotifications,
}
app.EnableBashCompletion = true
err := app.Run(os.Args)
if err != nil {
// app.Run already exits for errors implementing ErrorCoder,
// so we only handle generic errors with code 1 here.
fmt.Fprintf(app.ErrWriter, "Error: %v\n", err)
os.Exit(1)
log.Fatalf("Failed to run app with %s: %v", os.Args, err)
}
}
@ -66,56 +58,3 @@ func formatBuiltWith(Tags string) string {
return " built with: " + strings.Replace(Tags, " ", ", ", -1)
}
var appDescription = `tea is a productivity helper for Gitea. It can be used to manage most entities on one
or multiple Gitea instances and provides local helpers like 'tea pull checkout'.
tea makes use of context provided by the repository in $PWD if available, but is still
usable independently of $PWD. Configuration is persisted in $XDG_CONFIG_HOME/tea.
`
var helpTemplate = bold(`
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}}`) + `
{{if .Version}}{{if not .HideVersion}}version {{.Version}}{{end}}{{end}}
USAGE
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .Commands}} command [subcommand] [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}}
DESCRIPTION
{{.Description | nindent 3 | trim}}{{end}}{{if .VisibleCommands}}
COMMANDS{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
OPTIONS
{{range $index, $option := .VisibleFlags}}{{if $index}}
{{end}}{{$option}}{{end}}{{end}}
EXAMPLES
tea login add # add a login once to get started
tea pulls # list open pulls for the repo in $PWD
tea pulls --repo $HOME/foo # list open pulls for the repo in $HOME/foo
tea pulls --remote upstream # list open pulls for the repo pointed at by
# your local "upstream" git remote
# list open pulls for any gitea repo at the given login instance
tea pulls --repo gitea/tea --login gitea.com
tea milestone issues 0.7.0 # view open issues for milestone '0.7.0'
tea issue 189 # view contents of issue 189
tea open 189 # open web ui for issue 189
tea open milestones # open web ui for milestones
# send gitea desktop notifications every 5 minutes (bash + libnotify)
while :; do tea notifications --all -o simple | xargs -i notify-send {}; sleep 300; done
ABOUT
Written & maintained by The Gitea Authors.
If you find a bug or want to contribute, we'll welcome you at https://gitea.com/gitea/tea.
More info about Gitea itself on https://gitea.io.
`
func bold(t string) string {
return fmt.Sprintf("\033[1m%s\033[0m", t)
}

View File

@ -1,86 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package config
import (
"fmt"
"io/ioutil"
"log"
"path/filepath"
"sync"
"code.gitea.io/tea/modules/utils"
"github.com/adrg/xdg"
"gopkg.in/yaml.v2"
)
// LocalConfig represents local configurations
type LocalConfig struct {
Logins []Login `yaml:"logins"`
}
var (
// config contain if loaded local tea config
config LocalConfig
loadConfigOnce sync.Once
)
// GetConfigPath return path to tea config file
func GetConfigPath() string {
configFilePath, err := xdg.ConfigFile("tea/config.yml")
var exists bool
if err != nil {
exists = false
} else {
exists, _ = utils.PathExists(configFilePath)
}
// fallback to old config if no new one exists
if !exists {
file := filepath.Join(xdg.Home, ".tea", "tea.yml")
exists, _ = utils.PathExists(file)
if exists {
return file
}
}
if err != nil {
log.Fatal("unable to get or create config file")
}
return configFilePath
}
// loadConfig load config from file
func loadConfig() (err error) {
loadConfigOnce.Do(func() {
ymlPath := GetConfigPath()
exist, _ := utils.FileExist(ymlPath)
if exist {
bs, err := ioutil.ReadFile(ymlPath)
if err != nil {
err = fmt.Errorf("Failed to read config file: %s", ymlPath)
}
err = yaml.Unmarshal(bs, &config)
if err != nil {
err = fmt.Errorf("Failed to parse contents of config file: %s", ymlPath)
}
}
})
return
}
// saveConfig save config to file
func saveConfig() error {
ymlPath := GetConfigPath()
bs, err := yaml.Marshal(config)
if err != nil {
return err
}
return ioutil.WriteFile(ymlPath, bs, 0660)
}

View File

@ -1,180 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package config
import (
"crypto/tls"
"errors"
"fmt"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"code.gitea.io/sdk/gitea"
)
// Login represents a login to a gitea server, you even could add multiple logins for one gitea server
type Login struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
Token string `yaml:"token"`
Default bool `yaml:"default"`
SSHHost string `yaml:"ssh_host"`
// optional path to the private key
SSHKey string `yaml:"ssh_key"`
Insecure bool `yaml:"insecure"`
// User is username from gitea
User string `yaml:"user"`
// Created is auto created unix timestamp
Created int64 `yaml:"created"`
}
// GetLogins return all login available by config
func GetLogins() ([]Login, error) {
if err := loadConfig(); err != nil {
return nil, err
}
return config.Logins, nil
}
// GetDefaultLogin return the default login
func GetDefaultLogin() (*Login, error) {
if err := loadConfig(); err != nil {
return nil, err
}
if len(config.Logins) == 0 {
return nil, errors.New("No available login")
}
for _, l := range config.Logins {
if l.Default {
return &l, nil
}
}
return &config.Logins[0], nil
}
// SetDefaultLogin set the default login by name (case insensitive)
func SetDefaultLogin(name string) error {
if err := loadConfig(); err != nil {
return err
}
loginExist := false
for i := range config.Logins {
config.Logins[i].Default = false
if strings.ToLower(config.Logins[i].Name) == strings.ToLower(name) {
config.Logins[i].Default = true
loginExist = true
}
}
if !loginExist {
return fmt.Errorf("login '%s' not found", name)
}
return saveConfig()
}
// GetLoginByName get login by name (case insensitive)
func GetLoginByName(name string) *Login {
err := loadConfig()
if err != nil {
log.Fatal(err)
}
for _, l := range config.Logins {
if strings.ToLower(l.Name) == strings.ToLower(name) {
return &l
}
}
return nil
}
// GetLoginByToken get login by token
func GetLoginByToken(token string) *Login {
err := loadConfig()
if err != nil {
log.Fatal(err)
}
for _, l := range config.Logins {
if l.Token == token {
return &l
}
}
return nil
}
// DeleteLogin delete a login by name from config
func DeleteLogin(name string) error {
var idx = -1
for i, l := range config.Logins {
if l.Name == name {
idx = i
break
}
}
if idx == -1 {
return fmt.Errorf("can not delete login '%s', does not exist", name)
}
config.Logins = append(config.Logins[:idx], config.Logins[idx+1:]...)
return saveConfig()
}
// AddLogin save a login to config
func AddLogin(login *Login) error {
if err := loadConfig(); err != nil {
return err
}
// save login to global var
config.Logins = append(config.Logins, *login)
// save login to config file
return saveConfig()
}
// Client returns a client to operate Gitea API
func (l *Login) Client() *gitea.Client {
httpClient := &http.Client{}
if l.Insecure {
cookieJar, _ := cookiejar.New(nil)
httpClient = &http.Client{
Jar: cookieJar,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}}
}
client, err := gitea.NewClient(l.URL,
gitea.SetToken(l.Token),
gitea.SetHTTPClient(httpClient),
)
if err != nil {
log.Fatal(err)
}
return client
}
// GetSSHHost returns SSH host name
func (l *Login) GetSSHHost() string {
if l.SSHHost != "" {
return l.SSHHost
}
u, err := url.Parse(l.URL)
if err != nil {
return ""
}
return u.Hostname()
}

View File

@ -1,197 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package context
import (
"errors"
"fmt"
"log"
"os"
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/utils"
gogit "github.com/go-git/go-git/v5"
"github.com/urfave/cli/v2"
)
var (
errNotAGiteaRepo = errors.New("No Gitea login found. You might want to specify --repo (and --login) to work outside of a repository")
)
// TeaContext contains all context derived during command initialization and wraps cli.Context
type TeaContext struct {
*cli.Context
Login *config.Login // config data & client for selected login
RepoSlug string // <owner>/<repo>, optional
Owner string // repo owner as derived from context or provided in flag, optional
Repo string // repo name as derived from context or provided in flag, optional
Output string // value of output flag
LocalRepo *git.TeaRepo // is set if flags specified a local repo via --repo, or if $PWD is a git repo
}
// GetListOptions return ListOptions based on PaginationFlags
func (ctx *TeaContext) GetListOptions() gitea.ListOptions {
page := ctx.Int("page")
limit := ctx.Int("limit")
if limit != 0 && page == 0 {
page = 1
}
return gitea.ListOptions{
Page: page,
PageSize: limit,
}
}
// Ensure checks if requirements on the context are set, and terminates otherwise.
func (ctx *TeaContext) Ensure(req CtxRequirement) {
if req.LocalRepo && ctx.LocalRepo == nil {
fmt.Println("Local repository required: Execute from a repo dir, or specify a path with --repo.")
os.Exit(1)
}
if req.RemoteRepo && len(ctx.RepoSlug) == 0 {
fmt.Println("Remote repository required: Specify ID via --repo or execute from a local git repo.")
os.Exit(1)
}
}
// CtxRequirement specifies context needed for operation
type CtxRequirement struct {
// ensures a local git repo is available & ctx.LocalRepo is set. Implies .RemoteRepo
LocalRepo bool
// ensures ctx.RepoSlug, .Owner, .Repo are set
RemoteRepo bool
}
// InitCommand resolves the application context, and returns the active login, and if
// available the repo slug. It does this by reading the config file for logins, parsing
// the remotes of the .git repo specified in repoFlag or $PWD, and using overrides from
// command flags. If a local git repo can't be found, repo slug values are unset.
func InitCommand(ctx *cli.Context) *TeaContext {
// these flags are used as overrides to the context detection via local git repo
repoFlag := ctx.String("repo")
loginFlag := ctx.String("login")
remoteFlag := ctx.String("remote")
var (
c TeaContext
err error
repoPath string // empty means PWD
repoFlagPathExists bool
)
// check if repoFlag can be interpreted as path to local repo.
if len(repoFlag) != 0 {
if repoFlagPathExists, err = utils.DirExists(repoFlag); err != nil {
log.Fatal(err.Error())
}
if repoFlagPathExists {
repoPath = repoFlag
}
}
if len(repoFlag) == 0 || repoFlagPathExists {
// try to read git repo & extract context, ignoring if PWD is not a repo
if c.LocalRepo, c.Login, c.RepoSlug, err = contextFromLocalRepo(repoPath, remoteFlag); err != nil {
if err == errNotAGiteaRepo || err == gogit.ErrRepositoryNotExists {
// we can deal with that, commands needing the optional values use ctx.Ensure()
} else {
log.Fatal(err.Error())
}
}
}
if len(repoFlag) != 0 && !repoFlagPathExists {
// if repoFlag is not a valid path, use it to override repoSlug
c.RepoSlug = repoFlag
}
// override login from flag, or use default login if repo based detection failed
if len(loginFlag) != 0 {
c.Login = config.GetLoginByName(loginFlag)
if c.Login == nil {
log.Fatalf("Login name '%s' does not exist", loginFlag)
}
} else if c.Login == nil {
if c.Login, err = config.GetDefaultLogin(); err != nil {
log.Fatal(err.Error())
}
}
// parse reposlug (owner falling back to login owner if reposlug contains only repo name)
c.Owner, c.Repo = utils.GetOwnerAndRepo(c.RepoSlug, c.Login.User)
c.Context = ctx
c.Output = ctx.String("output")
return &c
}
// contextFromLocalRepo discovers login & repo slug from the default branch remote of the given local repo
func contextFromLocalRepo(repoPath, remoteValue string) (*git.TeaRepo, *config.Login, string, error) {
repo, err := git.RepoFromPath(repoPath)
if err != nil {
return nil, nil, "", err
}
gitConfig, err := repo.Config()
if err != nil {
return repo, nil, "", err
}
// if no remote
if len(gitConfig.Remotes) == 0 {
return repo, nil, "", errors.New("No remote(s) found in this Git repository")
}
// if only one remote exists
if len(gitConfig.Remotes) >= 1 && len(remoteValue) == 0 {
for remote := range gitConfig.Remotes {
remoteValue = remote
}
if len(gitConfig.Remotes) > 1 {
// if master branch is present, use it as the default remote
masterBranch, ok := gitConfig.Branches["master"]
if ok {
if len(masterBranch.Remote) > 0 {
remoteValue = masterBranch.Remote
}
}
}
}
remoteConfig, ok := gitConfig.Remotes[remoteValue]
if !ok || remoteConfig == nil {
return repo, nil, "", fmt.Errorf("Remote '%s' not found in this Git repository", remoteValue)
}
logins, err := config.GetLogins()
if err != nil {
return repo, nil, "", err
}
for _, l := range logins {
for _, u := range remoteConfig.URLs {
p, err := git.ParseURL(strings.TrimSpace(u))
if err != nil {
return repo, nil, "", fmt.Errorf("Git remote URL parse failed: %s", err.Error())
}
if strings.EqualFold(p.Scheme, "http") || strings.EqualFold(p.Scheme, "https") {
if strings.HasPrefix(u, l.URL) {
ps := strings.Split(p.Path, "/")
path := strings.Join(ps[len(ps)-2:], "/")
return repo, &l, strings.TrimSuffix(path, ".git"), nil
}
} else if strings.EqualFold(p.Scheme, "ssh") {
if l.GetSSHHost() == strings.Split(p.Host, ":")[0] {
return repo, &l, strings.TrimLeft(strings.TrimSuffix(p.Path, ".git"), "/"), nil
}
}
}
}
return repo, nil, "", errNotAGiteaRepo
}

View File

@ -5,50 +5,71 @@
package git
import (
"bufio"
"fmt"
"io/ioutil"
"net/url"
"os"
"os/user"
"path/filepath"
"strings"
"code.gitea.io/tea/modules/utils"
git_transport "github.com/go-git/go-git/v5/plumbing/transport"
gogit_http "github.com/go-git/go-git/v5/plumbing/transport/http"
gogit_ssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
git_transport "gopkg.in/src-d/go-git.v4/plumbing/transport"
gogit_http "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
gogit_ssh "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh"
)
type pwCallback = func(string) (string, error)
// GetAuthForURL returns the appropriate AuthMethod to be used in Push() / Pull()
// operations depending on the protocol, and prompts the user for credentials if
// necessary.
func GetAuthForURL(remoteURL *url.URL, authToken, keyFile string, passwordCallback pwCallback) (git_transport.AuthMethod, error) {
func GetAuthForURL(remoteURL *url.URL, httpUser, keyFile string) (auth git_transport.AuthMethod, err error) {
user := remoteURL.User.Username()
switch remoteURL.Scheme {
case "http", "https":
// gitea supports push/pull via app token as username.
return &gogit_http.BasicAuth{Password: "", Username: authToken}, nil
case "https":
if httpUser != "" {
user = httpUser
}
if user == "" {
user, err = promptUser(remoteURL.Host)
if err != nil {
return nil, err
}
}
pass, isSet := remoteURL.User.Password()
if !isSet {
pass, err = promptPass(remoteURL.Host)
if err != nil {
return nil, err
}
}
auth = &gogit_http.BasicAuth{Password: pass, Username: user}
case "ssh":
// try to select right key via ssh-agent. if it fails, try to read a key manually
user := remoteURL.User.Username()
auth, err := gogit_ssh.DefaultAuthBuilder(user)
auth, err = gogit_ssh.DefaultAuthBuilder(user)
if err != nil {
signer, err2 := readSSHPrivKey(keyFile, passwordCallback)
if err2 != nil {
return nil, err2
signer, err := readSSHPrivKey(keyFile)
if err != nil {
return nil, err
}
auth = &gogit_ssh.PublicKeys{User: user, Signer: signer}
}
return auth, nil
}
default:
return nil, fmt.Errorf("don't know how to handle url scheme %v", remoteURL.Scheme)
}
return auth, nil
}
func readSSHPrivKey(keyFile string, passwordCallback pwCallback) (sig ssh.Signer, err error) {
func readSSHPrivKey(keyFile string) (sig ssh.Signer, err error) {
if keyFile != "" {
keyFile, err = utils.AbsPathWithExpansion(keyFile)
keyFile, err = absPathWithExpansion(keyFile)
} else {
keyFile, err = utils.AbsPathWithExpansion("~/.ssh/id_rsa")
keyFile, err = absPathWithExpansion("~/.ssh/id_rsa")
}
if err != nil {
return nil, err
@ -58,19 +79,42 @@ func readSSHPrivKey(keyFile string, passwordCallback pwCallback) (sig ssh.Signer
return nil, err
}
sig, err = ssh.ParsePrivateKey(sshKey)
if _, ok := err.(*ssh.PassphraseMissingError); ok && passwordCallback != nil {
// allow for up to 3 password attempts
for i := 0; i < 3; i++ {
var pass string
pass, err = passwordCallback(keyFile)
if err != nil {
pass, err := promptPass(keyFile)
if err != nil {
return nil, err
}
sig, err = ssh.ParsePrivateKeyWithPassphrase(sshKey, []byte(pass))
if err == nil {
break
}
if err != nil {
return nil, err
}
}
return sig, err
}
func promptUser(domain string) (string, error) {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("%s username: ", domain)
username, err := reader.ReadString('\n')
return strings.TrimSpace(username), err
}
func promptPass(domain string) (string, error) {
fmt.Printf("%s password: ", domain)
pass, err := terminal.ReadPassword(0)
return string(pass), err
}
func absPathWithExpansion(p string) (string, error) {
u, err := user.Current()
if err != nil {
return "", err
}
if p == "~" {
return u.HomeDir, nil
} else if strings.HasPrefix(p, "~/") {
return filepath.Join(u.HomeDir, p[2:]), nil
} else {
return filepath.Abs(p)
}
}

View File

@ -8,10 +8,10 @@ import (
"fmt"
"strings"
"github.com/go-git/go-git/v5"
git_config "github.com/go-git/go-git/v5/config"
git_plumbing "github.com/go-git/go-git/v5/plumbing"
git_transport "github.com/go-git/go-git/v5/plumbing/transport"
"gopkg.in/src-d/go-git.v4"
git_config "gopkg.in/src-d/go-git.v4/config"
git_plumbing "gopkg.in/src-d/go-git.v4/plumbing"
git_transport "gopkg.in/src-d/go-git.v4/plumbing/transport"
)
// TeaCreateBranch creates a new branch in the repo, tracking from another branch.
@ -38,36 +38,42 @@ func (r TeaRepo) TeaCreateBranch(localBranchName, remoteBranchName, remoteName s
}
// TeaCheckout checks out the given branch in the worktree.
func (r TeaRepo) TeaCheckout(ref git_plumbing.ReferenceName) error {
func (r TeaRepo) TeaCheckout(branchName string) error {
tree, err := r.Worktree()
if err != nil {
return err
}
return tree.Checkout(&git.CheckoutOptions{Branch: ref})
localBranchRefName := git_plumbing.NewBranchReferenceName(branchName)
return tree.Checkout(&git.CheckoutOptions{Branch: localBranchRefName})
}
// TeaDeleteLocalBranch removes the given branch locally
func (r TeaRepo) TeaDeleteLocalBranch(branch *git_config.Branch) error {
// TeaDeleteBranch removes the given branch locally, and if `remoteBranch` is
// not empty deletes it at it's remote repo.
func (r TeaRepo) TeaDeleteBranch(branch *git_config.Branch, remoteBranch string, auth git_transport.AuthMethod) error {
err := r.DeleteBranch(branch.Name)
// if the branch is not found that's ok, as .git/config may have no entry if
// no remote tracking branch is configured for it (eg push without -u flag)
if err != nil && err.Error() != "branch not found" {
return err
}
return r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name))
}
err = r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name))
if err != nil {
return err
}
// TeaDeleteRemoteBranch removes the given branch on the given remote via git protocol
func (r TeaRepo) TeaDeleteRemoteBranch(remoteName, remoteBranch string, auth git_transport.AuthMethod) error {
if remoteBranch != "" {
// delete remote branch via git protocol:
// an empty source in the refspec means remote deletion to git 🙃
refspec := fmt.Sprintf(":%s", git_plumbing.NewBranchReferenceName(remoteBranch))
return r.Push(&git.PushOptions{
RemoteName: remoteName,
err = r.Push(&git.PushOptions{
RemoteName: branch.Remote,
RefSpecs: []git_config.RefSpec{git_config.RefSpec(refspec)},
Prune: true,
Auth: auth,
})
}
return err
}
// TeaFindBranchBySha returns a branch that is at the the given SHA and syncs to the
@ -169,59 +175,3 @@ func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *git_config.
}
return b, b.Validate()
}
// TeaFindBranchRemote gives the first remote that has a branch with the same name or sha,
// depending on what is passed in.
// This function is needed, as git does not always define branches in .git/config with remote entries.
func (r TeaRepo) TeaFindBranchRemote(branchName, hash string) (*git.Remote, error) {
remotes, err := r.Remotes()
if err != nil {
return nil, err
}
switch {
case len(remotes) == 0:
return nil, nil
case len(remotes) == 1:
return remotes[0], nil
}
// check if the given remote has our branch (.git/refs/remotes/<remoteName>/*)
iter, err := r.References()
if err != nil {
return nil, err
}
defer iter.Close()
var match *git.Remote
err = iter.ForEach(func(ref *git_plumbing.Reference) error {
if ref.Name().IsRemote() {
names := strings.SplitN(ref.Name().Short(), "/", 2)
remote := names[0]
branch := names[1]
hashMatch := hash != "" && hash == ref.Hash().String()
nameMatch := branchName != "" && branchName == branch
if hashMatch || nameMatch {
match, err = r.Remote(remote)
return err
}
}
return nil
})
return match, err
}
// TeaGetCurrentBranchName return the name of the branch witch is currently active
func (r TeaRepo) TeaGetCurrentBranchName() (string, error) {
localHead, err := r.Head()
if err != nil {
return "", err
}
if !localHead.Name().IsBranch() {
return "", fmt.Errorf("active ref is no branch")
}
return localHead.Name().Short(), nil
}

View File

@ -8,8 +8,8 @@ import (
"fmt"
"net/url"
"github.com/go-git/go-git/v5"
git_config "github.com/go-git/go-git/v5/config"
"gopkg.in/src-d/go-git.v4"
git_config "gopkg.in/src-d/go-git.v4/config"
)
// GetRemote tries to match a Remote of the repo via the given URL.

View File

@ -5,7 +5,7 @@
package git
import (
"github.com/go-git/go-git/v5"
"gopkg.in/src-d/go-git.v4"
)
// TeaRepo is a go-git Repository, with an extended high level interface.
@ -16,15 +16,7 @@ type TeaRepo struct {
// RepoForWorkdir tries to open the git repository in the local directory
// for reading or modification.
func RepoForWorkdir() (*TeaRepo, error) {
return RepoFromPath("")
}
// RepoFromPath tries to open the git repository by path
func RepoFromPath(path string) (*TeaRepo, error) {
if len(path) == 0 {
path = "./"
}
repo, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{
repo, err := git.PlainOpenWithOptions("./", &git.PlainOpenOptions{
DetectDotGit: true,
})
if err != nil {

View File

@ -1,80 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package interact
import (
"fmt"
"os"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"github.com/AlecAivazis/survey/v2"
"golang.org/x/crypto/ssh/terminal"
)
// ShowCommentsMaybeInteractive fetches & prints comments, depending on the --comments flag.
// If that flag is unset, and output is not piped, prompts the user first.
func ShowCommentsMaybeInteractive(ctx *context.TeaContext, idx int64, totalComments int) error {
if ctx.Bool("comments") {
opts := gitea.ListIssueCommentOptions{ListOptions: ctx.GetListOptions()}
c := ctx.Login.Client()
comments, _, err := c.ListIssueComments(ctx.Owner, ctx.Repo, idx, opts)
if err != nil {
return err
}
print.Comments(comments)
} else if IsInteractive() && !ctx.IsSet("comments") {
// if we're interactive, but --comments hasn't been explicitly set to false
if err := ShowCommentsPaginated(ctx, idx, totalComments); err != nil {
fmt.Printf("error while loading comments: %v\n", err)
}
}
return nil
}
// ShowCommentsPaginated prompts if issue/pr comments should be shown and continues to do so.
func ShowCommentsPaginated(ctx *context.TeaContext, idx int64, totalComments int) error {
c := ctx.Login.Client()
opts := gitea.ListIssueCommentOptions{ListOptions: ctx.GetListOptions()}
prompt := "show comments?"
commentsLoaded := 0
// paginated fetch
// NOTE: as of gitea 1.13, pagination is not provided by this endpoint, but handles
// this function gracefully anyways.
for {
loadComments := false
confirm := survey.Confirm{Message: prompt, Default: true}
if err := survey.AskOne(&confirm, &loadComments); err != nil {
return err
} else if !loadComments {
break
} else {
if comments, _, err := c.ListIssueComments(ctx.Owner, ctx.Repo, idx, opts); err != nil {
return err
} else if len(comments) != 0 {
print.Comments(comments)
commentsLoaded += len(comments)
}
if commentsLoaded >= totalComments {
break
}
opts.ListOptions.Page++
prompt = "load more?"
}
}
return nil
}
// IsInteractive checks if the output is piped, but NOT if the session is run interactively..
func IsInteractive() bool {
return terminal.IsTerminal(int(os.Stdout.Fd()))
}
// IsStdinPiped checks if stdin is piped
func IsStdinPiped() bool {
return !terminal.IsTerminal(int(os.Stdin.Fd()))
}

View File

@ -1,162 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package interact
import (
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/task"
"github.com/AlecAivazis/survey/v2"
)
// CreateIssue interactively creates an issue
func CreateIssue(login *config.Login, owner, repo string) error {
owner, repo, err := promptRepoSlug(owner, repo)
if err != nil {
return err
}
var opts gitea.CreateIssueOption
if err := promptIssueProperties(login, owner, repo, &opts); err != nil {
return err
}
return task.CreateIssue(login, owner, repo, opts)
}
func promptIssueProperties(login *config.Login, owner, repo string, o *gitea.CreateIssueOption) error {
var milestoneName string
var labels []string
var err error
selectableChan := make(chan (issueSelectables), 1)
go fetchIssueSelectables(login, owner, repo, selectableChan)
// title
promptOpts := survey.WithValidator(survey.Required)
promptI := &survey.Input{Message: "Issue title:", Default: o.Title}
if err = survey.AskOne(promptI, &o.Title, promptOpts); err != nil {
return err
}
// description
promptD := &survey.Multiline{Message: "Issue description:", Default: o.Body}
if err = survey.AskOne(promptD, &o.Body); err != nil {
return err
}
// wait until selectables are fetched
selectables := <-selectableChan
if selectables.Err != nil {
return selectables.Err
}
// skip remaining props if we don't have permission to set them
if !selectables.Repo.Permissions.Push {
return nil
}
// assignees
if o.Assignees, err = promptMultiSelect("Assignees:", selectables.Collaborators, "[other]"); err != nil {
return err
}
// milestone
if len(selectables.MilestoneList) != 0 {
if milestoneName, err = promptSelect("Milestone:", selectables.MilestoneList, "", "[none]"); err != nil {
return err
}
o.Milestone = selectables.MilestoneMap[milestoneName]
}
// labels
if len(selectables.LabelList) != 0 {
promptL := &survey.MultiSelect{Message: "Labels:", Options: selectables.LabelList, VimMode: true, Default: o.Labels}
if err := survey.AskOne(promptL, &labels); err != nil {
return err
}
o.Labels = make([]int64, len(labels))
for i, l := range labels {
o.Labels[i] = selectables.LabelMap[l]
}
}
// deadline
if o.Deadline, err = promptDatetime("Due date:"); err != nil {
return err
}
return nil
}
type issueSelectables struct {
Repo *gitea.Repository
Collaborators []string
MilestoneList []string
MilestoneMap map[string]int64
LabelList []string
LabelMap map[string]int64
Err error
}
func fetchIssueSelectables(login *config.Login, owner, repo string, done chan issueSelectables) {
// TODO PERF make these calls concurrent
r := issueSelectables{}
c := login.Client()
r.Repo, _, r.Err = c.GetRepo(owner, repo)
if r.Err != nil {
done <- r
return
}
// we can set the following properties only if we have write access to the repo
// so we fastpath this if not.
if !r.Repo.Permissions.Push {
done <- r
return
}
// FIXME: this should ideally be ListAssignees(), https://github.com/go-gitea/gitea/issues/14856
colabs, _, err := c.ListCollaborators(owner, repo, gitea.ListCollaboratorsOptions{})
if err != nil {
r.Err = err
done <- r
return
}
r.Collaborators = make([]string, len(colabs)+1)
r.Collaborators[0] = login.User
for i, u := range colabs {
r.Collaborators[i+1] = u.UserName
}
milestones, _, err := c.ListRepoMilestones(owner, repo, gitea.ListMilestoneOption{})
if err != nil {
r.Err = err
done <- r
return
}
r.MilestoneMap = make(map[string]int64)
r.MilestoneList = make([]string, len(milestones))
for i, m := range milestones {
r.MilestoneMap[m.Title] = m.ID
r.MilestoneList[i] = m.Title
}
labels, _, err := c.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{})
if err != nil {
r.Err = err
done <- r
return
}
r.LabelMap = make(map[string]int64)
r.LabelList = make([]string, len(labels))
for i, l := range labels {
r.LabelMap[l.Name] = l.ID
r.LabelList[i] = l.Name
}
done <- r
}

View File

@ -1,91 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package interact
import (
"fmt"
"strings"
"code.gitea.io/tea/modules/task"
"github.com/AlecAivazis/survey/v2"
)
// CreateLogin create an login interactive
func CreateLogin() error {
var name, token, user, passwd, sshKey, giteaURL string
var insecure = false
promptI := &survey.Input{Message: "URL of Gitea instance: "}
if err := survey.AskOne(promptI, &giteaURL, survey.WithValidator(survey.Required)); err != nil {
return err
}
giteaURL = strings.TrimSuffix(strings.TrimSpace(giteaURL), "/")
if len(giteaURL) == 0 {
fmt.Println("URL is required!")
return nil
}
name, err := task.GenerateLoginName(giteaURL, "")
if err != nil {
return err
}
promptI = &survey.Input{Message: "Name of new Login [" + name + "]: "}
if err := survey.AskOne(promptI, &name); err != nil {
return err
}
var hasToken bool
promptYN := &survey.Confirm{
Message: "Do you have an access token?",
Default: false,
}
if err = survey.AskOne(promptYN, &hasToken); err != nil {
return err
}
if hasToken {
promptI = &survey.Input{Message: "Token: "}
if err := survey.AskOne(promptI, &token, survey.WithValidator(survey.Required)); err != nil {
return err
}
} else {
promptI = &survey.Input{Message: "Username: "}
if err = survey.AskOne(promptI, &user, survey.WithValidator(survey.Required)); err != nil {
return err
}
promptPW := &survey.Password{Message: "Password: "}
if err = survey.AskOne(promptPW, &passwd, survey.WithValidator(survey.Required)); err != nil {
return err
}
}
var optSettings bool
promptYN = &survey.Confirm{
Message: "Set Optional settings: ",
Default: false,
}
if err = survey.AskOne(promptYN, &optSettings); err != nil {
return err
}
if optSettings {
promptI = &survey.Input{Message: "SSH Key Path (leave empty for auto-discovery):"}
if err := survey.AskOne(promptI, &sshKey); err != nil {
return err
}
promptYN = &survey.Confirm{
Message: "Allow Insecure connections: ",
Default: false,
}
if err = survey.AskOne(promptYN, &insecure); err != nil {
return err
}
}
return task.CreateLogin(name, token, user, passwd, sshKey, giteaURL, insecure)
}

View File

@ -1,54 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package interact
import (
"time"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/task"
"code.gitea.io/sdk/gitea"
"github.com/AlecAivazis/survey/v2"
)
// CreateMilestone interactively creates a milestone
func CreateMilestone(login *config.Login, owner, repo string) error {
var title, description string
var deadline *time.Time
// owner, repo
owner, repo, err := promptRepoSlug(owner, repo)
if err != nil {
return err
}
// title
promptOpts := survey.WithValidator(survey.Required)
promptI := &survey.Input{Message: "Milestone title:"}
if err := survey.AskOne(promptI, &title, promptOpts); err != nil {
return err
}
// description
promptM := &survey.Multiline{Message: "Milestone description:"}
if err := survey.AskOne(promptM, &description); err != nil {
return err
}
// deadline
if deadline, err = promptDatetime("Milestone deadline:"); err != nil {
return err
}
return task.CreateMilestone(
login,
owner,
repo,
title,
description,
deadline,
gitea.StateOpen)
}

View File

@ -1,165 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package interact
import (
"fmt"
"strings"
"time"
"code.gitea.io/tea/modules/utils"
"github.com/AlecAivazis/survey/v2"
"github.com/araddon/dateparse"
)
// PromptMultiline runs a textfield-style prompt and blocks until input was made.
func PromptMultiline(message string) (content string, err error) {
err = survey.AskOne(&survey.Multiline{Message: message}, &content)
return
}
// PromptPassword asks for a password and blocks until input was made.
func PromptPassword(name string) (pass string, err error) {
promptPW := &survey.Password{Message: name + " password:"}
err = survey.AskOne(promptPW, &pass, survey.WithValidator(survey.Required))
return
}
// promptRepoSlug interactively prompts for a Gitea repository or returns the current one
func promptRepoSlug(defaultOwner, defaultRepo string) (owner, repo string, err error) {
prompt := "Target repo:"
defaultVal := ""
required := true
if len(defaultOwner) != 0 && len(defaultRepo) != 0 {
defaultVal = fmt.Sprintf("%s/%s", defaultOwner, defaultRepo)
required = false
}
var repoSlug string
owner = defaultOwner
repo = defaultRepo
err = survey.AskOne(
&survey.Input{
Message: prompt,
Default: defaultVal,
},
&repoSlug,
survey.WithValidator(func(input interface{}) error {
if str, ok := input.(string); ok {
if !required && len(str) == 0 {
return nil
}
split := strings.Split(str, "/")
if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
return fmt.Errorf("must follow the <owner>/<repo> syntax")
}
} else {
return fmt.Errorf("invalid result type")
}
return nil
}),
)
if err == nil && len(repoSlug) != 0 {
repoSlugSplit := strings.Split(repoSlug, "/")
owner = repoSlugSplit[0]
repo = repoSlugSplit[1]
}
return
}
// promptDatetime prompts for a date or datetime string.
// Supports all formats understood by araddon/dateparse.
func promptDatetime(prompt string) (val *time.Time, err error) {
var input string
err = survey.AskOne(
&survey.Input{Message: prompt},
&input,
survey.WithValidator(func(input interface{}) error {
if str, ok := input.(string); ok {
if len(str) == 0 {
return nil
}
t, err := dateparse.ParseAny(str)
if err != nil {
return err
}
val = &t
} else {
return fmt.Errorf("invalid result type")
}
return nil
}),
)
return
}
// promptSelect creates a generic multiselect prompt, with processing of custom values.
func promptMultiSelect(prompt string, options []string, customVal string) ([]string, error) {
var selection []string
promptA := &survey.MultiSelect{
Message: prompt,
Options: makeSelectOpts(options, customVal, ""),
VimMode: true,
}
if err := survey.AskOne(promptA, &selection); err != nil {
return nil, err
}
return promptCustomVal(prompt, customVal, selection)
}
// promptSelect creates a generic select prompt, with processing of custom values or none-option.
func promptSelect(prompt string, options []string, customVal, noneVal string) (string, error) {
var selection string
promptA := &survey.Select{
Message: prompt,
Options: makeSelectOpts(options, customVal, noneVal),
VimMode: true,
Default: noneVal,
}
if err := survey.AskOne(promptA, &selection); err != nil {
return "", err
}
if noneVal != "" && selection == noneVal {
return "", nil
}
if customVal != "" {
sel, err := promptCustomVal(prompt, customVal, []string{selection})
if err != nil {
return "", err
}
selection = sel[0]
}
return selection, nil
}
// makeSelectOpts adds cusotmVal & noneVal to opts if set.
func makeSelectOpts(opts []string, customVal, noneVal string) []string {
if customVal != "" {
opts = append(opts, customVal)
}
if noneVal != "" {
opts = append(opts, noneVal)
}
return opts
}
// promptCustomVal checks if customVal is present in selection, and prompts
// for custom input to add to the selection instead.
func promptCustomVal(prompt, customVal string, selection []string) ([]string, error) {
// check for custom value & prompt again with text input
// HACK until https://github.com/AlecAivazis/survey/issues/339 is implemented
if otherIndex := utils.IndexOf(selection, customVal); otherIndex != -1 {
var customAssignees string
promptA := &survey.Input{Message: prompt, Help: "comma separated list"}
if err := survey.AskOne(promptA, &customAssignees); err != nil {
return nil, err
}
selection = append(selection[:otherIndex], selection[otherIndex+1:]...)
selection = append(selection, strings.Split(customAssignees, ",")...)
}
return selection, nil
}

View File

@ -1,69 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package interact
import (
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/task"
"github.com/AlecAivazis/survey/v2"
)
// CreatePull interactively creates a PR
func CreatePull(login *config.Login, owner, repo string) error {
var base, head string
// owner, repo
owner, repo, err := promptRepoSlug(owner, repo)
if err != nil {
return err
}
// base
base, err = task.GetDefaultPRBase(login, owner, repo)
if err != nil {
return err
}
promptI := &survey.Input{Message: "Target branch:", Default: base}
if err := survey.AskOne(promptI, &base); err != nil {
return err
}
// head
localRepo, err := git.RepoForWorkdir()
if err != nil {
return err
}
promptOpts := survey.WithValidator(survey.Required)
headOwner, headBranch, err := task.GetDefaultPRHead(localRepo)
if err == nil {
promptOpts = nil
}
promptI = &survey.Input{Message: "Source repo owner:", Default: headOwner}
if err := survey.AskOne(promptI, &headOwner); err != nil {
return err
}
promptI = &survey.Input{Message: "Source branch:", Default: headBranch}
if err := survey.AskOne(promptI, &headBranch, promptOpts); err != nil {
return err
}
head = task.GetHeadSpec(headOwner, headBranch, owner)
opts := gitea.CreateIssueOption{Title: task.GetDefaultPRTitle(head)}
if err = promptIssueProperties(login, owner, repo, &opts); err != nil {
return err
}
return task.CreatePull(
login,
owner,
repo,
base,
head,
&opts)
}

View File

@ -1,80 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package interact
import (
"fmt"
"os"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"code.gitea.io/sdk/gitea"
"github.com/AlecAivazis/survey/v2"
)
var reviewStates = map[string]gitea.ReviewStateType{
"approve": gitea.ReviewStateApproved,
"comment": gitea.ReviewStateComment,
"request changes": gitea.ReviewStateRequestChanges,
}
var reviewStateOptions = []string{"comment", "request changes", "approve"}
// ReviewPull interactively reviews a PR
func ReviewPull(ctx *context.TeaContext, idx int64) error {
var state gitea.ReviewStateType
var comment string
var codeComments []gitea.CreatePullReviewComment
var err error
// codeComments
var reviewDiff bool
promptDiff := &survey.Confirm{Message: "Review / comment the diff?", Default: true}
if err = survey.AskOne(promptDiff, &reviewDiff); err != nil {
return err
}
if reviewDiff {
if codeComments, err = DoDiffReview(ctx, idx); err != nil {
fmt.Printf("Error during diff review: %s\n", err)
}
fmt.Printf("Found %d code comments in your review\n", len(codeComments))
}
// state
var stateString string
promptState := &survey.Select{Message: "Your assessment:", Options: reviewStateOptions, VimMode: true}
if err = survey.AskOne(promptState, &stateString); err != nil {
return err
}
state = reviewStates[stateString]
// comment
var promptOpts survey.AskOpt
if state == gitea.ReviewStateComment || state == gitea.ReviewStateRequestChanges {
promptOpts = survey.WithValidator(survey.Required)
}
err = survey.AskOne(&survey.Multiline{Message: "Concluding comment:"}, &comment, promptOpts)
if err != nil {
return err
}
return task.CreatePullReview(ctx, idx, state, comment, codeComments)
}
// DoDiffReview (1) fetches & saves diff in tempfile, (2) starts $EDITOR to comment on diff,
// (3) parses resulting file into code comments.
func DoDiffReview(ctx *context.TeaContext, idx int64) ([]gitea.CreatePullReviewComment, error) {
tmpFile, err := task.SavePullDiff(ctx, idx)
if err != nil {
return nil, err
}
defer os.Remove(tmpFile)
if err = task.OpenFileInEditor(tmpFile); err != nil {
return nil, err
}
return task.ParseDiffComments(tmpFile)
}

View File

@ -1,50 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package print
import (
"fmt"
"strings"
"code.gitea.io/sdk/gitea"
)
// Comments renders a list of comments to stdout
func Comments(comments []*gitea.Comment) {
var baseURL string
if len(comments) != 0 {
baseURL = comments[0].HTMLURL
}
var out = make([]string, len(comments))
for i, c := range comments {
out[i] = formatComment(c)
}
outputMarkdown(fmt.Sprintf(
// this will become a heading by means of the first --- from a comment
"Comments\n%s",
strings.Join(out, "\n"),
), baseURL)
}
// Comment renders a comment to stdout
func Comment(c *gitea.Comment) {
outputMarkdown(formatComment(c), c.HTMLURL)
}
func formatComment(c *gitea.Comment) string {
edited := ""
if c.Updated.After(c.Created) {
edited = fmt.Sprintf(" *(edited on %s)*", FormatTime(c.Updated))
}
return fmt.Sprintf(
"---\n\n**@%s** wrote on %s%s:\n\n%s\n",
c.Poster.UserName,
FormatTime(c.Created),
edited,
c.Body,
)
}

View File

@ -1,74 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package print
import (
"fmt"
"time"
"code.gitea.io/sdk/gitea"
"github.com/muesli/termenv"
)
// formatSize get kb in int and return string
func formatSize(kb int64) string {
if kb < 1024 {
return fmt.Sprintf("%d Kb", kb)
}
mb := kb / 1024
if mb < 1024 {
return fmt.Sprintf("%d Mb", mb)
}
gb := mb / 1024
if gb < 1024 {
return fmt.Sprintf("%d Gb", gb)
}
return fmt.Sprintf("%d Tb", gb/1024)
}
// FormatTime give a date-time in local timezone if available
func FormatTime(t time.Time) string {
location, err := time.LoadLocation("Local")
if err != nil {
return t.Format("2006-01-02 15:04 UTC")
}
return t.In(location).Format("2006-01-02 15:04")
}
func formatDuration(seconds int64, outputType string) string {
if isMachineReadable(outputType) {
return fmt.Sprint(seconds)
}
return time.Duration(1e9 * seconds).String()
}
func formatLabel(label *gitea.Label, allowColor bool, text string) string {
colorProfile := termenv.Ascii
if allowColor {
colorProfile = termenv.EnvColorProfile()
}
if len(text) == 0 {
text = label.Name
}
styled := termenv.String(text)
styled = styled.Foreground(colorProfile.Color("#" + label.Color))
return fmt.Sprint(styled)
}
func formatPermission(p *gitea.Permission) string {
if p.Admin {
return "admin"
} else if p.Push {
return "write"
}
return "read"
}
func formatUserName(u *gitea.User) string {
if len(u.FullName) == 0 {
return u.UserName
}
return u.FullName
}

View File

@ -1,126 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package print
import (
"fmt"
"strings"
"code.gitea.io/sdk/gitea"
)
// IssueDetails print an issue rendered to stdout
func IssueDetails(issue *gitea.Issue) {
outputMarkdown(fmt.Sprintf(
"# #%d %s (%s)\n@%s created %s\n\n%s\n",
issue.Index,
issue.Title,
issue.State,
issue.Poster.UserName,
FormatTime(issue.Created),
issue.Body,
), issue.HTMLURL)
}
// IssuesPullsList prints a listing of issues & pulls
func IssuesPullsList(issues []*gitea.Issue, output string, fields []string) {
printIssues(issues, output, fields)
}
// IssueFields are all available fields to print with IssuesList()
var IssueFields = []string{
"index",
"state",
"kind",
"author",
"author-id",
"url",
"title",
"body",
"created",
"updated",
"deadline",
"assignees",
"milestone",
"labels",
"comments",
}
func printIssues(issues []*gitea.Issue, output string, fields []string) {
labelMap := map[int64]string{}
var printables = make([]printable, len(issues))
for i, x := range issues {
// pre-serialize labels for performance
for _, label := range x.Labels {
if _, ok := labelMap[label.ID]; !ok {
labelMap[label.ID] = formatLabel(label, !isMachineReadable(output), "")
}
}
// store items with printable interface
printables[i] = &printableIssue{x, &labelMap}
}
t := tableFromItems(fields, printables)
t.print(output)
}
type printableIssue struct {
*gitea.Issue
formattedLabels *map[int64]string
}
func (x printableIssue) FormatField(field string) string {
switch field {
case "index":
return fmt.Sprintf("%d", x.Index)
case "state":
return string(x.State)
case "kind":
if x.PullRequest != nil {
return "Pull"
}
return "Issue"
case "author":
return formatUserName(x.Poster)
case "author-id":
return x.Poster.UserName
case "url":
return x.HTMLURL
case "title":
return x.Title
case "body":
return x.Body
case "created":
return FormatTime(x.Created)
case "updated":
return FormatTime(x.Updated)
case "deadline":
return FormatTime(*x.Deadline)
case "milestone":
if x.Milestone != nil {
return x.Milestone.Title
}
return ""
case "labels":
var labels = make([]string, len(x.Labels))
for i, l := range x.Labels {
labels[i] = (*x.formattedLabels)[l.ID]
}
return strings.Join(labels, " ")
case "assignees":
var assignees = make([]string, len(x.Assignees))
for i, a := range x.Assignees {
assignees[i] = formatUserName(a)
}
return strings.Join(assignees, " ")
case "comments":
return fmt.Sprintf("%d", x.Comments)
}
return ""
}

View File

@ -1,31 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package print
import (
"strconv"
"code.gitea.io/sdk/gitea"
)
// LabelsList prints a listing of labels
func LabelsList(labels []*gitea.Label, output string) {
t := tableWithHeader(
"Index",
"Color",
"Name",
"Description",
)
for _, label := range labels {
t.addRow(
strconv.FormatInt(label.ID, 10),
formatLabel(label, !isMachineReadable(output), label.Color),
label.Name,
label.Description,
)
}
t.print(output)
}

View File

@ -1,55 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package print
import (
"fmt"
"strings"
"time"
"code.gitea.io/tea/modules/config"
)
// LoginDetails print login entry to stdout
func LoginDetails(login *config.Login) {
in := fmt.Sprintf("# %s\n\n[@%s](%s/%s)\n",
login.Name,
login.User,
strings.TrimSuffix(login.URL, "/"),
login.User,
)
if len(login.SSHKey) != 0 {
in += fmt.Sprintf("\nSSH Key: '%s' via %s\n",
login.SSHKey,
login.SSHHost,
)
}
in += fmt.Sprintf("\nCreated: %s", time.Unix(login.Created, 0).Format(time.RFC822))
outputMarkdown(in, "")
}
// LoginsList prints a listing of logins
func LoginsList(logins []config.Login, output string) {
t := tableWithHeader(
"Name",
"URL",
"SSHHost",
"User",
"Default",
)
for _, l := range logins {
t.addRow(
l.Name,
l.URL,
l.GetSSHHost(),
l.User,
fmt.Sprint(l.Default),
)
}
t.print(output)
}

View File

@ -1,51 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package print
import (
"fmt"
"os"
"github.com/charmbracelet/glamour"
"golang.org/x/crypto/ssh/terminal"
)
// outputMarkdown prints markdown to stdout, formatted for terminals.
// If the input could not be parsed, it is printed unformatted, the error
// is returned anyway.
func outputMarkdown(markdown string, baseURL string) error {
renderer, err := glamour.NewTermRenderer(
glamour.WithAutoStyle(),
glamour.WithBaseURL(baseURL),
glamour.WithWordWrap(getWordWrap()),
)
if err != nil {
fmt.Printf(markdown)
return err
}
out, err := renderer.Render(markdown)
if err != nil {
fmt.Printf(markdown)
return err
}
fmt.Print(out)
return nil
}
// stolen from https://github.com/charmbracelet/glow/blob/e9d728c/main.go#L152-L165
func getWordWrap() int {
fd := int(os.Stdout.Fd())
width := 80
if terminal.IsTerminal(fd) {
if w, _, err := terminal.GetSize(fd); err == nil {
width = w
}
}
if width > 120 {
width = 120
}
return width
}

View File

@ -1,63 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package print
import (
"fmt"
"code.gitea.io/sdk/gitea"
)
// MilestoneDetails print an milestone formatted to stdout
func MilestoneDetails(milestone *gitea.Milestone) {
fmt.Printf("%s\n",
milestone.Title,
)
if len(milestone.Description) != 0 {
fmt.Printf("\n%s\n", milestone.Description)
}
if milestone.Deadline != nil && !milestone.Deadline.IsZero() {
fmt.Printf("\nDeadline: %s\n", FormatTime(*milestone.Deadline))
}
}
// MilestonesList prints a listing of milestones
func MilestonesList(miles []*gitea.Milestone, output string, state gitea.StateType) {
headers := []string{
"Title",
}
if state == gitea.StateAll {
headers = append(headers, "State")
}
headers = append(headers,
"Open/Closed Issues",
"DueDate",
)
t := table{headers: headers}
for _, m := range miles {
var deadline = ""
if m.Deadline != nil && !m.Deadline.IsZero() {
deadline = FormatTime(*m.Deadline)
}
item := []string{
m.Title,
}
if state == gitea.StateAll {
item = append(item, string(m.State))
}
item = append(item,
fmt.Sprintf("%d/%d", m.OpenIssues, m.ClosedIssues),
deadline,
)
t.addRowSlice(item)
}
t.sort(0, true)
t.print(output)
}

Some files were not shown because too many files have changed in this diff Show More