1 Commits

Author SHA1 Message Date
8372dad929 Changelog v0.4.1 (#176)
Changelog v0.4.1

Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/176
Reviewed-by: Andrew Thornton <art27@cantab.net>
Reviewed-by: Gary Kim <gary@garykim.dev>
2020-09-13 21:48:36 +00:00
1109 changed files with 300184 additions and 9374 deletions

View File

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

View File

@ -1,2 +0,0 @@
Dockerfile
tea

View File

@ -6,26 +6,23 @@ platform:
os: linux os: linux
arch: amd64 arch: amd64
steps: workspace:
- name: vendor base: /go
pull: always path: src/code.gitea.io/tea
image: golang:1.18
environment:
GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not
commands:
- make vendor # use vendor folder as cache
steps:
- name: build - name: build
pull: always pull: always
image: golang:1.18 image: golang:1.13
environment: environment:
GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not GOPROXY: https://goproxy.cn
commands: commands:
- make clean - make clean
- make vet - make vet
- make lint - make lint
- make fmt-check - make fmt-check
- make misspell-check - make misspell-check
- make test-vendor
- make build - make build
when: when:
event: event:
@ -34,28 +31,26 @@ steps:
- pull_request - pull_request
- name: unit-test - name: unit-test
image: golang:1.18 pull: always
image: golang:1.13
commands: commands:
- make unit-test-coverage - make unit-test-coverage
settings: settings:
group: test group: test
environment:
GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not
when: when:
branch: branch:
- main - master
event: event:
- push - push
- pull_request - pull_request
- name: release-test - name: release-test
image: golang:1.18 pull: always
image: golang:1.13
commands: commands:
- make test - make test
settings: settings:
group: test group: test
environment:
GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not
when: when:
branch: branch:
- "release/*" - "release/*"
@ -65,22 +60,22 @@ steps:
- name: tag-test - name: tag-test
pull: always pull: always
image: golang:1.18 image: golang:1.13
commands: commands:
- make test - make test
settings: settings:
group: test group: test
environment:
GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not
when: when:
event: event:
- tag - tag
- name: static - name: static
image: golang:1.18 pull: always
image: techknowlogick/xgo:latest
environment: environment:
GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not GOPROXY: https://goproxy.cn
commands: commands:
- export PATH=$PATH:$GOPATH/bin
- make release - make release
when: when:
event: event:
@ -108,12 +103,11 @@ steps:
- name: tag-release - name: tag-release
pull: always pull: always
image: woodpeckerci/plugin-s3:latest image: plugins/s3:1
settings: settings:
acl: public-read acl: public-read
bucket: gitea-artifacts bucket: releases
endpoint: endpoint: https://storage.gitea.io
from_secret: aws_endpoint
path_style: true path_style: true
source: "dist/release/*" source: "dist/release/*"
strip_prefix: dist/release/ strip_prefix: dist/release/
@ -129,12 +123,12 @@ steps:
- name: release-branch-release - name: release-branch-release
pull: always pull: always
image: woodpeckerci/plugin-s3:latest image: plugins/s3:1
settings: settings:
acl: public-read acl: public-read
bucket: gitea-artifacts bucket: releases
endpoint: endpoint: https://storage.gitea.io
from_secret: aws_endpoint path_style: true
source: "dist/release/*" source: "dist/release/*"
strip_prefix: dist/release/ strip_prefix: dist/release/
target: "/tea/${DRONE_BRANCH##release/v}" target: "/tea/${DRONE_BRANCH##release/v}"
@ -151,15 +145,15 @@ steps:
- name: release - name: release
pull: always pull: always
image: woodpeckerci/plugin-s3:latest image: plugins/s3:1
settings: settings:
acl: public-read acl: public-read
bucket: gitea-artifacts bucket: releases
endpoint: endpoint: https://storage.gitea.io
from_secret: aws_endpoint path_style: true
source: "dist/release/*" source: "dist/release/*"
strip_prefix: dist/release/ strip_prefix: dist/release/
target: /tea/main target: /tea/master
environment: environment:
AWS_ACCESS_KEY_ID: AWS_ACCESS_KEY_ID:
from_secret: aws_access_key_id from_secret: aws_access_key_id
@ -167,7 +161,7 @@ steps:
from_secret: aws_secret_access_key from_secret: aws_secret_access_key
when: when:
branch: branch:
- main - master
event: event:
- push - push

View File

@ -1,30 +0,0 @@
---
name: "Bug Report"
about: "Use this template when reporting a bug, so you don't forget important information we'd ask for later."
title: "Bug: "
labels:
- kind/bug
---
### describe your environment
- tea version used (`tea -v`):
- [ ] I also reproduced the issue [with the latest master build](https://dl.gitea.io/tea/master)
- Gitea version used:
- [ ] the issue only occurred after updating gitea recently
- operating system:
- I make use of...
- [ ] non-standard default branch names (no `main`,`master`, or `trunk`)
- [ ] .ssh/config or .gitconfig host aliases in my git remotes
- [ ] ssh_agent or similar
- [ ] non-standard ports for gitea and/or ssh
- [ ] something else that's likely to interact badly with tea: ...
Please provide the output of `git remote -v` (if the issue is related to tea not finding resources on Gitea):
```
```
### describe the issue (observed vs expected behaviour)

3
.gitignore vendored
View File

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

View File

@ -16,6 +16,7 @@ warningCode = 1
[rule.increment-decrement] [rule.increment-decrement]
[rule.var-naming] [rule.var-naming]
[rule.var-declaration] [rule.var-declaration]
[rule.package-comments]
[rule.range] [rule.range]
[rule.receiver-naming] [rule.receiver-naming]
[rule.time-naming] [rule.time-naming]

View File

@ -1,167 +1,5 @@
# Changelog # Changelog
## [v0.9.1](https://gitea.com/gitea/tea/releases/tag/v0.9.1) - 2023-02-15
* BUGFIXES
* Print pull dont crash if it has TeamReviewRequests (#517)
## [v0.9.0](https://gitea.com/gitea/tea/releases/tag/v0.9.0) - 2022-09-13
* BREAKING
* Rename master branch to main (#495)
* Return RFC3339 UTC timestamps for machine-readable output (#470)
* FEATURES
* Allow editing multiline prompts with external text editor (#429)
* Add `tea admin user list` (#427)
* Add `tea whoami` command (#426)
* Add `tea org create <name>` (#420)
* Add `tea clone` (#411)
* Add `tea repo fork` (#410)
* Add `tea repo create-from-template` (#408)
* BUGFIXES
* Fetch all items where needed. (#475)
* Fix running in repos without remote (#472)
* Add TSV to machine-readable formats (#467)
* Fix create milestone with deadline bug (#462)
* Fix resolving of URLs in markdown (#401)
* ENHANCEMENTS
* Don't emit ANSI sequences when not emitting to TTY for markdown (#491)
* Show more version info (#486)
* Add preference `flag_defaults.remote`, refactor (#466)
* Add `--fields` to notification & milestone listings (#422)
* PR listing: add --fields & expose additional fields (#415)
* Add more flags to `tea repo create` (#409)
* Implement more issue filters (#400)
* MISC
* Simplify build & update installation instructions (#437)
* Clarify command descriptions when no arguments are taken (#496)
* Improve Documentation (#433)
* Use golang v1.18 and drop vendor folder (#478)
* Correct spelling of "wether" to "whether" in usage output (#453)
## [v0.8.0](https://gitea.com/gitea/tea/releases/tag/v0.8.0) - 2021-09-22
* BREAKING
* `tea notifications --all` has moved to `tea notifications --mine` (#389)
* `tea notifications` now only works with the context of a remote repo. (#389)
To run this outside of a local git dir, run either tea n `--mine` or `tea n --repo <my/repo>`
* FEATURES
* Add `tea pr merge` (#348)
* BUGFIXES
* Don't skip reading the local repo when `--repo` specifies a repo slug (#398)
* Fix adding login without token on private instances (#392)
* Correctly match login by ssh host with port (#391)
* Fix printing issue deadline (#388)
* Return useful error on wrong sshkey path (#374)
* Fix parsing of `--description` for issue/pr create (#371)
* Add missing flags (#369)
* Check negative limit command parameter (#358) (#359)
* Add missing flags to org & labels subcommands (#357)
* ENHANCEMENTS
* Don't require a body for comment PR reviews (#399)
* Accept more main branch names for login detection (#396)
* Make local repo optional for `tea pr create`(#393)
* Notifications Add State Field (#384)
* Improve error messages (#370)
* Add tab completion for fish shell (#364)
* Text editor selection: follow unix defacto standards (#356)
* MISC
* Update Dependencies (#390)
## [v0.7.1](https://gitea.com/gitea/tea/releases/tag/v0.7.1) - 2021-08-27
* BUILD
* Enable release builds for darwin/arm64 (#360)
## [v0.7.0](https://gitea.com/gitea/tea/releases/tag/v0.7.0) - 2021-03-12
* BREAKING
* `tea issue create`: move `-b` flag to `-d` (#331)
* Drop `tea notif` shorthand in favor of `tea n` (#307)
* FEATURES
* Add commands for reviews (#315)
* Add `tea comment` and show comments of issues/pulls (#313)
* Add interactive mode for `tea milestone create` (#310)
* Add command to install shell completion (#309)
* Implement PR closing and reopening (#304)
* Add interactive mode for `tea issue create` (#302)
* BUGFIXES
* Introduce workaround for missing pull head sha (#340)
* Don't exit if we can't find a local repo with a remote matching to a login (#336)
* Don't push before creating a pull (#334)
* InitCommand() robustness (#327)
* `tea comment`: handle piped stdin (#322)
* ENHANCEMENTS
* Allow checking out PRs with deleted head branch (#341)
* Markdown renderer: detect terminal width, resolve relative URLs (#332)
* Add more issue / pr creation parameters (#331)
* Improve `tea time` (#319)
* `tea pr checkout`: dont create local branches (#314)
* Add `tea issues --fields`, allow printing labels (#312)
* Add more command shorthands (#307)
* Show PR CI status (#306)
* Make PR workflow helpers more robust (#300)
## [v0.6.0](https://gitea.com/gitea/tea/releases/tag/v0.6.0) - 2020-12-11
* BREAKING
* Add `tea repos search`, improve repo listing (#215)
* Add Detail View for Login (#212)
* FEATURES
* Add interactive mode for `tea pr create` (#279)
* Add organization delete command (#270)
* Add organization list command (#264)
* BUGFIXES
* Forces needed arguments to `tea ms issues` (#297)
* Subcommands work outside of git repos (#285)
* Fix repo flag ignores local repo for login detection (#285)
* Improve ssh handling (#277)
* Issue create return web url (#257)
* Support prerelease gitea instances (#252)
* Fix `tea pr create` within same repo (#248)
* Handle login name case-insensitive on all comands (#227)
* ENHANCEMENTS
* Add `tea login delete` (#296)
* Release delete: add --delete-tag & --confirm (#286)
* Sorted milestones list (#281)
* Pull clean & checkout use token for http(s) auth (#275)
* Show more infos in pull detail view (#271)
* Specify fields to print on `tea repos list` (#223)
* Print times in local timezone (#217)
* Issue create/edit print details (#214)
* Improve `tea logout` (#213)
* Added a shorthand for notifications (#209)
* Common subcommand naming scheme (#208)
* `tea pr checkout`: fetch via ssh if available (#192)
* Major refactor of codebase
* BUILD
* Use gox to cross-compile (#274)
* DOCS
* Update Docs to new code structure (#247)
## [v0.5.0](https://gitea.com/gitea/tea/releases/tag/v0.5.0) - 2020-09-27
* BREAKING
* Add Login Manage Functions (#182)
* FEATURES
* Add Release Subcomands (#195)
* Render Markdown and colorize labels table (#181)
* Add BasicAuth & Interactive for Login (#174)
* Add milestones subcomands (#149)
* BUGFIXES
* Fix Pulls Create (#202)
* Pulls create: detect head branch repo owner (#193)
* Fix Labels Delete (#180)
* ENHANCEMENTS
* Add Pagination Options for List Subcomands (#204)
* Issues/Pulls: Details show State (#196)
* Make issues & pulls subcommands consistent (#188)
* Update SDK to v0.13.0 (#179)
* More Options To Specify Repo (#178)
* Add Repo Create subcomand & enhancements (#173)
* Times: format duration as seconds for machine-readable outputs (#168)
* Add user message to login list view (#166)
## [v0.4.1](https://gitea.com/gitea/tea/releases/tag/v0.4.1) - 2020-09-13 ## [v0.4.1](https://gitea.com/gitea/tea/releases/tag/v0.4.1) - 2020-09-13
* BUGFIXES * BUGFIXES

View File

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

View File

@ -1,25 +0,0 @@
ARG GOVERSION="1.16.2"
FROM golang:${GOVERSION}-alpine AS buildenv
ARG GOOS="linux"
COPY . $GOPATH/src/
WORKDIR $GOPATH/src
RUN apk add --quiet --no-cache \
build-base \
make \
git && \
make clean build STATIC=true
FROM scratch
ARG VERSION="0.7.0"
LABEL org.opencontainers.image.title="tea - CLI for Gitea - git with a cup of tea"
LABEL org.opencontainers.image.description="A command line tool to interact with Gitea servers"
LABEL org.opencontainers.image.version="${VERSION}"
LABEL org.opencontainers.image.authors="Tamás Gérczei <tamas@gerczei.eu>"
LABEL org.opencontainers.image.vendor="The Gitea Authors"
COPY --from=buildenv /go/src/tea /
ENV HOME="/app"
ENTRYPOINT ["/tea"]

View File

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

134
Makefile
View File

@ -1,15 +1,31 @@
DIST := dist DIST := dist
IMPORT := code.gitea.io/tea
export GO111MODULE=on export GO111MODULE=on
export CGO_ENABLED=0
GO ?= go GO ?= go
SED_INPLACE := sed -i
SHASUM ?= shasum -a 256 SHASUM ?= shasum -a 256
export PATH := $($(GO) env GOPATH)/bin:$(PATH) export PATH := $($(GO) env GOPATH)/bin:$(PATH)
ifeq ($(OS), Windows_NT)
EXECUTABLE := tea.exe
else
EXECUTABLE := tea
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
SED_INPLACE := sed -i ''
endif
endif
GOFILES := $(shell find . -name "*.go" -type f ! -path "./vendor/*" ! -path "*/bindata.go") GOFILES := $(shell find . -name "*.go" -type f ! -path "./vendor/*" ! -path "*/bindata.go")
GOFMT ?= gofmt -s GOFMT ?= gofmt -s
GOFLAGS := -i -v
EXTRA_GOFLAGS ?=
MAKE_VERSION := $(shell make -v | head -n 1)
ifneq ($(DRONE_TAG),) ifneq ($(DRONE_TAG),)
VERSION ?= $(subst v,,$(DRONE_TAG)) VERSION ?= $(subst v,,$(DRONE_TAG))
TEA_VERSION ?= $(VERSION) TEA_VERSION ?= $(VERSION)
@ -21,28 +37,23 @@ else
endif endif
TEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//') TEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
endif endif
TEA_VERSION_TAG ?= $(shell sed 's/+/_/' <<< $(TEA_VERSION))
TAGS ?= LDFLAGS := -X "main.Version=$(TEA_VERSION)" -X "main.Tags=$(TAGS)"
SDK ?= $(shell $(GO) list -f '{{.Version}}' -m code.gitea.io/sdk/gitea)
LDFLAGS := -X "main.Version=$(TEA_VERSION)" -X "main.Tags=$(TAGS)" -X "main.SDK=$(SDK)" -s -w
# override to allow passing additional goflags via make CLI
override GOFLAGS := $(GOFLAGS) -mod=vendor -tags '$(TAGS)' -ldflags '$(LDFLAGS)'
PACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/) PACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/)
SOURCES ?= $(shell find . -name "*.go" -type f) SOURCES ?= $(shell find . -name "*.go" -type f)
# OS specific vars. TAGS ?=
ifeq ($(OS), Windows_NT) ifeq ($(OS), Windows_NT)
EXECUTABLE := tea.exe EXECUTABLE := tea.exe
else else
EXECUTABLE := tea EXECUTABLE := tea
ifneq ($(shell uname -s), OpenBSD)
override BUILDMODE := -buildmode=pie
endif
endif endif
# $(call strip-suffix,filename)
strip-suffix = $(firstword $(subst ., ,$(1)))
.PHONY: all .PHONY: all
all: build all: build
@ -60,19 +71,28 @@ vet:
# Default vet # Default vet
$(GO) vet -mod=vendor $(PACKAGES) $(GO) vet -mod=vendor $(PACKAGES)
# Custom vet # Custom vet
$(GO) build -mod=vendor code.gitea.io/gitea-vet $(GO) build -mod=vendor gitea.com/jolheiser/gitea-vet
$(GO) vet -vettool=gitea-vet $(PACKAGES) $(GO) vet -vettool=gitea-vet $(PACKAGES)
.PHONY: lint .PHONY: lint
lint: install-lint-tools lint:
@hash revive > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
cd /tmp && $(GO) get -u github.com/mgechev/revive; \
fi
revive -config .revive.toml -exclude=./vendor/... ./... || exit 1 revive -config .revive.toml -exclude=./vendor/... ./... || exit 1
.PHONY: misspell-check .PHONY: misspell-check
misspell-check: install-lint-tools misspell-check:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
cd /tmp && $(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi
misspell -error -i unknwon,destory $(GOFILES) misspell -error -i unknwon,destory $(GOFILES)
.PHONY: misspell .PHONY: misspell
misspell: install-lint-tools misspell:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
cd /tmp && $(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi
misspell -w -i unknwon $(GOFILES) misspell -w -i unknwon $(GOFILES)
.PHONY: fmt-check .PHONY: fmt-check
@ -97,56 +117,76 @@ unit-test-coverage:
vendor: vendor:
$(GO) mod tidy && $(GO) mod vendor $(GO) mod tidy && $(GO) mod vendor
.PHONY: test-vendor
test-vendor: vendor
@diff=$$(git diff vendor/); \
if [ -n "$$diff" ]; then \
echo "Please run 'make vendor' and commit the result:"; \
echo "$${diff}"; \
exit 1; \
fi;
.PHONY: check .PHONY: check
check: test check: test
.PHONY: install .PHONY: install
install: $(SOURCES) install: $(wildcard *.go)
@echo "installing to $(GOPATH)/bin/$(EXECUTABLE)" $(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)'
$(GO) install -v $(BUILDMODE) $(GOFLAGS)
.PHONY: build .PHONY: build
build: $(EXECUTABLE) build: $(EXECUTABLE)
$(EXECUTABLE): $(SOURCES) $(EXECUTABLE): $(SOURCES)
$(GO) build $(BUILDMODE) $(GOFLAGS) -o $@ $(GO) build -mod=vendor $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
.PHONY: build-image
build-image:
docker build --build-arg VERSION=$(TEA_VERSION) -t gitea/tea:$(TEA_VERSION_TAG) .
.PHONY: release .PHONY: release
release: release-dirs install-release-tools release-os release-compress release-check release: release-dirs release-windows release-linux release-darwin release-copy release-compress release-check
.PHONY: release-dirs .PHONY: release-dirs
release-dirs: release-dirs:
mkdir -p $(DIST)/binaries $(DIST)/release mkdir -p $(DIST)/binaries $(DIST)/release
.PHONY: release-os .PHONY: release-windows
release-os: release-windows:
CGO_ENABLED=0 gox -verbose -cgo=false $(GOFLAGS) -osarch='!darwin/386 !darwin/arm' -os="windows linux darwin" -arch="386 amd64 arm arm64" -output="$(DIST)/release/tea-$(VERSION)-{{.OS}}-{{.Arch}}" @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
cd /tmp && $(GO) get -u src.techknowlogick.com/xgo; \
fi
GO111MODULE=off xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out tea-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-linux
release-linux:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
cd /tmp && $(GO) get -u src.techknowlogick.com/xgo; \
fi
GO111MODULE=off xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/mips64le,linux/mips,linux/mipsle' -out tea-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-darwin
release-darwin:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
cd /tmp && $(GO) get -u src.techknowlogick.com/xgo; \
fi
GO111MODULE=off xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin/*' -out tea-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-copy
release-copy:
cd $(DIST); for file in `find /build -type f -name "*"`; do cp $${file} ./release/; done;
.PHONY: release-compress .PHONY: release-compress
release-compress: install-release-tools release-compress:
@hash gxz > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
GO111MODULE=off $(GO) get -u github.com/ulikunitz/xz/cmd/gxz; \
fi
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && gxz -k -9 $${file}; done; cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && gxz -k -9 $${file}; done;
.PHONY: release-check .PHONY: release-check
release-check: install-release-tools release-check:
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "checksumming $${file}" && $(SHASUM) `echo $${file} | sed 's/^..//'` > $${file}.sha256; done; cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "checksumming $${file}" && $(SHASUM) `echo $${file} | sed 's/^..//'` > $${file}.sha256; done;
### tools
install-release-tools:
@hash gox > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install github.com/mitchellh/gox@latest; \
fi
@hash gxz > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install github.com/ulikunitz/xz/cmd/gxz@latest; \
fi
install-lint-tools:
@hash revive > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install github.com/mgechev/revive@latest; \
fi
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install github.com/client9/misspell/cmd/misspell@latest; \
fi

152
README.md
View File

@ -2,125 +2,83 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Release](https://raster.shields.io/badge/dynamic/json.svg?label=release&url=https://gitea.com/api/v1/repos/gitea/tea/releases&query=$[0].tag_name)](https://gitea.com/gitea/tea/releases) [![Build Status](https://drone.gitea.com/api/badges/gitea/tea/status.svg)](https://drone.gitea.com/gitea/tea) [![Join the chat at https://img.shields.io/discord/322538954119184384.svg](https://img.shields.io/discord/322538954119184384.svg)](https://discord.gg/Gitea) [![Go Report Card](https://goreportcard.com/badge/code.gitea.io/tea)](https://goreportcard.com/report/code.gitea.io/tea) [![GoDoc](https://godoc.org/code.gitea.io/tea?status.svg)](https://godoc.org/code.gitea.io/tea) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Release](https://raster.shields.io/badge/dynamic/json.svg?label=release&url=https://gitea.com/api/v1/repos/gitea/tea/releases&query=$[0].tag_name)](https://gitea.com/gitea/tea/releases) [![Build Status](https://drone.gitea.com/api/badges/gitea/tea/status.svg)](https://drone.gitea.com/gitea/tea) [![Join the chat at https://img.shields.io/discord/322538954119184384.svg](https://img.shields.io/discord/322538954119184384.svg)](https://discord.gg/Gitea) [![Go Report Card](https://goreportcard.com/badge/code.gitea.io/tea)](https://goreportcard.com/report/code.gitea.io/tea) [![GoDoc](https://godoc.org/code.gitea.io/tea?status.svg)](https://godoc.org/code.gitea.io/tea)
### The official CLI for Gitea ## The official CLI interface for gitea
![demo gif](./demo.gif) Tea is a command line tool for interacting on one or more Gitea instances.
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.8.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 & provides local helpers like 'tea pr checkout'.
tea tries to make use of context provided by the repository in $PWD if available.
tea works best in a upstream/fork workflow, when the local main branch tracks the
upstream repo. tea assumes that local git state is published on the remote before
doing operations with tea. 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
comment, c Add a comment to an issue / pr
HELPERS:
open, o Open something of the repository in web browser
notifications, notification, n Show notifications
clone, C Clone a repository locally
SETUP:
logins, login Log in to a Gitea server
logout Log out from a Gitea server
shellcompletion, autocomplete Install shell completion for tea
whoami Show current logged in user
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 --mine -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
There are different ways to get `tea`: You can use the prebuilt binaries from [dl.gitea.io](https://dl.gitea.io/tea/)
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-git](https://aur.archlinux.org/packages/gitea-tea-git), thirdparty)
- alpine linux ([tea](https://pkgs.alpinelinux.org/packages?name=tea&branch=edge), thirdparty)
2. Use the prebuilt binaries from [dl.gitea.io](https://dl.gitea.io/tea/) To install from source, go 1.12 or newer is required:
```sh
go get code.gitea.io/tea
go install code.gitea.io/tea
```
3. Install from source: [see *Compilation*](#compilation)
4. Docker (thirdparty): [tgerczei/tea](https://hub.docker.com/r/tgerczei/tea) If you have `brew` installed, you can install `tea` via:
```sh
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`.
```sh
login Log in to a Gitea server
logout Log out from a Gitea server
issues List, create and update issues
pulls List, checkout and clean pull requests
releases Create 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
```
To fetch issues from different repos, use the `--remote` flag (when inside a gitea repository directory) or `--login` & `--repo` flags.
## Compilation ## Compilation
Make sure you have a current go version installed (1.13 or newer). Make sure you have installed a current go version.
To compile the sources yourself run the following:
- To compile the source yourself with the recommended flags & tags: ```sh
```sh git clone https://gitea.com/gitea/tea.git
git clone https://gitea.com/gitea/tea.git # or: tea clone gitea.com/gitea/tea ;) cd tea
cd tea make
make ```
```
Note that GNU Make (gmake on OpenBSD) is required.
- For a quick installation without `git` & `make`:
```sh
go install code.gitea.io/tea@latest
```
## Contributing ## Contributing
Fork -> Patch -> Push -> Pull Request Fork -> Patch -> Push -> Pull Request
- `make test` run testsuite - `make test` run testsuite
- `make vet` run checks (check the order of imports; preventing failure on CI pipeline beforehand)
- `make vendor` when adding new dependencies - `make vendor` when adding new dependencies
- ... (for other development tasks, check the `Makefile`) - ... (for other development tasks, check the `Makefile`)
**Please** read the [CONTRIBUTING](CONTRIBUTING.md) documentation, it will tell you about internal structures and concepts. ## Authors
* [Maintainers](https://github.com/orgs/go-gitea/people)
* [Contributors](https://github.com/go-gitea/tea/graphs/contributors)
## License ## License

View File

@ -1,8 +1,7 @@
// Copyright 2020 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.
//go:build vendor //+build vendor
// +build vendor
package main package main
@ -11,5 +10,5 @@ package main
import ( import (
// for vet // for vet
_ "code.gitea.io/gitea-vet" _ "gitea.com/jolheiser/gitea-vet"
) )

View File

@ -1,55 +0,0 @@
// 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 cmd
import (
"code.gitea.io/tea/cmd/admin/users"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"github.com/urfave/cli/v2"
)
// CmdAdmin represents the namespace of admin commands.
// The command itself has no functionality, but hosts subcommands.
var CmdAdmin = cli.Command{
Name: "admin",
Usage: "Operations requiring admin access on the Gitea instance",
Aliases: []string{"a"},
Category: catMisc,
Action: func(cmd *cli.Context) error {
return cli.ShowSubcommandHelp(cmd)
},
Subcommands: []*cli.Command{
&cmdAdminUsers,
},
}
var cmdAdminUsers = cli.Command{
Name: "users",
Aliases: []string{"u"},
Usage: "Manage registered users",
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() == 1 {
return runAdminUserDetail(ctx, ctx.Args().First())
}
return users.RunUserList(ctx)
},
Subcommands: []*cli.Command{
&users.CmdUserList,
},
Flags: users.CmdUserList.Flags,
}
func runAdminUserDetail(cmd *cli.Context, u string) error {
ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
user, _, err := client.GetUserInfo(u)
if err != nil {
return err
}
print.UserDetails(user)
return nil
}

View File

@ -1,54 +0,0 @@
// 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 users
import (
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
var userFieldsFlag = flags.FieldsFlag(print.UserFields, []string{
"id", "login", "full_name", "email", "activated",
})
// CmdUserList represents a sub command of users to list users
var CmdUserList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List Users",
Description: "List users",
Action: RunUserList,
Flags: append([]cli.Flag{
userFieldsFlag,
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...),
}
// RunUserList list users
func RunUserList(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
fields, err := userFieldsFlag.GetValues(cmd)
if err != nil {
return err
}
client := ctx.Login.Client()
users, _, err := client.AdminListUsers(gitea.AdminListUsersOptions{
ListOptions: ctx.GetListOptions(),
})
if err != nil {
return err
}
print.UserList(users, ctx.Output, fields)
return nil
}

View File

@ -1,138 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"github.com/adrg/xdg"
"github.com/urfave/cli/v2"
)
// CmdAutocomplete manages autocompletion
var CmdAutocomplete = cli.Command{
Name: "shellcompletion",
Aliases: []string{"autocomplete"},
Category: catSetup,
Usage: "Install shell completion for tea",
Description: "Install shell completion for tea",
ArgsUsage: "<shell type> (bash, zsh, powershell, fish)",
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"
case "fish":
// fish is different, in that urfave/cli provides a generator for the shell script needed.
// this also means that the fish completion can become out of sync with the tea binary!
// writing to this directory suffices, as fish reads files there on startup, no cmds needed.
return writeFishAutoCompleteFile(ctx)
default:
return fmt.Errorf("Must specify valid %s", ctx.Command.ArgsUsage)
}
localPath, err := xdg.ConfigFile("tea/" + localFile)
if err != nil {
return err
}
cmds = fmt.Sprintf(cmds, localPath)
if err = writeRemoteAutoCompleteFile(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 writeRemoteAutoCompleteFile(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
}
func writeFishAutoCompleteFile(ctx *cli.Context) error {
// NOTE: to make sure this file is in sync with tea commands, we'd need to
// - check if the file exists
// - if it does, check if the tea version that wrote it is the currently running version
// - if not, rewrite the file
// on each application run
// NOTE: this generates a completion that also suggests file names, which looks kinda messy..
script, err := ctx.App.ToFishCompletion()
if err != nil {
return err
}
localPath, err := xdg.ConfigFile("fish/conf.d/tea_completion.fish")
if err != nil {
return err
}
writer, err := os.Create(localPath)
if err != nil {
return err
}
if _, err = io.WriteString(writer, script); err != nil {
return err
}
fmt.Printf("Installed tab completion to %s\n", localPath)
return nil
}

View File

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

View File

@ -1,89 +0,0 @@
// 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 cmd
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2"
)
// CmdRepoClone represents a sub command of repos to create a local copy
var CmdRepoClone = cli.Command{
Name: "clone",
Aliases: []string{"C"},
Usage: "Clone a repository locally",
Description: `Clone a repository locally, without a local git installation required.
The repo slug can be specified in different formats:
gitea/tea
tea
gitea.com/gitea/tea
git@gitea.com:gitea/tea
https://gitea.com/gitea/tea
ssh://gitea.com:22/gitea/tea
When a host is specified in the repo-slug, it will override the login specified with --login.
`,
Category: catHelpers,
Action: runRepoClone,
ArgsUsage: "<repo-slug> [target dir]",
Flags: []cli.Flag{
&cli.IntFlag{
Name: "depth",
Aliases: []string{"d"},
Usage: "num commits to fetch, defaults to all",
},
&flags.LoginFlag,
},
}
func runRepoClone(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
args := ctx.Args()
if args.Len() < 1 {
return cli.ShowCommandHelp(cmd, "clone")
}
dir := args.Get(1)
var (
login *config.Login = ctx.Login
owner string = ctx.Login.User
repo string
)
// parse first arg as repo specifier
repoSlug := args.Get(0)
url, err := git.ParseURL(repoSlug)
if err != nil {
return err
}
owner, repo = utils.GetOwnerAndRepo(url.Path, login.User)
if url.Host != "" {
login = config.GetLoginByHost(url.Host)
if login == nil {
return fmt.Errorf("No login configured matching host '%s', run `tea login add` first", url.Host)
}
}
_, err = task.RepoClone(
dir,
login,
owner,
repo,
interact.PromptPassword,
ctx.Int("depth"),
)
return err
}

View File

@ -1,83 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"fmt"
"io/ioutil"
"strings"
"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/print"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/AlecAivazis/survey/v2"
"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 err = survey.AskOne(interact.NewMultiline(interact.Multiline{
Message: "Comment:",
Syntax: "md",
UseEditor: config.GetPreferences().Editor,
}), &body); 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
}

255
cmd/config.go Normal file
View File

@ -0,0 +1,255 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"path/filepath"
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/utils"
"gopkg.in/yaml.v2"
)
// Login represents a login to a gitea server, you even could add multiple logins for one gitea server
type Login struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
Token string `yaml:"token"`
Active bool `yaml:"active"`
SSHHost string `yaml:"ssh_host"`
// optional path to the private key
SSHKey string `yaml:"ssh_key"`
Insecure bool `yaml:"insecure"`
// optional gitea username
User string `yaml:"user"`
}
// Client returns a client to operate Gitea API
func (l *Login) Client() *gitea.Client {
client := gitea.NewClient(l.URL, l.Token)
if l.Insecure {
cookieJar, _ := cookiejar.New(nil)
client.SetHTTPClient(&http.Client{
Jar: cookieJar,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
})
}
return client
}
// GetSSHHost returns SSH host name
func (l *Login) GetSSHHost() string {
if l.SSHHost != "" {
return l.SSHHost
}
u, err := url.Parse(l.URL)
if err != nil {
return ""
}
return u.Hostname()
}
// Config reprensents local configurations
type Config struct {
Logins []Login `yaml:"logins"`
}
var (
config Config
yamlConfigPath string
)
func init() {
homeDir, err := utils.Home()
if err != nil {
log.Fatal("Retrieve home dir failed")
}
dir := filepath.Join(homeDir, ".tea")
err = os.MkdirAll(dir, os.ModePerm)
if err != nil {
log.Fatal("Init tea config dir " + dir + " failed")
}
yamlConfigPath = filepath.Join(dir, "tea.yml")
}
func splitRepo(repoPath string) (string, string) {
p := strings.Split(repoPath, "/")
if len(p) >= 2 {
return p[0], p[1]
}
return repoPath, ""
}
func getActiveLogin() (*Login, error) {
if len(config.Logins) == 0 {
return nil, errors.New("No available login")
}
for _, l := range config.Logins {
if l.Active {
return &l, nil
}
}
return &config.Logins[0], nil
}
func getLoginByName(name string) *Login {
for _, l := range config.Logins {
if l.Name == name {
return &l
}
}
return nil
}
func addLogin(login Login) error {
for _, l := range config.Logins {
if l.Name == login.Name {
if l.URL == login.URL && l.Token == login.Token {
return nil
}
return errors.New("Login name has already been used")
}
if l.URL == login.URL && l.Token == login.Token {
return errors.New("URL has been added")
}
}
u, err := url.Parse(login.URL)
if err != nil {
return err
}
if login.SSHHost == "" {
login.SSHHost = u.Hostname()
}
config.Logins = append(config.Logins, login)
return nil
}
func isFileExist(fileName string) (bool, error) {
f, err := os.Stat(fileName)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
if f.IsDir() {
return false, errors.New("A directory with the same name exists")
}
return true, nil
}
func loadConfig(ymlPath string) error {
exist, _ := isFileExist(ymlPath)
if exist {
Println("Found config file", ymlPath)
bs, err := ioutil.ReadFile(ymlPath)
if err != nil {
return err
}
err = yaml.Unmarshal(bs, &config)
if err != nil {
return err
}
}
return nil
}
func saveConfig(ymlPath string) error {
bs, err := yaml.Marshal(&config)
if err != nil {
return err
}
return ioutil.WriteFile(ymlPath, bs, 0660)
}
func curGitRepoPath(path string) (*Login, string, error) {
var err error
var repo *git.TeaRepo
if len(path) == 0 {
repo, err = git.RepoForWorkdir()
} else {
repo, err = git.RepoFromPath(path)
}
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")
}

126
cmd/flags.go Normal file
View File

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

View File

@ -1,53 +0,0 @@
// 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 flags
import (
"fmt"
"strings"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2"
)
// CsvFlag is a wrapper around cli.StringFlag, with an added GetValues() method
// to retrieve comma separated string values as a slice.
type CsvFlag struct {
cli.StringFlag
AvailableFields []string
}
// NewCsvFlag creates a CsvFlag, while setting its usage string and default values
func NewCsvFlag(name, usage string, aliases, availableValues, defaults []string) *CsvFlag {
var availableDesc string
if len(availableValues) != 0 {
availableDesc = " Available values:"
}
return &CsvFlag{
AvailableFields: availableValues,
StringFlag: cli.StringFlag{
Name: name,
Aliases: aliases,
Value: strings.Join(defaults, ","),
Usage: fmt.Sprintf(`Comma-separated list of %s.%s
%s
`, usage, availableDesc, strings.Join(availableValues, ",")),
},
}
}
// GetValues returns the value of the flag, parsed as a commaseparated list
func (f CsvFlag) GetValues(ctx *cli.Context) ([]string, error) {
val := ctx.String(f.Name)
selection := strings.Split(val, ",")
if f.AvailableFields != nil && val != "" {
for _, field := range selection {
if !utils.Contains(f.AvailableFields, field) {
return nil, fmt.Errorf("Invalid field '%s'", field)
}
}
}
return selection, nil
}

View File

@ -1,106 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package flags
import (
"github.com/urfave/cli/v2"
)
// LoginFlag provides flag to specify tea login profile
var LoginFlag = cli.StringFlag{
Name: "login",
Aliases: []string{"l"},
Usage: "Use a different Gitea Login. Optional",
}
// RepoFlag provides flag to specify repository
var RepoFlag = cli.StringFlag{
Name: "repo",
Aliases: []string{"r"},
Usage: "Override local repository path or gitea repository slug to interact with. Optional",
}
// RemoteFlag provides flag to specify remote repository
var RemoteFlag = cli.StringFlag{
Name: "remote",
Aliases: []string{"R"},
Usage: "Discover Gitea login from remote. Optional",
}
// OutputFlag provides flag to specify output type
var OutputFlag = cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Output format. (csv, simple, table, tsv, yaml)",
}
// PaginationPageFlag provides flag for pagination options
var PaginationPageFlag = cli.StringFlag{
Name: "page",
Aliases: []string{"p"},
Usage: "specify page, default is 1",
}
// PaginationLimitFlag provides flag for pagination options
var PaginationLimitFlag = cli.StringFlag{
Name: "limit",
Aliases: []string{"lm"},
Usage: "specify limit of items per page",
}
// LoginOutputFlags defines login and output flags that should
// added to all subcommands and appended to the flags of the
// subcommand to work around issue and provide --login and --output:
// https://github.com/urfave/cli/issues/585
var LoginOutputFlags = []cli.Flag{
&LoginFlag,
&OutputFlag,
}
// LoginRepoFlags defines login and repo flags that should
// be used for all subcommands and appended to the flags of
// the subcommand to work around issue and provide --login and --repo:
// https://github.com/urfave/cli/issues/585
var LoginRepoFlags = []cli.Flag{
&LoginFlag,
&RepoFlag,
&RemoteFlag,
}
// AllDefaultFlags defines flags that should be available
// for all subcommands working with dedicated repositories
// to work around issue and provide --login, --repo and --output:
// https://github.com/urfave/cli/issues/585
var AllDefaultFlags = append([]cli.Flag{
&RepoFlag,
&RemoteFlag,
}, LoginOutputFlags...)
// NotificationFlags defines flags that should be available on notifications.
var NotificationFlags = append([]cli.Flag{
NotificationStateFlag,
&cli.BoolFlag{
Name: "mine",
Aliases: []string{"m"},
Usage: "Show notifications across all your repositories instead of the current repository only",
},
&PaginationPageFlag,
&PaginationLimitFlag,
}, AllDefaultFlags...)
// NotificationStateFlag is a csv flag applied to all notification subcommands as filter
var NotificationStateFlag = NewCsvFlag(
"states",
"notification states to filter by",
[]string{"s"},
[]string{"pinned", "unread", "read"},
[]string{"unread", "pinned"},
)
// FieldsFlag generates a flag selecting printable fields.
// To retrieve the value, use f.GetValues()
func FieldsFlag(availableFields, defaultFields []string) *CsvFlag {
return NewCsvFlag("fields", "fields to print", []string{"f"}, availableFields, defaultFields)
}

View File

@ -1,161 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package flags
import (
"fmt"
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v2"
)
// StateFlag provides flag to specify issue/pr state, defaulting to "open"
var StateFlag = cli.StringFlag{
Name: "state",
Usage: "Filter by state (all|open|closed)",
DefaultText: "open",
}
// MilestoneFilterFlag is a CSV flag used to filter issues by milestones
var MilestoneFilterFlag = NewCsvFlag(
"milestones",
"milestones to match issues against",
[]string{"m"}, nil, nil)
// LabelFilterFlag is a CSV flag used to filter issues by labels
var LabelFilterFlag = NewCsvFlag(
"labels",
"labels to match issues against",
[]string{"L"}, nil, nil)
// PRListingFlags defines flags that should be available on pr listing flags.
var PRListingFlags = append([]cli.Flag{
&StateFlag,
&PaginationPageFlag,
&PaginationLimitFlag,
}, AllDefaultFlags...)
// IssueListingFlags defines flags that should be available on issue listing flags.
var IssueListingFlags = append([]cli.Flag{
&StateFlag,
&cli.StringFlag{
Name: "kind",
Aliases: []string{"K"},
Usage: "Whether to return `issues`, `pulls`, or `all` (you can use this to apply advanced search filters to PRs)",
DefaultText: "issues",
},
&cli.StringFlag{
Name: "keyword",
Aliases: []string{"k"},
Usage: "Filter by search string",
},
LabelFilterFlag,
MilestoneFilterFlag,
&cli.StringFlag{
Name: "author",
Aliases: []string{"A"},
},
&cli.StringFlag{
Name: "assignee",
Aliases: []string{"a"},
},
&cli.StringFlag{
Name: "mentions",
Aliases: []string{"M"},
},
&cli.StringFlag{
Name: "from",
Aliases: []string{"F"},
Usage: "Filter by activity after this date",
},
&cli.StringFlag{
Name: "until",
Aliases: []string{"u"},
Usage: "Filter by activity before this date",
},
&PaginationPageFlag,
&PaginationLimitFlag,
}, AllDefaultFlags...)
// IssuePREditFlags defines flags for properties of issues and PRs
var IssuePREditFlags = append([]cli.Flag{
&cli.StringFlag{
Name: "title",
Aliases: []string{"t"},
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
},
&cli.StringFlag{
Name: "assignees",
Aliases: []string{"a"},
Usage: "Comma-separated list of usernames to assign",
},
&cli.StringFlag{
Name: "labels",
Aliases: []string{"L"},
Usage: "Comma-separated list of labels to assign",
},
&cli.StringFlag{
Name: "deadline",
Aliases: []string{"D"},
Usage: "Deadline timestamp to assign",
},
&cli.StringFlag{
Name: "milestone",
Aliases: []string{"m"},
Usage: "Milestone to assign",
},
}, LoginRepoFlags...)
// GetIssuePREditFlags parses all IssuePREditFlags
func GetIssuePREditFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, error) {
opts := gitea.CreateIssueOption{
Title: ctx.String("title"),
Body: ctx.String("description"),
Assignees: strings.Split(ctx.String("assignees"), ","),
}
var err error
date := ctx.String("deadline")
if date != "" {
t, err := dateparse.ParseAny(date)
if err != nil {
return nil, err
}
opts.Deadline = &t
}
client := ctx.Login.Client()
labelNames := strings.Split(ctx.String("labels"), ",")
if len(labelNames) != 0 {
if client == nil {
client = ctx.Login.Client()
}
if opts.Labels, err = task.ResolveLabelNames(client, ctx.Owner, ctx.Repo, labelNames); err != nil {
return nil, err
}
}
if milestoneName := ctx.String("milestone"); len(milestoneName) != 0 {
if client == nil {
client = ctx.Login.Client()
}
ms, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestoneName)
if err != nil {
return nil, fmt.Errorf("Milestone '%s' not found", milestoneName)
}
opts.Milestone = ms.ID
}
return &opts, nil
}

View File

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

View File

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

View File

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

View File

@ -1,108 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package issues
import (
"fmt"
"time"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v2"
)
var issueFieldsFlag = flags.FieldsFlag(print.IssueFields, []string{
"index", "title", "state", "author", "milestone", "labels",
})
// CmdIssuesList represents a sub command of issues to list issues
var CmdIssuesList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List issues of the repository",
Description: `List issues of the repository`,
ArgsUsage: " ", // command does not accept arguments
Action: RunIssuesList,
Flags: append([]cli.Flag{issueFieldsFlag}, flags.IssueListingFlags...),
}
// RunIssuesList list issues
func RunIssuesList(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
state := gitea.StateOpen
switch ctx.String("state") {
case "all":
state = gitea.StateAll
case "", "open":
state = gitea.StateOpen
case "closed":
state = gitea.StateClosed
default:
return fmt.Errorf("unknown state '%s'", ctx.String("state"))
}
kind := gitea.IssueTypeIssue
switch ctx.String("kind") {
case "", "issues", "issue":
kind = gitea.IssueTypeIssue
case "pulls", "pull", "pr":
kind = gitea.IssueTypePull
case "all":
kind = gitea.IssueTypeAll
default:
return fmt.Errorf("unknown kind '%s'", ctx.String("kind"))
}
var err error
var from, until time.Time
if ctx.IsSet("from") {
from, err = dateparse.ParseLocal(ctx.String("from"))
if err != nil {
return err
}
}
if ctx.IsSet("until") {
until, err = dateparse.ParseLocal(ctx.String("until"))
if err != nil {
return err
}
}
// ignore error, as we don't do any input validation on these flags
labels, _ := flags.LabelFilterFlag.GetValues(cmd)
milestones, _ := flags.MilestoneFilterFlag.GetValues(cmd)
issues, _, err := ctx.Login.Client().ListRepoIssues(ctx.Owner, ctx.Repo, gitea.ListIssueOption{
ListOptions: ctx.GetListOptions(),
State: state,
Type: kind,
KeyWord: ctx.String("keyword"),
CreatedBy: ctx.String("author"),
AssignedBy: ctx.String("assigned-to"),
MentionedBy: ctx.String("mentions"),
Labels: labels,
Milestones: milestones,
Since: from,
Before: until,
})
if err != nil {
return err
}
fields, err := issueFieldsFlag.GetValues(cmd)
if err != nil {
return err
}
print.IssuesPullsList(issues, ctx.Output, fields)
return nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT-style // 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.
package labels package cmd
import ( import (
"bufio" "bufio"

111
cmd/log.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,25 +5,105 @@
package cmd package cmd
import ( import (
"code.gitea.io/tea/cmd/notifications" "log"
"strings"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// 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", "n"}, Usage: "show notifications",
Category: catHelpers, Description: "show notifications, by default based of the current repo and unread one",
Usage: "Show notifications", Action: runNotifications,
Description: "Show notifications, by default based on the current repo if available", Flags: append([]cli.Flag{
Action: notifications.RunNotificationsList, &cli.BoolFlag{
Subcommands: []*cli.Command{ Name: "all",
&notifications.CmdNotificationsList, Aliases: []string{"a"},
&notifications.CmdNotificationsMarkRead, Usage: "show all notifications of related gitea instance",
&notifications.CmdNotificationsMarkUnread, },
&notifications.CmdNotificationsMarkPinned, /* // not supported jet
&notifications.CmdNotificationsUnpin, &cli.BoolFlag{
}, Name: "read",
Flags: notifications.CmdNotificationsList.Flags, Aliases: []string{"rd"},
Usage: "show read notifications instead unread",
},
*/
&cli.IntFlag{
Name: "page",
Aliases: []string{"p"},
Usage: "specify page, default is 1",
Value: 1,
},
&cli.IntFlag{
Name: "limit",
Aliases: []string{"lm"},
Usage: "specify limit of items per page",
},
}, AllDefaultFlags...),
}
func runNotifications(ctx *cli.Context) error {
var news []*gitea.NotificationThread
var err error
listOpts := gitea.ListOptions{
Page: ctx.Int("page"),
PageSize: ctx.Int("limit"),
}
if ctx.Bool("all") {
login := initCommandLoginOnly()
news, err = login.Client().ListNotifications(gitea.ListNotificationOptions{
ListOptions: listOpts,
})
} else {
login, owner, repo := initCommand()
news, err = login.Client().ListRepoNotifications(owner, repo, gitea.ListNotificationOptions{
ListOptions: listOpts,
})
}
if err != nil {
log.Fatal(err)
}
headers := []string{
"Type",
"Index",
"Title",
}
if ctx.Bool("all") {
headers = append(headers, "Repository")
}
var values [][]string
for _, n := range news {
if n.Subject == nil {
continue
}
// if pull or Issue get Index
var index string
if n.Subject.Type == "Issue" || n.Subject.Type == "Pull" {
index = n.Subject.URL
urlParts := strings.Split(n.Subject.URL, "/")
if len(urlParts) != 0 {
index = urlParts[len(urlParts)-1]
}
index = "#" + index
}
item := []string{n.Subject.Type, index, n.Subject.Title}
if ctx.Bool("all") {
item = append(item, n.Repository.FullName)
}
values = append(values, item)
}
if len(values) != 0 {
Output(outputValue, headers, values)
}
return nil
} }

View File

@ -1,107 +0,0 @@
// 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 notifications
import (
"log"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
var notifyFieldsFlag = flags.FieldsFlag(print.NotificationFields, []string{
"id", "status", "index", "type", "state", "title",
})
var notifyTypeFlag = flags.NewCsvFlag("types", "subject types to filter by", []string{"t"},
[]string{"issue", "pull", "repository", "commit"}, nil)
// CmdNotificationsList represents a sub command of notifications to list notifications
var CmdNotificationsList = cli.Command{
Name: "ls",
Aliases: []string{"list"},
Usage: "List notifications",
Description: `List notifications`,
ArgsUsage: " ", // command does not accept arguments
Action: RunNotificationsList,
Flags: append([]cli.Flag{
notifyFieldsFlag,
notifyTypeFlag,
}, flags.NotificationFlags...),
}
// RunNotificationsList list notifications
func RunNotificationsList(ctx *cli.Context) error {
var states []gitea.NotifyStatus
statesStr, err := flags.NotificationStateFlag.GetValues(ctx)
if err != nil {
return err
}
for _, s := range statesStr {
states = append(states, gitea.NotifyStatus(s))
}
var types []gitea.NotifySubjectType
typesStr, err := notifyTypeFlag.GetValues(ctx)
if err != nil {
return err
}
for _, t := range typesStr {
types = append(types, gitea.NotifySubjectType(t))
}
return listNotifications(ctx, states, types)
}
// listNotifications will get the notifications based on status and subject type
func listNotifications(cmd *cli.Context, status []gitea.NotifyStatus, subjects []gitea.NotifySubjectType) error {
var news []*gitea.NotificationThread
var err error
ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
all := ctx.Bool("mine")
// This enforces pagination (see https://github.com/go-gitea/gitea/issues/16733)
listOpts := ctx.GetListOptions()
if listOpts.Page == 0 {
listOpts.Page = 1
}
fields, err := notifyFieldsFlag.GetValues(cmd)
if err != nil {
return err
}
if all {
// add repository to the default fields
if !cmd.IsSet("fields") {
fields = append(fields, "repository")
}
news, _, err = client.ListNotifications(gitea.ListNotificationOptions{
ListOptions: listOpts,
Status: status,
SubjectTypes: subjects,
})
} else {
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
news, _, err = client.ListRepoNotifications(ctx.Owner, ctx.Repo, gitea.ListNotificationOptions{
ListOptions: listOpts,
Status: status,
SubjectTypes: subjects,
})
}
if err != nil {
log.Fatal(err)
}
print.NotificationsList(news, ctx.Output, fields)
return nil
}

View File

@ -1,139 +0,0 @@
// 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 notifications
import (
"fmt"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2"
)
// CmdNotificationsMarkRead represents a sub command of notifications to list read notifications
var CmdNotificationsMarkRead = cli.Command{
Name: "read",
Aliases: []string{"r"},
Usage: "Mark all filtered or a specific notification as read",
Description: "Mark all filtered or a specific notification as read",
ArgsUsage: "[all | <notification id>]",
Flags: flags.NotificationFlags,
Action: func(ctx *cli.Context) error {
cmd := context.InitCommand(ctx)
filter, err := flags.NotificationStateFlag.GetValues(ctx)
if err != nil {
return err
}
if !cmd.IsSet(flags.NotificationStateFlag.Name) {
filter = []string{string(gitea.NotifyStatusUnread)}
}
return markNotificationAs(cmd, filter, gitea.NotifyStatusRead)
},
}
// CmdNotificationsMarkUnread will mark notifications as unread.
var CmdNotificationsMarkUnread = cli.Command{
Name: "unread",
Aliases: []string{"u"},
Usage: "Mark all filtered or a specific notification as unread",
Description: "Mark all filtered or a specific notification as unread",
ArgsUsage: "[all | <notification id>]",
Flags: flags.NotificationFlags,
Action: func(ctx *cli.Context) error {
cmd := context.InitCommand(ctx)
filter, err := flags.NotificationStateFlag.GetValues(ctx)
if err != nil {
return err
}
if !cmd.IsSet(flags.NotificationStateFlag.Name) {
filter = []string{string(gitea.NotifyStatusRead)}
}
return markNotificationAs(cmd, filter, gitea.NotifyStatusUnread)
},
}
// CmdNotificationsMarkPinned will mark notifications as unread.
var CmdNotificationsMarkPinned = cli.Command{
Name: "pin",
Aliases: []string{"p"},
Usage: "Mark all filtered or a specific notification as pinned",
Description: "Mark all filtered or a specific notification as pinned",
ArgsUsage: "[all | <notification id>]",
Flags: flags.NotificationFlags,
Action: func(ctx *cli.Context) error {
cmd := context.InitCommand(ctx)
filter, err := flags.NotificationStateFlag.GetValues(ctx)
if err != nil {
return err
}
if !cmd.IsSet(flags.NotificationStateFlag.Name) {
filter = []string{string(gitea.NotifyStatusUnread)}
}
return markNotificationAs(cmd, filter, gitea.NotifyStatusPinned)
},
}
// CmdNotificationsUnpin will mark pinned notifications as unread.
var CmdNotificationsUnpin = cli.Command{
Name: "unpin",
Usage: "Unpin all pinned or a specific notification",
Description: "Marks all pinned or a specific notification as read",
ArgsUsage: "[all | <notification id>]",
Flags: flags.NotificationFlags,
Action: func(ctx *cli.Context) error {
cmd := context.InitCommand(ctx)
filter := []string{string(gitea.NotifyStatusPinned)}
// NOTE: we implicitly mark it as read, to match web UI semantics. marking as unread might be more useful?
return markNotificationAs(cmd, filter, gitea.NotifyStatusRead)
},
}
func markNotificationAs(cmd *context.TeaContext, filterStates []string, targetState gitea.NotifyStatus) (err error) {
client := cmd.Login.Client()
subject := cmd.Args().First()
allRepos := cmd.Bool("mine")
states := []gitea.NotifyStatus{}
for _, s := range filterStates {
states = append(states, gitea.NotifyStatus(s))
}
switch subject {
case "", "all":
opts := gitea.MarkNotificationOptions{Status: states, ToStatus: targetState}
if allRepos {
_, _, err = client.ReadNotifications(opts)
} else {
cmd.Ensure(context.CtxRequirement{RemoteRepo: true})
_, _, err = client.ReadRepoNotifications(cmd.Owner, cmd.Repo, opts)
}
// TODO: print all affected notification subject URLs
// (not supported by API currently, https://github.com/go-gitea/gitea/issues/16797)
default:
id, err := utils.ArgToIndex(subject)
if err != nil {
return err
}
_, _, err = client.ReadNotification(id, targetState)
if err != nil {
return err
}
n, _, err := client.GetNotification(id)
if err != nil {
return err
}
// FIXME: this is an API URL, we want to display a web ui link..
fmt.Println(n.Subject.URL)
return nil
}
return err
}

View File

@ -5,11 +5,10 @@
package cmd package cmd
import ( import (
"log"
"path" "path"
"strings" "strings"
"code.gitea.io/tea/cmd/flags"
"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"
@ -19,17 +18,14 @@ 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",
Aliases: []string{"o"}, Usage: "Open something of the repository on web browser",
Category: catHelpers, Description: `Open something of the repository on web browser`,
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{}, LoginRepoFlags...),
} }
func runOpen(cmd *cli.Context) error { func runOpen(ctx *cli.Context) error {
ctx := context.InitCommand(cmd) login, owner, repo := initCommand()
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
var suffix string var suffix string
number := ctx.Args().Get(0) number := ctx.Args().Get(0)
@ -43,11 +39,12 @@ func runOpen(cmd *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 {
return err log.Fatal(err)
} }
b, err := repo.Head() b, err := repo.Head()
if err != nil { if err != nil {
return err log.Fatal(err)
return nil
} }
name := b.Name() name := b.Name()
switch { switch {
@ -74,6 +71,11 @@ func runOpen(cmd *cli.Context) error {
suffix = number suffix = number
} }
u := path.Join(ctx.Login.URL, ctx.Owner, ctx.Repo, suffix) u := path.Join(login.URL, owner, repo, suffix)
return open.Run(u) err := open.Run(u)
if err != nil {
log.Fatal(err)
}
return nil
} }

View File

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

View File

@ -1,90 +0,0 @@
// 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 organizations
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdOrganizationCreate represents a sub command of organizations to delete a given user organization
var CmdOrganizationCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create an organization",
Description: "Create an organization",
Action: RunOrganizationCreate,
ArgsUsage: "<organization name>",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
},
&cli.StringFlag{
Name: "website",
Aliases: []string{"w"},
},
&cli.StringFlag{
Name: "location",
Aliases: []string{"L"},
},
&cli.StringFlag{
Name: "visibility",
Aliases: []string{"v"},
},
&cli.BoolFlag{
Name: "repo-admins-can-change-team-access",
},
&flags.LoginFlag,
},
}
// RunOrganizationCreate sets up a new organization
func RunOrganizationCreate(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
if ctx.Args().Len() < 1 {
return fmt.Errorf("You have to specify the organization name you want to create")
}
var visibility gitea.VisibleType
switch ctx.String("visibility") {
case "", "public":
visibility = gitea.VisibleTypePublic
case "private":
visibility = gitea.VisibleTypePrivate
case "limited":
visibility = gitea.VisibleTypeLimited
default:
return fmt.Errorf("unknown visibility '%s'", ctx.String("visibility"))
}
org, _, err := ctx.Login.Client().CreateOrg(gitea.CreateOrgOption{
Name: ctx.Args().First(),
// FullName: , // not really meaningful for orgs (not displayed in webui, use description instead?)
Description: ctx.String("description"),
Website: ctx.String("website"),
Location: ctx.String("location"),
RepoAdminChangeTeamAccess: ctx.Bool("repo-admins-can-change-team-access"),
Visibility: visibility,
})
if err != nil {
return err
}
print.OrganizationDetails(org)
return err
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,70 +0,0 @@
// 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 pulls
import (
"fmt"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v2"
)
// CmdPullsMerge merges a PR
var CmdPullsMerge = cli.Command{
Name: "merge",
Aliases: []string{"m"},
Usage: "Merge a pull request",
Description: "Merge a pull request",
ArgsUsage: "<pull index>",
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "style",
Aliases: []string{"s"},
Usage: "Kind of merge to perform: merge, rebase, squash, rebase-merge",
Value: "merge",
},
&cli.StringFlag{
Name: "title",
Aliases: []string{"t"},
Usage: "Merge commit title",
},
&cli.StringFlag{
Name: "message",
Aliases: []string{"m"},
Usage: "Merge commit message",
},
}, flags.AllDefaultFlags...),
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
}
success, _, err := ctx.Login.Client().MergePullRequest(ctx.Owner, ctx.Repo, idx, gitea.MergePullRequestOption{
Style: gitea.MergeStyle(ctx.String("style")),
Title: ctx.String("title"),
Message: ctx.String("message"),
})
if err != nil {
return err
}
if !success {
return fmt.Errorf("Failed to merge PR. Is it still open?")
}
return nil
},
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,111 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package releases
import (
"fmt"
"net/http"
"os"
"path/filepath"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdReleaseCreate represents a sub command of Release to create release
var CmdReleaseCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create a release",
Description: `Create a release for a new or existing git tag`,
ArgsUsage: "[<tag>]",
Action: runReleaseCreate,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "tag",
Usage: "Tag name. If the tag does not exist yet, it will be created by Gitea",
},
&cli.StringFlag{
Name: "target",
Usage: "Target branch name or commit hash. Defaults to the default branch of the repo",
},
&cli.StringFlag{
Name: "title",
Aliases: []string{"t"},
Usage: "Release title",
},
&cli.StringFlag{
Name: "note",
Aliases: []string{"n"},
Usage: "Release notes",
},
&cli.BoolFlag{
Name: "draft",
Aliases: []string{"d"},
Usage: "Is a draft",
},
&cli.BoolFlag{
Name: "prerelease",
Aliases: []string{"p"},
Usage: "Is a pre-release",
},
&cli.StringSliceFlag{
Name: "asset",
Aliases: []string{"a"},
Usage: "Path to file attachment. Can be specified multiple times",
},
}, flags.AllDefaultFlags...),
}
func runReleaseCreate(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
tag := ctx.String("tag")
if cmd.Args().Present() {
if len(tag) != 0 {
return fmt.Errorf("ambiguous arguments: provide tagname via --tag or argument, but not both")
}
tag = cmd.Args().First()
}
release, resp, err := ctx.Login.Client().CreateRelease(ctx.Owner, ctx.Repo, gitea.CreateReleaseOption{
TagName: tag,
Target: ctx.String("target"),
Title: ctx.String("title"),
Note: ctx.String("note"),
IsDraft: ctx.Bool("draft"),
IsPrerelease: ctx.Bool("prerelease"),
})
if err != nil {
if resp != nil && resp.StatusCode == http.StatusConflict {
return fmt.Errorf("There already is a release for this tag")
}
return err
}
for _, asset := range ctx.StringSlice("asset") {
var file *os.File
if file, err = os.Open(asset); err != nil {
return err
}
filePath := filepath.Base(asset)
if _, _, err = ctx.Login.Client().CreateReleaseAttachment(ctx.Owner, ctx.Repo, release.ID, file, filePath); err != nil {
file.Close()
return err
}
file.Close()
}
return nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,121 +0,0 @@
// 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 repos
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdRepoCreateFromTemplate represents a sub command of repos to generate one from a template repo
var CmdRepoCreateFromTemplate = cli.Command{
Name: "create-from-template",
Aliases: []string{"ct"},
Usage: "Create a repository based on an existing template",
Description: "Create a repository based on an existing template",
Action: runRepoCreateFromTemplate,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "template",
Aliases: []string{"t"},
Required: true,
Usage: "source template to copy from",
},
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Required: true,
Usage: "name of new repo",
},
&cli.StringFlag{
Name: "owner",
Aliases: []string{"O"},
Usage: "name of repo owner",
},
&cli.BoolFlag{
Name: "private",
Usage: "make new repo private",
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"desc"},
Usage: "add custom description to repo",
},
&cli.BoolFlag{
Name: "content",
Value: true,
Usage: "copy git content from template",
},
&cli.BoolFlag{
Name: "githooks",
Value: true,
Usage: "copy git hooks from template",
},
&cli.BoolFlag{
Name: "avatar",
Value: true,
Usage: "copy repo avatar from template",
},
&cli.BoolFlag{
Name: "labels",
Value: true,
Usage: "copy repo labels from template",
},
&cli.BoolFlag{
Name: "topics",
Value: true,
Usage: "copy topics from template",
},
&cli.BoolFlag{
Name: "webhooks",
Usage: "copy webhooks from template",
},
}, flags.LoginOutputFlags...),
}
func runRepoCreateFromTemplate(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
templateOwner, templateRepo := utils.GetOwnerAndRepo(ctx.String("template"), ctx.Login.User)
owner := ctx.Login.User
if ctx.IsSet("owner") {
owner = ctx.String("owner")
}
opts := gitea.CreateRepoFromTemplateOption{
Name: ctx.String("name"),
Owner: owner,
Description: ctx.String("description"),
Private: ctx.Bool("private"),
GitContent: ctx.Bool("content"),
GitHooks: ctx.Bool("githooks"),
Avatar: ctx.Bool("avatar"),
Labels: ctx.Bool("labels"),
Topics: ctx.Bool("topics"),
Webhooks: ctx.Bool("webhooks"),
}
repo, _, err := client.CreateRepoFromTemplate(templateOwner, templateRepo, opts)
if err != nil {
return err
}
topics, _, err := client.ListRepoTopics(repo.Owner.UserName, repo.Name, gitea.ListRepoTopicsOptions{})
if err != nil {
return err
}
print.RepoDetails(repo, topics)
fmt.Printf("%s\n", repo.HTMLURL)
return nil
}

View File

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

View File

@ -1,59 +0,0 @@
// 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 repos
import (
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v2"
)
// CmdRepoFork represents a sub command of repos to fork an existing repo
var CmdRepoFork = cli.Command{
Name: "fork",
Aliases: []string{"f"},
Usage: "Fork an existing repository",
Description: "Create a repository from an existing repo",
ArgsUsage: " ", // command does not accept arguments
Action: runRepoFork,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "owner",
Aliases: []string{"O"},
Usage: "name of fork's owner, defaults to current user",
},
}, flags.LoginRepoFlags...),
}
func runRepoFork(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client()
opts := gitea.CreateForkOption{}
if ctx.IsSet("owner") {
owner := ctx.String("owner")
opts.Organization = &owner
}
repo, _, err := client.CreateFork(ctx.Owner, ctx.Repo, opts)
if err != nil {
return err
}
topics, _, err := client.ListRepoTopics(repo.Owner.UserName, repo.Name, gitea.ListRepoTopicsOptions{})
if err != nil {
return err
}
print.RepoDetails(repo, topics)
fmt.Printf("%s\n", repo.HTMLURL)
return nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +0,0 @@
// 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 cmd
import (
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"github.com/urfave/cli/v2"
)
// CmdWhoami represents the command to show current logged in user
var CmdWhoami = cli.Command{
Name: "whoami",
Category: catMisc,
Description: `For debugging purposes, show the user that is currently logged in.`,
Usage: "Show current logged in user",
ArgsUsage: " ", // command does not accept arguments
Action: func(cmd *cli.Context) error {
ctx := context.InitCommand(cmd)
client := ctx.Login.Client()
user, _, _ := client.GetMyUserInfo()
print.UserDetails(user)
return nil
},
}

View File

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

View File

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

View File

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

BIN
demo.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

78
go.mod
View File

@ -1,68 +1,22 @@
module code.gitea.io/tea module code.gitea.io/tea
go 1.18 go 1.12
require ( require (
code.gitea.io/gitea-vet v0.2.1 code.gitea.io/sdk/gitea v0.12.1
code.gitea.io/sdk/gitea v0.15.1-0.20220831004139-a0127ed0e7fe gitea.com/jolheiser/gitea-vet v0.1.0
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1
github.com/AlecAivazis/survey/v2 v2.3.6 github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/adrg/xdg v0.4.0 github.com/go-git/go-git/v5 v5.1.0
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/hashicorp/go-version v1.2.1 // indirect
github.com/charmbracelet/glamour v0.5.0 github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/enescakir/emoji v1.0.0 github.com/olekukonko/tablewriter v0.0.4
github.com/go-git/go-git/v5 v5.4.2
github.com/muesli/termenv v0.12.0
github.com/olekukonko/tablewriter v0.0.5
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.7.0 github.com/stretchr/testify v1.5.1
github.com/urfave/cli/v2 v2.16.3 github.com/urfave/cli/v2 v2.2.0
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
gopkg.in/yaml.v2 v2.4.0 golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
) golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed // indirect
require ( gopkg.in/yaml.v2 v2.3.0
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cloudflare/circl v1.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/microcosm-cc/bluemonday v1.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/xanzy/ssh-agent v0.3.2 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/yuin/goldmark v1.4.14 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect
golang.org/x/sys v0.0.0-20220913153101-76c7481b5158 // indirect
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.12 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

274
go.sum
View File

@ -1,258 +1,144 @@
code.gitea.io/gitea-vet v0.2.1 h1:b30by7+3SkmiftK0RjuXqFvZg2q4p68uoPGuxhzBN0s= code.gitea.io/sdk/gitea v0.12.1 h1:bMgjEqPnNX/i6TpVwXwpjJtFOnUSuC9P6yy/jjy8sjY=
code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= code.gitea.io/sdk/gitea v0.12.1/go.mod h1:z3uwDV/b9Ls47NGukYM9XhnHtqPh/J+t40lsUrR6JDY=
code.gitea.io/sdk/gitea v0.15.1-0.20220831004139-a0127ed0e7fe h1:PeLyxnUZE85QuJtBZ4P8qCQcgWG5Ked67mlNgr0WkCQ= gitea.com/jolheiser/gitea-vet v0.1.0 h1:gJEms9YWbIcrPOEmDOJ+5JZXCYFxNpwxlI73uRulAi4=
code.gitea.io/sdk/gitea v0.15.1-0.20220831004139-a0127ed0e7fe/go.mod h1:aRmrQC3CAHdJAU1LQt0C9zqzqI8tUB/5oQtNE746aYE= gitea.com/jolheiser/gitea-vet v0.1.0/go.mod h1:2Oa6TAdEp1N/38oBNh3ZeiSEER60D/CeDaBFv2sdH58=
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c h1:8fTkq2UaVkLHZCF+iB4wTxINmVAToe2geZGayk9LMbA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c/go.mod h1:Fc8iyPm4NINRWujeIk2bTfcbGc4ZYY29/oMAAGcr4qI= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 h1:NsReiLpErIPzRrnogAXYwSoU7txA977LjDGrbkewJbg=
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
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-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1 h1:TEBmxO80TM04L8IuMWk77SGL1HomBmKTdzdJLLWznxI=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 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/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/bwesterb/go-ristretto v1.2.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g=
github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec=
github.com/cloudflare/circl v1.2.0/go.mod h1:Ch2UgYr6ti2KTtlejELlROl0YIYj7SLjAC8M+INXlMk=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 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/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
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/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog=
github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc=
github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= github.com/go-git/go-git/v5 v5.1.0 h1:HxJn9g/E7eYvKW3Fm7Jt4ee8LXfPOm/H1cdDu8vEssk=
github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJAozQrZuyM=
github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
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/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 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/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
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/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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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/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.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/microcosm-cc/bluemonday v1.0.20 h1:flpzsq4KU3QIYAYGV/szUat7H+GPOXR0B2JU5A1Wp8Y=
github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50=
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.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc=
github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A=
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.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 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/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/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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
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/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/seletskiy/tplutil v0.0.0-20200921103632-f880f6245597 h1:nZY1S2jo+VtDrUfjO9XYI137O41hhRkxZNV5Fb5ixCA=
github.com/seletskiy/tplutil v0.0.0-20200921103632-f880f6245597/go.mod h1:F8CBHSOjnzjx9EeXyWJTAzJyVxN+Y8JH2WjLMn4utiw=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 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.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
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/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xanzy/ssh-agent v0.3.2 h1:eKj4SX2Fe7mui28ZgnFW5fmTz1EIr7ugo5s6wDxdHBM=
github.com/xanzy/ssh-agent v0.3.2/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
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.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
github.com/yuin/goldmark v1.4.14 h1:jwww1XQfhJN7Zm+/a1ZA/3WUiEBEroYFNTiV3dKwM8U=
github.com/yuin/goldmark v1.4.14/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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-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-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-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
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.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
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/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220913153101-76c7481b5158 h1:XQphkCZeKYaMRSo28HqvvNYuLOoM5CIOOvTZfthvTgI=
golang.org/x/sys v0.0.0-20220913153101-76c7481b5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
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.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed h1:+qzWo37K31KxduIYaBeMqJ8MUOyTayOQKpH9aDPLMSY=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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.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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

113
main.go
View File

@ -1,4 +1,4 @@
// Copyright 2020 The Gitea Authors. All rights reserved. // Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // 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,12 +6,12 @@
package main // import "code.gitea.io/tea" package main // import "code.gitea.io/tea"
import ( import (
"fmt" "log"
"os" "os"
"runtime"
"strings" "strings"
"code.gitea.io/tea/cmd" "code.gitea.io/tea/cmd"
"code.gitea.io/tea/modules/setting"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -22,119 +22,40 @@ var Version = "development"
// Tags holds the build tags used // Tags holds the build tags used
var Tags = "" var Tags = ""
// SDK holds the sdk version from go.mod func init() {
var SDK = "" setting.AppVer = Version
setting.AppBuiltWith = formatBuiltWith(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 = appDescription app.Description = ``
app.CustomAppHelpTemplate = helpTemplate app.Version = Version + formatBuiltWith(Tags)
app.Version = formatVersion()
app.Commands = []*cli.Command{ app.Commands = []*cli.Command{
&cmd.CmdLogin, &cmd.CmdLogin,
&cmd.CmdLogout, &cmd.CmdLogout,
&cmd.CmdAutocomplete,
&cmd.CmdWhoami,
&cmd.CmdIssues, &cmd.CmdIssues,
&cmd.CmdPulls, &cmd.CmdPulls,
&cmd.CmdLabels,
&cmd.CmdMilestones,
&cmd.CmdReleases, &cmd.CmdReleases,
&cmd.CmdTrackedTimes,
&cmd.CmdOrgs,
&cmd.CmdRepos, &cmd.CmdRepos,
&cmd.CmdAddComment, &cmd.CmdLabels,
&cmd.CmdTrackedTimes,
&cmd.CmdOpen, &cmd.CmdOpen,
&cmd.CmdNotifications, &cmd.CmdNotifications,
&cmd.CmdRepoClone,
&cmd.CmdAdmin,
} }
app.EnableBashCompletion = true app.EnableBashCompletion = true
err := app.Run(os.Args) err := app.Run(os.Args)
if err != nil { if err != nil {
// app.Run already exits for errors implementing ErrorCoder, log.Fatalf("Failed to run app with %s: %v", os.Args, err)
// so we only handle generic errors with code 1 here.
fmt.Fprintf(app.ErrWriter, "Error: %v\n", err)
os.Exit(1)
} }
} }
func formatVersion() string { func formatBuiltWith(Tags string) string {
version := fmt.Sprintf("Version: %s\tgolang: %s", if len(Tags) == 0 {
bold(Version), return ""
strings.ReplaceAll(runtime.Version(), "go", ""))
if len(Tags) != 0 {
version += fmt.Sprintf("\tbuilt with: %s", strings.Replace(Tags, " ", ", ", -1))
} }
if len(SDK) != 0 { return " built with: " + strings.Replace(Tags, " ", ", ", -1)
version += fmt.Sprintf("\tgo-sdk: %s", SDK)
}
return version
}
var appDescription = `tea is a productivity helper for Gitea. It can be used to manage most entities on
one or multiple Gitea instances & provides local helpers like 'tea pr checkout'.
tea tries to make use of context provided by the repository in $PWD if available.
tea works best in a upstream/fork workflow, when the local main branch tracks the
upstream repo. tea assumes that local git state is published on the remote before
doing operations with tea. 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 --mine -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,109 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package config
import (
"fmt"
"io/ioutil"
"log"
"path/filepath"
"sync"
"code.gitea.io/tea/modules/utils"
"github.com/adrg/xdg"
"gopkg.in/yaml.v2"
)
// FlagDefaults defines all flags that can be overridden with a default value
// via the config file
type FlagDefaults struct {
// Prefer a specific git remote to use for selecting a repository on gitea,
// instead of relying on the remote associated with main/master/trunk branch.
// The --remote flag still has precedence over this value.
Remote string `yaml:"remote"`
}
// Preferences that are stored in and read from the config file
type Preferences struct {
// Prefer using an external text editor over inline multiline prompts
Editor bool `yaml:"editor"`
FlagDefaults FlagDefaults `yaml:"flag_defaults"`
}
// LocalConfig represents local configurations
type LocalConfig struct {
Logins []Login `yaml:"logins"`
Prefs Preferences `yaml:"preferences"`
}
var (
// config contain if loaded local tea config
config LocalConfig
loadConfigOnce sync.Once
)
// GetConfigPath return path to tea config file
func GetConfigPath() string {
configFilePath, err := xdg.ConfigFile("tea/config.yml")
var exists bool
if err != nil {
exists = false
} else {
exists, _ = utils.PathExists(configFilePath)
}
// fallback to old config if no new one exists
if !exists {
file := filepath.Join(xdg.Home, ".tea", "tea.yml")
exists, _ = utils.PathExists(file)
if exists {
return file
}
}
if err != nil {
log.Fatal("unable to get or create config file")
}
return configFilePath
}
// GetPreferences returns preferences based on the config file
func GetPreferences() Preferences {
loadConfig()
return config.Prefs
}
// loadConfig load config from file
func loadConfig() (err error) {
loadConfigOnce.Do(func() {
ymlPath := GetConfigPath()
exist, _ := utils.FileExist(ymlPath)
if exist {
bs, err := ioutil.ReadFile(ymlPath)
if err != nil {
err = fmt.Errorf("Failed to read config file: %s", ymlPath)
}
err = yaml.Unmarshal(bs, &config)
if err != nil {
err = fmt.Errorf("Failed to parse contents of config file: %s", ymlPath)
}
}
})
return
}
// saveConfig save config to file
func saveConfig() error {
ymlPath := GetConfigPath()
bs, err := yaml.Marshal(config)
if err != nil {
return err
}
return ioutil.WriteFile(ymlPath, bs, 0660)
}

View File

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

View File

@ -1,226 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package context
import (
"errors"
"fmt"
"log"
"os"
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/utils"
gogit "github.com/go-git/go-git/v5"
"github.com/urfave/cli/v2"
)
var (
errNotAGiteaRepo = errors.New("No Gitea login found. You might want to specify --repo (and --login) to work outside of a repository")
)
// TeaContext contains all context derived during command initialization and wraps cli.Context
type TeaContext struct {
*cli.Context
Login *config.Login // config data & client for selected login
RepoSlug string // <owner>/<repo>, optional
Owner string // repo owner as derived from context or provided in flag, optional
Repo string // repo name as derived from context or provided in flag, optional
Output string // value of output flag
LocalRepo *git.TeaRepo // is set if flags specified a local repo via --repo, or if $PWD is a git repo
}
// GetListOptions return ListOptions based on PaginationFlags
func (ctx *TeaContext) GetListOptions() gitea.ListOptions {
page := ctx.Int("page")
limit := ctx.Int("limit")
if limit < 0 {
limit = 0
}
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(remoteFlag) == 0 {
remoteFlag = config.GetPreferences().FlagDefaults.Remote
}
// try to read local git repo & extract context: if repoFlag specifies a valid path, read repo in that dir,
// otherwise attempt PWD. if no repo is found, continue with default login
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 {
if err.Error() == "No available login" {
// TODO: maybe we can directly start interact.CreateLogin() (only if
// we're sure we can interactively!), as gh cli does.
fmt.Println(`No gitea login configured. To start using tea, first run
tea login add
and then run your command again.`)
}
os.Exit(1)
}
fmt.Printf("NOTE: no gitea login detected, falling back to login '%s'\n", c.Login.Name)
}
// 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 len(gitConfig.Remotes) == 0 {
return repo, nil, "", errNotAGiteaRepo
}
// When no preferred value is given, choose a remote to find a
// matching login based on its URL.
if len(gitConfig.Remotes) > 1 && len(remoteValue) == 0 {
// if master branch is present, use it as the default remote
mainBranches := []string{"main", "master", "trunk"}
for _, b := range mainBranches {
masterBranch, ok := gitConfig.Branches[b]
if ok {
if len(masterBranch.Remote) > 0 {
remoteValue = masterBranch.Remote
}
break
}
}
// if no branch has matched, default to origin or upstream remote.
if len(remoteValue) == 0 {
if _, ok := gitConfig.Remotes["upstream"]; ok {
remoteValue = "upstream"
} else if _, ok := gitConfig.Remotes["origin"]; ok {
remoteValue = "origin"
}
}
}
// make sure a remote is selected
if len(remoteValue) == 0 {
for remote := range gitConfig.Remotes {
remoteValue = remote
break
}
}
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 {
sshHost := l.GetSSHHost()
for _, u := range remoteConfig.URLs {
p, err := git.ParseURL(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 sshHost == p.Host {
return repo, &l, strings.TrimLeft(p.Path, "/"), nil
}
}
}
}
return repo, nil, "", errNotAGiteaRepo
}

View File

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

View File

@ -38,36 +38,42 @@ 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(ref git_plumbing.ReferenceName) error { func (r TeaRepo) TeaCheckout(branchName string) error {
tree, err := r.Worktree() tree, err := r.Worktree()
if err != nil { if err != nil {
return err return err
} }
return tree.Checkout(&git.CheckoutOptions{Branch: ref}) localBranchRefName := git_plumbing.NewBranchReferenceName(branchName)
return tree.Checkout(&git.CheckoutOptions{Branch: localBranchRefName})
} }
// TeaDeleteLocalBranch removes the given branch locally // TeaDeleteBranch removes the given branch locally, and if `remoteBranch` is
func (r TeaRepo) TeaDeleteLocalBranch(branch *git_config.Branch) error { // not empty deletes it at it's remote repo.
func (r TeaRepo) TeaDeleteBranch(branch *git_config.Branch, remoteBranch string, auth git_transport.AuthMethod) error {
err := r.DeleteBranch(branch.Name) 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
} }
return r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name)) err = r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name))
} if err != nil {
return err
}
// TeaDeleteRemoteBranch removes the given branch on the given remote via git protocol if remoteBranch != "" {
func (r TeaRepo) TeaDeleteRemoteBranch(remoteName, remoteBranch string, auth git_transport.AuthMethod) error { // delete remote branch via git protocol:
// delete remote branch via git protocol: // an empty source in the refspec means remote deletion to git 🙃
// an empty source in the refspec means remote deletion to git 🙃 refspec := fmt.Sprintf(":%s", git_plumbing.NewBranchReferenceName(remoteBranch))
refspec := fmt.Sprintf(":%s", git_plumbing.NewBranchReferenceName(remoteBranch)) err = r.Push(&git.PushOptions{
return r.Push(&git.PushOptions{ RemoteName: branch.Remote,
RemoteName: remoteName, RefSpecs: []git_config.RefSpec{git_config.RefSpec(refspec)},
RefSpecs: []git_config.RefSpec{git_config.RefSpec(refspec)}, Prune: true,
Prune: true, Auth: auth,
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
@ -170,48 +176,6 @@ func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *git_config.
return b, b.Validate() return b, b.Validate()
} }
// TeaFindBranchRemote gives the first remote that has a branch with the same name or sha,
// depending on what is passed in.
// This function is needed, as git does not always define branches in .git/config with remote entries.
func (r TeaRepo) TeaFindBranchRemote(branchName, hash string) (*git.Remote, error) {
remotes, err := r.Remotes()
if err != nil {
return nil, err
}
switch {
case len(remotes) == 0:
return nil, nil
case len(remotes) == 1:
return remotes[0], nil
}
// check if the given remote has our branch (.git/refs/remotes/<remoteName>/*)
iter, err := r.References()
if err != nil {
return nil, err
}
defer iter.Close()
var match *git.Remote
err = iter.ForEach(func(ref *git_plumbing.Reference) error {
if ref.Name().IsRemote() {
names := strings.SplitN(ref.Name().Short(), "/", 2)
remote := names[0]
branch := names[1]
hashMatch := hash != "" && hash == ref.Hash().String()
nameMatch := branchName != "" && branchName == branch
if hashMatch || nameMatch {
match, err = r.Remote(remote)
return err
}
}
return nil
})
return match, err
}
// TeaGetCurrentBranchName return the name of the branch witch is currently active // TeaGetCurrentBranchName return the name of the branch witch is currently active
func (r TeaRepo) TeaGetCurrentBranchName() (string, error) { func (r TeaRepo) TeaGetCurrentBranchName() (string, error) {
localHead, err := r.Head() localHead, err := r.Head()
@ -223,5 +187,5 @@ func (r TeaRepo) TeaGetCurrentBranchName() (string, error) {
return "", fmt.Errorf("active ref is no branch") return "", fmt.Errorf("active ref is no branch")
} }
return localHead.Name().Short(), nil return strings.TrimLeft(localHead.Name().String(), "refs/heads/"), nil
} }

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