commit 9d5cda4bfec2e6e0c09e802cf3bf7dd3de69f504 Author: Lunny Xiao Date: Mon Sep 3 14:43:00 2018 +0800 init project diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..13f1ce4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,271 @@ +# Contribution Guidelines + +## Table of Contents + +- [Contribution Guidelines](#contribution-guidelines) + - [Introduction](#introduction) + - [Bug reports](#bug-reports) + - [Discuss your design](#discuss-your-design) + - [Testing redux](#testing-redux) + - [Vendoring](#vendoring) + - [Translation](#translation) + - [Code review](#code-review) + - [Styleguide](#styleguide) + - [Sign-off your work](#sign-off-your-work) + - [Release Cycle](#release-cycle) + - [Maintainers](#maintainers) + - [Owners](#owners) + - [Versions](#versions) + - [Copyright](#copyright) + +## Introduction + +This document explains how to contribute changes to the Gitea project. +It assumes you have followed the +[installation instructions](https://docs.gitea.io/en-us/). +Sensitive security-related issues should be reported to +[security@gitea.io](mailto:security@gitea.io). + +For configuring IDE or code editor to develop Gitea see [IDE and code editor configuration](contrib/ide/) + +## Bug reports + +Please search the issues on the issue tracker with a variety of keywords +to ensure your bug is not already reported. + +If unique, [open an issue](https://github.com/go-gitea/gitea/issues/new) +and answer the questions so we can understand and reproduce the +problematic behavior. + +To show us that the issue you are having is in Gitea itself, please +write clear, concise instructions so we can reproduce the behavior— +even if it seems obvious. The more detailed and specific you are, +the faster we can fix the issue. Check out [How to Report Bugs +Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html). + +Please be kind, remember that Gitea comes at no cost to you, and you're +getting free help. + +## Discuss your design + +The project welcomes submissions. If you want to change or add something, +please let everyone know what you're working on—[file an issue](https://github.com/go-gitea/gitea/issues/new)! +Significant changes must go through the change proposal process +before they can be accepted. To create a proposal, file an issue with +your proposed changes documented, and make sure to note in the title +of the issue that it is a proposal. + +This process gives everyone a chance to validate the design, helps +prevent duplication of effort, and ensures that the idea fits inside +the goals for the project and tools. It also checks that the design is +sound before code is written; the code review tool is not the place for +high-level discussions. + +## Testing redux + +Before sending code out for review, run all the tests for the +whole tree to make sure the changes don't break other usage +and keep the compatibility on upgrade. To make sure you are +running the test suite exactly like we do, you should install +the CLI for [Drone CI](https://github.com/drone/drone), as +we are using the server for continous testing, following [these +instructions](http://docs.drone.io/cli-installation/). After that, +you can simply call `drone exec --local --build-event "pull_request"` within +your working directory and it will try to run the test suite locally. + +## Vendoring + +We keep a cached copy of dependencies within the `vendor/` directory, +managing updates via [dep](https://github.com/golang/dep). + +Pull requests should only include `vendor/` updates if they are part of +the same change, be it a bugfix or a feature addition. + +The `vendor/` update needs to be justified as part of the PR description, +and must be verified by the reviewers and/or merger to always reference +an existing upstream commit. + +You can find more information on how to get started with it on the [dep project website](https://golang.github.io/dep/docs/introduction.html). + +## Translation + +We do all translation work inside [Crowdin](https://crowdin.com/project/gitea). +The only translation that is maintained in this git repository is +[`en_US.ini`](https://github.com/go-gitea/gitea/blob/master/options/locale/locale_en-US.ini) +and is synced regularily to Crowdin. Once a translation has reached +A SATISFACTORY PERCENTAGE it will be synced back into this repo and +included in the next released version. + +## Building Gitea + +Generally, the go build tools are installed as-needed in the `Makefile`. +An exception are the tools to build the CSS and images. + +- To build CSS: Install [Node.js](https://nodejs.org/en/download/package-manager) + with `npm` and then run `npm install` and `make generate-stylesheets`. +- To build Images: ImageMagick, inkscape and zopflipng binaries must be + available in your `PATH` to run `make generate-images`. + +## Code review + +Changes to Gitea must be reviewed before they are accepted—no matter who +makes the change, even if they are an owner or a maintainer. We use GitHub's +pull request workflow to do that. And, we also use [LGTM](http://lgtm.co) +to ensure every PR is reviewed by at least 2 maintainers. + +Please try to make your pull request easy to review for us. And, please read +the *[How to get faster PR reviews](https://github.com/kubernetes/community/blob/261cb0fd089b64002c91e8eddceebf032462ccd6/contributors/guide/pull-requests.md#best-practices-for-faster-reviews)* guide; +it has lots of useful tips for any project you may want to contribute. +Some of the key points: + +* Make small pull requests. The smaller, the faster to review and the + more likely it will be merged soon. +* Don't make changes unrelated to your PR. Maybe there are typos on + some comments, maybe refactoring would be welcome on a function... but + if that is not related to your PR, please make *another* PR for that. +* Split big pull requests into multiple small ones. An incremental change + will be faster to review than a huge PR. + +## Styleguide + +For imports you should use the following format (_without_ the comments) +```go +import ( + // stdlib + "encoding/json" + "fmt" + + // local packages + "code.gitea.io/gitea/models" + "code.gitea.io/sdk/gitea" + + // external packages + "github.com/foo/bar" + "gopkg.io/baz.v1" +) +``` + +## Sign-off your work + +The sign-off is a simple line at the end of the explanation for the +patch. Your signature certifies that you wrote the patch or otherwise +have the right to pass it on as an open-source patch. The rules are +pretty simple: If you can certify [DCO](DCO), then you just add a line +to every git commit message: + +``` +Signed-off-by: Joe Smith +``` + +Please use your real name; we really dislike pseudonyms or anonymous +contributions. We are in the open-source world without secrets. If you +set your `user.name` and `user.email` git configs, you can sign-off your +commit automatically with `git commit -s`. + +## 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 + +To make sure every PR is checked, we have [team +maintainers](MAINTAINERS). Every PR **MUST** be reviewed by at least +two maintainers (or owners) before it can get merged. A maintainer +should be a contributor of Gitea (or Gogs) and contributed at least +4 accepted PRs. A contributor should apply as a maintainer in the +[Discord](https://discord.gg/NsatcWJ) #develop channel. The owners +or the team maintainers may invite the contributor. A maintainer +should spend some time on code reviews. If a maintainer has no +time to do that, they should apply to leave the maintainers team +and we will give them the honor of being a member of the [advisors +team](https://github.com/orgs/go-gitea/teams/advisors). Of course, if +an advisor has time to code review, we will gladly welcome them back +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 +him or her from the maintainers team to the advisors team. +For security reasons, Maintainers should use 2FA for their accounts and +if possible provide gpg signed commits. +https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/ +https://help.github.com/articles/signing-commits-with-gpg/ + +## Owners + +Since Gitea is a pure community organization without any company support, +to keep the development healthy we will elect three owners every year. All +contributors may vote to elect up to three candidates, one of which will +be the main owner, and the other two the assistant owners. When the new +owners have been elected, the old owners will give up ownership to the +newly elected owners. If an owner is unable to do so, the other owners +will assist in ceding ownership to the newly elected owners. +For security reasons, Owners or any account with write access (like a bot) +must use 2FA. +https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/ + +After the election, the new owners should proactively agree +with our [CONTRIBUTING](CONTRIBUTING.md) requirements in the +[Discord](https://discord.gg/NsatcWJ) #general channel. Below are the +words to speak: + +``` +I'm honored to having been elected an owner of Gitea, I agree with +[CONTRIBUTING](CONTRIBUTING.md). I will spend part of my time on 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) + * [Thomas Boerger](https://github.com/tboerger) + * [Kim Carlbäcker](https://github.com/bkcsoft) + +* 2018-01-01 ~ 2018-12-31 + * [Lunny Xiao](https://github.com/lunny) + * [Lauris Bukšis-Haberkorns](https://github.com/lafriks) + * [Kim Carlbäcker](https://github.com/bkcsoft) + +## Versions + +Gitea has the `master` branch as a tip branch and has version branches +such as `release/v0.9`. `release/v0.9` is a release branch and we will +tag `v0.9.0` for binary download. If `v0.9.0` has bugs, we will accept +pull requests on the `release/v0.9` branch and publish a `v0.9.1` tag, +after bringing the bug fix also to the master branch. + +Since the `master` branch is a tip version, if you wish to use Gitea +in production, please download the latest release tag version. All the +branches will be protected via GitHub, all the PRs to every branch must +be reviewed by two maintainers and must pass the automatic tests. + +## Copyright + +Code that you contribute should use the standard copyright header: + +``` +// 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. +``` + +Files in the repository contain copyright from the year they are added +to the year they are last changed. If the copyright author is changed, +just paste the header below the old one. diff --git a/DCO b/DCO new file mode 100644 index 0000000..3aca339 --- /dev/null +++ b/DCO @@ -0,0 +1,36 @@ +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +660 York Street, Suite 102, +San Francisco, CA 94110 USA + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a8d4b49 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016 The Gitea Authors +Copyright (c) 2015 The Gogs Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad228bc --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# Gitea Command Line Tool for Go + +This project acts as a command line tool for operating one or multiple Gitea instances. It depends on [code.gitea.io/sdk](https://code.gitea.io/sdk) client SDK implementation written in Go to interact with +the Gitea API implementation. + +## Installation + +``` +go get github.com/go-gitea/tea +go install github.com/go-gitea/tea +``` + +## Usage + +First of all, you have to create a token on your personal settings -> application. + +``` +git clone git@try.gitea.io:gitea/gitea.git +cd gitea +tea login add --name=try --url=https://try.gitea.io --token=xxxxxx +tea issues +``` + +## Contributing + +Fork -> Patch -> Push -> Pull Request + +## Authors + +* [Maintainers](https://github.com/orgs/go-gitea/people) +* [Contributors](https://github.com/go-gitea/tea/graphs/contributors) + +## License + +This project is under the MIT License. See the [LICENSE](LICENSE) file for the +full license text. diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..78cd625 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,212 @@ +// 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/git" + "code.gitea.io/sdk/gitea" + local_git "code.gitea.io/tea/modules/git" + "code.gitea.io/tea/modules/utils" + + "github.com/go-gitea/yaml" +) + +type Login struct { + Name string `yaml:"name"` + URL string `yaml:"url"` + Token string `yaml:"token"` + Active bool `yaml:"active"` + SSHHost string `yaml:"ssh_host"` + Insecure bool `yaml:"insecure"` +} + +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 +} + +func (l *Login) GetSSHHost() string { + if l.SSHHost != "" { + return l.SSHHost + } + + u, err := url.Parse(l.URL) + if err != nil { + return "" + } + + return u.Hostname() +} + +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("the same name directory exist") + } + return true, nil +} + +func loadConfig(ymlPath string) error { + exist, _ := isFileExist(ymlPath) + if exist { + Println("Found config file", ymlPath) + bs, err := ioutil.ReadFile(ymlPath) + if err != nil { + return err + } + + err = yaml.Unmarshal(bs, &config) + if err != nil { + return err + } + } + + return nil +} + +func saveConfig(ymlPath string) error { + bs, err := yaml.Marshal(&config) + if err != nil { + return err + } + return ioutil.WriteFile(ymlPath, bs, 0660) +} + +func curGitRepoPath() (*Login, string, error) { + cmd := git.NewCommand("remote", "get-url", "origin") + u, err := cmd.RunInDir(filepath.Dir(os.Args[0])) + if err != nil || len(u) == 0 { + return nil, "", errors.New("You have to indicated a repo or execute the command in a repo") + } + + p, err := local_git.ParseURL(strings.TrimSpace(u)) + if err != nil { + return nil, "", fmt.Errorf("Git remote URL parse failed: %s", err.Error()) + } + + for _, l := range config.Logins { + if p.Scheme == "http" || 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 p.Scheme == "ssh" { + if l.GetSSHHost() == p.Host { + return &l, strings.TrimLeft(strings.TrimSuffix(p.Path, ".git"), "/"), nil + } + } + } + + return nil, "", errors.New("No Gitea login found") +} diff --git a/cmd/issues.go b/cmd/issues.go new file mode 100644 index 0000000..ff6fdd2 --- /dev/null +++ b/cmd/issues.go @@ -0,0 +1,179 @@ +// 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" + "log" + "os" + "strconv" + "strings" + + "code.gitea.io/sdk/gitea" + + "github.com/urfave/cli" +) + +// CmdIssues represents to login a gitea server. +var CmdIssues = cli.Command{ + Name: "issues", + Usage: "Log in a Gitea server", + Description: `Log in a Gitea server`, + Action: runIssues, + Subcommands: []cli.Command{ + CmdIssuesList, + CmdIssuesCreate, + }, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "login, l", + Usage: "Indicate one login", + }, + cli.StringFlag{ + Name: "repo, r", + Usage: "Indicate one repository", + }, + }, +} + +var CmdIssuesList = cli.Command{ + Name: "ls", + Usage: "Log in a Gitea server", + Description: `Log in a Gitea server`, + Action: runIssuesList, +} + +func runIssues(ctx *cli.Context) error { + if len(os.Args) == 3 { + return runIssueDetail(ctx, os.Args[2]) + } + return runIssuesList(ctx) +} + +func runIssueDetail(ctx *cli.Context, index string) error { + login, owner, repo := initCommand(ctx) + + if strings.HasPrefix(index, "#") { + index = index[1:] + } + + idx, err := strconv.ParseInt(index, 10, 64) + if err != nil { + return err + } + + issue, err := login.Client().GetIssue(owner, repo, idx) + if err != nil { + return err + } + + fmt.Printf("#%d %s\n%s created %s\n\n%s", issue.Index, + issue.Title, + issue.Poster.UserName, + issue.Created.Format("2006-01-02 15:04:05"), + issue.Body, + ) + return nil +} + +func runIssuesList(ctx *cli.Context) error { + login, owner, repo := initCommand(ctx) + + issues, err := login.Client().ListRepoIssues(owner, repo, gitea.ListIssueOption{ + Page: 0, + State: string(gitea.StateOpen), + }) + + if err != nil { + log.Fatal(err) + } + + if len(issues) == 0 { + fmt.Println("No issues left") + return nil + } + + for _, issue := range issues { + name := issue.Poster.FullName + if len(name) == 0 { + name = issue.Poster.UserName + } + fmt.Printf("#%d\t%s\t%s\t%s\n", issue.Index, name, issue.Updated.Format("2006-01-02 15:04:05"), issue.Title) + } + + return nil +} + +var CmdIssuesCreate = cli.Command{ + Name: "create", + Usage: "Create an issue on repository", + Description: `Create an issue on repository`, + Action: runIssuesCreate, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "title, t", + Usage: "issue title to create", + }, + cli.StringFlag{ + Name: "body, b", + Usage: "issue body to create", + }, + }, +} + +func initCommand(ctx *cli.Context) (*Login, string, string) { + err := loadConfig(yamlConfigPath) + if err != nil { + log.Fatal("load config file failed", yamlConfigPath) + } + + var login *Login + if ctx.IsSet("login") { + login = getLoginByName(ctx.String("login")) + if login == nil { + log.Fatal("indicated login name", ctx.String("login"), "is not exist") + } + } else { + login, err = getActiveLogin() + if err != nil { + log.Fatal("get active login failed") + } + } + + var repoPath string + if !ctx.IsSet("repo") { + login, repoPath, err = curGitRepoPath() + if err != nil { + log.Fatal(err.Error()) + } + } else { + repoPath = ctx.String("repo") + } + + owner, repo := splitRepo(repoPath) + return login, owner, repo +} + +func runIssuesCreate(ctx *cli.Context) error { + login, owner, repo := initCommand(ctx) + + _, 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 +} diff --git a/cmd/log.go b/cmd/log.go new file mode 100644 index 0000000..0e8325e --- /dev/null +++ b/cmd/log.go @@ -0,0 +1,35 @@ +// 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" + +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...) +} diff --git a/cmd/login.go b/cmd/login.go new file mode 100644 index 0000000..36d05f0 --- /dev/null +++ b/cmd/login.go @@ -0,0 +1,135 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package cmd + +import ( + "crypto/tls" + "fmt" + "log" + "net/http" + "net/http/cookiejar" + + "code.gitea.io/sdk/gitea" + + "github.com/urfave/cli" +) + +// CmdLogin represents to login a gitea server. +var CmdLogin = cli.Command{ + Name: "login", + Usage: "Log in a Gitea server", + Description: `Log in a Gitea server`, + Action: runLoginList, + Subcommands: []cli.Command{ + cmdLoginList, + cmdLoginAdd, + }, +} + +// CmdLogin represents to login a gitea server. +var cmdLoginAdd = cli.Command{ + Name: "add", + Usage: "Log in a Gitea server", + Description: `Log in a Gitea server`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "name, n", + Usage: "Name for the gitea login", + }, + cli.StringFlag{ + Name: "url, u", + Value: "https://try.gitea.io", + EnvVar: "GITEA_SERVER_URL", + Usage: "Gitea server URL", + }, + cli.StringFlag{ + Name: "token, t", + Value: "", + EnvVar: "GITEA_SERVER_TOKEN", + Usage: "token for operating the Gitea login", + }, + cli.BoolFlag{ + Name: "insecure, i", + Usage: "insecure visit gitea server", + }, + }, + Action: runLoginAdd, +} + +func runLoginAdd(ctx *cli.Context) error { + if !ctx.IsSet("url") { + log.Fatal("You have to input Gitea server URL") + } + + if !ctx.IsSet("token") { + log.Fatal("No token found") + } + + if !ctx.IsSet("name") { + log.Fatal("You have to set a name for the login") + } + + err := loadConfig(yamlConfigPath) + if err != nil { + log.Fatal("load config file failed", 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"), + }) + if err != nil { + log.Fatal(err) + } + + err = saveConfig(yamlConfigPath) + if err != nil { + log.Fatal(err) + } + + return nil +} + +// CmdLogin represents to login a gitea server. +var cmdLoginList = cli.Command{ + Name: "ls", + Usage: "Log in a Gitea server", + Description: `Log in a Gitea server`, + Action: runLoginList, +} + +func runLoginList(ctx *cli.Context) error { + err := loadConfig(yamlConfigPath) + if err != nil { + log.Fatal("load config file failed", yamlConfigPath) + } + + fmt.Printf("Name\tURL\tSSHHost\n") + for _, l := range config.Logins { + fmt.Printf("%s\t%s\t%s\n", l.Name, l.URL, l.GetSSHHost()) + } + + return nil +} diff --git a/cmd/logout.go b/cmd/logout.go new file mode 100644 index 0000000..9d5a573 --- /dev/null +++ b/cmd/logout.go @@ -0,0 +1,60 @@ +// 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 ( + "errors" + "log" + "os" + + "github.com/urfave/cli" +) + +// CmdLogout represents to logout a gitea server. +var CmdLogout = cli.Command{ + Name: "logout", + Usage: "Log out from a Gitea server", + Description: `Log out from a Gitea server`, + Action: runLogout, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "name, n", + Usage: "name wants to log out", + }, + }, +} + +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("need log out server name") + } + + err := loadConfig(yamlConfigPath) + if err != nil { + log.Fatal("load config file failed", 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("save config file failed", yamlConfigPath) + } + } + + return nil +} diff --git a/cmd/pulls.go b/cmd/pulls.go new file mode 100644 index 0000000..96ab542 --- /dev/null +++ b/cmd/pulls.go @@ -0,0 +1,60 @@ +// 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" + "log" + + "code.gitea.io/sdk/gitea" + + "github.com/urfave/cli" +) + +// CmdPulls represents to login a gitea server. +var CmdPulls = cli.Command{ + Name: "pulls", + Usage: "Log in a Gitea server", + Description: `Log in a Gitea server`, + Action: runPulls, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "login, l", + Usage: "Indicate one login", + }, + cli.StringFlag{ + Name: "repo, r", + Usage: "Indicate one repository", + }, + }, +} + +func runPulls(ctx *cli.Context) error { + login, owner, repo := initCommand(ctx) + + prs, err := login.Client().ListRepoPullRequests(owner, repo, gitea.ListPullRequestsOptions{ + Page: 0, + State: string(gitea.StateOpen), + }) + + if err != nil { + log.Fatal(err) + } + + if len(prs) == 0 { + fmt.Println("No pull requests left") + return nil + } + + for _, pr := range prs { + name := pr.Poster.FullName + if len(name) == 0 { + name = pr.Poster.UserName + } + fmt.Printf("#%d\t%s\t%s\t%s\n", pr.Index, name, pr.Updated.Format("2006-01-02 15:04:05"), pr.Title) + } + + return nil +} diff --git a/cmd/releases.go b/cmd/releases.go new file mode 100644 index 0000000..5734bff --- /dev/null +++ b/cmd/releases.go @@ -0,0 +1,53 @@ +// 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" + "log" + + "github.com/urfave/cli" +) + +// CmdReleases represents to login a gitea server. +var CmdReleases = cli.Command{ + Name: "releases", + Usage: "Log in a Gitea server", + Description: `Log in a Gitea server`, + Action: runReleases, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "login, l", + Usage: "Indicate one login", + }, + cli.StringFlag{ + Name: "repo, r", + Usage: "Indicate one repository", + }, + }, +} + +func runReleases(ctx *cli.Context) error { + login, owner, repo := initCommand(ctx) + + releases, err := login.Client().ListReleases(owner, repo) + if err != nil { + log.Fatal(err) + } + + if len(releases) == 0 { + fmt.Println("No Releases") + return nil + } + + for _, release := range releases { + fmt.Printf("#%s\t%s\t%s\t%s\n", release.TagName, + release.Title, + release.PublishedAt.Format("2006-01-02 15:04:05"), + release.TarURL) + } + + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..e8afc23 --- /dev/null +++ b/main.go @@ -0,0 +1,55 @@ +// 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. + +// Tea is command line tool for Gitea. +package main // import "code.gitea.io/tea" + +import ( + "log" + "os" + "strings" + + "code.gitea.io/tea/cmd" + "code.gitea.io/tea/modules/setting" + + "github.com/urfave/cli" +) + +// Version holds the current Gitea version +var Version = "0.1.0-dev" + +// Tags holds the build tags used +var Tags = "" + +func init() { + setting.AppVer = Version + setting.AppBuiltWith = formatBuiltWith(Tags) +} + +func main() { + app := cli.NewApp() + app.Name = "Tea" + app.Usage = "Command line tool to interactive with Gitea" + app.Description = `` + app.Version = Version + formatBuiltWith(Tags) + app.Commands = []cli.Command{ + cmd.CmdLogin, + cmd.CmdLogout, + cmd.CmdIssues, + cmd.CmdPulls, + cmd.CmdReleases, + } + err := app.Run(os.Args) + if err != nil { + log.Fatal(4, "Failed to run app with %s: %v", os.Args, err) + } +} + +func formatBuiltWith(Tags string) string { + if len(Tags) == 0 { + return "" + } + + return " built with: " + strings.Replace(Tags, " ", ", ", -1) +} diff --git a/modules/git/url.go b/modules/git/url.go new file mode 100644 index 0000000..82dae0e --- /dev/null +++ b/modules/git/url.go @@ -0,0 +1,43 @@ +package git + +import ( + "net/url" + "regexp" + "strings" +) + +var ( + protocolRe = regexp.MustCompile("^[a-zA-Z_+-]+://") +) + +type URLParser struct { +} + +func (p *URLParser) Parse(rawURL string) (u *url.URL, err error) { + if !protocolRe.MatchString(rawURL) && + strings.Contains(rawURL, ":") && + // not a Windows path + !strings.Contains(rawURL, "\\") { + rawURL = "ssh://" + strings.Replace(rawURL, ":", "/", 1) + } + + u, err = url.Parse(rawURL) + if err != nil { + return + } + + if u.Scheme == "git+ssh" { + u.Scheme = "ssh" + } + + if strings.HasPrefix(u.Path, "//") { + u.Path = strings.TrimPrefix(u.Path, "/") + } + + return +} + +func ParseURL(rawURL string) (u *url.URL, err error) { + p := &URLParser{} + return p.Parse(rawURL) +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go new file mode 100644 index 0000000..b07adb5 --- /dev/null +++ b/modules/setting/setting.go @@ -0,0 +1,10 @@ +// 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 setting + +var ( + AppVer string + AppBuiltWith string +) diff --git a/modules/utils/home.go b/modules/utils/home.go new file mode 100644 index 0000000..47361c9 --- /dev/null +++ b/modules/utils/home.go @@ -0,0 +1,95 @@ +// 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 utils + +import ( + "bytes" + "errors" + "os" + "os/exec" + "os/user" + "runtime" + "strconv" + "strings" +) + +// Home returns the home directory for the executing user. +// +// This uses an OS-specific method for discovering the home directory. +// An error is returned if a home directory cannot be detected. +func Home() (string, error) { + user, err := user.Current() + if nil == err { + return user.HomeDir, nil + } + + // cross compile support + if "windows" == runtime.GOOS { + return homeWindows() + } + + // Unix-like system, so just assume Unix + return homeUnix() +} + +func homeUnix() (string, error) { + // First prefer the HOME environmental variable + if home := os.Getenv("HOME"); home != "" { + return home, nil + } + + // If that fails, try getent + var stdout bytes.Buffer + cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid())) + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + // If the error is ErrNotFound, we ignore it. Otherwise, return it. + if err != exec.ErrNotFound { + return "", err + } + } else { + if passwd := strings.TrimSpace(stdout.String()); passwd != "" { + // username:password:uid:gid:gecos:home:shell + passwdParts := strings.SplitN(passwd, ":", 7) + if len(passwdParts) > 5 { + return passwdParts[5], nil + } + } + } + + // If all else fails, try the shell + stdout.Reset() + cmd = exec.Command("sh", "-c", "cd && pwd") + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return "", err + } + + result := strings.TrimSpace(stdout.String()) + if result == "" { + return "", errors.New("blank output when reading home directory") + } + + return result, nil +} + +func homeWindows() (string, error) { + // First prefer the HOME environmental variable + if home := os.Getenv("HOME"); home != "" { + return home, nil + } + + drive := os.Getenv("HOMEDRIVE") + path := os.Getenv("HOMEPATH") + home := drive + path + if drive == "" || path == "" { + home = os.Getenv("USERPROFILE") + } + if home == "" { + return "", errors.New("HOMEDRIVE, HOMEPATH, and USERPROFILE are blank") + } + + return home, nil +}