30 Commits

Author SHA1 Message Date
9fb8b883f3 darwin/arm64 & update s3 bucket 2021-08-26 18:15:51 -04:00
2319724bb2 Update Changelog (#346)
smal nit's missing

Co-authored-by: Norwin Roosen <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/346
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: techknowlogick <techknowlogick@gitea.io>
Reviewed-by: 6543 <6543@obermui.de>
Co-authored-by: Norwin <noerw@noreply.gitea.io>
Co-committed-by: Norwin <noerw@noreply.gitea.io>
2021-03-12 20:44:41 +08:00
222d0501df Detect markdown line width, resolve relative URLs (#332)
~~this is semi-blocked by https://github.com/charmbracelet/glamour/pull/96, but behaviour isn't really worse than the previous behaviour (most links work, some are still broken)~~

#### testcase for link resolver
```
tea pr 332
tea checkout 332 && make install && tea pr 332
```

- [rel](./332)
- [abs](/gitea/tea/pulls/332)
- [full](https://gitea.com/gitea/tea/pulls/332)

Co-authored-by: Norwin Roosen <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/332
Reviewed-by: 6543 <6543@obermui.de>
Reviewed-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: Norwin <noerw@noreply.gitea.io>
Co-committed-by: Norwin <noerw@noreply.gitea.io>
2021-03-12 20:28:46 +08:00
cb404b53b5 Changelog v0.7.0 (#345)
Reviewed-on: https://gitea.com/gitea/tea/pulls/345
Reviewed-by: techknowlogick <techknowlogick@gitea.io>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: 6543 <6543@obermui.de>
Co-committed-by: 6543 <6543@obermui.de>
2021-03-12 08:41:54 +08:00
3abc5a5b42 Allow checking out PRs with deleted head branch (#341)
..by explicitly fetching `refs/pulls/:idx/head` from the base repo.

Sorry, I mixed this with a split-up of `PullCheckout()`. I can try to separate that, if preferred

Co-authored-by: Norwin Roosen <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/341
Reviewed-by: 6543 <6543@obermui.de>
Reviewed-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: Norwin <noerw@noreply.gitea.io>
Co-committed-by: Norwin <noerw@noreply.gitea.io>
2021-03-12 02:16:02 +08:00
6f738df4a5 Add more issue / pr creation params (#331)
adds assignees, labels, deadline, milestone params

- [x] add flags to `tea issue create` (this is BREAKING, `-b` moved to `-d` for consistency with pr create)
- [x] add interactive mode to `tea issue create`
- [x] add flags to `tea pr create`
- [x] add interactive mode to `tea pr create`

fixes #171, fixes #303

Co-authored-by: Norwin Roosen <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/331
Reviewed-by: 6543 <6543@obermui.de>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Norwin <noerw@noreply.gitea.io>
Co-committed-by: Norwin <noerw@noreply.gitea.io>
2021-03-08 19:48:03 +08:00
d22b314701 Introduce workaround for missing pull head sha (#340)
fix #318

test with `tea pr 58`

Co-authored-by: Norwin Roosen <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/340
Reviewed-by: Norwin <noerw@noreply.gitea.io>
Reviewed-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: 6543 <6543@obermui.de>
Co-committed-by: 6543 <6543@obermui.de>
2021-03-08 03:45:50 +08:00
786c713ff5 [CI] use golang v1.16 (#339)
Reviewed-on: https://gitea.com/gitea/tea/pulls/339
Reviewed-by: Andrew Thornton <art27@cantab.net>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: 6543 <6543@obermui.de>
Co-committed-by: 6543 <6543@obermui.de>
2021-03-05 20:37:50 +08:00
d474883e90 don't push before creating a pull (#334)
Not sure if this is the best way, but it's the simplest way to fix #333.
Everything else is overly complex due to a chicken-egg problem:
Knowing which remote / branch to push involves requires prompting the user,
which requires to have a upstream branch pushed to detect default values.

Co-authored-by: Norwin Roosen <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/334
Reviewed-by: 6543 <6543@obermui.de>
Reviewed-by: Andrew Thornton <art27@cantab.net>
Reviewed-by: khmarbaise <khmarbaise@noreply.gitea.io>
Co-authored-by: Norwin <noerw@noreply.gitea.io>
Co-committed-by: Norwin <noerw@noreply.gitea.io>
2021-03-05 18:27:09 +08:00
0d98cbd657 Update Vendors (#337)
* update & migrate gitea sdk (Fix Delete Tag Issue)
* upgraded github.com/AlecAivazis/survey v2.2.7 => v2.2.8
* upgraded github.com/adrg/xdg v0.2.3 => v0.3.1
* upgraded github.com/araddon/dateparse
* upgraded github.com/olekukonko/tablewriter v0.0.4 => v0.0.5
* upgraded gopkg.in/yaml.v2 v2.3.0 => v2.4.0

Reviewed-on: https://gitea.com/gitea/tea/pulls/337
Reviewed-by: Norwin <noerw@noreply.gitea.io>
Reviewed-by: khmarbaise <khmarbaise@noreply.gitea.io>
Co-authored-by: 6543 <6543@obermui.de>
Co-committed-by: 6543 <6543@obermui.de>
2021-03-05 18:06:25 +08:00
15c4edba1a Don't exit if we can't find a local repo with a remote matching to a login (#336)
This enables to run commands that need minimal context (i.e. `tea n --all`) to run anywhere.

fixes #329

Co-authored-by: Norwin Roosen <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/336
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: 6543 <6543@obermui.de>
Co-authored-by: Norwin <noerw@noreply.gitea.io>
Co-committed-by: Norwin <noerw@noreply.gitea.io>
2021-03-05 16:56:15 +08:00
e96cfdbbe7 tea pr checkout: dont create local branches (#314)
This avoids creation of local branches, to avoid cluttering the local repo:
- if the commit already exists on the tip of a local branch, check that one out
- otherwise check out the remote tracking branch (`refs/remotes/<remote>/<head>`), and suggest what to do if you want to make changes.

I'm not certain this behaviour is actually better, I suggest leaving this open for a while for people to try out the new behaviour:
```
tea pr checkout 314
make install
```

fixes #293

Co-authored-by: Norwin Roosen <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/314
Reviewed-by: 6543 <6543@obermui.de>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Norwin <noerw@noreply.gitea.io>
Co-committed-by: Norwin <noerw@noreply.gitea.io>
2021-03-02 21:50:11 +08:00
3c1efd33e2 InitCommand() robustness (#327)
fixes #320

Co-authored-by: Norwin Roosen <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/327
Reviewed-by: Andrew Thornton <art27@cantab.net>
Reviewed-by: 6543 <6543@obermui.de>
Co-authored-by: Norwin <noerw@noreply.gitea.io>
Co-committed-by: Norwin <noerw@noreply.gitea.io>
2021-03-01 06:29:26 +08:00
9c8321f2e0 tea comment: handle piped stdin (#322)
fixes #321

Co-authored-by: Norwin Roosen <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/322
Reviewed-by: 6543 <6543@obermui.de>
Reviewed-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: Norwin <noerw@noreply.gitea.io>
Co-committed-by: Norwin <noerw@noreply.gitea.io>
2021-03-01 01:47:36 +08:00
b5c670ebf8 Improve tea time (#319)
better docs

add --mine flag

hm, is there a better name? 🤔

do time filtering serverside

make printed fields dynamic

add --fields to tea times ls

code review

Co-authored-by: Norwin Roosen <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/319
Reviewed-by: 6543 <6543@obermui.de>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
2020-12-23 12:58:36 +08:00
95ef061711 Update dependencies (#316)
update xdg

update survey

update go-sdk

Co-authored-by: Norwin Roosen <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/316
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: 6543 <6543@obermui.de>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
2020-12-22 01:11:08 +08:00
32b7b771cc Add tea comment and show comments of issues/pulls (#313)
show comments of PR

TODO: there needs to be a way to force running non-interactively

add `tea comment` to post a comment

add --comments flag, prompt only if necessary

don't prompt if --comments is provided, or output is piped

show comments for issues, add --comments flag

tea comment: print resulting comment

Merge branch 'master' into issue-172-comments

remove debug print statement

unrelated, but better than opening another PR for this ;)

Merge remote-tracking branch 'upstream/master' into issue-172-comments

ret err

fix lint

Co-authored-by: Norwin Roosen <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/313
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: 6543 <6543@obermui.de>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
2020-12-22 00:07:35 +08:00
9efee7bf99 Add tea issues --fields, allow printing labels (#312)
generalize list printing with dynamic fields

refactor print.IssuesList to use tableFromItems()

preparatory refactor

print.IssuesList: allow printing labels

move formatters to formatters.go

expose more printable fields on issue

add generic flags.FieldsFlag

add fields flag to tea issues, tea ms issues

validate provided fields

add strict username, or formatted user fields

change default fields

tea issues -> replace updated with labels
tea ms issues -> replace author with labels, reorder

Validate provided fields

Co-authored-by: Norwin Roosen <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/312
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: 6543 <6543@obermui.de>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
2020-12-21 23:41:07 +08:00
8bb5c15745 Add commands for reviews (#315)
add interactive `tea pr review`

it's amazingly simple

vendor gitea.com/noerw/unidiff-comments

add `tea pr lgtm|reject` shorthands

vendor slimmed down diff parser

review diff: default to true

if users want a shortcut, they can use lgtm or reject subcmds

`tea pr approve`: accept optional comment

Co-authored-by: Norwin Roosen <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/315
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: 6543 <6543@obermui.de>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
2020-12-21 23:22:22 +08:00
43a58bdba1 Proper help text & new README structure (#311)
add cli.AppHelpTemplate for customization

customize tea help view

tea --version : improve parseability

Rework README to include tea help output

It's an antipattern to have different help texts aimed at the same
users. So now that we have a good cli help text, lets use it here.
This eases maintenance, and at the same time gives an honest impression
on what we have to offer, while also encouraging to improve the internal
help text in the future.

I feel a bit sad for the GIF, but it was becoming outdated anyway..

group commands by category

add new demo gif

shows the (probably) most useful workflow

readme improvement

Merge branch 'master' into improve-app-help

code review

Merge branch 'master' into improve-app-help

restructure installation section

Co-authored-by: Norwin Roosen <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/311
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: khmarbaise <khmarbaise@noreply.gitea.io>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
2020-12-21 21:37:20 +08:00
43e9943757 Add interactive mode for tea milestone create (#310)
Implement interactive milestone creation

Return fmt.Errorf when title is empty

Incorporate deadline functionality

Use dateparse and cleanup CreateMilestone task

Signed-off-by: Martin Reboredo <yakoyoku@gmail.com>
Co-authored-by: Martin Reboredo <yakoyoku@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/310
Reviewed-by: Norwin <noerw@noreply.gitea.io>
Reviewed-by: 6543 <6543@obermui.de>
Co-Authored-By: Martin Reboredo <yakoyakoyokuyoku@noreply.gitea.io>
Co-Committed-By: Martin Reboredo <yakoyakoyokuyoku@noreply.gitea.io>
2020-12-18 02:50:07 +08:00
8b588f5313 make PR workflow helpers more robust (#300)
improve handling of remote deleted branches

split git.TeaDeleteBranch

only delete remote branch if we have permission

add missing err check

Co-authored-by: Norwin Roosen <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/300
Reviewed-by: 6543 <6543@obermui.de>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
2020-12-17 22:00:16 +08:00
a2e8b47c57 Implement PR closing and reopening (#304)
Implement pull request closing/reopening

Signed-off-by: Martin Reboredo <yakoyoku@gmail.com>

Correct year and `pull` description

Apply changes from #291

Return fmt.Errorf instead of log.Fatal if no pull index was supplied

Co-authored-by: Martin Reboredo <yakoyoku@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/304
Reviewed-by: Norwin <noerw@noreply.gitea.io>
Reviewed-by: appleboy <appleboy.tw@gmail.com>
Co-Authored-By: Martin Reboredo <yakoyakoyokuyoku@noreply.gitea.io>
Co-Committed-By: Martin Reboredo <yakoyakoyokuyoku@noreply.gitea.io>
2020-12-17 06:47:12 +08:00
83b73ce78e Show PR CI status (#306)
fix layout of pr reviews

show PR CI status

put conflict info in status list

remove line

show merged state

deduplicate reviews by user

Co-authored-by: Norwin Roosen <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/306
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: 6543 <6543@obermui.de>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
2020-12-17 01:16:50 +08:00
782a6318f3 Add more command shorthands (#307)
add more command aliases

breaking: s/notif/n

Co-authored-by: Norwin Roosen <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/307
Reviewed-by: 6543 <6543@obermui.de>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
2020-12-17 00:47:40 +08:00
a948fd7e10 Refactor error handling (#308)
use fmt instead of log

log.Fatal -> return err

set non-zero exit code on error

print to default err log

cleanup

fix vet

Co-authored-by: Norwin Roosen <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/308
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: 6543 <6543@obermui.de>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
2020-12-17 00:18:10 +08:00
287df8a715 Add command to install shell completion (#309)
add autocompletion files to contrib/

curl -o contrib/autocomplete.zsh https://raw.githubusercontent.com/urfave/cli/master/autocomplete/zsh_autocomplete
curl -o contrib/autocomplete.sh https://raw.githubusercontent.com/urfave/cli/master/autocomplete/bash_autocomplete
add powershell

add `tea meta autocomplete`

closes #86

update docs

Co-authored-by: Norwin Roosen <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/309
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: 6543 <6543@obermui.de>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
2020-12-17 00:01:59 +08:00
dc67630b64 replace flag globals, require context for commands (#291)
introduce TeaContext

clean up InitCommand

move GetListOptions to TeaContext

ensure context for each command

so we fail early with a good error message instead of "Error: 404" etc

make linter happy

Merge branch 'master' into refactor-global-flags

move TeaContext & InitCommand to modules/context

Merge branch 'master' into refactor-global-flags

CI.restart()

Merge branch 'master' into refactor-global-flags

Merge branch 'master' into refactor-global-flags

Co-authored-by: Norwin Roosen <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/291
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: khmarbaise <khmarbaise@noreply.gitea.io>
Reviewed-by: 6543 <6543@obermui.de>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
2020-12-16 01:38:22 +08:00
e5cdad554e Add feature comparison chart between forge CLIs (#294)
WIP: add comparison

Merge branch 'master' into issue-194-comparison

move file

hint in readme

Co-authored-by: Norwin Roosen <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/294
Reviewed-by: 6543 <6543@obermui.de>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
2020-12-15 12:59:49 +08:00
b9f5ba0702 Add interactive mode for tea issue create (#302)
Implement interactive issue creation

Comment PromptRepoSlug

Move PromptRepoSlug to the right place

Hide promptRepoSlug

Signed-off-by: Martin Reboredo <yakoyoku@gmail.com>
Co-authored-by: Martin Reboredo <yakoyoku@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/302
Reviewed-by: Norwin <noerw@noreply.gitea.io>
Reviewed-by: khmarbaise <khmarbaise@noreply.gitea.io>
Reviewed-by: 6543 <6543@obermui.de>
Co-Authored-By: Martin Reboredo <yakoyakoyokuyoku@noreply.gitea.io>
Co-Committed-By: Martin Reboredo <yakoyakoyokuyoku@noreply.gitea.io>
2020-12-15 04:05:31 +08:00
613 changed files with 30035 additions and 4606 deletions

View File

@ -9,7 +9,7 @@ platform:
steps: steps:
- name: build - name: build
pull: always pull: always
image: golang:1.15 image: golang:1.16
environment: environment:
GOPROXY: https://goproxy.cn GOPROXY: https://goproxy.cn
commands: commands:
@ -27,7 +27,7 @@ steps:
- pull_request - pull_request
- name: unit-test - name: unit-test
image: golang:1.15 image: golang:1.16
commands: commands:
- make unit-test-coverage - make unit-test-coverage
settings: settings:
@ -40,7 +40,7 @@ steps:
- pull_request - pull_request
- name: release-test - name: release-test
image: golang:1.15 image: golang:1.16
commands: commands:
- make test - make test
settings: settings:
@ -54,7 +54,7 @@ steps:
- name: tag-test - name: tag-test
pull: always pull: always
image: golang:1.15 image: golang:1.16
commands: commands:
- make test - make test
settings: settings:
@ -64,7 +64,7 @@ steps:
- tag - tag
- name: static - name: static
image: golang:1.15 image: golang:1.16
environment: environment:
GOPROXY: https://goproxy.cn GOPROXY: https://goproxy.cn
commands: commands:
@ -99,7 +99,7 @@ steps:
image: plugins/s3:1 image: plugins/s3:1
settings: settings:
acl: public-read acl: public-read
bucket: releases bucket: gitea-artifacts
endpoint: https://storage.gitea.io endpoint: https://storage.gitea.io
path_style: true path_style: true
source: "dist/release/*" source: "dist/release/*"
@ -119,7 +119,7 @@ steps:
image: plugins/s3:1 image: plugins/s3:1
settings: settings:
acl: public-read acl: public-read
bucket: releases bucket: gitea-artifacts
endpoint: https://storage.gitea.io endpoint: https://storage.gitea.io
path_style: true path_style: true
source: "dist/release/*" source: "dist/release/*"
@ -141,7 +141,7 @@ steps:
image: plugins/s3:1 image: plugins/s3:1
settings: settings:
acl: public-read acl: public-read
bucket: releases bucket: gitea-artifacts
endpoint: https://storage.gitea.io endpoint: https://storage.gitea.io
path_style: true path_style: true
source: "dist/release/*" source: "dist/release/*"

2
.gitignore vendored
View File

@ -4,3 +4,5 @@ tea
.idea/ .idea/
.history/ .history/
dist/ dist/
.vscode/

View File

@ -1,5 +1,34 @@
# Changelog # 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 ## [v0.6.0](https://gitea.com/gitea/tea/releases/tag/v0.6.0) - 2020-12-11
* BREAKING * BREAKING

63
FEATURE-COMPARISON.md Normal file
View File

@ -0,0 +1,63 @@
# 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

@ -151,7 +151,7 @@ release-os:
@hash gox > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash gox > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
cd /tmp && $(GO) get -u github.com/mitchellh/gox; \ cd /tmp && $(GO) get -u github.com/mitchellh/gox; \
fi fi
CGO_ENABLED=0 gox -verbose -cgo=false -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -osarch='!darwin/386 !darwin/arm64 !darwin/arm' -os="windows linux darwin" -arch="386 amd64 arm arm64" -output="$(DIST)/release/tea-$(VERSION)-{{.OS}}-{{.Arch}}" 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}}"
.PHONY: release-compress .PHONY: release-compress
release-compress: release-compress:

127
README.md
View File

@ -2,62 +2,95 @@
[![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 interface for gitea ### The official CLI for Gitea
Tea is a command line tool for interacting on one or more Gitea instances. ![demo gif](./demo.gif)
It uses [code.gitea.io/sdk](https://code.gitea.io/sdk) and interacts with the Gitea API
![demo gif](https://dl.gitea.io/screenshots/tea_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.
## Installation ## Installation
You can use the prebuilt binaries from [dl.gitea.io](https://dl.gitea.io/tea/) There are different ways to get `tea`:
To install from source, go 1.13 or newer is required: 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 2. Use the prebuilt binaries from [dl.gitea.io](https://dl.gitea.io/tea/)
go get code.gitea.io/tea
go install code.gitea.io/tea
```
If you have `brew` installed, you can install `tea` via: 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 4. Docker (thirdparty): [tgerczei/tea](https://hub.docker.com/r/tgerczei/tea)
brew tap gitea/tap https://gitea.com/gitea/homebrew-gitea
brew install tea
```
Distribution packages exist for: **alpinelinux ([tea](https://pkgs.alpinelinux.org/packages?name=tea&branch=edge))** and **archlinux ([gitea-tea](https://aur.archlinux.org/packages/gitea-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 following `tea` subcommands.
Detailed usage information is available via `tea <command> --help`.
```none
login Log in to a Gitea server
logout Log out from a Gitea server
issues List, create and update issues
pulls List, create, checkout and clean pull requests
releases List, create, update and delete releases
repos Operate with repositories
labels Manage issue labels
times Operate on tracked times of a repositorys issues and pulls
open Open something of the repository on web browser
notifications Show notifications
milestones List and create milestones
organizations List, create, delete organizations
help, h Shows a list of commands or help for one command
```
To fetch issues from different repos, use the `--remote` flag (when inside a gitea repository directory) or `--login` & `--repo` flags.
## Compilation ## Compilation

106
cmd/autocomplete.go Normal file
View File

@ -0,0 +1,106 @@
// 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
}

11
cmd/categories.go Normal file
View File

@ -0,0 +1,11 @@
// 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"
)

77
cmd/comment.go Normal file
View File

@ -0,0 +1,77 @@
// 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
}

View File

@ -5,53 +5,44 @@
package flags package flags
import ( import (
"code.gitea.io/sdk/gitea" "fmt"
"github.com/urfave/cli/v2" "strings"
)
// create global variables for global Flags to simplify "code.gitea.io/sdk/gitea"
// access to the options without requiring cli.Context "code.gitea.io/tea/modules/context"
var ( "code.gitea.io/tea/modules/task"
// GlobalLoginValue contain value of --login|-l arg "code.gitea.io/tea/modules/utils"
GlobalLoginValue string
// GlobalRepoValue contain value of --repo|-r arg "github.com/araddon/dateparse"
GlobalRepoValue string "github.com/urfave/cli/v2"
// GlobalOutputValue contain value of --output|-o arg
GlobalOutputValue string
// GlobalRemoteValue contain value of --remote|-R arg
GlobalRemoteValue string
) )
// LoginFlag provides flag to specify tea login profile // LoginFlag provides flag to specify tea login profile
var LoginFlag = cli.StringFlag{ var LoginFlag = cli.StringFlag{
Name: "login", Name: "login",
Aliases: []string{"l"}, Aliases: []string{"l"},
Usage: "Use a different Gitea login. Optional", Usage: "Use a different Gitea Login. Optional",
Destination: &GlobalLoginValue,
} }
// RepoFlag provides flag to specify repository // RepoFlag provides flag to specify repository
var RepoFlag = cli.StringFlag{ var RepoFlag = cli.StringFlag{
Name: "repo", Name: "repo",
Aliases: []string{"r"}, Aliases: []string{"r"},
Usage: "Override local repository path or gitea repository slug to interact with. Optional", Usage: "Override local repository path or gitea repository slug to interact with. Optional",
Destination: &GlobalRepoValue,
} }
// RemoteFlag provides flag to specify remote repository // RemoteFlag provides flag to specify remote repository
var RemoteFlag = cli.StringFlag{ var RemoteFlag = cli.StringFlag{
Name: "remote", Name: "remote",
Aliases: []string{"R"}, Aliases: []string{"R"},
Usage: "Discover Gitea login from remote. Optional", Usage: "Discover Gitea login from remote. Optional",
Destination: &GlobalRemoteValue,
} }
// OutputFlag provides flag to specify output type // OutputFlag provides flag to specify output type
var OutputFlag = cli.StringFlag{ var OutputFlag = cli.StringFlag{
Name: "output", Name: "output",
Aliases: []string{"o"}, Aliases: []string{"o"},
Usage: "Output format. (csv, simple, table, tsv, yaml)", Usage: "Output format. (csv, simple, table, tsv, yaml)",
Destination: &GlobalOutputValue,
} }
// StateFlag provides flag to specify issue/pr state, defaulting to "open" // StateFlag provides flag to specify issue/pr state, defaulting to "open"
@ -110,15 +101,105 @@ var IssuePRFlags = append([]cli.Flag{
&PaginationLimitFlag, &PaginationLimitFlag,
}, AllDefaultFlags...) }, AllDefaultFlags...)
// GetListOptions return ListOptions based on PaginationFlags // IssuePREditFlags defines flags for properties of issues and PRs
func GetListOptions(ctx *cli.Context) gitea.ListOptions { var IssuePREditFlags = append([]cli.Flag{
page := ctx.Int("page") &cli.StringFlag{
limit := ctx.Int("limit") Name: "title",
if limit != 0 && page == 0 { Aliases: []string{"t"},
page = 1 },
&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"), ","),
} }
return gitea.ListOptions{ var err error
Page: page,
PageSize: limit, 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

@ -5,9 +5,11 @@
package cmd package cmd
import ( import (
"code.gitea.io/tea/cmd/flags" "fmt"
"code.gitea.io/tea/cmd/issues" "code.gitea.io/tea/cmd/issues"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
@ -17,9 +19,10 @@ import (
// CmdIssues represents to login a gitea server. // CmdIssues represents to login a gitea server.
var CmdIssues = cli.Command{ var CmdIssues = cli.Command{
Name: "issues", Name: "issues",
Aliases: []string{"issue"}, Aliases: []string{"issue", "i"},
Category: catEntities,
Usage: "List, create and update issues", Usage: "List, create and update issues",
Description: "List, create and update issues", Description: `Lists issues when called without argument. If issue index is provided, will show it in detail.`,
ArgsUsage: "[<issue index>]", ArgsUsage: "[<issue index>]",
Action: runIssues, Action: runIssues,
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
@ -28,27 +31,41 @@ var CmdIssues = cli.Command{
&issues.CmdIssuesReopen, &issues.CmdIssuesReopen,
&issues.CmdIssuesClose, &issues.CmdIssuesClose,
}, },
Flags: flags.IssuePRFlags, Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "comments",
Usage: "Wether to display comments (will prompt if not provided & run interactively)",
},
}, issues.CmdIssuesList.Flags...),
} }
func runIssues(ctx *cli.Context) error { func runIssues(ctx *cli.Context) error {
if ctx.Args().Len() == 1 { if ctx.Args().Len() == 1 {
return runIssueDetail(ctx.Args().First()) return runIssueDetail(ctx, ctx.Args().First())
} }
return issues.RunIssuesList(ctx) return issues.RunIssuesList(ctx)
} }
func runIssueDetail(index string) error { func runIssueDetail(cmd *cli.Context, index string) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
idx, err := utils.ArgToIndex(index) idx, err := utils.ArgToIndex(index)
if err != nil { if err != nil {
return err return err
} }
issue, _, err := login.Client().GetIssue(owner, repo, idx) issue, _, err := ctx.Login.Client().GetIssue(ctx.Owner, ctx.Repo, idx)
if err != nil { if err != nil {
return err return err
} }
print.IssueDetails(issue) 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)
}
}
return nil return nil
} }

View File

@ -5,10 +5,10 @@
package issues package issues
import ( import (
"log" "fmt"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
@ -30,10 +30,11 @@ var CmdIssuesClose = cli.Command{
} }
// editIssueState abstracts the arg parsing to edit the given issue // editIssueState abstracts the arg parsing to edit the given issue
func editIssueState(ctx *cli.Context, opts gitea.EditIssueOption) error { func editIssueState(cmd *cli.Context, opts gitea.EditIssueOption) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() == 0 { if ctx.Args().Len() == 0 {
log.Fatal(ctx.Command.ArgsUsage) return fmt.Errorf(ctx.Command.ArgsUsage)
} }
index, err := utils.ArgToIndex(ctx.Args().First()) index, err := utils.ArgToIndex(ctx.Args().First())
@ -41,7 +42,7 @@ func editIssueState(ctx *cli.Context, opts gitea.EditIssueOption) error {
return err return err
} }
issue, _, err := login.Client().EditIssue(owner, repo, index, opts) issue, _, err := ctx.Login.Client().EditIssue(ctx.Owner, ctx.Repo, index, opts)
if err != nil { if err != nil {
return err return err
} }

View File

@ -5,57 +5,41 @@
package issues package issues
import ( import (
"fmt"
"log"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// CmdIssuesCreate represents a sub command of issues to create issue // CmdIssuesCreate represents a sub command of issues to create issue
var CmdIssuesCreate = cli.Command{ var CmdIssuesCreate = cli.Command{
Name: "create", Name: "create",
Aliases: []string{"c"},
Usage: "Create an issue on repository", Usage: "Create an issue on repository",
Description: `Create an issue on repository`, Description: `Create an issue on repository`,
Action: runIssuesCreate, Action: runIssuesCreate,
Flags: append([]cli.Flag{ Flags: flags.IssuePREditFlags,
&cli.StringFlag{
Name: "title",
Aliases: []string{"t"},
Usage: "issue title to create",
},
&cli.StringFlag{
Name: "body",
Aliases: []string{"b"},
Usage: "issue body to create",
},
}, flags.LoginRepoFlags...),
} }
func runIssuesCreate(ctx *cli.Context) error { func runIssuesCreate(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
issue, _, err := login.Client().CreateIssue(owner, repo, gitea.CreateIssueOption{ if ctx.NumFlags() == 0 {
Title: ctx.String("title"), return interact.CreateIssue(ctx.Login, ctx.Owner, ctx.Repo)
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)
} }
print.IssueDetails(issue) opts, err := flags.GetIssuePREditFlags(ctx)
fmt.Println(issue.HTMLURL) if err != nil {
return nil return err
}
return task.CreateIssue(
ctx.Login,
ctx.Owner,
ctx.Repo,
*opts,
)
} }

View File

@ -5,10 +5,8 @@
package issues package issues
import ( import (
"log"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
@ -17,17 +15,22 @@ import (
// CmdIssuesList represents a sub command of issues to list issues // CmdIssuesList represents a sub command of issues to list issues
var CmdIssuesList = cli.Command{ var CmdIssuesList = cli.Command{
Name: "ls", Name: "list",
Aliases: []string{"list"}, Aliases: []string{"ls"},
Usage: "List issues of the repository", Usage: "List issues of the repository",
Description: `List issues of the repository`, Description: `List issues of the repository`,
Action: RunIssuesList, Action: RunIssuesList,
Flags: flags.IssuePRFlags, Flags: append([]cli.Flag{
flags.FieldsFlag(print.IssueFields, []string{
"index", "title", "state", "author", "milestone", "labels",
}),
}, flags.IssuePRFlags...),
} }
// RunIssuesList list issues // RunIssuesList list issues
func RunIssuesList(ctx *cli.Context) error { func RunIssuesList(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
state := gitea.StateOpen state := gitea.StateOpen
switch ctx.String("state") { switch ctx.String("state") {
@ -39,16 +42,21 @@ func RunIssuesList(ctx *cli.Context) error {
state = gitea.StateClosed state = gitea.StateClosed
} }
issues, _, err := login.Client().ListRepoIssues(owner, repo, gitea.ListIssueOption{ issues, _, err := ctx.Login.Client().ListRepoIssues(ctx.Owner, ctx.Repo, gitea.ListIssueOption{
ListOptions: flags.GetListOptions(ctx), ListOptions: ctx.GetListOptions(),
State: state, State: state,
Type: gitea.IssueTypeIssue, Type: gitea.IssueTypeIssue,
}) })
if err != nil { if err != nil {
log.Fatal(err) return err
} }
print.IssuesList(issues, flags.GlobalOutputValue) fields, err := flags.GetFields(cmd, print.IssueFields)
if err != nil {
return err
}
print.IssuesPullsList(issues, ctx.Output, fields)
return nil return nil
} }

View File

@ -5,7 +5,7 @@
package cmd package cmd
import ( import (
"log" "fmt"
"code.gitea.io/tea/cmd/labels" "code.gitea.io/tea/cmd/labels"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -15,6 +15,7 @@ import (
var CmdLabels = cli.Command{ var CmdLabels = cli.Command{
Name: "labels", Name: "labels",
Aliases: []string{"label"}, Aliases: []string{"label"},
Category: catEntities,
Usage: "Manage issue labels", Usage: "Manage issue labels",
Description: `Manage issue labels`, Description: `Manage issue labels`,
Action: runLabels, Action: runLabels,
@ -34,6 +35,5 @@ func runLabels(ctx *cli.Context) error {
} }
func runLabelsDetails(ctx *cli.Context) error { func runLabelsDetails(ctx *cli.Context) error {
log.Fatal("Not yet implemented.") return fmt.Errorf("Not yet implemented")
return nil
} }

View File

@ -10,8 +10,7 @@ import (
"os" "os"
"strings" "strings"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/config"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -20,6 +19,7 @@ import (
// CmdLabelCreate represents a sub command of labels to create label. // CmdLabelCreate represents a sub command of labels to create label.
var CmdLabelCreate = cli.Command{ var CmdLabelCreate = cli.Command{
Name: "create", Name: "create",
Aliases: []string{"c"},
Usage: "Create a label", Usage: "Create a label",
Description: `Create a label`, Description: `Create a label`,
Action: runLabelCreate, Action: runLabelCreate,
@ -43,13 +43,14 @@ var CmdLabelCreate = cli.Command{
}, },
} }
func runLabelCreate(ctx *cli.Context) error { func runLabelCreate(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
labelFile := ctx.String("file") labelFile := ctx.String("file")
var err error var err error
if len(labelFile) == 0 { if len(labelFile) == 0 {
_, _, err = login.Client().CreateLabel(owner, repo, gitea.CreateLabelOption{ _, _, err = ctx.Login.Client().CreateLabel(ctx.Owner, ctx.Repo, gitea.CreateLabelOption{
Name: ctx.String("name"), Name: ctx.String("name"),
Color: ctx.String("color"), Color: ctx.String("color"),
Description: ctx.String("description"), Description: ctx.String("description"),
@ -69,7 +70,7 @@ func runLabelCreate(ctx *cli.Context) error {
if color == "" || name == "" { if color == "" || name == "" {
log.Printf("Line %d ignored because lack of enough fields: %s\n", i, line) log.Printf("Line %d ignored because lack of enough fields: %s\n", i, line)
} else { } else {
_, _, err = login.Client().CreateLabel(owner, repo, gitea.CreateLabelOption{ _, _, err = ctx.Login.Client().CreateLabel(ctx.Owner, ctx.Repo, gitea.CreateLabelOption{
Name: name, Name: name,
Color: color, Color: color,
Description: description, Description: description,
@ -80,11 +81,7 @@ func runLabelCreate(ctx *cli.Context) error {
} }
} }
if err != nil { return err
log.Fatal(err)
}
return nil
} }
func splitLabelLine(line string) (string, string, string) { func splitLabelLine(line string) (string, string, string) {

View File

@ -5,10 +5,7 @@
package labels package labels
import ( import (
"log" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -16,6 +13,7 @@ import (
// CmdLabelDelete represents a sub command of labels to delete label. // CmdLabelDelete represents a sub command of labels to delete label.
var CmdLabelDelete = cli.Command{ var CmdLabelDelete = cli.Command{
Name: "delete", Name: "delete",
Aliases: []string{"rm"},
Usage: "Delete a label", Usage: "Delete a label",
Description: `Delete a label`, Description: `Delete a label`,
Action: runLabelDelete, Action: runLabelDelete,
@ -27,13 +25,10 @@ var CmdLabelDelete = cli.Command{
}, },
} }
func runLabelDelete(ctx *cli.Context) error { func runLabelDelete(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
_, err := login.Client().DeleteLabel(owner, repo, ctx.Int64("id")) _, err := ctx.Login.Client().DeleteLabel(ctx.Owner, ctx.Repo, ctx.Int64("id"))
if err != nil { return err
log.Fatal(err)
}
return nil
} }

View File

@ -5,10 +5,8 @@
package labels package labels
import ( import (
"log"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/task" "code.gitea.io/tea/modules/task"
@ -18,8 +16,8 @@ import (
// CmdLabelsList represents a sub command of labels to list labels // CmdLabelsList represents a sub command of labels to list labels
var CmdLabelsList = cli.Command{ var CmdLabelsList = cli.Command{
Name: "ls", Name: "list",
Aliases: []string{"list"}, Aliases: []string{"ls"},
Usage: "List labels", Usage: "List labels",
Description: "List labels", Description: "List labels",
Action: RunLabelsList, Action: RunLabelsList,
@ -35,18 +33,22 @@ var CmdLabelsList = cli.Command{
} }
// RunLabelsList list labels. // RunLabelsList list labels.
func RunLabelsList(ctx *cli.Context) error { func RunLabelsList(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
labels, _, err := login.Client().ListRepoLabels(owner, repo, gitea.ListLabelsOptions{ListOptions: flags.GetListOptions(ctx)}) client := ctx.Login.Client()
labels, _, err := client.ListRepoLabels(ctx.Owner, ctx.Repo, gitea.ListLabelsOptions{
ListOptions: ctx.GetListOptions(),
})
if err != nil { if err != nil {
log.Fatal(err) return err
} }
if ctx.IsSet("save") { if ctx.IsSet("save") {
return task.LabelsExport(labels, ctx.String("save")) return task.LabelsExport(labels, ctx.String("save"))
} }
print.LabelsList(labels, flags.GlobalOutputValue) print.LabelsList(labels, ctx.Output)
return nil return nil
} }

View File

@ -5,10 +5,7 @@
package labels package labels
import ( import (
"log" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -40,8 +37,9 @@ var CmdLabelUpdate = cli.Command{
}, },
} }
func runLabelUpdate(ctx *cli.Context) error { func runLabelUpdate(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
id := ctx.Int64("id") id := ctx.Int64("id")
var pName, pColor, pDescription *string var pName, pColor, pDescription *string
@ -61,14 +59,14 @@ func runLabelUpdate(ctx *cli.Context) error {
} }
var err error var err error
_, _, err = login.Client().EditLabel(owner, repo, id, gitea.EditLabelOption{ _, _, err = ctx.Login.Client().EditLabel(ctx.Owner, ctx.Repo, id, gitea.EditLabelOption{
Name: pName, Name: pName,
Color: pColor, Color: pColor,
Description: pDescription, Description: pDescription,
}) })
if err != nil { if err != nil {
log.Fatal(err) return err
} }
return nil return nil

View File

@ -7,7 +7,6 @@ package cmd
import ( import (
"fmt" "fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/cmd/login" "code.gitea.io/tea/cmd/login"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
@ -19,6 +18,7 @@ import (
var CmdLogin = cli.Command{ var CmdLogin = cli.Command{
Name: "logins", Name: "logins",
Aliases: []string{"login"}, Aliases: []string{"login"},
Category: catSetup,
Usage: "Log in to a Gitea server", Usage: "Log in to a Gitea server",
Description: `Log in to a Gitea server`, Description: `Log in to a Gitea server`,
ArgsUsage: "[<login name>]", ArgsUsage: "[<login name>]",
@ -46,6 +46,6 @@ func runLoginDetail(name string) error {
return nil return nil
} }
print.LoginDetails(l, flags.GlobalOutputValue) print.LoginDetails(l)
return nil return nil
} }

View File

@ -15,6 +15,7 @@ import (
// CmdLoginEdit represents to login a gitea server. // CmdLoginEdit represents to login a gitea server.
var CmdLoginEdit = cli.Command{ var CmdLoginEdit = cli.Command{
Name: "edit", Name: "edit",
Aliases: []string{"e"},
Usage: "Edit Gitea logins", Usage: "Edit Gitea logins",
Description: `Edit Gitea logins`, Description: `Edit Gitea logins`,
Action: runLoginEdit, Action: runLoginEdit,

View File

@ -5,8 +5,6 @@
package login package login
import ( import (
"log"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
@ -16,8 +14,8 @@ import (
// CmdLoginList represents to login a gitea server. // CmdLoginList represents to login a gitea server.
var CmdLoginList = cli.Command{ var CmdLoginList = cli.Command{
Name: "ls", Name: "list",
Aliases: []string{"list"}, Aliases: []string{"ls"},
Usage: "List Gitea logins", Usage: "List Gitea logins",
Description: `List Gitea logins`, Description: `List Gitea logins`,
Action: RunLoginList, Action: RunLoginList,
@ -25,12 +23,11 @@ var CmdLoginList = cli.Command{
} }
// RunLoginList list all logins // RunLoginList list all logins
func RunLoginList(_ *cli.Context) error { func RunLoginList(cmd *cli.Context) error {
logins, err := config.GetLogins() logins, err := config.GetLogins()
if err != nil { if err != nil {
log.Fatal(err) return err
} }
print.LoginsList(logins, cmd.String("output"))
print.LoginsList(logins, flags.GlobalOutputValue)
return nil return nil
} }

View File

@ -13,6 +13,7 @@ import (
// CmdLogout represents to logout a gitea server. // CmdLogout represents to logout a gitea server.
var CmdLogout = cli.Command{ var CmdLogout = cli.Command{
Name: "logout", Name: "logout",
Category: catSetup,
Usage: "Log out from a Gitea server", Usage: "Log out from a Gitea server",
Description: `Log out from a Gitea server`, Description: `Log out from a Gitea server`,
ArgsUsage: "<login name>", ArgsUsage: "<login name>",

View File

@ -5,9 +5,8 @@
package cmd package cmd
import ( import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/cmd/milestones" "code.gitea.io/tea/cmd/milestones"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -17,6 +16,7 @@ import (
var CmdMilestones = cli.Command{ var CmdMilestones = cli.Command{
Name: "milestones", Name: "milestones",
Aliases: []string{"milestone", "ms"}, Aliases: []string{"milestone", "ms"},
Category: catEntities,
Usage: "List and create milestones", Usage: "List and create milestones",
Description: `List and create milestones`, Description: `List and create milestones`,
ArgsUsage: "[<milestone name>]", ArgsUsage: "[<milestone name>]",
@ -29,21 +29,22 @@ var CmdMilestones = cli.Command{
&milestones.CmdMilestonesReopen, &milestones.CmdMilestonesReopen,
&milestones.CmdMilestonesIssues, &milestones.CmdMilestonesIssues,
}, },
Flags: flags.AllDefaultFlags, Flags: milestones.CmdMilestonesList.Flags,
} }
func runMilestones(ctx *cli.Context) error { func runMilestones(ctx *cli.Context) error {
if ctx.Args().Len() == 1 { if ctx.Args().Len() == 1 {
return runMilestoneDetail(ctx.Args().First()) return runMilestoneDetail(ctx, ctx.Args().First())
} }
return milestones.RunMilestonesList(ctx) return milestones.RunMilestonesList(ctx)
} }
func runMilestoneDetail(name string) error { func runMilestoneDetail(cmd *cli.Context, name string) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
milestone, _, err := client.GetMilestoneByName(owner, repo, name) milestone, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, name)
if err != nil { if err != nil {
return err return err
} }

View File

@ -5,20 +5,22 @@
package milestones package milestones
import ( import (
"fmt" "time"
"log"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// CmdMilestonesCreate represents a sub command of milestones to create milestone // CmdMilestonesCreate represents a sub command of milestones to create milestone
var CmdMilestonesCreate = cli.Command{ var CmdMilestonesCreate = cli.Command{
Name: "create", Name: "create",
Aliases: []string{"c"},
Usage: "Create an milestone on repository", Usage: "Create an milestone on repository",
Description: `Create an milestone on repository`, Description: `Create an milestone on repository`,
Action: runMilestonesCreate, Action: runMilestonesCreate,
@ -33,6 +35,11 @@ var CmdMilestonesCreate = cli.Command{
Aliases: []string{"d"}, Aliases: []string{"d"},
Usage: "milestone description to create", Usage: "milestone description to create",
}, },
&cli.StringFlag{
Name: "deadline",
Aliases: []string{"expires", "x"},
Usage: "set milestone deadline (default is no due date)",
},
&cli.StringFlag{ &cli.StringFlag{
Name: "state", Name: "state",
Usage: "set milestone state (default is open)", Usage: "set milestone state (default is open)",
@ -41,13 +48,17 @@ var CmdMilestonesCreate = cli.Command{
}, flags.AllDefaultFlags...), }, flags.AllDefaultFlags...),
} }
func runMilestonesCreate(ctx *cli.Context) error { func runMilestonesCreate(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
title := ctx.String("title") date := ctx.String("deadline")
if len(title) == 0 { deadline := &time.Time{}
fmt.Printf("Title is required\n") if date != "" {
return nil t, err := dateparse.ParseAny(date)
if err == nil {
return err
}
deadline = &t
} }
state := gitea.StateOpen state := gitea.StateOpen
@ -55,15 +66,17 @@ func runMilestonesCreate(ctx *cli.Context) error {
state = gitea.StateClosed state = gitea.StateClosed
} }
mile, _, err := login.Client().CreateMilestone(owner, repo, gitea.CreateMilestoneOption{ if ctx.NumFlags() == 0 {
Title: title, return interact.CreateMilestone(ctx.Login, ctx.Owner, ctx.Repo)
Description: ctx.String("description"),
State: state,
})
if err != nil {
log.Fatal(err)
} }
print.MilestoneDetails(mile) return task.CreateMilestone(
return nil ctx.Login,
ctx.Owner,
ctx.Repo,
ctx.String("title"),
ctx.String("description"),
deadline,
state,
)
} }

View File

@ -6,7 +6,7 @@ package milestones
import ( import (
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -22,10 +22,11 @@ var CmdMilestonesDelete = cli.Command{
Flags: flags.AllDefaultFlags, Flags: flags.AllDefaultFlags,
} }
func deleteMilestone(ctx *cli.Context) error { func deleteMilestone(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
_, err := client.DeleteMilestoneByName(owner, repo, ctx.Args().First()) _, err := client.DeleteMilestoneByName(ctx.Owner, ctx.Repo, ctx.Args().First())
return err return err
} }

View File

@ -8,7 +8,7 @@ import (
"fmt" "fmt"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
@ -40,6 +40,9 @@ var CmdMilestonesIssues = cli.Command{
}, },
&flags.PaginationPageFlag, &flags.PaginationPageFlag,
&flags.PaginationLimitFlag, &flags.PaginationLimitFlag,
flags.FieldsFlag(print.IssueFields, []string{
"index", "kind", "title", "state", "updated", "labels",
}),
}, flags.AllDefaultFlags...), }, flags.AllDefaultFlags...),
} }
@ -65,9 +68,10 @@ var CmdMilestoneRemoveIssue = cli.Command{
Flags: flags.AllDefaultFlags, Flags: flags.AllDefaultFlags,
} }
func runMilestoneIssueList(ctx *cli.Context) error { func runMilestoneIssueList(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
state := gitea.StateOpen state := gitea.StateOpen
switch ctx.String("state") { switch ctx.String("state") {
@ -91,13 +95,13 @@ func runMilestoneIssueList(ctx *cli.Context) error {
milestone := ctx.Args().First() milestone := ctx.Args().First()
// make sure milestone exist // make sure milestone exist
_, _, err := client.GetMilestoneByName(owner, repo, milestone) _, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestone)
if err != nil { if err != nil {
return err return err
} }
issues, _, err := client.ListRepoIssues(owner, repo, gitea.ListIssueOption{ issues, _, err := client.ListRepoIssues(ctx.Owner, ctx.Repo, gitea.ListIssueOption{
ListOptions: flags.GetListOptions(ctx), ListOptions: ctx.GetListOptions(),
Milestones: []string{milestone}, Milestones: []string{milestone},
Type: kind, Type: kind,
State: state, State: state,
@ -106,13 +110,18 @@ func runMilestoneIssueList(ctx *cli.Context) error {
return err return err
} }
print.IssuesPullsList(issues, flags.GlobalOutputValue) fields, err := flags.GetFields(cmd, print.IssueFields)
if err != nil {
return err
}
print.IssuesPullsList(issues, ctx.Output, fields)
return nil return nil
} }
func runMilestoneIssueAdd(ctx *cli.Context) error { func runMilestoneIssueAdd(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
if ctx.Args().Len() != 2 { if ctx.Args().Len() != 2 {
return fmt.Errorf("need two arguments") return fmt.Errorf("need two arguments")
} }
@ -125,20 +134,21 @@ func runMilestoneIssueAdd(ctx *cli.Context) error {
} }
// make sure milestone exist // make sure milestone exist
mile, _, err := client.GetMilestoneByName(owner, repo, mileName) mile, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, mileName)
if err != nil { if err != nil {
return err return err
} }
_, _, err = client.EditIssue(owner, repo, idx, gitea.EditIssueOption{ _, _, err = client.EditIssue(ctx.Owner, ctx.Repo, idx, gitea.EditIssueOption{
Milestone: &mile.ID, Milestone: &mile.ID,
}) })
return err return err
} }
func runMilestoneIssueRemove(ctx *cli.Context) error { func runMilestoneIssueRemove(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
if ctx.Args().Len() != 2 { if ctx.Args().Len() != 2 {
return fmt.Errorf("need two arguments") return fmt.Errorf("need two arguments")
} }
@ -150,7 +160,7 @@ func runMilestoneIssueRemove(ctx *cli.Context) error {
return err return err
} }
issue, _, err := client.GetIssue(owner, repo, idx) issue, _, err := client.GetIssue(ctx.Owner, ctx.Repo, idx)
if err != nil { if err != nil {
return err return err
} }
@ -164,7 +174,7 @@ func runMilestoneIssueRemove(ctx *cli.Context) error {
} }
zero := int64(0) zero := int64(0)
_, _, err = client.EditIssue(owner, repo, idx, gitea.EditIssueOption{ _, _, err = client.EditIssue(ctx.Owner, ctx.Repo, idx, gitea.EditIssueOption{
Milestone: &zero, Milestone: &zero,
}) })
return err return err

View File

@ -5,10 +5,8 @@
package milestones package milestones
import ( import (
"log"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
@ -17,8 +15,8 @@ import (
// CmdMilestonesList represents a sub command of milestones to list milestones // CmdMilestonesList represents a sub command of milestones to list milestones
var CmdMilestonesList = cli.Command{ var CmdMilestonesList = cli.Command{
Name: "ls", Name: "list",
Aliases: []string{"list"}, Aliases: []string{"ls"},
Usage: "List milestones of the repository", Usage: "List milestones of the repository",
Description: `List milestones of the repository`, Description: `List milestones of the repository`,
Action: RunMilestonesList, Action: RunMilestonesList,
@ -34,8 +32,9 @@ var CmdMilestonesList = cli.Command{
} }
// RunMilestonesList list milestones // RunMilestonesList list milestones
func RunMilestonesList(ctx *cli.Context) error { func RunMilestonesList(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
state := gitea.StateOpen state := gitea.StateOpen
switch ctx.String("state") { switch ctx.String("state") {
@ -45,15 +44,16 @@ func RunMilestonesList(ctx *cli.Context) error {
state = gitea.StateClosed state = gitea.StateClosed
} }
milestones, _, err := login.Client().ListRepoMilestones(owner, repo, gitea.ListMilestoneOption{ client := ctx.Login.Client()
ListOptions: flags.GetListOptions(ctx), milestones, _, err := client.ListRepoMilestones(ctx.Owner, ctx.Repo, gitea.ListMilestoneOption{
ListOptions: ctx.GetListOptions(),
State: state, State: state,
}) })
if err != nil { if err != nil {
log.Fatal(err) return err
} }
print.MilestonesList(milestones, flags.GlobalOutputValue, state) print.MilestonesList(milestones, ctx.Output, state)
return nil return nil
} }

View File

@ -6,7 +6,7 @@ package milestones
import ( import (
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -25,15 +25,16 @@ var CmdMilestonesReopen = cli.Command{
Flags: flags.AllDefaultFlags, Flags: flags.AllDefaultFlags,
} }
func editMilestoneStatus(ctx *cli.Context, close bool) error { func editMilestoneStatus(cmd *cli.Context, close bool) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
state := gitea.StateOpen state := gitea.StateOpen
if close { if close {
state = gitea.StateClosed state = gitea.StateClosed
} }
_, _, err := client.EditMilestoneByName(owner, repo, ctx.Args().First(), gitea.EditMilestoneOption{ _, _, err := client.EditMilestoneByName(ctx.Owner, ctx.Repo, ctx.Args().First(), gitea.EditMilestoneOption{
State: &state, State: &state,
Title: ctx.Args().First(), Title: ctx.Args().First(),
}) })

View File

@ -5,10 +5,8 @@
package cmd package cmd
import ( import (
"log"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
@ -18,7 +16,8 @@ import (
// CmdNotifications is the main command to operate with notifications // CmdNotifications is the main command to operate with notifications
var CmdNotifications = cli.Command{ var CmdNotifications = cli.Command{
Name: "notifications", Name: "notifications",
Aliases: []string{"notification", "notif"}, Aliases: []string{"notification", "n"},
Category: catHelpers,
Usage: "Show notifications", Usage: "Show notifications",
Description: "Show notifications, by default based of the current repo and unread one", Description: "Show notifications, by default based of the current repo and unread one",
Action: runNotifications, Action: runNotifications,
@ -43,13 +42,14 @@ var CmdNotifications = cli.Command{
}, flags.AllDefaultFlags...), }, flags.AllDefaultFlags...),
} }
func runNotifications(ctx *cli.Context) error { func runNotifications(cmd *cli.Context) error {
var news []*gitea.NotificationThread var news []*gitea.NotificationThread
var err error var err error
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
listOpts := flags.GetListOptions(ctx) listOpts := ctx.GetListOptions()
if listOpts.Page == 0 { if listOpts.Page == 0 {
listOpts.Page = 1 listOpts.Page = 1
} }
@ -63,20 +63,21 @@ func runNotifications(ctx *cli.Context) error {
} }
if ctx.Bool("all") { if ctx.Bool("all") {
news, _, err = login.Client().ListNotifications(gitea.ListNotificationOptions{ news, _, err = client.ListNotifications(gitea.ListNotificationOptions{
ListOptions: listOpts, ListOptions: listOpts,
Status: status, Status: status,
}) })
} else { } else {
news, _, err = login.Client().ListRepoNotifications(owner, repo, gitea.ListNotificationOptions{ ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
news, _, err = client.ListRepoNotifications(ctx.Owner, ctx.Repo, gitea.ListNotificationOptions{
ListOptions: listOpts, ListOptions: listOpts,
Status: status, Status: status,
}) })
} }
if err != nil { if err != nil {
log.Fatal(err) return err
} }
print.NotificationsList(news, flags.GlobalOutputValue, ctx.Bool("all")) print.NotificationsList(news, ctx.Output, ctx.Bool("all"))
return nil return nil
} }

View File

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

View File

@ -5,7 +5,7 @@
package cmd package cmd
import ( import (
"log" "fmt"
"code.gitea.io/tea/cmd/organizations" "code.gitea.io/tea/cmd/organizations"
@ -16,6 +16,7 @@ import (
var CmdOrgs = cli.Command{ var CmdOrgs = cli.Command{
Name: "organizations", Name: "organizations",
Aliases: []string{"organization", "org"}, Aliases: []string{"organization", "org"},
Category: catEntities,
Usage: "List, create, delete organizations", Usage: "List, create, delete organizations",
Description: "Show organization details", Description: "Show organization details",
ArgsUsage: "[<organization>]", ArgsUsage: "[<organization>]",
@ -34,7 +35,5 @@ func runOrganizations(ctx *cli.Context) error {
} }
func runOrganizationDetail(path string) error { func runOrganizationDetail(path string) error {
return fmt.Errorf("Not yet implemented")
log.Fatal("Not yet implemented.")
return nil
} }

View File

@ -5,10 +5,9 @@
package organizations package organizations
import ( import (
"log" "fmt"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/config"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -23,20 +22,18 @@ var CmdOrganizationDelete = cli.Command{
} }
// RunOrganizationDelete delete user organization // RunOrganizationDelete delete user organization
func RunOrganizationDelete(ctx *cli.Context) error { func RunOrganizationDelete(cmd *cli.Context) error {
login, _, _ := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() client := ctx.Login.Client()
if ctx.Args().Len() < 1 { if ctx.Args().Len() < 1 {
log.Fatal("You have to specify the organization name you want to delete.") return fmt.Errorf("You have to specify the organization name you want to delete")
return nil
} }
response, err := client.DeleteOrg(ctx.Args().First()) response, err := client.DeleteOrg(ctx.Args().First())
if response != nil && response.StatusCode == 404 { if response != nil && response.StatusCode == 404 {
log.Fatal("The given organization does not exist.") return fmt.Errorf("The given organization does not exist")
return nil
} }
return err return err

View File

@ -5,10 +5,8 @@
package organizations package organizations
import ( import (
"log"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
@ -17,8 +15,8 @@ import (
// CmdOrganizationList represents a sub command of organizations to list users organizations // CmdOrganizationList represents a sub command of organizations to list users organizations
var CmdOrganizationList = cli.Command{ var CmdOrganizationList = cli.Command{
Name: "ls", Name: "list",
Aliases: []string{"list"}, Aliases: []string{"ls"},
Usage: "List Organizations", Usage: "List Organizations",
Description: "List users organizations", Description: "List users organizations",
Action: RunOrganizationList, Action: RunOrganizationList,
@ -29,17 +27,18 @@ var CmdOrganizationList = cli.Command{
} }
// RunOrganizationList list user organizations // RunOrganizationList list user organizations
func RunOrganizationList(ctx *cli.Context) error { func RunOrganizationList(cmd *cli.Context) error {
login, _, _ := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
client := login.Client() userOrganizations, _, err := client.ListUserOrgs(ctx.Login.User, gitea.ListOrgsOptions{
ListOptions: ctx.GetListOptions(),
userOrganizations, _, err := client.ListUserOrgs(login.User, gitea.ListOrgsOptions{ListOptions: flags.GetListOptions(ctx)}) })
if err != nil { if err != nil {
log.Fatal(err) return err
} }
print.OrganizationsList(userOrganizations, flags.GlobalOutputValue) print.OrganizationsList(userOrganizations, ctx.Output)
return nil return nil
} }

View File

@ -7,11 +7,12 @@ package cmd
import ( import (
"fmt" "fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/cmd/pulls" "code.gitea.io/tea/cmd/pulls"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
"code.gitea.io/tea/modules/workaround"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -21,44 +22,72 @@ import (
var CmdPulls = cli.Command{ var CmdPulls = cli.Command{
Name: "pulls", Name: "pulls",
Aliases: []string{"pull", "pr"}, Aliases: []string{"pull", "pr"},
Usage: "List, create, checkout and clean pull requests", Category: catEntities,
Description: `List, create, checkout and clean pull requests`, 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>]", ArgsUsage: "[<pull index>]",
Action: runPulls, Action: runPulls,
Flags: flags.IssuePRFlags, Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "comments",
Usage: "Wether to display comments (will prompt if not provided & run interactively)",
},
}, pulls.CmdPullsList.Flags...),
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
&pulls.CmdPullsList, &pulls.CmdPullsList,
&pulls.CmdPullsCheckout, &pulls.CmdPullsCheckout,
&pulls.CmdPullsClean, &pulls.CmdPullsClean,
&pulls.CmdPullsCreate, &pulls.CmdPullsCreate,
&pulls.CmdPullsClose,
&pulls.CmdPullsReopen,
&pulls.CmdPullsReview,
&pulls.CmdPullsApprove,
&pulls.CmdPullsReject,
}, },
} }
func runPulls(ctx *cli.Context) error { func runPulls(ctx *cli.Context) error {
if ctx.Args().Len() == 1 { if ctx.Args().Len() == 1 {
return runPullDetail(ctx.Args().First()) return runPullDetail(ctx, ctx.Args().First())
} }
return pulls.RunPullsList(ctx) return pulls.RunPullsList(ctx)
} }
func runPullDetail(index string) error { func runPullDetail(cmd *cli.Context, index string) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
idx, err := utils.ArgToIndex(index) idx, err := utils.ArgToIndex(index)
if err != nil { if err != nil {
return err return err
} }
client := login.Client() client := ctx.Login.Client()
pr, _, err := client.GetPullRequest(owner, repo, idx) pr, _, err := client.GetPullRequest(ctx.Owner, ctx.Repo, idx)
if err != nil { if err != nil {
return err return err
} }
if err := workaround.FixPullHeadSha(client, pr); err != nil {
return err
}
reviews, _, err := client.ListPullReviews(owner, repo, idx, gitea.ListPullReviewsOptions{}) reviews, _, err := client.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{})
if err != nil { if err != nil {
fmt.Printf("error while loading reviews: %v\n", err) fmt.Printf("error while loading reviews: %v\n", err)
} }
print.PullDetails(pr, reviews) ci, _, err := client.GetCombinedStatus(ctx.Owner, ctx.Repo, pr.Head.Sha)
if err != nil {
fmt.Printf("error while loading CI: %v\n", err)
}
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)
}
}
return nil return nil
} }

45
cmd/pulls/approve.go Normal file
View File

@ -0,0 +1,45 @@
// 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

@ -5,10 +5,10 @@
package pulls package pulls
import ( import (
"log" "fmt"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact" "code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task" "code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
@ -19,22 +19,30 @@ import (
// CmdPullsCheckout is a command to locally checkout the given PR // CmdPullsCheckout is a command to locally checkout the given PR
var CmdPullsCheckout = cli.Command{ var CmdPullsCheckout = cli.Command{
Name: "checkout", Name: "checkout",
Aliases: []string{"co"},
Usage: "Locally check out the given PR", Usage: "Locally check out the given PR",
Description: `Locally check out the given PR`, Description: `Locally check out the given PR`,
Action: runPullsCheckout, Action: runPullsCheckout,
ArgsUsage: "<pull index>", ArgsUsage: "<pull index>",
Flags: flags.AllDefaultFlags, 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(ctx *cli.Context) error { func runPullsCheckout(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{LocalRepo: true})
if ctx.Args().Len() != 1 { if ctx.Args().Len() != 1 {
log.Fatal("Must specify a PR index") return fmt.Errorf("Must specify a PR index")
} }
idx, err := utils.ArgToIndex(ctx.Args().First()) idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil { if err != nil {
return err return err
} }
return task.PullCheckout(login, owner, repo, idx, interact.PromptPassword) return task.PullCheckout(ctx.Login, ctx.Owner, ctx.Repo, ctx.Bool("branch"), idx, interact.PromptPassword)
} }

View File

@ -8,7 +8,7 @@ import (
"fmt" "fmt"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact" "code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task" "code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
@ -31,8 +31,9 @@ var CmdPullsClean = cli.Command{
}, flags.AllDefaultFlags...), }, flags.AllDefaultFlags...),
} }
func runPullsClean(ctx *cli.Context) error { func runPullsClean(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{LocalRepo: true})
if ctx.Args().Len() != 1 { if ctx.Args().Len() != 1 {
return fmt.Errorf("Must specify a PR index") return fmt.Errorf("Must specify a PR index")
} }
@ -42,5 +43,5 @@ func runPullsClean(ctx *cli.Context) error {
return err return err
} }
return task.PullClean(login, owner, repo, idx, ctx.Bool("ignore-sha"), interact.PromptPassword) return task.PullClean(ctx.Login, ctx.Owner, ctx.Repo, idx, ctx.Bool("ignore-sha"), interact.PromptPassword)
} }

25
cmd/pulls/close.go Normal file
View File

@ -0,0 +1,25 @@
// 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

@ -6,7 +6,7 @@ package pulls
import ( import (
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact" "code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task" "code.gitea.io/tea/modules/task"
@ -16,6 +16,7 @@ import (
// CmdPullsCreate creates a pull request // CmdPullsCreate creates a pull request
var CmdPullsCreate = cli.Command{ var CmdPullsCreate = cli.Command{
Name: "create", Name: "create",
Aliases: []string{"c"},
Usage: "Create a pull-request", Usage: "Create a pull-request",
Description: "Create a pull-request", Description: "Create a pull-request",
Action: runPullsCreate, Action: runPullsCreate,
@ -29,35 +30,30 @@ var CmdPullsCreate = cli.Command{
Aliases: []string{"b"}, Aliases: []string{"b"},
Usage: "Set base branch (default is default branch)", Usage: "Set base branch (default is default branch)",
}, },
&cli.StringFlag{ }, flags.IssuePREditFlags...),
Name: "title",
Aliases: []string{"t"},
Usage: "Set title of pull (default is head branch name)",
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
Usage: "Set body of new pull",
},
}, flags.AllDefaultFlags...),
} }
func runPullsCreate(ctx *cli.Context) error { func runPullsCreate(cmd *cli.Context) error {
login, ownerArg, repoArg := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{LocalRepo: true})
// no args -> interactive mode // no args -> interactive mode
if ctx.NumFlags() == 0 { if ctx.NumFlags() == 0 {
return interact.CreatePull(login, ownerArg, repoArg) return interact.CreatePull(ctx.Login, ctx.Owner, ctx.Repo)
} }
// else use args to create PR // else use args to create PR
opts, err := flags.GetIssuePREditFlags(ctx)
if err != nil {
return err
}
return task.CreatePull( return task.CreatePull(
login, ctx.Login,
ownerArg, ctx.Owner,
repoArg, ctx.Repo,
ctx.String("base"), ctx.String("base"),
ctx.String("head"), ctx.String("head"),
ctx.String("title"), opts,
ctx.String("description"),
) )
} }

38
cmd/pulls/edit.go Normal file
View File

@ -0,0 +1,38 @@
// 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

@ -5,10 +5,8 @@
package pulls package pulls
import ( import (
"log"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
@ -17,8 +15,8 @@ import (
// CmdPullsList represents a sub command of issues to list pulls // CmdPullsList represents a sub command of issues to list pulls
var CmdPullsList = cli.Command{ var CmdPullsList = cli.Command{
Name: "ls", Name: "list",
Aliases: []string{"list"}, Aliases: []string{"ls"},
Usage: "List pull requests of the repository", Usage: "List pull requests of the repository",
Description: `List pull requests of the repository`, Description: `List pull requests of the repository`,
Action: RunPullsList, Action: RunPullsList,
@ -26,8 +24,9 @@ var CmdPullsList = cli.Command{
} }
// RunPullsList return list of pulls // RunPullsList return list of pulls
func RunPullsList(ctx *cli.Context) error { func RunPullsList(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
state := gitea.StateOpen state := gitea.StateOpen
switch ctx.String("state") { switch ctx.String("state") {
@ -39,14 +38,14 @@ func RunPullsList(ctx *cli.Context) error {
state = gitea.StateClosed state = gitea.StateClosed
} }
prs, _, err := login.Client().ListRepoPullRequests(owner, repo, gitea.ListPullRequestsOptions{ prs, _, err := ctx.Login.Client().ListRepoPullRequests(ctx.Owner, ctx.Repo, gitea.ListPullRequestsOptions{
State: state, State: state,
}) })
if err != nil { if err != nil {
log.Fatal(err) return err
} }
print.PullsList(prs, flags.GlobalOutputValue) print.PullsList(prs, ctx.Output)
return nil return nil
} }

44
cmd/pulls/reject.go Normal file
View File

@ -0,0 +1,44 @@
// 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,
}

26
cmd/pulls/reopen.go Normal file
View File

@ -0,0 +1,26 @@
// 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,
}

40
cmd/pulls/review.go Normal file
View File

@ -0,0 +1,40 @@
// 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

@ -15,7 +15,8 @@ import (
// ToDo: ReleaseDetails // ToDo: ReleaseDetails
var CmdReleases = cli.Command{ var CmdReleases = cli.Command{
Name: "releases", Name: "releases",
Aliases: []string{"release"}, Aliases: []string{"release", "r"},
Category: catEntities,
Usage: "Manage releases", Usage: "Manage releases",
Description: "Manage releases", Description: "Manage releases",
Action: releases.RunReleasesList, Action: releases.RunReleasesList,

View File

@ -6,13 +6,12 @@ package releases
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -21,6 +20,7 @@ import (
// CmdReleaseCreate represents a sub command of Release to create release // CmdReleaseCreate represents a sub command of Release to create release
var CmdReleaseCreate = cli.Command{ var CmdReleaseCreate = cli.Command{
Name: "create", Name: "create",
Aliases: []string{"c"},
Usage: "Create a release", Usage: "Create a release",
Description: `Create a release`, Description: `Create a release`,
Action: runReleaseCreate, Action: runReleaseCreate,
@ -61,10 +61,11 @@ var CmdReleaseCreate = cli.Command{
}, flags.AllDefaultFlags...), }, flags.AllDefaultFlags...),
} }
func runReleaseCreate(ctx *cli.Context) error { func runReleaseCreate(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
release, resp, err := login.Client().CreateRelease(owner, repo, gitea.CreateReleaseOption{ release, resp, err := ctx.Login.Client().CreateRelease(ctx.Owner, ctx.Repo, gitea.CreateReleaseOption{
TagName: ctx.String("tag"), TagName: ctx.String("tag"),
Target: ctx.String("target"), Target: ctx.String("target"),
Title: ctx.String("title"), Title: ctx.String("title"),
@ -75,24 +76,23 @@ func runReleaseCreate(ctx *cli.Context) error {
if err != nil { if err != nil {
if resp != nil && resp.StatusCode == http.StatusConflict { if resp != nil && resp.StatusCode == http.StatusConflict {
fmt.Println("error: There already is a release for this tag") return fmt.Errorf("There already is a release for this tag")
return nil
} }
log.Fatal(err) return err
} }
for _, asset := range ctx.StringSlice("asset") { for _, asset := range ctx.StringSlice("asset") {
var file *os.File var file *os.File
if file, err = os.Open(asset); err != nil { if file, err = os.Open(asset); err != nil {
log.Fatal(err) return err
} }
filePath := filepath.Base(asset) filePath := filepath.Base(asset)
if _, _, err = login.Client().CreateReleaseAttachment(owner, repo, release.ID, file, filePath); err != nil { if _, _, err = ctx.Login.Client().CreateReleaseAttachment(ctx.Owner, ctx.Repo, release.ID, file, filePath); err != nil {
file.Close() file.Close()
log.Fatal(err) return err
} }
file.Close() file.Close()

View File

@ -8,7 +8,7 @@ import (
"fmt" "fmt"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -16,6 +16,7 @@ import (
// CmdReleaseDelete represents a sub command of Release to delete a release // CmdReleaseDelete represents a sub command of Release to delete a release
var CmdReleaseDelete = cli.Command{ var CmdReleaseDelete = cli.Command{
Name: "delete", Name: "delete",
Aliases: []string{"rm"},
Usage: "Delete a release", Usage: "Delete a release",
Description: `Delete a release`, Description: `Delete a release`,
ArgsUsage: "<release tag>", ArgsUsage: "<release tag>",
@ -33,9 +34,10 @@ var CmdReleaseDelete = cli.Command{
}, flags.AllDefaultFlags...), }, flags.AllDefaultFlags...),
} }
func runReleaseDelete(ctx *cli.Context) error { func runReleaseDelete(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
tag := ctx.Args().First() tag := ctx.Args().First()
if len(tag) == 0 { if len(tag) == 0 {
@ -48,7 +50,7 @@ func runReleaseDelete(ctx *cli.Context) error {
return nil return nil
} }
release, err := getReleaseByTag(owner, repo, tag, client) release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
if err != nil { if err != nil {
return err return err
} }
@ -56,13 +58,13 @@ func runReleaseDelete(ctx *cli.Context) error {
return nil return nil
} }
_, err = client.DeleteRelease(owner, repo, release.ID) _, err = client.DeleteRelease(ctx.Owner, ctx.Repo, release.ID)
if err != nil { if err != nil {
return err return err
} }
if ctx.Bool("delete-tag") { if ctx.Bool("delete-tag") {
_, err = client.DeleteReleaseTag(owner, repo, tag) _, err = client.DeleteTag(ctx.Owner, ctx.Repo, tag)
return err return err
} }

View File

@ -9,7 +9,7 @@ import (
"strings" "strings"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -18,6 +18,7 @@ import (
// CmdReleaseEdit represents a sub command of Release to edit releases // CmdReleaseEdit represents a sub command of Release to edit releases
var CmdReleaseEdit = cli.Command{ var CmdReleaseEdit = cli.Command{
Name: "edit", Name: "edit",
Aliases: []string{"e"},
Usage: "Edit a release", Usage: "Edit a release",
Description: `Edit a release`, Description: `Edit a release`,
ArgsUsage: "<release tag>", ArgsUsage: "<release tag>",
@ -56,9 +57,10 @@ var CmdReleaseEdit = cli.Command{
}, flags.AllDefaultFlags...), }, flags.AllDefaultFlags...),
} }
func runReleaseEdit(ctx *cli.Context) error { func runReleaseEdit(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
tag := ctx.Args().First() tag := ctx.Args().First()
if len(tag) == 0 { if len(tag) == 0 {
@ -66,7 +68,7 @@ func runReleaseEdit(ctx *cli.Context) error {
return nil return nil
} }
release, err := getReleaseByTag(owner, repo, tag, client) release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
if err != nil { if err != nil {
return err return err
} }
@ -82,7 +84,7 @@ func runReleaseEdit(ctx *cli.Context) error {
isPre = gitea.OptionalBool(strings.ToLower(ctx.String("prerelease"))[:1] == "t") isPre = gitea.OptionalBool(strings.ToLower(ctx.String("prerelease"))[:1] == "t")
} }
_, _, err = client.EditRelease(owner, repo, release.ID, gitea.EditReleaseOption{ _, _, err = client.EditRelease(ctx.Owner, ctx.Repo, release.ID, gitea.EditReleaseOption{
TagName: ctx.String("tag"), TagName: ctx.String("tag"),
Target: ctx.String("target"), Target: ctx.String("target"),
Title: ctx.String("title"), Title: ctx.String("title"),

View File

@ -6,10 +6,9 @@ package releases
import ( import (
"fmt" "fmt"
"log"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
@ -18,8 +17,8 @@ import (
// CmdReleaseList represents a sub command of Release to list releases // CmdReleaseList represents a sub command of Release to list releases
var CmdReleaseList = cli.Command{ var CmdReleaseList = cli.Command{
Name: "ls", Name: "list",
Aliases: []string{"list"}, Aliases: []string{"ls"},
Usage: "List Releases", Usage: "List Releases",
Description: "List Releases", Description: "List Releases",
Action: RunReleasesList, Action: RunReleasesList,
@ -30,15 +29,18 @@ var CmdReleaseList = cli.Command{
} }
// RunReleasesList list releases // RunReleasesList list releases
func RunReleasesList(ctx *cli.Context) error { func RunReleasesList(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
releases, _, err := login.Client().ListReleases(owner, repo, gitea.ListReleasesOptions{ListOptions: flags.GetListOptions(ctx)}) releases, _, err := ctx.Login.Client().ListReleases(ctx.Owner, ctx.Repo, gitea.ListReleasesOptions{
ListOptions: ctx.GetListOptions(),
})
if err != nil { if err != nil {
log.Fatal(err) return err
} }
print.ReleasesList(releases, flags.GlobalOutputValue) print.ReleasesList(releases, ctx.Output)
return nil return nil
} }

View File

@ -5,9 +5,8 @@
package cmd package cmd
import ( import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/cmd/repos" "code.gitea.io/tea/cmd/repos"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
@ -19,6 +18,7 @@ import (
var CmdRepos = cli.Command{ var CmdRepos = cli.Command{
Name: "repos", Name: "repos",
Aliases: []string{"repo"}, Aliases: []string{"repo"},
Category: catEntities,
Usage: "Show repository details", Usage: "Show repository details",
Description: "Show repository details", Description: "Show repository details",
ArgsUsage: "[<repo owner>/<repo name>]", ArgsUsage: "[<repo owner>/<repo name>]",
@ -33,20 +33,20 @@ var CmdRepos = cli.Command{
func runRepos(ctx *cli.Context) error { func runRepos(ctx *cli.Context) error {
if ctx.Args().Len() == 1 { if ctx.Args().Len() == 1 {
return runRepoDetail(ctx.Args().First()) return runRepoDetail(ctx, ctx.Args().First())
} }
return repos.RunReposList(ctx) return repos.RunReposList(ctx)
} }
func runRepoDetail(path string) error { func runRepoDetail(cmd *cli.Context, path string) error {
login, ownerFallback, _ := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() client := ctx.Login.Client()
repoOwner, repoName := utils.GetOwnerAndRepo(path, ownerFallback) repoOwner, repoName := utils.GetOwnerAndRepo(path, ctx.Owner)
repo, _, err := client.GetRepo(repoOwner, repoName) repo, _, err := client.GetRepo(repoOwner, repoName)
if err != nil { if err != nil {
return err return err
} }
topics, _, err := client.ListRepoTopics(repo.Owner.UserName, repo.Name, gitea.ListRepoTopicsOptions{}) topics, _, err := client.ListRepoTopics(repoOwner, repoName, gitea.ListRepoTopicsOptions{})
if err != nil { if err != nil {
return err return err
} }

View File

@ -8,7 +8,7 @@ import (
"fmt" "fmt"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
@ -82,9 +82,9 @@ var CmdRepoCreate = cli.Command{
}, flags.LoginOutputFlags...), }, flags.LoginOutputFlags...),
} }
func runRepoCreate(ctx *cli.Context) error { func runRepoCreate(cmd *cli.Context) error {
login, _, _ := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() client := ctx.Login.Client()
var ( var (
repo *gitea.Repository repo *gitea.Repository
err error err error

View File

@ -6,28 +6,11 @@ package repos
import ( import (
"fmt" "fmt"
"strings"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// printFieldsFlag provides a selection of fields to print
var printFieldsFlag = cli.StringFlag{
Name: "fields",
Aliases: []string{"f"},
Usage: fmt.Sprintf(`Comma-separated list of fields to print. Available values:
%s
`, strings.Join(print.RepoFields, ",")),
Value: "owner,name,type,ssh",
}
func getFields(ctx *cli.Context) []string {
return strings.Split(ctx.String("fields"), ",")
}
var typeFilterFlag = cli.StringFlag{ var typeFilterFlag = cli.StringFlag{
Name: "type", Name: "type",
Aliases: []string{"T"}, Aliases: []string{"T"},

View File

@ -6,7 +6,7 @@ package repos
import ( import (
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
@ -27,7 +27,9 @@ var CmdReposListFlags = append([]cli.Flag{
Required: false, Required: false,
Usage: "List your starred repos instead", Usage: "List your starred repos instead",
}, },
&printFieldsFlag, flags.FieldsFlag(print.RepoFields, []string{
"owner", "name", "type", "ssh",
}),
&typeFilterFlag, &typeFilterFlag,
&flags.PaginationPageFlag, &flags.PaginationPageFlag,
&flags.PaginationLimitFlag, &flags.PaginationLimitFlag,
@ -35,8 +37,8 @@ var CmdReposListFlags = append([]cli.Flag{
// CmdReposList represents a sub command of repos to list them // CmdReposList represents a sub command of repos to list them
var CmdReposList = cli.Command{ var CmdReposList = cli.Command{
Name: "ls", Name: "list",
Aliases: []string{"list"}, Aliases: []string{"ls"},
Usage: "List repositories you have access to", Usage: "List repositories you have access to",
Description: "List repositories you have access to", Description: "List repositories you have access to",
Action: RunReposList, Action: RunReposList,
@ -44,11 +46,11 @@ var CmdReposList = cli.Command{
} }
// RunReposList list repositories // RunReposList list repositories
func RunReposList(ctx *cli.Context) error { func RunReposList(cmd *cli.Context) error {
login, _, _ := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() client := ctx.Login.Client()
typeFilter, err := getTypeFilter(ctx) typeFilter, err := getTypeFilter(cmd)
if err != nil { if err != nil {
return err return err
} }
@ -60,14 +62,14 @@ func RunReposList(ctx *cli.Context) error {
return err return err
} }
rps, _, err = client.SearchRepos(gitea.SearchRepoOptions{ rps, _, err = client.SearchRepos(gitea.SearchRepoOptions{
ListOptions: flags.GetListOptions(ctx), ListOptions: ctx.GetListOptions(),
StarredByUserID: user.ID, StarredByUserID: user.ID,
}) })
} else if ctx.Bool("watched") { } else if ctx.Bool("watched") {
rps, _, err = client.GetMyWatchedRepos() // TODO: this does not expose pagination.. rps, _, err = client.GetMyWatchedRepos() // TODO: this does not expose pagination..
} else { } else {
rps, _, err = client.ListMyRepos(gitea.ListReposOptions{ rps, _, err = client.ListMyRepos(gitea.ListReposOptions{
ListOptions: flags.GetListOptions(ctx), ListOptions: ctx.GetListOptions(),
}) })
} }
@ -80,7 +82,12 @@ func RunReposList(ctx *cli.Context) error {
reposFiltered = filterReposByType(rps, typeFilter) reposFiltered = filterReposByType(rps, typeFilter)
} }
print.ReposList(reposFiltered, flags.GlobalOutputValue, getFields(ctx)) fields, err := flags.GetFields(cmd, print.RepoFields)
if err != nil {
return err
}
print.ReposList(reposFiltered, ctx.Output, fields)
return nil return nil
} }

View File

@ -5,11 +5,11 @@
package repos package repos
import ( import (
"log" "fmt"
"strings" "strings"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
@ -50,15 +50,17 @@ var CmdReposSearch = cli.Command{
Required: false, Required: false,
Usage: "Filter archived repos (true|false)", Usage: "Filter archived repos (true|false)",
}, },
&printFieldsFlag, flags.FieldsFlag(print.RepoFields, []string{
"owner", "name", "type", "ssh",
}),
&flags.PaginationPageFlag, &flags.PaginationPageFlag,
&flags.PaginationLimitFlag, &flags.PaginationLimitFlag,
}, flags.LoginOutputFlags...), }, flags.LoginOutputFlags...),
} }
func runReposSearch(ctx *cli.Context) error { func runReposSearch(cmd *cli.Context) error {
login, _, _ := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() client := ctx.Login.Client()
var ownerID int64 var ownerID int64
if ctx.IsSet("owner") { if ctx.IsSet("owner") {
@ -67,7 +69,7 @@ func runReposSearch(ctx *cli.Context) error {
if err != nil { if err != nil {
// HACK: the client does not return a response on 404, so we can't check res.StatusCode // HACK: the client does not return a response on 404, so we can't check res.StatusCode
if err.Error() != "404 Not Found" { if err.Error() != "404 Not Found" {
log.Fatal("could not find owner: ", err) return fmt.Errorf("Could not find owner: %s", err)
} }
// if owner is no org, its a user // if owner is no org, its a user
@ -93,7 +95,7 @@ func runReposSearch(ctx *cli.Context) error {
isPrivate = &private isPrivate = &private
} }
mode, err := getTypeFilter(ctx) mode, err := getTypeFilter(cmd)
if err != nil { if err != nil {
return err return err
} }
@ -109,7 +111,7 @@ func runReposSearch(ctx *cli.Context) error {
} }
rps, _, err := client.SearchRepos(gitea.SearchRepoOptions{ rps, _, err := client.SearchRepos(gitea.SearchRepoOptions{
ListOptions: flags.GetListOptions(ctx), ListOptions: ctx.GetListOptions(),
OwnerID: ownerID, OwnerID: ownerID,
IsPrivate: isPrivate, IsPrivate: isPrivate,
IsArchived: isArchived, IsArchived: isArchived,
@ -123,6 +125,10 @@ func runReposSearch(ctx *cli.Context) error {
return err return err
} }
print.ReposList(rps, flags.GlobalOutputValue, getFields(ctx)) fields, err := flags.GetFields(cmd, nil)
if err != nil {
return err
}
print.ReposList(rps, ctx.Output, fields)
return nil return nil
} }

View File

@ -11,22 +11,20 @@ import (
// CmdTrackedTimes represents the command to operate repositories' times. // CmdTrackedTimes represents the command to operate repositories' times.
var CmdTrackedTimes = cli.Command{ var CmdTrackedTimes = cli.Command{
Name: "times", Name: "times",
Aliases: []string{"time"}, Aliases: []string{"time", "t"},
Usage: "Operate on tracked times of a repository's issues & pulls", Category: catEntities,
Usage: "Operate on tracked times of a repository's issues & pulls",
Description: `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 Depending on your permissions on the repository, only your own tracked
times might be listed.`, times might be listed.`,
ArgsUsage: "[username | #issue]", ArgsUsage: "[username | #issue]",
Action: runTrackedTimes, Action: times.RunTimesList,
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
&times.CmdTrackedTimesAdd, &times.CmdTrackedTimesAdd,
&times.CmdTrackedTimesDelete, &times.CmdTrackedTimesDelete,
&times.CmdTrackedTimesReset, &times.CmdTrackedTimesReset,
&times.CmdTrackedTimesList, &times.CmdTrackedTimesList,
}, },
} Flags: times.CmdTrackedTimesList.Flags,
func runTrackedTimes(ctx *cli.Context) error {
return times.RunTimesList(ctx)
} }

View File

@ -6,12 +6,11 @@ package times
import ( import (
"fmt" "fmt"
"log"
"strings" "strings"
"time" "time"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
@ -21,6 +20,7 @@ import (
// CmdTrackedTimesAdd represents a sub command of times to add time to an issue // CmdTrackedTimesAdd represents a sub command of times to add time to an issue
var CmdTrackedTimesAdd = cli.Command{ var CmdTrackedTimesAdd = cli.Command{
Name: "add", Name: "add",
Aliases: []string{"a"},
Usage: "Track spent time on an issue", Usage: "Track spent time on an issue",
UsageText: "tea times add <issue> <duration>", UsageText: "tea times add <issue> <duration>",
Description: `Track spent time on an issue Description: `Track spent time on an issue
@ -31,8 +31,9 @@ var CmdTrackedTimesAdd = cli.Command{
Flags: flags.LoginRepoFlags, Flags: flags.LoginRepoFlags,
} }
func runTrackedTimesAdd(ctx *cli.Context) error { func runTrackedTimesAdd(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() < 2 { if ctx.Args().Len() < 2 {
return fmt.Errorf("No issue or duration specified.\nUsage:\t%s", ctx.Command.UsageText) return fmt.Errorf("No issue or duration specified.\nUsage:\t%s", ctx.Command.UsageText)
@ -40,20 +41,16 @@ func runTrackedTimesAdd(ctx *cli.Context) error {
issue, err := utils.ArgToIndex(ctx.Args().First()) issue, err := utils.ArgToIndex(ctx.Args().First())
if err != nil { if err != nil {
log.Fatal(err) return err
} }
duration, err := time.ParseDuration(strings.Join(ctx.Args().Tail(), "")) duration, err := time.ParseDuration(strings.Join(ctx.Args().Tail(), ""))
if err != nil { if err != nil {
log.Fatal(err) return err
} }
_, _, err = login.Client().AddTime(owner, repo, issue, gitea.AddTimeOption{ _, _, err = ctx.Login.Client().AddTime(ctx.Owner, ctx.Repo, issue, gitea.AddTimeOption{
Time: int64(duration.Seconds()), Time: int64(duration.Seconds()),
}) })
if err != nil { return err
log.Fatal(err)
}
return nil
} }

View File

@ -6,11 +6,10 @@ package times
import ( import (
"fmt" "fmt"
"log"
"strconv" "strconv"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -26,9 +25,10 @@ var CmdTrackedTimesDelete = cli.Command{
Flags: flags.LoginRepoFlags, Flags: flags.LoginRepoFlags,
} }
func runTrackedTimesDelete(ctx *cli.Context) error { func runTrackedTimesDelete(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
if ctx.Args().Len() < 2 { if ctx.Args().Len() < 2 {
return fmt.Errorf("No issue or time ID specified.\nUsage:\t%s", ctx.Command.UsageText) return fmt.Errorf("No issue or time ID specified.\nUsage:\t%s", ctx.Command.UsageText)
@ -36,18 +36,14 @@ func runTrackedTimesDelete(ctx *cli.Context) error {
issue, err := utils.ArgToIndex(ctx.Args().First()) issue, err := utils.ArgToIndex(ctx.Args().First())
if err != nil { if err != nil {
log.Fatal(err) return err
} }
timeID, err := strconv.ParseInt(ctx.Args().Get(1), 10, 64) timeID, err := strconv.ParseInt(ctx.Args().Get(1), 10, 64)
if err != nil { if err != nil {
log.Fatal(err) return err
} }
_, err = client.DeleteTime(owner, repo, issue, timeID) _, err = client.DeleteTime(ctx.Owner, ctx.Repo, issue, timeID)
if err != nil { return err
log.Fatal(err)
}
return nil
} }

View File

@ -10,7 +10,7 @@ import (
"time" "time"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
@ -21,13 +21,15 @@ import (
// CmdTrackedTimesList represents a sub command of times to list them // CmdTrackedTimesList represents a sub command of times to list them
var CmdTrackedTimesList = cli.Command{ var CmdTrackedTimesList = cli.Command{
Name: "ls", Name: "list",
Aliases: []string{"list"}, Aliases: []string{"ls"},
Action: RunTimesList, Action: RunTimesList,
Usage: "Operate on tracked times of a repository's issues & pulls", Usage: "List tracked times on issues & pulls",
Description: `Operate on tracked times of a repository's issues & pulls. Description: `List tracked times, across repos, or on a single repo or issue:
Depending on your permissions on the repository, only your own tracked - given a username all times on a repo by that user are shown,
times might be listed.`, - 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]", ArgsUsage: "[username | #issue]",
Flags: append([]cli.Flag{ Flags: append([]cli.Flag{
@ -46,52 +48,79 @@ var CmdTrackedTimesList = cli.Command{
Aliases: []string{"t"}, Aliases: []string{"t"},
Usage: "Print the total duration at the end", 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...), }, flags.AllDefaultFlags...),
} }
// RunTimesList list repositories // RunTimesList list repositories
func RunTimesList(ctx *cli.Context) error { func RunTimesList(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
var times []*gitea.TrackedTime var times []*gitea.TrackedTime
var err error 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 := utils.ArgToIndex(user)
if err != nil {
return err
}
times, _, err = client.ListTrackedTimes(owner, repo, issue, gitea.ListTrackedTimesOptions{})
} 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 var from, until time.Time
if ctx.String("from") != "" { var fields []string
if ctx.IsSet("from") {
from, err = dateparse.ParseLocal(ctx.String("from")) from, err = dateparse.ParseLocal(ctx.String("from"))
if err != nil { if err != nil {
return err return err
} }
} }
if ctx.String("until") != "" { if ctx.IsSet("until") {
until, err = dateparse.ParseLocal(ctx.String("until")) until, err = dateparse.ParseLocal(ctx.String("until"))
if err != nil { if err != nil {
return err return err
} }
} }
print.TrackedTimesList(times, flags.GlobalOutputValue, from, until, ctx.Bool("total")) 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 return nil
} }

View File

@ -6,10 +6,9 @@ package times
import ( import (
"fmt" "fmt"
"log"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -25,9 +24,10 @@ var CmdTrackedTimesReset = cli.Command{
Flags: flags.LoginRepoFlags, Flags: flags.LoginRepoFlags,
} }
func runTrackedTimesReset(ctx *cli.Context) error { func runTrackedTimesReset(cmd *cli.Context) error {
login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) ctx := context.InitCommand(cmd)
client := login.Client() ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
if ctx.Args().Len() != 1 { if ctx.Args().Len() != 1 {
return fmt.Errorf("No issue specified.\nUsage:\t%s", ctx.Command.UsageText) return fmt.Errorf("No issue specified.\nUsage:\t%s", ctx.Command.UsageText)
@ -35,13 +35,9 @@ func runTrackedTimesReset(ctx *cli.Context) error {
issue, err := utils.ArgToIndex(ctx.Args().First()) issue, err := utils.ArgToIndex(ctx.Args().First())
if err != nil { if err != nil {
log.Fatal(err) return err
} }
_, err = client.ResetIssueTime(owner, repo, issue) _, err = client.ResetIssueTime(ctx.Owner, ctx.Repo, issue)
if err != nil { return err
log.Fatal(err)
}
return nil
} }

9
contrib/autocomplete.ps1 Normal file
View File

@ -0,0 +1,9 @@
$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', $_)
}
}

21
contrib/autocomplete.sh Normal file
View File

@ -0,0 +1,21 @@
#! /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

23
contrib/autocomplete.zsh Normal file
View File

@ -0,0 +1,23 @@
#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 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

37
go.mod
View File

@ -4,34 +4,35 @@ go 1.13
require ( require (
code.gitea.io/gitea-vet v0.2.1 code.gitea.io/gitea-vet v0.2.1
code.gitea.io/sdk/gitea v0.13.1-0.20201209180822-68eec69f472e code.gitea.io/sdk/gitea v0.13.1-0.20210304201955-ff82113459b5
github.com/AlecAivazis/survey/v2 v2.2.2 gitea.com/noerw/unidiff-comments v0.0.0-20201219085024-64aec5658f2b
github.com/Microsoft/go-winio v0.4.15 // indirect github.com/AlecAivazis/survey/v2 v2.2.8
github.com/adrg/xdg v0.2.2 github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/alecthomas/chroma v0.8.1 // indirect github.com/adrg/xdg v0.3.1
github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4 github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e
github.com/charmbracelet/glamour v0.2.0 github.com/charmbracelet/glamour v0.2.0
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/go-git/go-git/v5 v5.2.0 github.com/go-git/go-git/v5 v5.2.0
github.com/imdario/mergo v0.3.11 // indirect github.com/imdario/mergo v0.3.11 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // 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/mattn/go-colorable v0.1.8 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/microcosm-cc/bluemonday v1.0.4 // indirect
github.com/muesli/reflow v0.2.0 // indirect
github.com/muesli/termenv v0.7.4 github.com/muesli/termenv v0.7.4
github.com/olekukonko/tablewriter v0.0.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/russross/blackfriday/v2 v2.1.0 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.3.0 github.com/urfave/cli/v2 v2.3.0
github.com/xanzy/ssh-agent v0.3.0 // indirect github.com/xanzy/ssh-agent v0.3.0 // indirect
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 // indirect golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf // indirect golang.org/x/sys v0.0.0-20210305034016-7844c3c200c3 // indirect
golang.org/x/text v0.3.4 // indirect golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
golang.org/x/tools v0.0.0-20201105220310-78b158585360 // indirect golang.org/x/text v0.3.5 // indirect
gopkg.in/yaml.v2 v2.3.0 golang.org/x/tools v0.1.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect gopkg.in/yaml.v2 v2.4.0
) )
replace github.com/charmbracelet/glamour => github.com/noerw/glamour v0.2.1-0.20210305125354-f0a29f1de0c2

122
go.sum
View File

@ -1,24 +1,23 @@
code.gitea.io/gitea-vet v0.2.1 h1:b30by7+3SkmiftK0RjuXqFvZg2q4p68uoPGuxhzBN0s= 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/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
code.gitea.io/sdk/gitea v0.13.1-0.20201209180822-68eec69f472e h1:oJOoT5TGbSYRNGUhEiiEz3MqFjU6wELN0/liCZ3RmVg= code.gitea.io/sdk/gitea v0.13.1-0.20210304201955-ff82113459b5 h1:va0KddYHN8bH6MCUaWf5e4p+il55blUw5J0ha5vTMaQ=
code.gitea.io/sdk/gitea v0.13.1-0.20201209180822-68eec69f472e/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs= code.gitea.io/sdk/gitea v0.13.1-0.20210304201955-ff82113459b5/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs=
github.com/AlecAivazis/survey/v2 v2.2.2 h1:1I4qBrNsHQE+91tQCqVlfrKe9DEL65949d1oKZWVELY= gitea.com/noerw/unidiff-comments v0.0.0-20201219085024-64aec5658f2b h1:CLYsMGcGLohESQDMth+RgJ4cB3CCHToxnj0zBbvB3sE=
github.com/AlecAivazis/survey/v2 v2.2.2/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk= 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=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.15 h1:qkLXKzb1QoVatRyd/YlXZ/Kg0m5K3SPuoD82jjSOaBc= github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.15/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= 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 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/adrg/xdg v0.2.2 h1:A7ZHKRz5KGOLJX/bg7IPzStryhvCzAE1wX+KWawPiAo= github.com/adrg/xdg v0.3.1 h1:uIyL9BYfXaFgDyVRKE8wjtm6ETQULweQqTofphEFJYY=
github.com/adrg/xdg v0.2.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ= 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 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 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 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.7.3 h1:NfdAERMy+esYQs8OXk0I868/qDxxCEo7FMz1WIqMAeI=
github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
github.com/alecthomas/chroma v0.8.1 h1:ym20sbvyC6RXz45u4qDglcgr8E313oPROshcuCHqiEE= github.com/alecthomas/chroma v0.8.1 h1:ym20sbvyC6RXz45u4qDglcgr8E313oPROshcuCHqiEE=
github.com/alecthomas/chroma v0.8.1/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM= 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 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
@ -28,17 +27,14 @@ github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkx
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= 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 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4 h1:OkS1BqB3CzLtGRznRyvriSY8jeaVk2CrDn2ZiRQgMUI= github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e h1:OjdSMCht0ZVX7IH0nTdf00xEustvbtUGRgMh3gbdmOg=
github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4/go.mod h1:hMAUZFIkk4B1FouGxqlogyMyU6BwY/UiVmmbbzz9Up8= github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 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/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 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/charmbracelet/glamour v0.2.0 h1:mTgaiNiumpqTZp3qVM6DH9UB0NlbY17wejoMf1kM8Pg=
github.com/charmbracelet/glamour v0.2.0/go.mod h1:UA27Kwj3QHialP74iU6C+Gpc8Y7IOAKupeKMLLBURWM=
github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU= 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/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-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 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@ -50,8 +46,6 @@ 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/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 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.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 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 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 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
@ -68,15 +62,12 @@ github.com/go-git/go-git/v5 v5.2.0 h1:YPBLG/3UK1we1ohRkncLjaXWLW+HKp5QNM/jTli2Jg
github.com/go-git/go-git/v5 v5.2.0/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs= github.com/go-git/go-git/v5 v5.2.0/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 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/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= 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/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 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 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 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 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 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
@ -85,107 +76,98 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 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 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 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-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 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 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/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/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.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ= 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.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 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 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 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.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 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 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-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 h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDEq1axMbGg= 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/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 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/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/muesli/reflow v0.1.0 h1:oQdpLfO56lr5pgLvqD0TcjW85rDjSYSBVdiG1Ch1ddM=
github.com/muesli/reflow v0.1.0/go.mod h1:I9bWAt7QTg/que/qmUCJBGlj7wEq8OAFBjPNjc6xK4I=
github.com/muesli/reflow v0.2.0 h1:2o0UBJPHHH4fa2GCXU4Rg4DwOtWPMekCeyc5EWbAQp0= github.com/muesli/reflow v0.2.0 h1:2o0UBJPHHH4fa2GCXU4Rg4DwOtWPMekCeyc5EWbAQp0=
github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8= github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8=
github.com/muesli/termenv v0.6.0 h1:zxvzTBmo4ZcxhNGGWeMz+Tttm51eF5bmPjfy4MCRYlk=
github.com/muesli/termenv v0.6.0/go.mod h1:SohX91w6swWA4AYU+QmPx+aSgXhWO0juiyID9UZmbpA=
github.com/muesli/termenv v0.7.4 h1:/pBqvU5CpkY53tU0vVn+xgs2ZTX63aH5nY+SSps5Xa8= 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/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 h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= 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.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 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/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 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/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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/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/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 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 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 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/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/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 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/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.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
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.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 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= 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.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.0 h1:WOOcyaJPlzb8fZ8TloxFe8QZkhOOJx87leDa9MIT9dc=
github.com/yuin/goldmark v1.2.0/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
github.com/yuin/goldmark v1.2.1/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-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-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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@ -193,12 +175,10 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/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 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-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/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -210,32 +190,32 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/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-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-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-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-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/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 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/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 h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf h1:kt3wY1Lu5MJAnKTfoMR52Cu4gwvna4VTzNOiT8tY73s= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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-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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224 h1:azwY/v0y0K4mFHVsg5+UrTgchqALYWpqVo6vL5OmkmI=
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20201105220310-78b158585360 h1:/9CzsU8hOpnSUCtem1vfWNgsVeCTgkMdx+VE5YIYxnU= golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.0.0-20201105220310-78b158585360/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -245,13 +225,11 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 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.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.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.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

82
main.go
View File

@ -1,4 +1,4 @@
// Copyright 2018 The Gitea Authors. All rights reserved. // Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -6,7 +6,7 @@
package main // import "code.gitea.io/tea" package main // import "code.gitea.io/tea"
import ( import (
"log" "fmt"
"os" "os"
"strings" "strings"
@ -22,29 +22,40 @@ var Version = "development"
var Tags = "" var Tags = ""
func main() { 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) }
app := cli.NewApp() app := cli.NewApp()
app.Name = "tea" app.Name = "tea"
app.Usage = "Command line tool to interact with Gitea" app.Usage = "command line tool to interact with Gitea"
app.Description = `` app.Description = appDescription
app.CustomAppHelpTemplate = helpTemplate
app.Version = Version + formatBuiltWith(Tags) app.Version = Version + formatBuiltWith(Tags)
app.Commands = []*cli.Command{ app.Commands = []*cli.Command{
&cmd.CmdLogin, &cmd.CmdLogin,
&cmd.CmdLogout, &cmd.CmdLogout,
&cmd.CmdAutocomplete,
&cmd.CmdIssues, &cmd.CmdIssues,
&cmd.CmdPulls, &cmd.CmdPulls,
&cmd.CmdReleases,
&cmd.CmdRepos,
&cmd.CmdLabels, &cmd.CmdLabels,
&cmd.CmdMilestones,
&cmd.CmdReleases,
&cmd.CmdTrackedTimes, &cmd.CmdTrackedTimes,
&cmd.CmdOrgs,
&cmd.CmdRepos,
&cmd.CmdAddComment,
&cmd.CmdOpen, &cmd.CmdOpen,
&cmd.CmdNotifications, &cmd.CmdNotifications,
&cmd.CmdMilestones,
&cmd.CmdOrgs,
} }
app.EnableBashCompletion = true app.EnableBashCompletion = true
err := app.Run(os.Args) err := app.Run(os.Args)
if err != nil { if err != nil {
log.Fatalf("Failed to run app with %s: %v", os.Args, err) // 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)
} }
} }
@ -55,3 +66,56 @@ func formatBuiltWith(Tags string) string {
return " built with: " + strings.Replace(Tags, " ", ", ", -1) 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,130 +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 (
"errors"
"fmt"
"log"
"strings"
"code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/utils"
gogit "github.com/go-git/go-git/v5"
)
// 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(repoFlag, loginFlag, remoteFlag string) (login *Login, owner string, reponame string) {
err := loadConfig()
if err != nil {
log.Fatal(err)
}
var repoSlug string
var repoPath string // empty means PWD
var repoFlagPathExists bool
// check if repoFlag can be interpreted as path to local repo.
if len(repoFlag) != 0 {
repoFlagPathExists, err = utils.PathExists(repoFlag)
if err != nil {
log.Fatal(err.Error())
}
if repoFlagPathExists {
repoPath = repoFlag
}
}
// try to read git repo & extract context, ignoring if PWD is not a repo
login, repoSlug, err = contextFromLocalRepo(repoPath, remoteFlag)
if err != nil && err != gogit.ErrRepositoryNotExists {
log.Fatal(err.Error())
}
// if repoFlag is not a path, use it to override repoSlug
if len(repoFlag) != 0 && !repoFlagPathExists {
repoSlug = repoFlag
}
// override login from flag, or use default login if repo based detection failed
if len(loginFlag) != 0 {
login = GetLoginByName(loginFlag)
if login == nil {
log.Fatalf("Login name '%s' does not exist", loginFlag)
}
} else if login == nil {
if login, err = GetDefaultLogin(); err != nil {
log.Fatal(err.Error())
}
}
// parse reposlug (owner falling back to login owner if reposlug contains only repo name)
owner, reponame = utils.GetOwnerAndRepo(repoSlug, login.User)
return
}
// contextFromLocalRepo discovers login & repo slug from the default branch remote of the given local repo
func contextFromLocalRepo(repoValue, remoteValue string) (*Login, string, error) {
repo, err := git.RepoFromPath(repoValue)
if err != nil {
return nil, "", err
}
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")
}

197
modules/context/context.go Normal file
View File

@ -0,0 +1,197 @@
// 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

@ -38,42 +38,36 @@ func (r TeaRepo) TeaCreateBranch(localBranchName, remoteBranchName, remoteName s
} }
// TeaCheckout checks out the given branch in the worktree. // TeaCheckout checks out the given branch in the worktree.
func (r TeaRepo) TeaCheckout(branchName string) error { func (r TeaRepo) TeaCheckout(ref git_plumbing.ReferenceName) error {
tree, err := r.Worktree() tree, err := r.Worktree()
if err != nil { if err != nil {
return err return err
} }
localBranchRefName := git_plumbing.NewBranchReferenceName(branchName) return tree.Checkout(&git.CheckoutOptions{Branch: ref})
return tree.Checkout(&git.CheckoutOptions{Branch: localBranchRefName})
} }
// TeaDeleteBranch removes the given branch locally, and if `remoteBranch` is // TeaDeleteLocalBranch removes the given branch locally
// not empty deletes it at it's remote repo. func (r TeaRepo) TeaDeleteLocalBranch(branch *git_config.Branch) error {
func (r TeaRepo) TeaDeleteBranch(branch *git_config.Branch, remoteBranch string, auth git_transport.AuthMethod) error {
err := r.DeleteBranch(branch.Name) err := r.DeleteBranch(branch.Name)
// if the branch is not found that's ok, as .git/config may have no entry if // 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) // no remote tracking branch is configured for it (eg push without -u flag)
if err != nil && err.Error() != "branch not found" { if err != nil && err.Error() != "branch not found" {
return err return err
} }
err = r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name)) return r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name))
if err != nil { }
return err
}
if remoteBranch != "" { // TeaDeleteRemoteBranch removes the given branch on the given remote via git protocol
// delete remote branch via git protocol: func (r TeaRepo) TeaDeleteRemoteBranch(remoteName, remoteBranch string, auth git_transport.AuthMethod) error {
// an empty source in the refspec means remote deletion to git 🙃 // delete remote branch via git protocol:
refspec := fmt.Sprintf(":%s", git_plumbing.NewBranchReferenceName(remoteBranch)) // an empty source in the refspec means remote deletion to git 🙃
err = r.Push(&git.PushOptions{ refspec := fmt.Sprintf(":%s", git_plumbing.NewBranchReferenceName(remoteBranch))
RemoteName: branch.Remote, return r.Push(&git.PushOptions{
RefSpecs: []git_config.RefSpec{git_config.RefSpec(refspec)}, RemoteName: remoteName,
Prune: true, RefSpecs: []git_config.RefSpec{git_config.RefSpec(refspec)},
Auth: auth, Prune: true,
}) Auth: auth,
} })
return err
} }
// TeaFindBranchBySha returns a branch that is at the the given SHA and syncs to the // TeaFindBranchBySha returns a branch that is at the the given SHA and syncs to the
@ -229,5 +223,5 @@ func (r TeaRepo) TeaGetCurrentBranchName() (string, error) {
return "", fmt.Errorf("active ref is no branch") return "", fmt.Errorf("active ref is no branch")
} }
return strings.TrimPrefix(localHead.Name().String(), "refs/heads/"), nil return localHead.Name().Short(), nil
} }

View File

@ -0,0 +1,80 @@
// 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

@ -0,0 +1,162 @@
// 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

@ -0,0 +1,54 @@
// 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

@ -5,12 +5,161 @@
package interact package interact
import ( import (
"fmt"
"strings"
"time"
"code.gitea.io/tea/modules/utils"
"github.com/AlecAivazis/survey/v2" "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. // PromptPassword asks for a password and blocks until input was made.
func PromptPassword(name string) (pass string, err error) { func PromptPassword(name string) (pass string, err error) {
promptPW := &survey.Password{Message: name + " password:"} promptPW := &survey.Password{Message: name + " password:"}
err = survey.AskOne(promptPW, &pass, survey.WithValidator(survey.Required)) err = survey.AskOne(promptPW, &pass, survey.WithValidator(survey.Required))
return 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

@ -5,9 +5,7 @@
package interact package interact
import ( import (
"fmt" "code.gitea.io/sdk/gitea"
"strings"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/git" "code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/task" "code.gitea.io/tea/modules/task"
@ -17,7 +15,7 @@ import (
// CreatePull interactively creates a PR // CreatePull interactively creates a PR
func CreatePull(login *config.Login, owner, repo string) error { func CreatePull(login *config.Login, owner, repo string) error {
var base, head, title, description string var base, head string
// owner, repo // owner, repo
owner, repo, err := promptRepoSlug(owner, repo) owner, repo, err := promptRepoSlug(owner, repo)
@ -26,17 +24,14 @@ func CreatePull(login *config.Login, owner, repo string) error {
} }
// base // base
baseBranch, err := task.GetDefaultPRBase(login, owner, repo) base, err = task.GetDefaultPRBase(login, owner, repo)
if err != nil { if err != nil {
return err return err
} }
promptI := &survey.Input{Message: "Target branch [" + baseBranch + "]:"} promptI := &survey.Input{Message: "Target branch:", Default: base}
if err := survey.AskOne(promptI, &base); err != nil { if err := survey.AskOne(promptI, &base); err != nil {
return err return err
} }
if len(base) == 0 {
base = baseBranch
}
// head // head
localRepo, err := git.RepoForWorkdir() localRepo, err := git.RepoForWorkdir()
@ -48,38 +43,19 @@ func CreatePull(login *config.Login, owner, repo string) error {
if err == nil { if err == nil {
promptOpts = nil promptOpts = nil
} }
var headOwnerInput, headBranchInput string promptI = &survey.Input{Message: "Source repo owner:", Default: headOwner}
promptI = &survey.Input{Message: "Source repo owner [" + headOwner + "]:"} if err := survey.AskOne(promptI, &headOwner); err != nil {
if err := survey.AskOne(promptI, &headOwnerInput); err != nil {
return err return err
} }
if len(headOwnerInput) != 0 { promptI = &survey.Input{Message: "Source branch:", Default: headBranch}
headOwner = headOwnerInput if err := survey.AskOne(promptI, &headBranch, promptOpts); err != nil {
}
promptI = &survey.Input{Message: "Source branch [" + headBranch + "]:"}
if err := survey.AskOne(promptI, &headBranchInput, promptOpts); err != nil {
return err return err
} }
if len(headBranchInput) != 0 {
headBranch = headBranchInput
}
head = task.GetHeadSpec(headOwner, headBranch, owner) head = task.GetHeadSpec(headOwner, headBranch, owner)
// title opts := gitea.CreateIssueOption{Title: task.GetDefaultPRTitle(head)}
title = task.GetDefaultPRTitle(head) if err = promptIssueProperties(login, owner, repo, &opts); err != nil {
promptOpts = survey.WithValidator(survey.Required)
if len(title) != 0 {
promptOpts = nil
}
promptI = &survey.Input{Message: "PR title [" + title + "]:"}
if err := survey.AskOne(promptI, &title, promptOpts); err != nil {
return err
}
// description
promptM := &survey.Multiline{Message: "PR description:"}
if err := survey.AskOne(promptM, &description); err != nil {
return err return err
} }
@ -89,45 +65,5 @@ func CreatePull(login *config.Login, owner, repo string) error {
repo, repo,
base, base,
head, head,
title, &opts)
description)
}
func promptRepoSlug(defaultOwner, defaultRepo string) (owner, repo string, err error) {
prompt := "Target repo:"
required := true
if len(defaultOwner) != 0 && len(defaultRepo) != 0 {
prompt = fmt.Sprintf("Target repo [%s/%s]:", defaultOwner, defaultRepo)
required = false
}
var repoSlug string
owner = defaultOwner
repo = defaultRepo
err = survey.AskOne(
&survey.Input{Message: prompt},
&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
} }

View File

@ -0,0 +1,80 @@
// 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)
}

50
modules/print/comment.go Normal file
View File

@ -0,0 +1,50 @@
// 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

@ -0,0 +1,74 @@
// 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

@ -6,7 +6,7 @@ package print
import ( import (
"fmt" "fmt"
"strconv" "strings"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
) )
@ -21,71 +21,106 @@ func IssueDetails(issue *gitea.Issue) {
issue.Poster.UserName, issue.Poster.UserName,
FormatTime(issue.Created), FormatTime(issue.Created),
issue.Body, issue.Body,
)) ), issue.HTMLURL)
}
// IssuesList prints a listing of issues
func IssuesList(issues []*gitea.Issue, output string) {
t := tableWithHeader(
"Index",
"Title",
"State",
"Author",
"Milestone",
"Updated",
)
for _, issue := range issues {
author := issue.Poster.FullName
if len(author) == 0 {
author = issue.Poster.UserName
}
mile := ""
if issue.Milestone != nil {
mile = issue.Milestone.Title
}
t.addRow(
strconv.FormatInt(issue.Index, 10),
issue.Title,
string(issue.State),
author,
mile,
FormatTime(issue.Updated),
)
}
t.print(output)
} }
// IssuesPullsList prints a listing of issues & pulls // IssuesPullsList prints a listing of issues & pulls
// TODO combine with IssuesList func IssuesPullsList(issues []*gitea.Issue, output string, fields []string) {
func IssuesPullsList(issues []*gitea.Issue, output string) { printIssues(issues, output, fields)
t := tableWithHeader( }
"Index",
"State",
"Kind",
"Author",
"Updated",
"Title",
)
for _, issue := range issues { // IssueFields are all available fields to print with IssuesList()
name := issue.Poster.FullName var IssueFields = []string{
if len(name) == 0 { "index",
name = issue.Poster.UserName "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), "")
}
} }
kind := "Issue" // store items with printable interface
if issue.PullRequest != nil { printables[i] = &printableIssue{x, &labelMap}
kind = "Pull"
}
t.addRow(
strconv.FormatInt(issue.Index, 10),
string(issue.State),
kind,
name,
FormatTime(issue.Updated),
issue.Title,
)
} }
t := tableFromItems(fields, printables)
t.print(output) 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

@ -5,11 +5,9 @@
package print package print
import ( import (
"fmt"
"strconv" "strconv"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/muesli/termenv"
) )
// LabelsList prints a listing of labels // LabelsList prints a listing of labels
@ -21,14 +19,10 @@ func LabelsList(labels []*gitea.Label, output string) {
"Description", "Description",
) )
p := termenv.ColorProfile()
for _, label := range labels { for _, label := range labels {
color := termenv.String(label.Color)
t.addRow( t.addRow(
strconv.FormatInt(label.ID, 10), strconv.FormatInt(label.ID, 10),
fmt.Sprint(color.Background(p.Color("#"+label.Color))), formatLabel(label, !isMachineReadable(output), label.Color),
label.Name, label.Name,
label.Description, label.Description,
) )

View File

@ -13,7 +13,7 @@ import (
) )
// LoginDetails print login entry to stdout // LoginDetails print login entry to stdout
func LoginDetails(login *config.Login, output string) { func LoginDetails(login *config.Login) {
in := fmt.Sprintf("# %s\n\n[@%s](%s/%s)\n", in := fmt.Sprintf("# %s\n\n[@%s](%s/%s)\n",
login.Name, login.Name,
login.User, login.User,
@ -28,7 +28,7 @@ func LoginDetails(login *config.Login, output string) {
} }
in += fmt.Sprintf("\nCreated: %s", time.Unix(login.Created, 0).Format(time.RFC822)) in += fmt.Sprintf("\nCreated: %s", time.Unix(login.Created, 0).Format(time.RFC822))
outputMarkdown(in) outputMarkdown(in, "")
} }
// LoginsList prints a listing of logins // LoginsList prints a listing of logins

View File

@ -6,15 +6,27 @@ package print
import ( import (
"fmt" "fmt"
"os"
"github.com/charmbracelet/glamour" "github.com/charmbracelet/glamour"
"golang.org/x/crypto/ssh/terminal"
) )
// outputMarkdown prints markdown to stdout, formatted for terminals. // outputMarkdown prints markdown to stdout, formatted for terminals.
// If the input could not be parsed, it is printed unformatted, the error // If the input could not be parsed, it is printed unformatted, the error
// is returned anyway. // is returned anyway.
func outputMarkdown(markdown string) error { func outputMarkdown(markdown string, baseURL string) error {
out, err := glamour.Render(markdown, "auto") 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 { if err != nil {
fmt.Printf(markdown) fmt.Printf(markdown)
return err return err
@ -22,3 +34,18 @@ func outputMarkdown(markdown string) error {
fmt.Print(out) fmt.Print(out)
return nil 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,35 +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"
)
// 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")
}

View File

@ -7,12 +7,21 @@ package print
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
) )
var ciStatusSymbols = map[gitea.StatusState]string{
gitea.StatusSuccess: "✓ ",
gitea.StatusPending: "⭮ ",
gitea.StatusWarning: "⚠ ",
gitea.StatusError: "✘ ",
gitea.StatusFailure: "❌ ",
}
// PullDetails print an pull rendered to stdout // PullDetails print an pull rendered to stdout
func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview) { func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview, ciStatus *gitea.CombinedStatus) {
base := pr.Base.Name base := pr.Base.Name
head := pr.Head.Name head := pr.Head.Name
if pr.Head.RepoID != pr.Base.RepoID { if pr.Head.RepoID != pr.Base.RepoID {
@ -23,11 +32,16 @@ func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview) {
} }
} }
state := pr.State
if pr.Merged != nil {
state = "merged"
}
out := fmt.Sprintf( out := fmt.Sprintf(
"# #%d %s (%s)\n@%s created %s\t**%s** <- **%s**\n\n%s\n", "# #%d %s (%s)\n@%s created %s\t**%s** <- **%s**\n\n%s\n\n",
pr.Index, pr.Index,
pr.Title, pr.Title,
pr.State, state,
pr.Poster.UserName, pr.Poster.UserName,
FormatTime(*pr.Created), FormatTime(*pr.Created),
base, base,
@ -35,27 +49,68 @@ func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview) {
pr.Body, pr.Body,
) )
if len(reviews) != 0 { if ciStatus != nil || len(reviews) != 0 || pr.State == gitea.StateOpen {
out += "\n" out += "---\n"
revMap := make(map[string]gitea.ReviewStateType) }
for _, review := range reviews {
switch review.State { out += formatReviews(reviews)
case gitea.ReviewStateApproved,
gitea.ReviewStateRequestChanges, if ciStatus != nil {
gitea.ReviewStateRequestReview: var summary, errors string
revMap[review.Reviewer.UserName] = review.State for _, s := range ciStatus.Statuses {
summary += ciStatusSymbols[s.State]
if s.State != gitea.StatusSuccess {
errors += fmt.Sprintf(" - [**%s**:\t%s](%s)\n", s.Context, s.Description, s.TargetURL)
} }
} }
for k, v := range revMap { if len(ciStatus.Statuses) != 0 {
out += fmt.Sprintf("\n @%s: %s", k, v) out += fmt.Sprintf("- CI: %s\n%s", summary, errors)
} }
} }
if pr.State == gitea.StateOpen && pr.Mergeable { if pr.State == gitea.StateOpen {
out += "\nNo Conflicts" if pr.Mergeable {
out += "- No Conflicts\n"
} else {
out += "- **Conflicting files**\n"
}
} }
outputMarkdown(out) outputMarkdown(out, pr.HTMLURL)
}
func formatReviews(reviews []*gitea.PullReview) string {
result := ""
if len(reviews) == 0 {
return result
}
// deduplicate reviews by user (via review time & userID),
reviewByUser := make(map[int64]*gitea.PullReview)
for _, review := range reviews {
switch review.State {
case gitea.ReviewStateApproved,
gitea.ReviewStateRequestChanges,
gitea.ReviewStateRequestReview:
if r, ok := reviewByUser[review.Reviewer.ID]; !ok || review.Submitted.After(r.Submitted) {
reviewByUser[review.Reviewer.ID] = review
}
}
}
// group reviews by type
usersByState := make(map[gitea.ReviewStateType][]string)
for _, r := range reviewByUser {
u := r.Reviewer.UserName
users := usersByState[r.State]
usersByState[r.State] = append(users, u)
}
// stringify
for state, user := range usersByState {
result += fmt.Sprintf("- %s by @%s\n", state, strings.Join(user, ", @"))
}
return result
} }
// PullsList prints a listing of pulls // PullsList prints a listing of pulls

View File

@ -6,91 +6,19 @@ package print
import ( import (
"fmt" "fmt"
"log"
"strings" "strings"
"time" "time"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
) )
type rp = *gitea.Repository
type fieldFormatter = func(*gitea.Repository) string
var (
fieldFormatters map[string]fieldFormatter
// RepoFields are the available fields to print with ReposList()
RepoFields []string
)
func init() {
fieldFormatters = map[string]fieldFormatter{
"description": func(r rp) string { return r.Description },
"forks": func(r rp) string { return fmt.Sprintf("%d", r.Forks) },
"id": func(r rp) string { return r.FullName },
"name": func(r rp) string { return r.Name },
"owner": func(r rp) string { return r.Owner.UserName },
"stars": func(r rp) string { return fmt.Sprintf("%d", r.Stars) },
"ssh": func(r rp) string { return r.SSHURL },
"updated": func(r rp) string { return FormatTime(r.Updated) },
"url": func(r rp) string { return r.HTMLURL },
"permission": func(r rp) string {
if r.Permissions.Admin {
return "admin"
} else if r.Permissions.Push {
return "write"
}
return "read"
},
"type": func(r rp) string {
if r.Fork {
return "fork"
}
if r.Mirror {
return "mirror"
}
return "source"
},
}
for f := range fieldFormatters {
RepoFields = append(RepoFields, f)
}
}
// ReposList prints a listing of the repos // ReposList prints a listing of the repos
func ReposList(repos []*gitea.Repository, output string, fields []string) { func ReposList(repos []*gitea.Repository, output string, fields []string) {
if len(repos) == 0 { var printables = make([]printable, len(repos))
fmt.Println("No repositories found") for i, r := range repos {
return printables[i] = &printableRepo{r}
} }
t := tableFromItems(fields, printables)
if len(fields) == 0 {
fmt.Println("No fields to print")
return
}
formatters := make([]fieldFormatter, len(fields))
values := make([][]string, len(repos))
// find field format functions by header name
for i, f := range fields {
if formatter, ok := fieldFormatters[strings.ToLower(f)]; ok {
formatters[i] = formatter
} else {
log.Fatalf("invalid field '%s'", f)
}
}
// extract values from each repo and store them in 2D table
for i, repo := range repos {
values[i] = make([]string, len(formatters))
for j, format := range formatters {
values[i][j] = format(repo)
}
}
t := table{headers: fields, values: values}
t.print(output) t.print(output)
} }
@ -142,7 +70,7 @@ func RepoDetails(repo *gitea.Repository, topics []string) {
perm := fmt.Sprintf( perm := fmt.Sprintf(
"- Permission:\t%s\n", "- Permission:\t%s\n",
fieldFormatters["permission"](repo), formatPermission(repo.Permissions),
) )
var tops string var tops string
@ -159,5 +87,56 @@ func RepoDetails(repo *gitea.Repository, topics []string) {
urls, urls,
perm, perm,
tops, tops,
)) ), repo.HTMLURL)
}
// RepoFields are the available fields to print with ReposList()
var RepoFields = []string{
"description",
"forks",
"id",
"name",
"owner",
"stars",
"ssh",
"updated",
"url",
"permission",
"type",
}
type printableRepo struct{ *gitea.Repository }
func (x printableRepo) FormatField(field string) string {
switch field {
case "description":
return x.Description
case "forks":
return fmt.Sprintf("%d", x.Forks)
case "id":
return x.FullName
case "name":
return x.Name
case "owner":
return x.Owner.UserName
case "stars":
return fmt.Sprintf("%d", x.Stars)
case "ssh":
return x.SSHURL
case "updated":
return FormatTime(x.Updated)
case "url":
return x.HTMLURL
case "permission":
return formatPermission(x.Permissions)
case "type":
if x.Fork {
return "fork"
}
if x.Mirror {
return "mirror"
}
return "source"
}
return ""
} }

View File

@ -22,6 +22,24 @@ type table struct {
sortColumn uint // ↑ sortColumn uint // ↑
} }
// printable can be implemented for structs to put fields dynamically into a table
type printable interface {
FormatField(field string) string
}
// high level api to print a table of items with dynamic fields
func tableFromItems(fields []string, values []printable) table {
t := table{headers: fields}
for _, v := range values {
row := make([]string, len(fields))
for i, f := range fields {
row[i] = v.FormatField(f)
}
t.addRowSlice(row)
}
return t
}
func tableWithHeader(header ...string) table { func tableWithHeader(header ...string) table {
return table{headers: header} return table{headers: header}
} }
@ -54,16 +72,16 @@ func (t table) Less(i, j int) bool {
} }
func (t *table) print(output string) { func (t *table) print(output string) {
switch { switch output {
case output == "" || output == "table": case "", "table":
outputtable(t.headers, t.values) outputtable(t.headers, t.values)
case output == "csv": case "csv":
outputdsv(t.headers, t.values, ",") outputdsv(t.headers, t.values, ",")
case output == "simple": case "simple":
outputsimple(t.headers, t.values) outputsimple(t.headers, t.values)
case output == "tsv": case "tsv":
outputdsv(t.headers, t.values, "\t") outputdsv(t.headers, t.values, "\t")
case output == "yaml": case "yml", "yaml":
outputyaml(t.headers, t.values) outputyaml(t.headers, t.values)
default: default:
fmt.Printf("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") fmt.Printf("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")
@ -119,3 +137,11 @@ func outputyaml(headers []string, values [][]string) {
} }
} }
} }
func isMachineReadable(outputFormat string) bool {
switch outputFormat {
case "yml", "yaml", "csv":
return true
}
return false
}

View File

@ -6,50 +6,59 @@ package print
import ( import (
"fmt" "fmt"
"strconv"
"time"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
) )
func formatDuration(seconds int64, outputType string) string {
switch outputType {
case "yaml":
case "csv":
return fmt.Sprint(seconds)
}
return time.Duration(1e9 * seconds).String()
}
// TrackedTimesList print list of tracked times to stdout // TrackedTimesList print list of tracked times to stdout
func TrackedTimesList(times []*gitea.TrackedTime, outputType string, from, until time.Time, printTotal bool) { func TrackedTimesList(times []*gitea.TrackedTime, outputType string, fields []string, printTotal bool) {
tab := tableWithHeader( var printables = make([]printable, len(times))
"Created",
"Issue",
"User",
"Duration",
)
var totalDuration int64 var totalDuration int64
for i, t := range times {
for _, t := range times {
if !from.IsZero() && from.After(t.Created) {
continue
}
if !until.IsZero() && until.Before(t.Created) {
continue
}
totalDuration += t.Time totalDuration += t.Time
tab.addRow( printables[i] = &printableTrackedTime{t, outputType}
FormatTime(t.Created),
"#"+strconv.FormatInt(t.Issue.Index, 10),
t.UserName,
formatDuration(t.Time, outputType),
)
} }
t := tableFromItems(fields, printables)
if printTotal { if printTotal {
tab.addRow("TOTAL", "", "", formatDuration(totalDuration, outputType)) total := make([]string, len(fields))
total[0] = "TOTAL"
total[len(fields)-1] = formatDuration(totalDuration, outputType)
t.addRowSlice(total)
} }
tab.print(outputType)
t.print(outputType)
}
// TrackedTimeFields contains all available fields for printing of tracked times.
var TrackedTimeFields = []string{
"id",
"created",
"repo",
"issue",
"user",
"duration",
}
type printableTrackedTime struct {
*gitea.TrackedTime
outputFormat string
}
func (t printableTrackedTime) FormatField(field string) string {
switch field {
case "id":
return fmt.Sprintf("%d", t.ID)
case "created":
return FormatTime(t.Created)
case "repo":
return t.Issue.Repository.FullName
case "issue":
return fmt.Sprintf("#%d", t.Issue.Index)
case "user":
return t.UserName
case "duration":
return formatDuration(t.Time, t.outputFormat)
}
return ""
} }

View File

@ -0,0 +1,33 @@
// 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 task
import (
"fmt"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/print"
)
// CreateIssue creates an issue in the given repo and prints the result
func CreateIssue(login *config.Login, repoOwner, repoName string, opts gitea.CreateIssueOption) error {
// title is required
if len(opts.Title) == 0 {
return fmt.Errorf("Title is required")
}
issue, _, err := login.Client().CreateIssue(repoOwner, repoName, opts)
if err != nil {
return fmt.Errorf("could not create issue: %s", err)
}
print.IssueDetails(issue)
fmt.Println(issue.HTMLURL)
return nil
}

25
modules/task/labels.go Normal file
View File

@ -0,0 +1,25 @@
// 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 task
import (
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/utils"
)
// ResolveLabelNames returns a list of label IDs for a given list of label names
func ResolveLabelNames(client *gitea.Client, owner, repo string, labelNames []string) ([]int64, error) {
labelIDs := make([]int64, len(labelNames))
labels, _, err := client.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{})
if err != nil {
return nil, err
}
for _, l := range labels {
if utils.Contains(labelNames, l.Name) {
labelIDs = append(labelIDs, l.ID)
}
}
return labelIDs, nil
}

View File

@ -6,7 +6,6 @@ package task
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
@ -16,7 +15,7 @@ import (
func LabelsExport(labels []*gitea.Label, path string) error { func LabelsExport(labels []*gitea.Label, path string) error {
f, err := os.Create(path) f, err := os.Create(path)
if err != nil { if err != nil {
log.Fatal(err) return err
} }
defer f.Close() defer f.Close()

View File

@ -6,7 +6,6 @@ package task
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"time" "time"
@ -21,7 +20,7 @@ func CreateLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bo
// checks ... // checks ...
// ... if we have a url // ... if we have a url
if len(giteaURL) == 0 { if len(giteaURL) == 0 {
log.Fatal("You have to input Gitea server URL") return fmt.Errorf("You have to input Gitea server URL")
} }
// ... if there already exist a login with same name // ... if there already exist a login with same name
@ -35,17 +34,17 @@ func CreateLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bo
// .. if we have enough information to authenticate // .. if we have enough information to authenticate
if len(token) == 0 && (len(user)+len(passwd)) == 0 { if len(token) == 0 && (len(user)+len(passwd)) == 0 {
log.Fatal("No token set") return fmt.Errorf("No token set")
} else if len(user) != 0 && len(passwd) == 0 { } else if len(user) != 0 && len(passwd) == 0 {
log.Fatal("No password set") return fmt.Errorf("No password set")
} else if len(user) == 0 && len(passwd) != 0 { } else if len(user) == 0 && len(passwd) != 0 {
log.Fatal("No user set") return fmt.Errorf("No user set")
} }
// Normalize URL // Normalize URL
serverURL, err := utils.NormalizeURL(giteaURL) serverURL, err := utils.NormalizeURL(giteaURL)
if err != nil { if err != nil {
log.Fatal("Unable to parse URL", err) return fmt.Errorf("Unable to parse URL: %s", err)
} }
login := config.Login{ login := config.Login{
@ -60,23 +59,21 @@ func CreateLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bo
client := login.Client() client := login.Client()
if len(token) == 0 { if len(token) == 0 {
login.Token, err = generateToken(client, user, passwd) if login.Token, err = generateToken(client, user, passwd); err != nil {
if err != nil { return err
log.Fatal(err)
} }
} }
// Verify if authentication works and get user info // Verify if authentication works and get user info
u, _, err := client.GetMyUserInfo() u, _, err := client.GetMyUserInfo()
if err != nil { if err != nil {
log.Fatal(err) return err
} }
login.User = u.UserName login.User = u.UserName
if len(login.Name) == 0 { if len(login.Name) == 0 {
login.Name, err = GenerateLoginName(giteaURL, login.User) if login.Name, err = GenerateLoginName(giteaURL, login.User); err != nil {
if err != nil { return err
log.Fatal(err)
} }
} }
@ -91,9 +88,8 @@ func CreateLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bo
} }
} }
err = config.AddLogin(&login) if err = config.AddLogin(&login); err != nil {
if err != nil { return err
log.Fatal(err)
} }
fmt.Printf("Login as %s on %s successful. Added this login as %s\n", login.User, login.URL, login.Name) fmt.Printf("Login as %s on %s successful. Added this login as %s\n", login.User, login.URL, login.Name)

View File

@ -0,0 +1,37 @@
// 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 task
import (
"fmt"
"time"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
)
// CreateMilestone creates a milestone in the given repo and prints the result
func CreateMilestone(login *config.Login, repoOwner, repoName, title, description string, deadline *time.Time, state gitea.StateType) error {
// title is required
if len(title) == 0 {
return fmt.Errorf("Title is required")
}
mile, _, err := login.Client().CreateMilestone(repoOwner, repoName, gitea.CreateMilestoneOption{
Title: title,
Description: description,
Deadline: deadline,
State: state,
})
if err != nil {
return err
}
print.MilestoneDetails(mile)
return nil
}

View File

@ -7,43 +7,42 @@ package task
import ( import (
"fmt" "fmt"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/config"
local_git "code.gitea.io/tea/modules/git" local_git "code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/workaround"
"github.com/go-git/go-git/v5" "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"
) )
// PullCheckout checkout current workdir to the head branch of specified pull request // PullCheckout checkout current workdir to the head branch of specified pull request
func PullCheckout(login *config.Login, repoOwner, repoName string, index int64, callback func(string) (string, error)) error { func PullCheckout(
login *config.Login,
repoOwner, repoName string,
forceCreateBranch bool,
index int64,
callback func(string) (string, error),
) error {
client := login.Client() client := login.Client()
pr, _, err := client.GetPullRequest(repoOwner, repoName, index)
if err != nil {
return err
}
if err := workaround.FixPullHeadSha(client, pr); err != nil {
return err
}
// FIXME: should use ctx.LocalRepo..?
localRepo, err := local_git.RepoForWorkdir() localRepo, err := local_git.RepoForWorkdir()
if err != nil { if err != nil {
return err return err
} }
// fetch PR source-localRepo & -branch from gitea // find or create a matching remote
pr, _, err := client.GetPullRequest(repoOwner, repoName, index) remoteURL := remoteURLForPR(login, pr)
if err != nil {
return err
}
remoteURL := pr.Head.Repository.CloneURL
if len(login.SSHKey) != 0 {
// login.SSHKey is nonempty, if user specified a key manually or we automatically
// found a matching private key on this machine during login creation.
// this means, we are very likely to have a working ssh setup.
remoteURL = pr.Head.Repository.SSHURL
}
// try to find a matching existing branch, otherwise return branch in pulls/ namespace
localBranchName := fmt.Sprintf("pulls/%v-%v", index, pr.Head.Ref)
if b, _ := localRepo.TeaFindBranchBySha(pr.Head.Sha, remoteURL); b != nil {
localBranchName = b.Name
}
newRemoteName := fmt.Sprintf("pulls/%v", pr.Head.Repository.Owner.UserName) newRemoteName := fmt.Sprintf("pulls/%v", pr.Head.Repository.Owner.UserName)
// verify related remote is in local repo, otherwise add it // verify related remote is in local repo, otherwise add it
localRemote, err := localRepo.GetOrCreateRemote(remoteURL, newRemoteName) localRemote, err := localRepo.GetOrCreateRemote(remoteURL, newRemoteName)
if err != nil { if err != nil {
@ -51,32 +50,118 @@ func PullCheckout(login *config.Login, repoOwner, repoName string, index int64,
} }
localRemoteName := localRemote.Config().Name localRemoteName := localRemote.Config().Name
localRemoteBranchName, err := doPRFetch(login, pr, localRepo, localRemote, callback)
if err != nil {
return err
}
return doPRCheckout(localRepo, pr, localRemoteName, localRemoteBranchName, remoteURL, forceCreateBranch)
}
func isRemoteDeleted(pr *gitea.PullRequest) bool {
return pr.Head.Ref == fmt.Sprintf("refs/pull/%d/head", pr.Index)
}
func remoteURLForPR(login *config.Login, pr *gitea.PullRequest) string {
repo := pr.Head.Repository
if isRemoteDeleted(pr) {
repo = pr.Base.Repository
}
if len(login.SSHKey) != 0 {
// login.SSHKey is nonempty, if user specified a key manually or we automatically
// found a matching private key on this machine during login creation.
// this means, we are very likely to have a working ssh setup.
return repo.SSHURL
}
return repo.CloneURL
}
func doPRFetch(
login *config.Login,
pr *gitea.PullRequest,
localRepo *local_git.TeaRepo,
localRemote *git.Remote,
callback func(string) (string, error),
) (string, error) {
localRemoteName := localRemote.Config().Name
localBranchName := pr.Head.Ref
// get auth & fetch remote via its configured protocol // get auth & fetch remote via its configured protocol
url, err := localRepo.TeaRemoteURL(localRemoteName) url, err := localRepo.TeaRemoteURL(localRemoteName)
if err != nil { if err != nil {
return err return "", err
} }
auth, err := local_git.GetAuthForURL(url, login.Token, login.SSHKey, callback) auth, err := local_git.GetAuthForURL(url, login.Token, login.SSHKey, callback)
if err != nil { if err != nil {
return err return "", err
} }
fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n", index, url, pr.Head.Ref, localRemoteName) fetchOpts := &git.FetchOptions{Auth: auth}
err = localRemote.Fetch(&git.FetchOptions{Auth: auth}) if isRemoteDeleted(pr) {
// When the head branch is already deleted, pr.Head.Ref points to
// `refs/pull/<idx>/head`, where the commits stay available.
// This ref must be fetched explicitly, and does not allow pushing, so we use it
// only in this case as fallback.
localBranchName = fmt.Sprintf("pulls/%d", pr.Index)
fetchOpts.RefSpecs = []git_config.RefSpec{git_config.RefSpec(fmt.Sprintf("%s:refs/remotes/%s/%s",
pr.Head.Ref,
localRemoteName,
localBranchName,
))}
}
fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n", pr.Index, url, pr.Head.Ref, localRemoteName)
err = localRemote.Fetch(fetchOpts)
if err == git.NoErrAlreadyUpToDate { if err == git.NoErrAlreadyUpToDate {
fmt.Println(err) fmt.Println(err)
} else if err != nil { } else if err != nil {
return err return "", err
} }
return localBranchName, nil
// checkout local branch }
err = localRepo.TeaCreateBranch(localBranchName, pr.Head.Ref, localRemoteName)
if err == nil { func doPRCheckout(
fmt.Printf("Created branch '%s'\n", localBranchName) localRepo *local_git.TeaRepo,
} else if err == git.ErrBranchExists { pr *gitea.PullRequest,
fmt.Println("There may be changes since you last checked out, run `git pull` to get them.") localRemoteName,
} else if err != nil { localRemoteBranchName,
return err remoteURL string,
} forceCreateBranch bool,
) error {
return localRepo.TeaCheckout(localBranchName) // determine the ref to checkout, depending on existence of a matching commit on a local branch
var info string
var checkoutRef git_plumbing.ReferenceName
if b, _ := localRepo.TeaFindBranchBySha(pr.Head.Sha, remoteURL); b != nil {
// if a matching branch exists, use that
checkoutRef = git_plumbing.NewBranchReferenceName(b.Name)
info = fmt.Sprintf("Found matching local branch %s, checking it out", checkoutRef.Short())
} else if forceCreateBranch {
// create a branch if wanted
localBranchName := fmt.Sprintf("pulls/%v", pr.Index)
if isRemoteDeleted(pr) {
localBranchName += "-" + pr.Head.Ref
}
checkoutRef = git_plumbing.NewBranchReferenceName(localBranchName)
if err := localRepo.TeaCreateBranch(localBranchName, localRemoteBranchName, localRemoteName); err == nil {
info = fmt.Sprintf("Created branch '%s'\n", localBranchName)
} else if err == git.ErrBranchExists {
info = "There may be changes since you last checked out, run `git pull` to get them."
} else {
return err
}
} else {
// use the remote tracking branch
checkoutRef = git_plumbing.NewRemoteReferenceName(localRemoteName, localRemoteBranchName)
info = fmt.Sprintf(
"Checking out remote tracking branch %s. To make changes, create a new branch:\n git checkout %s",
checkoutRef.String(), localRemoteBranchName)
}
fmt.Println(info)
return localRepo.TeaCheckout(checkoutRef)
} }

View File

@ -9,9 +9,11 @@ import (
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/config"
local_git "code.gitea.io/tea/modules/git" local_git "code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/workaround"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
git_config "github.com/go-git/go-git/v5/config" git_config "github.com/go-git/go-git/v5/config"
git_plumbing "github.com/go-git/go-git/v5/plumbing"
) )
// PullClean deletes local & remote feature-branches for a closed pull // PullClean deletes local & remote feature-branches for a closed pull
@ -19,6 +21,9 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign
client := login.Client() client := login.Client()
repo, _, err := client.GetRepo(repoOwner, repoName) repo, _, err := client.GetRepo(repoOwner, repoName)
if err != nil {
return err
}
defaultBranch := repo.DefaultBranch defaultBranch := repo.DefaultBranch
if len(defaultBranch) == 0 { if len(defaultBranch) == 0 {
defaultBranch = "master" defaultBranch = "master"
@ -29,11 +34,21 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign
if err != nil { if err != nil {
return err return err
} }
if err := workaround.FixPullHeadSha(client, pr); err != nil {
return err
}
if pr.State == gitea.StateOpen { if pr.State == gitea.StateOpen {
return fmt.Errorf("PR is still open, won't delete branches") return fmt.Errorf("PR is still open, won't delete branches")
} }
// IDEA: abort if PR.Head.Repository.CloneURL does not match login.URL? // if remote head branch is already deleted, pr.Head.Ref points to "pulls/<idx>/head"
remoteBranch := pr.Head.Ref
remoteDeleted := remoteBranch == fmt.Sprintf("refs/pull/%d/head", pr.Index)
if remoteDeleted {
remoteBranch = pr.Head.Name // this still holds the original branch name
fmt.Printf("Remote branch '%s' already deleted.\n", remoteBranch)
}
r, err := local_git.RepoForWorkdir() r, err := local_git.RepoForWorkdir()
if err != nil { if err != nil {
@ -43,7 +58,7 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign
// find a branch with matching sha or name, that has a remote matching the repo url // find a branch with matching sha or name, that has a remote matching the repo url
var branch *git_config.Branch var branch *git_config.Branch
if ignoreSHA { if ignoreSHA {
branch, err = r.TeaFindBranchByName(pr.Head.Ref, pr.Head.Repository.CloneURL) branch, err = r.TeaFindBranchByName(remoteBranch, pr.Head.Repository.CloneURL)
} else { } else {
branch, err = r.TeaFindBranchBySha(pr.Head.Sha, pr.Head.Repository.CloneURL) branch, err = r.TeaFindBranchBySha(pr.Head.Sha, pr.Head.Repository.CloneURL)
} }
@ -52,12 +67,12 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign
} }
if branch == nil { if branch == nil {
if ignoreSHA { if ignoreSHA {
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", remoteBranch)
} }
return fmt.Errorf(`Remote branch %s not found in local repo. 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. 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, 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) call me again with the --ignore-sha flag`, remoteBranch)
} }
// prepare deletion of local branch: // prepare deletion of local branch:
@ -67,20 +82,30 @@ call me again with the --ignore-sha flag`, pr.Head.Ref)
} }
if headRef.Name().Short() == branch.Name { if headRef.Name().Short() == branch.Name {
fmt.Printf("Checking out '%s' to delete local branch '%s'\n", defaultBranch, branch.Name) fmt.Printf("Checking out '%s' to delete local branch '%s'\n", defaultBranch, branch.Name)
if err = r.TeaCheckout(defaultBranch); err != nil { ref := git_plumbing.NewBranchReferenceName(defaultBranch)
if err = r.TeaCheckout(ref); err != nil {
return err return err
} }
} }
// remove local & remote branch // remove local & remote branch
fmt.Printf("Deleting local branch %s and remote branch %s\n", branch.Name, pr.Head.Ref) fmt.Printf("Deleting local branch %s\n", branch.Name)
url, err := r.TeaRemoteURL(branch.Remote) err = r.TeaDeleteLocalBranch(branch)
if err != nil { if err != nil {
return err return err
} }
auth, err := local_git.GetAuthForURL(url, login.Token, login.SSHKey, callback)
if err != nil { if !remoteDeleted && pr.Head.Repository.Permissions.Push {
return err fmt.Printf("Deleting remote branch %s\n", remoteBranch)
url, err := r.TeaRemoteURL(branch.Remote)
if err != nil {
return err
}
auth, err := local_git.GetAuthForURL(url, login.Token, login.SSHKey, callback)
if err != nil {
return err
}
err = r.TeaDeleteRemoteBranch(branch.Remote, remoteBranch, auth)
} }
return r.TeaDeleteBranch(branch, pr.Head.Ref, auth) return err
} }

View File

@ -6,7 +6,6 @@ package task
import ( import (
"fmt" "fmt"
"log"
"strings" "strings"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
@ -14,24 +13,14 @@ import (
local_git "code.gitea.io/tea/modules/git" local_git "code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
"github.com/go-git/go-git/v5"
) )
// CreatePull creates a PR in the given repo and prints the result // CreatePull creates a PR in the given repo and prints the result
func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, description string) error { func CreatePull(login *config.Login, repoOwner, repoName, base, head string, opts *gitea.CreateIssueOption) error {
// open local git repo // open local git repo
localRepo, err := local_git.RepoForWorkdir() localRepo, err := local_git.RepoForWorkdir()
if err != nil { if err != nil {
log.Fatal("could not open local repo: ", err) return fmt.Errorf("Could not open local repo: %s", err)
}
// push if possible
log.Println("git push")
err = localRepo.Push(&git.PushOptions{})
if err != nil && err != git.NoErrAlreadyUpToDate {
log.Printf("Error occurred during 'git push':\n%s\n", err.Error())
} }
// default is default branch // default is default branch
@ -58,26 +47,30 @@ func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, des
} }
// default is head branch name // default is head branch name
if len(title) == 0 { if len(opts.Title) == 0 {
title = GetDefaultPRTitle(head) opts.Title = GetDefaultPRTitle(head)
} }
// title is required // title is required
if len(title) == 0 { if len(opts.Title) == 0 {
return fmt.Errorf("Title is required") return fmt.Errorf("Title is required")
} }
pr, _, err := login.Client().CreatePullRequest(repoOwner, repoName, gitea.CreatePullRequestOption{ pr, _, err := login.Client().CreatePullRequest(repoOwner, repoName, gitea.CreatePullRequestOption{
Head: head, Head: head,
Base: base, Base: base,
Title: title, Title: opts.Title,
Body: description, Body: opts.Body,
Assignees: opts.Assignees,
Labels: opts.Labels,
Milestone: opts.Milestone,
Deadline: opts.Deadline,
}) })
if err != nil { if err != nil {
log.Fatalf("could not create PR from %s to %s:%s: %s", head, repoOwner, base, err) return fmt.Errorf("Could not create PR from %s to %s:%s: %s", head, repoOwner, base, err)
} }
print.PullDetails(pr, nil) print.PullDetails(pr, nil, nil)
fmt.Println(pr.HTMLURL) fmt.Println(pr.HTMLURL)
@ -93,32 +86,23 @@ func GetDefaultPRBase(login *config.Login, owner, repo string) (string, error) {
return meta.DefaultBranch, nil return meta.DefaultBranch, nil
} }
// GetDefaultPRHead uses the currently checked out branch, checks if // GetDefaultPRHead uses the currently checked out branch, tries to find a remote
// a remote currently holds the commit it points to, extracts the owner // that has a branch with the same name, and extracts the owner from its URL.
// from its URL, and assembles the result to a valid head spec for gitea. // If no remote matches, owner is empty, meaning same as head repo owner.
func GetDefaultPRHead(localRepo *local_git.TeaRepo) (owner, branch string, err error) { func GetDefaultPRHead(localRepo *local_git.TeaRepo) (owner, branch string, err error) {
headBranch, err := localRepo.Head() if branch, err = localRepo.TeaGetCurrentBranchName(); err != nil {
if err != nil {
return return
} }
sha := headBranch.Hash().String()
remote, err := localRepo.TeaFindBranchRemote("", sha) remote, err := localRepo.TeaFindBranchRemote(branch, "")
if err != nil { if err != nil {
err = fmt.Errorf("could not determine remote for current branch: %s", err) err = fmt.Errorf("could not determine remote for current branch: %s", err)
return return
} }
if remote == nil { if remote == nil {
// if no remote branch is found for the local hash, we abort: // if no remote branch is found for the local branch,
// user has probably not configured a remote for the local branch, // we leave owner empty, meaning "use same repo as head" to gitea.
// or local branch does not represent remote state.
err = fmt.Errorf("no matching remote found for this branch. try git push -u <remote> <branch>")
return
}
branch, err = localRepo.TeaGetCurrentBranchName()
if err != nil {
return return
} }

128
modules/task/pull_review.go Normal file
View File

@ -0,0 +1,128 @@
// 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 task
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"
"code.gitea.io/tea/modules/context"
"code.gitea.io/sdk/gitea"
unidiff "gitea.com/noerw/unidiff-comments"
)
var diffReviewHelp = `# This is the current diff of PR #%d on %s.
# To add code comments, just insert a line inside the diff with your comment,
# prefixed with '# '. For example:
#
# - foo: string,
# - bar: string,
# + foo: int,
# # This is a code comment
# + bar: int,
`
// CreatePullReview submits a review for a PR
func CreatePullReview(ctx *context.TeaContext, idx int64, status gitea.ReviewStateType, comment string, codeComments []gitea.CreatePullReviewComment) error {
c := ctx.Login.Client()
review, _, err := c.CreatePullReview(ctx.Owner, ctx.Repo, idx, gitea.CreatePullReviewOptions{
State: status,
Body: comment,
Comments: codeComments,
})
if err != nil {
return err
}
fmt.Println(review.HTMLURL)
return nil
}
// SavePullDiff fetches the diff of a pull request and stores it as a temporary file.
// The path to the file is returned.
func SavePullDiff(ctx *context.TeaContext, idx int64) (string, error) {
diff, _, err := ctx.Login.Client().GetPullRequestDiff(ctx.Owner, ctx.Repo, idx)
if err != nil {
return "", err
}
writer, err := ioutil.TempFile(os.TempDir(), fmt.Sprintf("pull-%d-review-*.diff", idx))
if err != nil {
return "", err
}
defer writer.Close()
// add a help header before the actual diff
if _, err = fmt.Fprintf(writer, diffReviewHelp, idx, ctx.RepoSlug); err != nil {
return "", err
}
if _, err = writer.Write(diff); err != nil {
return "", err
}
return writer.Name(), nil
}
// ParseDiffComments reads a diff, extracts comments from it & returns them in a gitea compatible struct
func ParseDiffComments(diffFile string) ([]gitea.CreatePullReviewComment, error) {
reader, err := os.Open(diffFile)
if err != nil {
return nil, fmt.Errorf("couldn't load diff: %s", err)
}
defer reader.Close()
changeset, err := unidiff.ReadChangeset(reader)
if err != nil {
return nil, fmt.Errorf("couldn't parse patch: %s", err)
}
var comments []gitea.CreatePullReviewComment
for _, file := range changeset.Diffs {
for _, c := range file.LineComments {
comment := gitea.CreatePullReviewComment{
Body: c.Text,
Path: c.Anchor.Path,
}
comment.Path = strings.TrimPrefix(comment.Path, "a/")
comment.Path = strings.TrimPrefix(comment.Path, "b/")
switch c.Anchor.LineType {
case "ADDED":
comment.NewLineNum = c.Anchor.Line
case "REMOVED", "CONTEXT":
comment.OldLineNum = c.Anchor.Line
}
comments = append(comments, comment)
}
}
return comments, nil
}
// OpenFileInEditor opens filename in a text editor, and blocks until the editor terminates.
func OpenFileInEditor(filename string) error {
editor := os.Getenv("EDITOR")
if editor == "" {
fmt.Println("No $EDITOR env is set, defaulting to vim")
editor = "vim"
}
// Get the full executable path for the editor.
executable, err := exec.LookPath(editor)
if err != nil {
return err
}
cmd := exec.Command(executable, filename)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}

View File

@ -26,15 +26,31 @@ func PathExists(path string) (bool, error) {
// FileExist returns whether the given file exists or not // FileExist returns whether the given file exists or not
func FileExist(fileName string) (bool, error) { func FileExist(fileName string) (bool, error) {
f, err := os.Stat(fileName) return exists(fileName, false)
}
// DirExists returns whether the given file exists or not
func DirExists(path string) (bool, error) {
return exists(path, true)
}
func exists(path string, expectDir bool) (bool, error) {
f, err := os.Stat(path)
if err != nil { if err != nil {
if os.IsNotExist(err) { if errors.Is(err, os.ErrNotExist) {
return false, nil
} else if err.(*os.PathError).Err.Error() == "not a directory" {
// some middle segment of path is a file, cannot traverse
// FIXME: catches error on linux; go does not provide a way to catch this properly..
return false, nil return false, nil
} }
return false, err return false, err
} }
if f.IsDir() { isDir := f.IsDir()
if isDir && !expectDir {
return false, errors.New("A directory with the same name exists") return false, errors.New("A directory with the same name exists")
} else if !isDir && expectDir {
return false, errors.New("A file with the same name exists")
} }
return true, nil return true, nil
} }

20
modules/utils/utils.go Normal file
View File

@ -0,0 +1,20 @@
// 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 utils
// Contains checks containment
func Contains(haystack []string, needle string) bool {
return IndexOf(haystack, needle) != -1
}
// IndexOf returns the index of first occurrence of needle in haystack
func IndexOf(haystack []string, needle string) int {
for i, s := range haystack {
if s == needle {
return i
}
}
return -1
}

View File

@ -0,0 +1,30 @@
// Copyright 2021 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 workaround
import (
"fmt"
"code.gitea.io/sdk/gitea"
)
// FixPullHeadSha is a workaround for https://github.com/go-gitea/gitea/issues/12675
// When no head sha is available, this is because the branch got deleted in the base repo.
// pr.Head.Ref points in this case not to the head repo branch name, but the base repo ref,
// which stays available to resolve the commit sha.
func FixPullHeadSha(client *gitea.Client, pr *gitea.PullRequest) error {
owner := pr.Base.Repository.Owner.UserName
repo := pr.Base.Repository.Name
if pr.Head != nil && pr.Head.Sha == "" {
refs, _, err := client.GetRepoRefs(owner, repo, pr.Head.Ref)
if err != nil {
return err
} else if len(refs) == 0 {
return fmt.Errorf("unable to resolve PR ref '%s'", pr.Head.Ref)
}
pr.Head.Sha = refs[0].Object.SHA
}
return nil
}

View File

@ -63,21 +63,21 @@ func (c *Client) AdminCreateUser(opt CreateUserOption) (*User, *Response, error)
// EditUserOption edit user options // EditUserOption edit user options
type EditUserOption struct { type EditUserOption struct {
SourceID int64 `json:"source_id"` SourceID int64 `json:"source_id"`
LoginName string `json:"login_name"` LoginName string `json:"login_name"`
FullName string `json:"full_name"` Email *string `json:"email"`
Email string `json:"email"` FullName *string `json:"full_name"`
Password string `json:"password"` Password string `json:"password"`
MustChangePassword *bool `json:"must_change_password"` MustChangePassword *bool `json:"must_change_password"`
Website string `json:"website"` Website *string `json:"website"`
Location string `json:"location"` Location *string `json:"location"`
Active *bool `json:"active"` Active *bool `json:"active"`
Admin *bool `json:"admin"` Admin *bool `json:"admin"`
AllowGitHook *bool `json:"allow_git_hook"` AllowGitHook *bool `json:"allow_git_hook"`
AllowImportLocal *bool `json:"allow_import_local"` AllowImportLocal *bool `json:"allow_import_local"`
MaxRepoCreation *int `json:"max_repo_creation"` MaxRepoCreation *int `json:"max_repo_creation"`
ProhibitLogin *bool `json:"prohibit_login"` ProhibitLogin *bool `json:"prohibit_login"`
AllowCreateOrganization *bool `json:"allow_create_organization"` AllowCreateOrganization *bool `json:"allow_create_organization"`
} }
// AdminEditUser modify user informations // AdminEditUser modify user informations

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