diff --git a/cmd/flags/flags.go b/cmd/flags/flags.go index aba9dad..d896a74 100644 --- a/cmd/flags/flags.go +++ b/cmd/flags/flags.go @@ -34,7 +34,7 @@ var LoginFlag = cli.StringFlag{ var RepoFlag = cli.StringFlag{ Name: "repo", Aliases: []string{"r"}, - Usage: "Repository to interact with. Optional", + Usage: "Override local repository path or gitea repository slug to interact with. Optional", Destination: &GlobalRepoValue, } diff --git a/cmd/notifications.go b/cmd/notifications.go index 348cad2..8ff2736 100644 --- a/cmd/notifications.go +++ b/cmd/notifications.go @@ -47,6 +47,8 @@ func runNotifications(ctx *cli.Context) error { var news []*gitea.NotificationThread var err error + login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) + listOpts := flags.GetListOptions(ctx) if listOpts.Page == 0 { listOpts.Page = 1 @@ -61,13 +63,11 @@ func runNotifications(ctx *cli.Context) error { } if ctx.Bool("all") { - login := config.InitCommandLoginOnly(flags.GlobalLoginValue) news, _, err = login.Client().ListNotifications(gitea.ListNotificationOptions{ ListOptions: listOpts, Status: status, }) } else { - login, owner, repo := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) news, _, err = login.Client().ListRepoNotifications(owner, repo, gitea.ListNotificationOptions{ ListOptions: listOpts, Status: status, diff --git a/cmd/organizations/delete.go b/cmd/organizations/delete.go index 998298e..3cdf82d 100644 --- a/cmd/organizations/delete.go +++ b/cmd/organizations/delete.go @@ -24,8 +24,7 @@ var CmdOrganizationDelete = cli.Command{ // RunOrganizationDelete delete user organization func RunOrganizationDelete(ctx *cli.Context) error { - //TODO: Reconsider the usage InitCommandLoginOnly related to #200 - login := config.InitCommandLoginOnly(flags.GlobalLoginValue) + login, _, _ := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) client := login.Client() diff --git a/cmd/organizations/list.go b/cmd/organizations/list.go index ddab497..d7c12f4 100644 --- a/cmd/organizations/list.go +++ b/cmd/organizations/list.go @@ -30,8 +30,7 @@ var CmdOrganizationList = cli.Command{ // RunOrganizationList list user organizations func RunOrganizationList(ctx *cli.Context) error { - //TODO: Reconsider the usage InitCommandLoginOnly related to #200 - login := config.InitCommandLoginOnly(flags.GlobalLoginValue) + login, _, _ := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) client := login.Client() diff --git a/cmd/repos.go b/cmd/repos.go index 8153883..2558b82 100644 --- a/cmd/repos.go +++ b/cmd/repos.go @@ -39,9 +39,9 @@ func runRepos(ctx *cli.Context) error { } func runRepoDetail(path string) error { - login := config.InitCommandLoginOnly(flags.GlobalLoginValue) + login, ownerFallback, _ := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) client := login.Client() - repoOwner, repoName := utils.GetOwnerAndRepo(path, login.User) + repoOwner, repoName := utils.GetOwnerAndRepo(path, ownerFallback) repo, _, err := client.GetRepo(repoOwner, repoName) if err != nil { return err diff --git a/cmd/repos/create.go b/cmd/repos/create.go index 1a21a3c..f146f79 100644 --- a/cmd/repos/create.go +++ b/cmd/repos/create.go @@ -83,7 +83,7 @@ var CmdRepoCreate = cli.Command{ } func runRepoCreate(ctx *cli.Context) error { - login := config.InitCommandLoginOnly(flags.GlobalLoginValue) + login, _, _ := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) client := login.Client() var ( repo *gitea.Repository diff --git a/cmd/repos/list.go b/cmd/repos/list.go index 22b7e21..2a251bc 100644 --- a/cmd/repos/list.go +++ b/cmd/repos/list.go @@ -45,7 +45,7 @@ var CmdReposList = cli.Command{ // RunReposList list repositories func RunReposList(ctx *cli.Context) error { - login := config.InitCommandLoginOnly(flags.GlobalLoginValue) + login, _, _ := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) client := login.Client() typeFilter, err := getTypeFilter(ctx) diff --git a/cmd/repos/search.go b/cmd/repos/search.go index 13f7cd3..f9e3462 100644 --- a/cmd/repos/search.go +++ b/cmd/repos/search.go @@ -57,7 +57,7 @@ var CmdReposSearch = cli.Command{ } func runReposSearch(ctx *cli.Context) error { - login := config.InitCommandLoginOnly(flags.GlobalLoginValue) + login, _, _ := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue) client := login.Client() var ownerID int64 diff --git a/modules/config/command.go b/modules/config/command.go new file mode 100644 index 0000000..46a5ce8 --- /dev/null +++ b/modules/config/command.go @@ -0,0 +1,130 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package config + +import ( + "errors" + "fmt" + "log" + "strings" + + "code.gitea.io/tea/modules/git" + "code.gitea.io/tea/modules/utils" + + gogit "github.com/go-git/go-git/v5" +) + +// InitCommand resolves the application context, and returns the active login, and if +// available the repo slug. It does this by reading the config file for logins, parsing +// the remotes of the .git repo specified in repoFlag or $PWD, and using overrides from +// command flags. If a local git repo can't be found, repo slug values are unset. +func InitCommand(repoFlag, loginFlag, remoteFlag string) (login *Login, owner string, reponame string) { + err := LoadConfig() + if err != nil { + log.Fatal(err) + } + + var repoSlug string + var repoPath string // empty means PWD + var repoFlagPathExists bool + + // check if repoFlag can be interpreted as path to local repo. + if len(repoFlag) != 0 { + repoFlagPathExists, err = utils.PathExists(repoFlag) + if err != nil { + log.Fatal(err.Error()) + } + if repoFlagPathExists { + repoPath = repoFlag + } + } + + // try to read git repo & extract context, ignoring if PWD is not a repo + login, repoSlug, err = contextFromLocalRepo(repoPath, remoteFlag) + if err != nil && err != gogit.ErrRepositoryNotExists { + log.Fatal(err.Error()) + } + + // if repoFlag is not a path, use it to override repoSlug + if len(repoFlag) != 0 && !repoFlagPathExists { + repoSlug = repoFlag + } + + // override login from flag, or use default login if repo based detection failed + if len(loginFlag) != 0 { + login = GetLoginByName(loginFlag) + if login == nil { + log.Fatalf("Login name '%s' does not exist", loginFlag) + } + } else if login == nil { + if login, err = GetDefaultLogin(); err != nil { + log.Fatal(err.Error()) + } + } + + // parse reposlug (owner falling back to login owner if reposlug contains only repo name) + owner, reponame = utils.GetOwnerAndRepo(repoSlug, login.User) + return +} + +// discovers login & repo slug from the default branch remote of the given local repo +func contextFromLocalRepo(repoValue, remoteValue string) (*Login, string, error) { + repo, err := git.RepoFromPath(repoValue) + if err != nil { + return nil, "", err + } + gitConfig, err := repo.Config() + if err != nil { + return nil, "", err + } + + // if no remote + if len(gitConfig.Remotes) == 0 { + return nil, "", errors.New("No remote(s) found in this Git repository") + } + + // if only one remote exists + if len(gitConfig.Remotes) >= 1 && len(remoteValue) == 0 { + for remote := range gitConfig.Remotes { + remoteValue = remote + } + if len(gitConfig.Remotes) > 1 { + // if master branch is present, use it as the default remote + masterBranch, ok := gitConfig.Branches["master"] + if ok { + if len(masterBranch.Remote) > 0 { + remoteValue = masterBranch.Remote + } + } + } + } + + remoteConfig, ok := gitConfig.Remotes[remoteValue] + if !ok || remoteConfig == nil { + return nil, "", errors.New("Remote " + remoteValue + " not found in this Git repository") + } + + for _, l := range Config.Logins { + for _, u := range remoteConfig.URLs { + p, err := git.ParseURL(strings.TrimSpace(u)) + if err != nil { + return nil, "", fmt.Errorf("Git remote URL parse failed: %s", err.Error()) + } + if strings.EqualFold(p.Scheme, "http") || strings.EqualFold(p.Scheme, "https") { + if strings.HasPrefix(u, l.URL) { + ps := strings.Split(p.Path, "/") + path := strings.Join(ps[len(ps)-2:], "/") + return &l, strings.TrimSuffix(path, ".git"), nil + } + } else if strings.EqualFold(p.Scheme, "ssh") { + if l.GetSSHHost() == strings.Split(p.Host, ":")[0] { + return &l, strings.TrimLeft(strings.TrimSuffix(p.Path, ".git"), "/"), nil + } + } + } + } + + return nil, "", errors.New("No Gitea login found. You might want to specify --repo (and --login) to work outside of a repository") +} diff --git a/modules/config/config.go b/modules/config/config.go index 34913af..407ead6 100644 --- a/modules/config/config.go +++ b/modules/config/config.go @@ -5,14 +5,11 @@ package config import ( - "errors" "fmt" "io/ioutil" "log" "path/filepath" - "strings" - "code.gitea.io/tea/modules/git" "code.gitea.io/tea/modules/utils" "github.com/adrg/xdg" @@ -84,68 +81,3 @@ func SaveConfig() error { } return ioutil.WriteFile(ymlPath, bs, 0660) } - -func curGitRepoPath(repoValue, remoteValue string) (*Login, string, error) { - var err error - var repo *git.TeaRepo - if len(repoValue) == 0 { - repo, err = git.RepoForWorkdir() - } else { - repo, err = git.RepoFromPath(repoValue) - } - if err != nil { - return nil, "", err - } - gitConfig, err := repo.Config() - if err != nil { - return nil, "", err - } - - // if no remote - if len(gitConfig.Remotes) == 0 { - return nil, "", errors.New("No remote(s) found in this Git repository") - } - - // if only one remote exists - if len(gitConfig.Remotes) >= 1 && len(remoteValue) == 0 { - for remote := range gitConfig.Remotes { - remoteValue = remote - } - if len(gitConfig.Remotes) > 1 { - // if master branch is present, use it as the default remote - masterBranch, ok := gitConfig.Branches["master"] - if ok { - if len(masterBranch.Remote) > 0 { - remoteValue = masterBranch.Remote - } - } - } - } - - remoteConfig, ok := gitConfig.Remotes[remoteValue] - if !ok || remoteConfig == nil { - return nil, "", errors.New("Remote " + remoteValue + " not found in this Git repository") - } - - for _, l := range Config.Logins { - for _, u := range remoteConfig.URLs { - p, err := git.ParseURL(strings.TrimSpace(u)) - if err != nil { - return nil, "", fmt.Errorf("Git remote URL parse failed: %s", err.Error()) - } - if strings.EqualFold(p.Scheme, "http") || strings.EqualFold(p.Scheme, "https") { - if strings.HasPrefix(u, l.URL) { - ps := strings.Split(p.Path, "/") - path := strings.Join(ps[len(ps)-2:], "/") - return &l, strings.TrimSuffix(path, ".git"), nil - } - } else if strings.EqualFold(p.Scheme, "ssh") { - if l.GetSSHHost() == strings.Split(p.Host, ":")[0] { - return &l, strings.TrimLeft(strings.TrimSuffix(p.Path, ".git"), "/"), nil - } - } - } - } - - return nil, "", errors.New("No Gitea login found. You might want to specify --repo (and --login) to work outside of a repository") -} diff --git a/modules/config/login.go b/modules/config/login.go index e4488a9..c347533 100644 --- a/modules/config/login.go +++ b/modules/config/login.go @@ -12,9 +12,6 @@ import ( "net/http" "net/http/cookiejar" "net/url" - "os" - "strings" - "time" "code.gitea.io/tea/modules/utils" @@ -37,6 +34,69 @@ type Login struct { Created int64 `yaml:"created"` } +// GetDefaultLogin return the default login +func GetDefaultLogin() (*Login, error) { + 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 +} + +// GetLoginByName get login by name +func GetLoginByName(name string) *Login { + for _, l := range Config.Logins { + if l.Name == name { + return &l + } + } + return nil +} + +// GenerateLoginName generates a name string based on instance URL & adds username if the result is not unique +func GenerateLoginName(url, user string) (string, error) { + parsedURL, err := utils.NormalizeURL(url) + if err != nil { + return "", err + } + name := parsedURL.Host + + // append user name if login name already exists + if len(user) != 0 { + for _, l := range Config.Logins { + if l.Name == name { + name += "_" + user + break + } + } + } + + return name, nil +} + +// DeleteLogin delete a login by name +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() +} + // Client returns a client to operate Gitea API func (l *Login) Client() *gitea.Client { httpClient := &http.Client{} @@ -73,235 +133,3 @@ func (l *Login) GetSSHHost() string { return u.Hostname() } - -// GenerateToken creates a new token when given BasicAuth credentials -func (l *Login) GenerateToken(user, pass string) (string, error) { - client := l.Client() - gitea.SetBasicAuth(user, pass)(client) - - host, _ := os.Hostname() - tl, _, err := client.ListAccessTokens(gitea.ListAccessTokensOptions{}) - if err != nil { - return "", err - } - tokenName := host + "-tea" - - for i := range tl { - if tl[i].Name == tokenName { - tokenName += time.Now().Format("2006-01-02_15-04-05") - break - } - } - - t, _, err := client.CreateAccessToken(gitea.CreateAccessTokenOption{Name: tokenName}) - return t.Token, err -} - -// GetDefaultLogin return the default login -func GetDefaultLogin() (*Login, error) { - 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 -} - -// GetLoginByName get login by name -func GetLoginByName(name string) *Login { - for _, l := range Config.Logins { - if l.Name == name { - return &l - } - } - return nil -} - -// AddLogin add login to config ( global var & file) -func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool) error { - // checks ... - // ... if we have a url - if len(giteaURL) == 0 { - log.Fatal("You have to input Gitea server URL") - } - - err := LoadConfig() - if err != nil { - log.Fatal(err) - } - - for _, l := range Config.Logins { - // ... if there already exist a login with same name - if strings.ToLower(l.Name) == strings.ToLower(name) { - return fmt.Errorf("login name '%s' has already been used", l.Name) - } - // ... if we already use this token - if l.Token == token { - return fmt.Errorf("token already been used, delete login '%s' first", l.Name) - } - } - - // .. if we have enough information to authenticate - if len(token) == 0 && (len(user)+len(passwd)) == 0 { - log.Fatal("No token set") - } else if len(user) != 0 && len(passwd) == 0 { - log.Fatal("No password set") - } else if len(user) == 0 && len(passwd) != 0 { - log.Fatal("No user set") - } - - // Normalize URL - serverURL, err := utils.NormalizeURL(giteaURL) - if err != nil { - log.Fatal("Unable to parse URL", err) - } - - login := Login{ - Name: name, - URL: serverURL.String(), - Token: token, - Insecure: insecure, - SSHKey: sshKey, - Created: time.Now().Unix(), - } - - if len(token) == 0 { - login.Token, err = login.GenerateToken(user, passwd) - if err != nil { - log.Fatal(err) - } - } - - // Verify if authentication works and get user info - u, _, err := login.Client().GetMyUserInfo() - if err != nil { - log.Fatal(err) - } - login.User = u.UserName - - if len(login.Name) == 0 { - login.Name, err = GenerateLoginName(giteaURL, login.User) - if err != nil { - log.Fatal(err) - } - } - - // we do not have a method to get SSH config from api, - // so we just use the hostname - login.SSHHost = serverURL.Hostname() - - // save login to global var - Config.Logins = append(Config.Logins, login) - - // save login to config file - err = SaveConfig() - if err != nil { - log.Fatal(err) - } - - fmt.Printf("Login as %s on %s successful. Added this login as %s\n", login.User, login.URL, login.Name) - - return nil -} - -// DeleteLogin delete a login by name -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() -} - -// GenerateLoginName generates a name string based on instance URL & adds username if the result is not unique -func GenerateLoginName(url, user string) (string, error) { - parsedURL, err := utils.NormalizeURL(url) - if err != nil { - return "", err - } - name := parsedURL.Host - - // append user name if login name already exists - if len(user) != 0 { - for _, l := range Config.Logins { - if l.Name == name { - name += "_" + user - break - } - } - } - - return name, nil -} - -// InitCommand returns repository and *Login based on flags -func InitCommand(repoValue, loginValue, remoteValue string) (*Login, string, string) { - var login *Login - - err := LoadConfig() - if err != nil { - log.Fatal(err) - } - - if login, err = GetDefaultLogin(); err != nil { - log.Fatal(err.Error()) - } - - exist, err := utils.PathExists(repoValue) - if err != nil { - log.Fatal(err.Error()) - } - - if exist || len(repoValue) == 0 { - login, repoValue, err = curGitRepoPath(repoValue, remoteValue) - 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 := utils.GetOwnerAndRepo(repoValue, login.User) - return login, owner, repo -} - -// InitCommandLoginOnly return *Login based on flags -func InitCommandLoginOnly(loginValue string) *Login { - err := LoadConfig() - if err != nil { - log.Fatal(err) - } - - var login *Login - if loginValue == "" { - login, err = GetDefaultLogin() - if err != nil { - log.Fatal(err) - } - } else { - login = GetLoginByName(loginValue) - if login == nil { - log.Fatal("Login name " + loginValue + " does not exist") - } - } - - return login -} diff --git a/modules/config/login_tasks.go b/modules/config/login_tasks.go new file mode 100644 index 0000000..3afce60 --- /dev/null +++ b/modules/config/login_tasks.go @@ -0,0 +1,126 @@ +// 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" + "log" + "os" + "strings" + "time" + + "code.gitea.io/tea/modules/utils" + + "code.gitea.io/sdk/gitea" +) + +// AddLogin add login to config ( global var & file) +func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool) error { + // checks ... + // ... if we have a url + if len(giteaURL) == 0 { + log.Fatal("You have to input Gitea server URL") + } + + err := LoadConfig() + if err != nil { + log.Fatal(err) + } + + for _, l := range Config.Logins { + // ... if there already exist a login with same name + if strings.ToLower(l.Name) == strings.ToLower(name) { + return fmt.Errorf("login name '%s' has already been used", l.Name) + } + // ... if we already use this token + if l.Token == token { + return fmt.Errorf("token already been used, delete login '%s' first", l.Name) + } + } + + // .. if we have enough information to authenticate + if len(token) == 0 && (len(user)+len(passwd)) == 0 { + log.Fatal("No token set") + } else if len(user) != 0 && len(passwd) == 0 { + log.Fatal("No password set") + } else if len(user) == 0 && len(passwd) != 0 { + log.Fatal("No user set") + } + + // Normalize URL + serverURL, err := utils.NormalizeURL(giteaURL) + if err != nil { + log.Fatal("Unable to parse URL", err) + } + + login := Login{ + Name: name, + URL: serverURL.String(), + Token: token, + Insecure: insecure, + SSHKey: sshKey, + Created: time.Now().Unix(), + } + + if len(token) == 0 { + login.Token, err = GenerateToken(login.Client(), user, passwd) + if err != nil { + log.Fatal(err) + } + } + + // Verify if authentication works and get user info + u, _, err := login.Client().GetMyUserInfo() + if err != nil { + log.Fatal(err) + } + login.User = u.UserName + + if len(login.Name) == 0 { + login.Name, err = GenerateLoginName(giteaURL, login.User) + if err != nil { + log.Fatal(err) + } + } + + // we do not have a method to get SSH config from api, + // so we just use the hostname + login.SSHHost = serverURL.Hostname() + + // save login to global var + Config.Logins = append(Config.Logins, login) + + // save login to config file + err = SaveConfig() + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Login as %s on %s successful. Added this login as %s\n", login.User, login.URL, login.Name) + + return nil +} + +// GenerateToken creates a new token when given BasicAuth credentials +func GenerateToken(client *gitea.Client, user, pass string) (string, error) { + gitea.SetBasicAuth(user, pass)(client) + + host, _ := os.Hostname() + tl, _, err := client.ListAccessTokens(gitea.ListAccessTokensOptions{}) + if err != nil { + return "", err + } + tokenName := host + "-tea" + + for i := range tl { + if tl[i].Name == tokenName { + tokenName += time.Now().Format("2006-01-02_15-04-05") + break + } + } + + t, _, err := client.CreateAccessToken(gitea.CreateAccessTokenOption{Name: tokenName}) + return t.Token, err +} diff --git a/modules/git/repo.go b/modules/git/repo.go index e6f03e8..cc5d01e 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -16,18 +16,14 @@ type TeaRepo struct { // RepoForWorkdir tries to open the git repository in the local directory // for reading or modification. func RepoForWorkdir() (*TeaRepo, error) { - repo, err := git.PlainOpenWithOptions("./", &git.PlainOpenOptions{ - DetectDotGit: true, - }) - if err != nil { - return nil, err - } - - return &TeaRepo{repo}, nil + return RepoFromPath("") } // RepoFromPath tries to open the git repository by path func RepoFromPath(path string) (*TeaRepo, error) { + if len(path) == 0 { + path = "./" + } repo, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{ DetectDotGit: true, })