From de03aea30b8a1652966208adb4de169d9f408009 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Fri, 6 Mar 2020 15:51:31 +0100 Subject: [PATCH 01/17] add `tea pulls checkout` --- cmd/pulls.go | 86 ++++++++++++++++++++++++++++++++++++++++++- modules/git/branch.go | 42 +++++++++++++++++++++ modules/git/remote.go | 47 +++++++++++++++++++++++ modules/git/repo.go | 23 ++++++++++++ modules/git/url.go | 5 +++ 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 modules/git/branch.go create mode 100644 modules/git/remote.go create mode 100644 modules/git/repo.go diff --git a/cmd/pulls.go b/cmd/pulls.go index 50ffbe0..cc06332 100644 --- a/cmd/pulls.go +++ b/cmd/pulls.go @@ -5,21 +5,29 @@ package cmd import ( + "fmt" "log" "strconv" + "strings" "code.gitea.io/sdk/gitea" + local_git "code.gitea.io/tea/modules/git" + + "gopkg.in/src-d/go-git.v4" "github.com/urfave/cli/v2" ) -// CmdPulls represents to login a gitea server. +// CmdPulls is the main command to operate on PRs var CmdPulls = cli.Command{ Name: "pulls", Usage: "List open pull requests", Description: `List open pull requests`, Action: runPulls, Flags: AllDefaultFlags, + Subcommands: []*cli.Command{ + &CmdPullsCheckout, + }, } func runPulls(ctx *cli.Context) error { @@ -70,3 +78,79 @@ func runPulls(ctx *cli.Context) error { 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: "", + 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 := pr.Head.Repository.Owner.UserName + localRemote, err := localRepo.GetOrCreateRemote(remoteURL, newRemoteName) + if err != nil { + return err + } + + localRemoteName := localRemote.Config().Name + localBranchName := fmt.Sprintf("pull-%v-%v", idx, remoteBranchName) + + // fetch remote + fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n", + idx, remoteURL, remoteBranchName, localRemoteName) + err = localRemote.Fetch(&git.FetchOptions{}) + 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 +} + +func argToIndex(arg string) (int64, error) { + if strings.HasPrefix(arg, "#") { + arg = arg[1:] + } + return strconv.ParseInt(arg, 10, 64) +} diff --git a/modules/git/branch.go b/modules/git/branch.go new file mode 100644 index 0000000..0133849 --- /dev/null +++ b/modules/git/branch.go @@ -0,0 +1,42 @@ +package git + +import ( + "gopkg.in/src-d/go-git.v4" + git_config "gopkg.in/src-d/go-git.v4/config" + git_plumbing "gopkg.in/src-d/go-git.v4/plumbing" +) + +// TeaCreateBranch creates a new branch in the repo, tracking from another branch. +// If remoteName is not-null, a remote branch is tracked. +func (r TeaRepo) TeaCreateBranch(localBranchName, remoteBranchName, remoteName string) error { + remoteBranchRefName := git_plumbing.NewBranchReferenceName(remoteBranchName) + err := r.CreateBranch(&git_config.Branch{ + Name: localBranchName, + Merge: remoteBranchRefName, + Remote: remoteName, + }) + if err != nil { + return err + } + + // serialize the branch to .git/refs/heads (otherwise branch is only defined + // in .git/.config) + localBranchRefName := git_plumbing.NewBranchReferenceName(localBranchName) + remoteBranchRef, err := r.Storer.Reference(remoteBranchRefName) + if err != nil { + return nil + } + localHashRef := git_plumbing.NewHashReference(localBranchRefName, remoteBranchRef.Hash()) + r.Storer.SetReference(localHashRef) + return nil +} + +// TeaCheckout checks out the given branch in the worktree. +func (r TeaRepo) TeaCheckout(branchName string) error { + tree, err := r.Worktree() + if err != nil { + return nil + } + localBranchRefName := git_plumbing.NewBranchReferenceName(branchName) + return tree.Checkout(&git.CheckoutOptions{Branch: localBranchRefName}) +} diff --git a/modules/git/remote.go b/modules/git/remote.go new file mode 100644 index 0000000..0f68f75 --- /dev/null +++ b/modules/git/remote.go @@ -0,0 +1,47 @@ +package git + +import ( + "gopkg.in/src-d/go-git.v4" + git_config "gopkg.in/src-d/go-git.v4/config" +) + +// GetOrCreateRemote tries to match a Remote of the repo via the given URL. +// If no match is found, a new Remote with `newRemoteName` is created. +// Matching is based on the normalized URL, accepting different protocols. +func (r TeaRepo) GetOrCreateRemote(remoteURL, newRemoteName string) (*git.Remote, error) { + repoURL, err := ParseURL(remoteURL) + if err != nil { + return nil, err + } + + remotes, err := r.Remotes() + if err != nil { + return nil, err + } + var localRemote *git.Remote = nil + for _, r := range remotes { + for _, u := range r.Config().URLs { + remoteURL, _ := ParseURL(u) + if remoteURL.Host == repoURL.Host && remoteURL.Path == repoURL.Path { + localRemote = r + break + } + } + if localRemote != nil { + break + } + } + + // if no match found, create a new remote + if localRemote == nil { + localRemote, err = r.CreateRemote(&git_config.RemoteConfig{ + Name: newRemoteName, + URLs: []string{remoteURL}, + }) + if err != nil { + return nil, err + } + } + + return localRemote, nil +} diff --git a/modules/git/repo.go b/modules/git/repo.go new file mode 100644 index 0000000..7980609 --- /dev/null +++ b/modules/git/repo.go @@ -0,0 +1,23 @@ +package git + +import ( + "gopkg.in/src-d/go-git.v4" +) + +// TeaRepo is a go-git Repository, with an extended high level interface. +type TeaRepo struct { + *git.Repository +} + +// 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 +} diff --git a/modules/git/url.go b/modules/git/url.go index 87bf00e..8af030b 100644 --- a/modules/git/url.go +++ b/modules/git/url.go @@ -36,6 +36,11 @@ func (p *URLParser) Parse(rawURL string) (u *url.URL, err error) { u.Path = strings.TrimPrefix(u.Path, "/") } + // .git suffix is optional and breaks normalization + if strings.HasSuffix(u.Path, ".git") { + u.Path = strings.TrimSuffix(u.Path, ".git") + } + return } -- 2.40.1 From aad74cb2ee6ba64f8ed49f9b7d00b198508eadd9 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Fri, 6 Mar 2020 17:37:15 +0100 Subject: [PATCH 02/17] refactor: use new git functions for old code --- cmd/config.go | 9 ++++----- cmd/issues.go | 11 +---------- cmd/times.go | 24 ++++++------------------ 3 files changed, 11 insertions(+), 33 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 255ebeb..d2beb92 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -18,9 +18,8 @@ import ( "strings" "code.gitea.io/sdk/gitea" - local_git "code.gitea.io/tea/modules/git" + "code.gitea.io/tea/modules/git" "code.gitea.io/tea/modules/utils" - go_git "gopkg.in/src-d/go-git.v4" "github.com/go-gitea/yaml" ) @@ -187,11 +186,11 @@ func saveConfig(ymlPath string) error { } func curGitRepoPath() (*Login, string, error) { - gitPath, err := go_git.PlainOpenWithOptions("./", &go_git.PlainOpenOptions{DetectDotGit: true}) + repo, err := git.RepoForWorkdir() if err != nil { return nil, "", errors.New("No Gitea login found") } - gitConfig, err := gitPath.Config() + gitConfig, err := repo.Config() if err != nil { return nil, "", err } @@ -224,7 +223,7 @@ func curGitRepoPath() (*Login, string, error) { for _, l := range config.Logins { for _, u := range remoteConfig.URLs { - p, err := local_git.ParseURL(strings.TrimSpace(u)) + p, err := git.ParseURL(strings.TrimSpace(u)) if err != nil { return nil, "", fmt.Errorf("Git remote URL parse failed: %s", err.Error()) } diff --git a/cmd/issues.go b/cmd/issues.go index 5fba5a9..6685409 100644 --- a/cmd/issues.go +++ b/cmd/issues.go @@ -9,7 +9,6 @@ import ( "log" "os" "strconv" - "strings" "code.gitea.io/sdk/gitea" @@ -48,15 +47,7 @@ func runIssues(ctx *cli.Context) error { func runIssueDetail(ctx *cli.Context, index string) error { login, owner, repo := initCommand() - if strings.HasPrefix(index, "#") { - index = index[1:] - } - - idx, err := strconv.ParseInt(index, 10, 64) - if err != nil { - return err - } - + idx, err := argToIndex(index) issue, err := login.Client().GetIssue(owner, repo, idx) if err != nil { return err diff --git a/cmd/times.go b/cmd/times.go index 9d8d32e..9baa5f3 100644 --- a/cmd/times.go +++ b/cmd/times.go @@ -69,9 +69,9 @@ func runTrackedTimes(ctx *cli.Context) error { times, err = client.GetRepoTrackedTimes(owner, repo) } else if strings.HasPrefix(user, "#") { // get all tracked times on the specified issue - issue, err2 := strconv.ParseInt(user[1:], 10, 64) - if err2 != nil { - return err2 + issue, err := argToIndex(user) + if err != nil { + return err } times, err = client.ListTrackedTimes(owner, repo, issue) } else { @@ -166,11 +166,7 @@ func runTrackedTimesAdd(ctx *cli.Context) error { return fmt.Errorf("No issue or duration specified.\nUsage:\t%s", ctx.Command.UsageText) } - issueStr := ctx.Args().First() - if strings.HasPrefix(issueStr, "#") { - issueStr = issueStr[1:] - } - issue, err := strconv.ParseInt(issueStr, 10, 64) + issue, err := argToIndex(ctx.Args().First()) if err != nil { log.Fatal(err) } @@ -212,11 +208,7 @@ func runTrackedTimesDelete(ctx *cli.Context) error { return fmt.Errorf("No issue or time ID specified.\nUsage:\t%s", ctx.Command.UsageText) } - issueStr := ctx.Args().First() - if strings.HasPrefix(issueStr, "#") { - issueStr = issueStr[1:] - } - issue, err := strconv.ParseInt(issueStr, 10, 64) + issue, err := argToIndex(ctx.Args().First()) if err != nil { log.Fatal(err) } @@ -256,11 +248,7 @@ func runTrackedTimesReset(ctx *cli.Context) error { return fmt.Errorf("No issue specified.\nUsage:\t%s", ctx.Command.UsageText) } - issueStr := ctx.Args().First() - if strings.HasPrefix(issueStr, "#") { - issueStr = issueStr[1:] - } - issue, err := strconv.ParseInt(issueStr, 10, 64) + issue, err := argToIndex(ctx.Args().First()) if err != nil { log.Fatal(err) } -- 2.40.1 From 7dbd346bfc682cd1f08ac656d3a6912e84bbe666 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Fri, 6 Mar 2020 17:55:10 +0100 Subject: [PATCH 03/17] make linter happy --- modules/git/remote.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/git/remote.go b/modules/git/remote.go index 0f68f75..925f388 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -18,7 +18,7 @@ func (r TeaRepo) GetOrCreateRemote(remoteURL, newRemoteName string) (*git.Remote if err != nil { return nil, err } - var localRemote *git.Remote = nil + var localRemote *git.Remote for _, r := range remotes { for _, u := range r.Config().URLs { remoteURL, _ := ParseURL(u) -- 2.40.1 From 8bff856f971fcc7f1c4f3ba0af105ed8b5c1e826 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Mon, 9 Mar 2020 23:38:51 +0100 Subject: [PATCH 04/17] add copyright to new files --- modules/git/branch.go | 4 ++++ modules/git/remote.go | 4 ++++ modules/git/repo.go | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/modules/git/branch.go b/modules/git/branch.go index 0133849..812d0b2 100644 --- a/modules/git/branch.go +++ b/modules/git/branch.go @@ -1,3 +1,7 @@ +// 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 git import ( diff --git a/modules/git/remote.go b/modules/git/remote.go index 925f388..716eef8 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -1,3 +1,7 @@ +// 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 git import ( diff --git a/modules/git/repo.go b/modules/git/repo.go index 7980609..abef488 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -1,3 +1,7 @@ +// 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 git import ( -- 2.40.1 From 52803b2dfefe48566f546cde539c4a9be2e6d044 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Fri, 6 Mar 2020 17:54:39 +0100 Subject: [PATCH 05/17] add `tea pulls clean` fixes #97 --- cmd/pulls.go | 68 +++++++++++++++++++++++++++++++++++++++++-- modules/git/branch.go | 65 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/cmd/pulls.go b/cmd/pulls.go index cc06332..8e64907 100644 --- a/cmd/pulls.go +++ b/cmd/pulls.go @@ -10,23 +10,24 @@ import ( "strconv" "strings" - "code.gitea.io/sdk/gitea" local_git "code.gitea.io/tea/modules/git" - "gopkg.in/src-d/go-git.v4" - + "code.gitea.io/sdk/gitea" "github.com/urfave/cli/v2" + "gopkg.in/src-d/go-git.v4" ) // CmdPulls is the main command to operate on PRs var CmdPulls = cli.Command{ Name: "pulls", + Aliases: []string{"pull", "pr"}, Usage: "List open pull requests", Description: `List open pull requests`, Action: runPulls, Flags: AllDefaultFlags, Subcommands: []*cli.Command{ &CmdPullsCheckout, + &CmdPullsClean, }, } @@ -148,6 +149,67 @@ func runPullsCheckout(ctx *cli.Context) error { 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: "", + Action: runPullsClean, + Flags: 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") + } + + // check if the feature branch is ours: + // don't check name (user may be inconsistent with naming locally & remote), + // instead compare hashes & matching remote. + r, err := local_git.RepoForWorkdir() + if err != nil { + return err + } + branch, err := r.TeaFindBranch(pr.Head.Sha, pr.Head.Repository.CloneURL) + if branch == nil { + return fmt.Errorf("Remote branch %s not found in local repo. Maybe the local branch has diverged from the remote?", 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'", 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) + err = r.TeaDeleteBranch(branch, pr.Head.Ref) + + return err +} + func argToIndex(arg string) (int64, error) { if strings.HasPrefix(arg, "#") { arg = arg[1:] diff --git a/modules/git/branch.go b/modules/git/branch.go index 812d0b2..811ddd5 100644 --- a/modules/git/branch.go +++ b/modules/git/branch.go @@ -5,6 +5,8 @@ package git import ( + "fmt" + "gopkg.in/src-d/go-git.v4" git_config "gopkg.in/src-d/go-git.v4/config" git_plumbing "gopkg.in/src-d/go-git.v4/plumbing" @@ -44,3 +46,66 @@ func (r TeaRepo) TeaCheckout(branchName string) error { localBranchRefName := git_plumbing.NewBranchReferenceName(branchName) return tree.Checkout(&git.CheckoutOptions{Branch: localBranchRefName}) } + +// TeaDeleteBranch removes the given branch locally, and if `remoteBranch` is +// not empty deletes it at it's remote repo. +func (r TeaRepo) TeaDeleteBranch(branch *git_config.Branch, remoteBranch string) error { + err := r.DeleteBranch(branch.Name) + if err != nil { + return err + } + err = r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name)) + if err != nil { + return err + } + + if remoteBranch != "" { + // delete remote branch via git protocol: + // an empty source in the refspec means remote deletion to git 🙃 + refspec := fmt.Sprintf(":%s", git_plumbing.NewBranchReferenceName(remoteBranch)) + err = r.Push(&git.PushOptions{ + RemoteName: branch.Remote, + RefSpecs: []git_config.RefSpec{git_config.RefSpec(refspec)}, + Prune: true, + }) + } + + return err +} + +// TeaFindBranch returns a branch that is at the the given SHA and syncs to the +// given remote repo. +func (r TeaRepo) TeaFindBranch(sha, repoURL string) (b *git_config.Branch, err error) { + url, err := ParseURL(repoURL) + if err != nil { + return nil, err + } + + branches, err := r.Branches() + if err != nil { + return nil, err + } + err = branches.ForEach(func(ref *git_plumbing.Reference) error { + name := ref.Name().Short() + if name != "master" && ref.Hash().String() == sha { + branch, _ := r.Branch(name) + repoConf, err := r.Config() + if err != nil { + return err + } + remote := repoConf.Remotes[branch.Remote] + for _, u := range remote.URLs { + remoteURL, err := ParseURL(u) + if err != nil { + return err + } + if remoteURL.Host == url.Host && remoteURL.Path == url.Path { + b = branch + } + } + } + return nil + }) + + return b, err +} -- 2.40.1 From a664e3c97c7baf2516d5420a975955e2d0159b42 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Wed, 11 Mar 2020 16:52:21 +0100 Subject: [PATCH 06/17] improve method of TeaFindBranch() now only checking .git/refs instead of looking up .git/config which may not list the branch --- cmd/pulls.go | 5 +++- modules/git/branch.go | 66 ++++++++++++++++++++++++++++--------------- modules/git/remote.go | 17 +++++++++-- 3 files changed, 61 insertions(+), 27 deletions(-) diff --git a/cmd/pulls.go b/cmd/pulls.go index 8e64907..acb8b99 100644 --- a/cmd/pulls.go +++ b/cmd/pulls.go @@ -186,6 +186,9 @@ func runPullsClean(ctx *cli.Context) error { return err } branch, err := r.TeaFindBranch(pr.Head.Sha, pr.Head.Repository.CloneURL) + if err != nil { + return err + } if branch == nil { return fmt.Errorf("Remote branch %s not found in local repo. Maybe the local branch has diverged from the remote?", pr.Head.Ref) } @@ -196,7 +199,7 @@ func runPullsClean(ctx *cli.Context) error { return err } if headRef.Name().Short() == branch.Name { - fmt.Printf("Checking out 'master' to delete local branch '%s'", branch.Name) + fmt.Printf("Checking out 'master' to delete local branch '%s'\n", branch.Name) err = r.TeaCheckout("master") if err != nil { return err diff --git a/modules/git/branch.go b/modules/git/branch.go index 811ddd5..643f5d1 100644 --- a/modules/git/branch.go +++ b/modules/git/branch.go @@ -6,6 +6,7 @@ package git import ( "fmt" + "strings" "gopkg.in/src-d/go-git.v4" git_config "gopkg.in/src-d/go-git.v4/config" @@ -30,7 +31,7 @@ func (r TeaRepo) TeaCreateBranch(localBranchName, remoteBranchName, remoteName s localBranchRefName := git_plumbing.NewBranchReferenceName(localBranchName) remoteBranchRef, err := r.Storer.Reference(remoteBranchRefName) if err != nil { - return nil + return err } localHashRef := git_plumbing.NewHashReference(localBranchRefName, remoteBranchRef.Hash()) r.Storer.SetReference(localHashRef) @@ -41,7 +42,7 @@ func (r TeaRepo) TeaCreateBranch(localBranchName, remoteBranchName, remoteName s func (r TeaRepo) TeaCheckout(branchName string) error { tree, err := r.Worktree() if err != nil { - return nil + return err } localBranchRefName := git_plumbing.NewBranchReferenceName(branchName) return tree.Checkout(&git.CheckoutOptions{Branch: localBranchRefName}) @@ -51,7 +52,9 @@ func (r TeaRepo) TeaCheckout(branchName string) error { // not empty deletes it at it's remote repo. func (r TeaRepo) TeaDeleteBranch(branch *git_config.Branch, remoteBranch string) error { err := r.DeleteBranch(branch.Name) - if err != nil { + // if the branch is not found that's ok, as .git/config may have no entry if + // no remote tracking branch is configured for it (eg push without -u flag) + if err != nil && err.Error() != "branch not found" { return err } err = r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name)) @@ -76,36 +79,53 @@ func (r TeaRepo) TeaDeleteBranch(branch *git_config.Branch, remoteBranch string) // TeaFindBranch returns a branch that is at the the given SHA and syncs to the // given remote repo. func (r TeaRepo) TeaFindBranch(sha, repoURL string) (b *git_config.Branch, err error) { - url, err := ParseURL(repoURL) + // find remote matching our repoURL + remote, err := r.GetRemote(repoURL) if err != nil { return nil, err } + if remote == nil { + return nil, fmt.Errorf("No remote found for '%s'", repoURL) + } + remoteName := remote.Config().Name - branches, err := r.Branches() + // check if the given remote has our branch (.git/refs/remotes//*) + iter, err := r.References() if err != nil { return nil, err } - err = branches.ForEach(func(ref *git_plumbing.Reference) error { - name := ref.Name().Short() - if name != "master" && ref.Hash().String() == sha { - branch, _ := r.Branch(name) - repoConf, err := r.Config() - if err != nil { - return err - } - remote := repoConf.Remotes[branch.Remote] - for _, u := range remote.URLs { - remoteURL, err := ParseURL(u) - if err != nil { - return err - } - if remoteURL.Host == url.Host && remoteURL.Path == url.Path { - b = branch - } + defer iter.Close() + var remoteRefName git_plumbing.ReferenceName + var localRefName git_plumbing.ReferenceName + err = iter.ForEach(func(ref *git_plumbing.Reference) error { + if ref.Name().IsRemote() { + name := ref.Name().Short() + if name != "master" && + ref.Hash().String() == sha && + strings.HasPrefix(name, remoteName) { + remoteRefName = ref.Name() } } + + if ref.Name().IsBranch() && ref.Hash().String() == sha { + localRefName = ref.Name() + } return nil }) + if err != nil { + return nil, err + } + if remoteRefName == "" { + // no remote tracking branch found, so a potential local branch + // can't be a match either + return nil, nil + } - return b, err + b = &git_config.Branch{ + Remote: remoteName, + Name: localRefName.Short(), + Merge: localRefName, + } + fmt.Println(b) + return b, b.Validate() } diff --git a/modules/git/remote.go b/modules/git/remote.go index 716eef8..f308a4d 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -9,10 +9,9 @@ import ( git_config "gopkg.in/src-d/go-git.v4/config" ) -// GetOrCreateRemote tries to match a Remote of the repo via the given URL. -// If no match is found, a new Remote with `newRemoteName` is created. +// GetRemote tries to match a Remote of the repo via the given URL. // Matching is based on the normalized URL, accepting different protocols. -func (r TeaRepo) GetOrCreateRemote(remoteURL, newRemoteName string) (*git.Remote, error) { +func (r TeaRepo) GetRemote(remoteURL string) (*git.Remote, error) { repoURL, err := ParseURL(remoteURL) if err != nil { return nil, err @@ -36,6 +35,18 @@ func (r TeaRepo) GetOrCreateRemote(remoteURL, newRemoteName string) (*git.Remote } } + return localRemote, err +} + +// GetOrCreateRemote tries to match a Remote of the repo via the given URL. +// If no match is found, a new Remote with `newRemoteName` is created. +// Matching is based on the normalized URL, accepting different protocols. +func (r TeaRepo) GetOrCreateRemote(remoteURL, newRemoteName string) (*git.Remote, error) { + localRemote, err := r.GetRemote(remoteURL) + if err != nil { + return nil, err + } + // if no match found, create a new remote if localRemote == nil { localRemote, err = r.CreateRemote(&git_config.RemoteConfig{ -- 2.40.1 From 18b8562adfb46f3233e71d77a72c9da18fd40933 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Wed, 11 Mar 2020 20:29:40 +0100 Subject: [PATCH 07/17] fix TeaCreateBranch() --- modules/git/branch.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/modules/git/branch.go b/modules/git/branch.go index 643f5d1..76cab43 100644 --- a/modules/git/branch.go +++ b/modules/git/branch.go @@ -16,26 +16,25 @@ import ( // TeaCreateBranch creates a new branch in the repo, tracking from another branch. // If remoteName is not-null, a remote branch is tracked. func (r TeaRepo) TeaCreateBranch(localBranchName, remoteBranchName, remoteName string) error { - remoteBranchRefName := git_plumbing.NewBranchReferenceName(remoteBranchName) + // save in .git/config to assign remote for future pulls + localBranchRefName := git_plumbing.NewBranchReferenceName(localBranchName) err := r.CreateBranch(&git_config.Branch{ Name: localBranchName, - Merge: remoteBranchRefName, + Merge: git_plumbing.NewBranchReferenceName(remoteBranchName), // FIXME: should be remoteBranchName Remote: remoteName, }) if err != nil { return err } - // serialize the branch to .git/refs/heads (otherwise branch is only defined - // in .git/.config) - localBranchRefName := git_plumbing.NewBranchReferenceName(localBranchName) + // serialize the branch to .git/refs/heads + remoteBranchRefName := git_plumbing.NewRemoteReferenceName(remoteName, remoteBranchName) remoteBranchRef, err := r.Storer.Reference(remoteBranchRefName) if err != nil { return err } localHashRef := git_plumbing.NewHashReference(localBranchRefName, remoteBranchRef.Hash()) - r.Storer.SetReference(localHashRef) - return nil + return r.Storer.SetReference(localHashRef) } // TeaCheckout checks out the given branch in the worktree. @@ -124,7 +123,7 @@ func (r TeaRepo) TeaFindBranch(sha, repoURL string) (b *git_config.Branch, err e b = &git_config.Branch{ Remote: remoteName, Name: localRefName.Short(), - Merge: localRefName, + Merge: localRefName, } fmt.Println(b) return b, b.Validate() -- 2.40.1 From f01a4bd693241666ed1736335ad2bf453c41b921 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Wed, 11 Mar 2020 20:32:37 +0100 Subject: [PATCH 08/17] use directory namespaces for branches & remotes --- cmd/pulls.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/pulls.go b/cmd/pulls.go index acb8b99..d644e2a 100644 --- a/cmd/pulls.go +++ b/cmd/pulls.go @@ -115,14 +115,14 @@ func runPullsCheckout(ctx *cli.Context) error { } // verify related remote is in local repo, otherwise add it - newRemoteName := pr.Head.Repository.Owner.UserName + 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("pull-%v-%v", idx, remoteBranchName) + localBranchName := fmt.Sprintf("pulls/%v-%v", idx, remoteBranchName) // fetch remote fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n", -- 2.40.1 From f757c69e15eb5b8b21a33b32ef21029c694c6234 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Mon, 30 Mar 2020 01:11:43 +0200 Subject: [PATCH 09/17] fix branch-not-found case --- modules/git/branch.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/git/branch.go b/modules/git/branch.go index 76cab43..7e9c54b 100644 --- a/modules/git/branch.go +++ b/modules/git/branch.go @@ -114,7 +114,7 @@ func (r TeaRepo) TeaFindBranch(sha, repoURL string) (b *git_config.Branch, err e if err != nil { return nil, err } - if remoteRefName == "" { + if remoteRefName == "" || localRefName == "" { // no remote tracking branch found, so a potential local branch // can't be a match either return nil, nil @@ -125,6 +125,5 @@ func (r TeaRepo) TeaFindBranch(sha, repoURL string) (b *git_config.Branch, err e Name: localRefName.Short(), Merge: localRefName, } - fmt.Println(b) return b, b.Validate() } -- 2.40.1 From e8008180c5208765b7ee16599f677f7392d6ba61 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Mon, 30 Mar 2020 14:12:29 +0200 Subject: [PATCH 10/17] add missing error check --- cmd/issues.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/issues.go b/cmd/issues.go index 55619df..b2547b7 100644 --- a/cmd/issues.go +++ b/cmd/issues.go @@ -53,6 +53,9 @@ func runIssueDetail(ctx *cli.Context, index string) error { login, owner, repo := initCommand() idx, err := argToIndex(index) + if err != nil { + return err + } issue, err := login.Client().GetIssue(owner, repo, idx) if err != nil { return err -- 2.40.1 From 9aa0bc7733313dcd5833ae6c1a4819cf9b48d6f8 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Tue, 31 Mar 2020 14:17:26 +0200 Subject: [PATCH 11/17] add --ignore-sha flag When set, the local branch is not matched against the remote sha, but the remote branch name. This makes the command more flexible with diverging branches. --- cmd/pulls.go | 24 +++++++++++++++--- modules/git/branch.go | 57 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/cmd/pulls.go b/cmd/pulls.go index 2a73891..ec6b7aa 100644 --- a/cmd/pulls.go +++ b/cmd/pulls.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/sdk/gitea" "github.com/urfave/cli/v2" "gopkg.in/src-d/go-git.v4" + git_config "gopkg.in/src-d/go-git.v4/config" ) // CmdPulls is the main command to operate on PRs @@ -174,7 +175,12 @@ var CmdPullsClean = cli.Command{ Description: `Deletes local & remote feature-branches for a closed pull request`, ArgsUsage: "", Action: runPullsClean, - Flags: AllDefaultFlags, + 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 { @@ -203,12 +209,24 @@ func runPullsClean(ctx *cli.Context) error { if err != nil { return err } - branch, err := r.TeaFindBranch(pr.Head.Sha, pr.Head.Repository.CloneURL) + + 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 { - return fmt.Errorf("Remote branch %s not found in local repo. Maybe the local branch has diverged from the remote?", pr.Head.Ref) + 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: diff --git a/modules/git/branch.go b/modules/git/branch.go index 7e9c54b..e88d144 100644 --- a/modules/git/branch.go +++ b/modules/git/branch.go @@ -75,9 +75,9 @@ func (r TeaRepo) TeaDeleteBranch(branch *git_config.Branch, remoteBranch string) return err } -// TeaFindBranch 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 // given remote repo. -func (r TeaRepo) TeaFindBranch(sha, repoURL string) (b *git_config.Branch, err error) { +func (r TeaRepo) TeaFindBranchBySha(sha, repoURL string) (b *git_config.Branch, err error) { // find remote matching our repoURL remote, err := r.GetRemote(repoURL) if err != nil { @@ -99,9 +99,7 @@ func (r TeaRepo) TeaFindBranch(sha, repoURL string) (b *git_config.Branch, err e err = iter.ForEach(func(ref *git_plumbing.Reference) error { if ref.Name().IsRemote() { name := ref.Name().Short() - if name != "master" && - ref.Hash().String() == sha && - strings.HasPrefix(name, remoteName) { + if ref.Hash().String() == sha && strings.HasPrefix(name, remoteName) { remoteRefName = ref.Name() } } @@ -127,3 +125,52 @@ func (r TeaRepo) TeaFindBranch(sha, repoURL string) (b *git_config.Branch, err e } return b, b.Validate() } + +// TeaFindBranchByName returns a branch that is at the the given local and +// remote names and syncs to the given remote repo. This method is less precise +// than TeaFindBranchBySha(), but may be desirable if local and remote branch +// have diverged. +func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *git_config.Branch, err error) { + // find remote matching our repoURL + remote, err := r.GetRemote(repoURL) + if err != nil { + return nil, err + } + if remote == nil { + return nil, fmt.Errorf("No remote found for '%s'", repoURL) + } + remoteName := remote.Config().Name + + // check if the given remote has our branch (.git/refs/remotes//*) + iter, err := r.References() + if err != nil { + return nil, err + } + defer iter.Close() + var remoteRefName git_plumbing.ReferenceName + var localRefName git_plumbing.ReferenceName + var remoteSearchingName = fmt.Sprintf("%s/%s", remoteName, branchName) + err = iter.ForEach(func(ref *git_plumbing.Reference) error { + if ref.Name().IsRemote() && ref.Name().Short() == remoteSearchingName { + remoteRefName = ref.Name() + } + n := ref.Name() + if n.IsBranch() && n.Short() == branchName { + localRefName = n + } + return nil + }) + if err != nil { + return nil, err + } + if remoteRefName == "" || localRefName == "" { + return nil, nil + } + + b = &git_config.Branch{ + Remote: remoteName, + Name: localRefName.Short(), + Merge: localRefName, + } + return b, b.Validate() +} -- 2.40.1 From b225c523f2ee4818b84177dcfd4a9f2326e94b9f Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Wed, 1 Apr 2020 17:06:15 +0200 Subject: [PATCH 12/17] adress code review --- cmd/pulls.go | 4 +--- modules/git/branch.go | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/cmd/pulls.go b/cmd/pulls.go index ec6b7aa..f32fb80 100644 --- a/cmd/pulls.go +++ b/cmd/pulls.go @@ -202,14 +202,12 @@ func runPullsClean(ctx *cli.Context) error { return fmt.Errorf("PR is still open, won't delete branches") } - // check if the feature branch is ours: - // don't check name (user may be inconsistent with naming locally & remote), - // instead compare hashes & matching remote. 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) diff --git a/modules/git/branch.go b/modules/git/branch.go index e88d144..3aae7f5 100644 --- a/modules/git/branch.go +++ b/modules/git/branch.go @@ -14,13 +14,12 @@ import ( ) // TeaCreateBranch creates a new branch in the repo, tracking from another branch. -// If remoteName is not-null, a remote branch is tracked. func (r TeaRepo) TeaCreateBranch(localBranchName, remoteBranchName, remoteName string) error { // save in .git/config to assign remote for future pulls localBranchRefName := git_plumbing.NewBranchReferenceName(localBranchName) err := r.CreateBranch(&git_config.Branch{ Name: localBranchName, - Merge: git_plumbing.NewBranchReferenceName(remoteBranchName), // FIXME: should be remoteBranchName + Merge: git_plumbing.NewBranchReferenceName(remoteBranchName), Remote: remoteName, }) if err != nil { -- 2.40.1 From 6ac292de1bf5c55a4a93412fb17073a4dc90c8af Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Wed, 1 Apr 2020 17:58:12 +0200 Subject: [PATCH 13/17] refactor GetRemote --- modules/git/remote.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/modules/git/remote.go b/modules/git/remote.go index f308a4d..7403c71 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -21,21 +21,19 @@ func (r TeaRepo) GetRemote(remoteURL string) (*git.Remote, error) { if err != nil { return nil, err } - var localRemote *git.Remote for _, r := range remotes { for _, u := range r.Config().URLs { - remoteURL, _ := ParseURL(u) - if remoteURL.Host == repoURL.Host && remoteURL.Path == repoURL.Path { - localRemote = r - break + remoteURL, err := ParseURL(u) + if err != nil { + return nil, err + } + if remoteURL.Host == repoURL.Host && remoteURL.Path == repoURL.Path { + return r, nil } - } - if localRemote != nil { - break } } - return localRemote, err + return nil, nil } // GetOrCreateRemote tries to match a Remote of the repo via the given URL. -- 2.40.1 From d52fe7e6eba591ff68181865d35f905942e67797 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Sun, 5 Apr 2020 22:15:29 +0200 Subject: [PATCH 14/17] refactor --- cmd/flags.go | 33 ++++++++++++--------------------- cmd/open.go | 6 +++++- modules/git/ref.go | 20 -------------------- 3 files changed, 17 insertions(+), 42 deletions(-) delete mode 100644 modules/git/ref.go diff --git a/cmd/flags.go b/cmd/flags.go index 998d992..7737972 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -81,24 +81,9 @@ var AllDefaultFlags = append([]cli.Flag{ // initCommand returns repository and *Login based on flags func initCommand() (*Login, string, string) { - err := loadConfig(yamlConfigPath) - if err != nil { - log.Fatal("Unable to load config file " + 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") - } - } + login := initCommandLoginOnly() + var err error repoPath := repoValue if repoPath == "" { login, repoPath, err = curGitRepoPath() @@ -119,10 +104,16 @@ func initCommandLoginOnly() *Login { } var login *Login - - login = getLoginByName(loginValue) - if login == nil { - log.Fatal("indicated login name ", loginValue, " does not exist") + 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 diff --git a/cmd/open.go b/cmd/open.go index a528c04..fbe67b4 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -37,7 +37,11 @@ func runOpen(ctx *cli.Context) error { case strings.EqualFold(number, "releases"): suffix = "releases" case strings.EqualFold(number, "commits"): - b, err := local_git.GetRepoReference("./") + repo, err := local_git.RepoForWorkdir() + if err != nil { + log.Fatal(err) + } + b, err := repo.Head() if err != nil { log.Fatal(err) return nil diff --git a/modules/git/ref.go b/modules/git/ref.go deleted file mode 100644 index 89aa8fd..0000000 --- a/modules/git/ref.go +++ /dev/null @@ -1,20 +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 git - -import ( - go_git "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing" -) - -// GetRepoReference returns the current repository's current branch or tag -func GetRepoReference(p string) (*plumbing.Reference, error) { - gitPath, err := go_git.PlainOpenWithOptions(p, &go_git.PlainOpenOptions{DetectDotGit: true}) - if err != nil { - return nil, err - } - - return gitPath.Head() -} -- 2.40.1 From c340b2dedde0d8449ed7693d26a57daacda228bb Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Sun, 5 Apr 2020 15:49:11 +0200 Subject: [PATCH 15/17] login: store username & optional keyfile --- cmd/config.go | 14 +++++++++----- cmd/login.go | 36 +++++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index d2beb92..2e4d15a 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -26,12 +26,16 @@ import ( // 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"` + 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 diff --git a/cmd/login.go b/cmd/login.go index 1a5cb83..6f827fb 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -34,23 +34,31 @@ var cmdLoginAdd = cli.Command{ Description: `Add a Gitea login`, Flags: []cli.Flag{ &cli.StringFlag{ - Name: "name", - Aliases: []string{"n"}, - Usage: "Login name", + 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", + 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", + Name: "token", + Aliases: []string{"t"}, + Value: "", + EnvVars: []string{"GITEA_SERVER_TOKEN"}, + Usage: "Access token. Can be obtained from Settings > Applications", + Required: true, + }, + &cli.BoolFlag{ + Name: "ssh-key", + Aliases: []string{"s"}, + Usage: "Path to a SSH key to use for pull/push operations", }, &cli.BoolFlag{ Name: "insecure", @@ -100,6 +108,8 @@ func runLoginAdd(ctx *cli.Context) error { 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) -- 2.40.1 From 95bdd3bab0525e14ae99b27c372a8e542e538a77 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Sun, 5 Apr 2020 01:03:11 +0200 Subject: [PATCH 16/17] pull/push: provide authentication method automatically select an AuthMethod according to the remote url type. If required, credentials are prompted for --- cmd/pulls.go | 26 ++++++++-- modules/git/auth.go | 117 ++++++++++++++++++++++++++++++++++++++++++ modules/git/branch.go | 4 +- modules/git/remote.go | 16 ++++++ 4 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 modules/git/auth.go diff --git a/cmd/pulls.go b/cmd/pulls.go index f32fb80..cfadd0f 100644 --- a/cmd/pulls.go +++ b/cmd/pulls.go @@ -146,7 +146,17 @@ func runPullsCheckout(ctx *cli.Context) error { // fetch remote fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n", idx, remoteURL, remoteBranchName, localRemoteName) - err = localRemote.Fetch(&git.FetchOptions{}) + + 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 { @@ -202,6 +212,8 @@ func runPullsClean(ctx *cli.Context) error { 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 @@ -242,9 +254,15 @@ call me again with the --ignore-sha flag`, pr.Head.Ref) // remove local & remote branch fmt.Printf("Deleting local branch %s and remote branch %s\n", branch.Name, pr.Head.Ref) - err = r.TeaDeleteBranch(branch, pr.Head.Ref) - - return err + url, err := r.TeaRemoteURL(branch.Remote) + if err != nil { + return err + } + auth, err := local_git.GetAuthForURL(url, login.User, login.SSHKey) + if err != nil { + return err + } + return r.TeaDeleteBranch(branch, pr.Head.Ref, auth) } func argToIndex(arg string) (int64, error) { diff --git a/modules/git/auth.go b/modules/git/auth.go new file mode 100644 index 0000000..eab6262 --- /dev/null +++ b/modules/git/auth.go @@ -0,0 +1,117 @@ +// 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 git + +import ( + "bufio" + "fmt" + "io/ioutil" + "net/url" + "os" + "os/user" + "path/filepath" + "strings" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/terminal" + git_transport "gopkg.in/src-d/go-git.v4/plumbing/transport" + gogit_http "gopkg.in/src-d/go-git.v4/plumbing/transport/http" + gogit_ssh "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh" +) + +// GetAuthForURL returns the appropriate AuthMethod to be used in Push() / Pull() +// operations depending on the protocol, and prompts the user for credentials if +// necessary. +func GetAuthForURL(remoteURL *url.URL, httpUser, keyFile string) (auth git_transport.AuthMethod, err error) { + user := remoteURL.User.Username() + + switch remoteURL.Scheme { + case "https": + if httpUser != "" { + user = httpUser + } + if user == "" { + user, err = promptUser(remoteURL.Host) + if err != nil { + return nil, err + } + } + pass, isSet := remoteURL.User.Password() + if !isSet { + pass, err = promptPass(remoteURL.Host) + if err != nil { + return nil, err + } + } + auth = &gogit_http.BasicAuth{Password: pass, Username: user} + + case "ssh": + // try to select right key via ssh-agent. if it fails, try to read a key manually + auth, err = gogit_ssh.DefaultAuthBuilder(user) + if err != nil { + signer, err := readSSHPrivKey(keyFile) + if err != nil { + return nil, err + } + auth = &gogit_ssh.PublicKeys{User: user, Signer: signer} + } + + default: + return nil, fmt.Errorf("don't know how to handle url scheme %v", remoteURL.Scheme) + } + + return auth, nil +} + +func readSSHPrivKey(keyFile string) (sig ssh.Signer, err error) { + if keyFile != "" { + keyFile, err = absPathWithExpansion(keyFile) + } else { + keyFile, err = absPathWithExpansion("~/.ssh/id_rsa") + } + if err != nil { + return nil, err + } + sshKey, err := ioutil.ReadFile(keyFile) + if err != nil { + return nil, err + } + sig, err = ssh.ParsePrivateKey(sshKey) + if err != nil { + pass, err := promptPass(keyFile) + sig, err = ssh.ParsePrivateKeyWithPassphrase(sshKey, []byte(pass)) + if err != nil { + return nil, err + } + } + return sig, err +} + +func promptUser(domain string) (string, error) { + reader := bufio.NewReader(os.Stdin) + fmt.Printf("%s username: ", domain) + username, err := reader.ReadString('\n') + return strings.TrimSpace(username), err +} + +func promptPass(domain string) (string, error) { + fmt.Printf("%s password: ", domain) + pass, err := terminal.ReadPassword(0) + return string(pass), err +} + +func absPathWithExpansion(p string) (string, error) { + u, err := user.Current() + if err != nil { + return "", err + } + if p == "~" { + return u.HomeDir, nil + } else if strings.HasPrefix(p, "~/") { + return filepath.Join(u.HomeDir, p[2:]), nil + } else { + return filepath.Abs(p) + } +} diff --git a/modules/git/branch.go b/modules/git/branch.go index 3aae7f5..f630ce6 100644 --- a/modules/git/branch.go +++ b/modules/git/branch.go @@ -11,6 +11,7 @@ import ( "gopkg.in/src-d/go-git.v4" git_config "gopkg.in/src-d/go-git.v4/config" git_plumbing "gopkg.in/src-d/go-git.v4/plumbing" + git_transport "gopkg.in/src-d/go-git.v4/plumbing/transport" ) // TeaCreateBranch creates a new branch in the repo, tracking from another branch. @@ -48,7 +49,7 @@ func (r TeaRepo) TeaCheckout(branchName string) error { // TeaDeleteBranch removes the given branch locally, and if `remoteBranch` is // not empty deletes it at it's remote repo. -func (r TeaRepo) TeaDeleteBranch(branch *git_config.Branch, remoteBranch string) error { +func (r TeaRepo) TeaDeleteBranch(branch *git_config.Branch, remoteBranch string, auth git_transport.AuthMethod) error { err := r.DeleteBranch(branch.Name) // if the branch is not found that's ok, as .git/config may have no entry if // no remote tracking branch is configured for it (eg push without -u flag) @@ -68,6 +69,7 @@ func (r TeaRepo) TeaDeleteBranch(branch *git_config.Branch, remoteBranch string) RemoteName: branch.Remote, RefSpecs: []git_config.RefSpec{git_config.RefSpec(refspec)}, Prune: true, + Auth: auth, }) } diff --git a/modules/git/remote.go b/modules/git/remote.go index 7403c71..c82a900 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -5,6 +5,9 @@ package git import ( + "fmt" + "net/url" + "gopkg.in/src-d/go-git.v4" git_config "gopkg.in/src-d/go-git.v4/config" ) @@ -58,3 +61,16 @@ func (r TeaRepo) GetOrCreateRemote(remoteURL, newRemoteName string) (*git.Remote return localRemote, nil } + +// TeaRemoteURL returns the first url entry for the given remote name +func (r TeaRepo) TeaRemoteURL(name string) (auth *url.URL, err error) { + remote, err := r.Remote(name) + if err != nil { + return nil, err + } + urls := remote.Config().URLs + if len(urls) == 0 { + return nil, fmt.Errorf("remote %s has no URL configured", name) + } + return ParseURL(remote.Config().URLs[0]) +} -- 2.40.1 From fb5cc503f348bc13c04b9637ffb6ef8f69371e19 Mon Sep 17 00:00:00 2001 From: Norwin Date: Sun, 5 Apr 2020 22:58:30 +0200 Subject: [PATCH 17/17] vendor terminal dependency --- go.mod | 1 + .../x/crypto/ssh/terminal/terminal.go | 966 ++++++++++++++++++ .../golang.org/x/crypto/ssh/terminal/util.go | 114 +++ .../x/crypto/ssh/terminal/util_aix.go | 12 + .../x/crypto/ssh/terminal/util_bsd.go | 12 + .../x/crypto/ssh/terminal/util_linux.go | 10 + .../x/crypto/ssh/terminal/util_plan9.go | 58 ++ .../x/crypto/ssh/terminal/util_solaris.go | 124 +++ .../x/crypto/ssh/terminal/util_windows.go | 105 ++ vendor/modules.txt | 1 + 10 files changed, 1403 insertions(+) create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/terminal.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_aix.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_linux.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_windows.go diff --git a/go.mod b/go.mod index 5e19f80..16e785b 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/stretchr/testify v1.4.0 github.com/urfave/cli/v2 v2.1.1 + golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 gopkg.in/src-d/go-git.v4 v4.13.1 gopkg.in/yaml.v2 v2.2.7 // indirect ) diff --git a/vendor/golang.org/x/crypto/ssh/terminal/terminal.go b/vendor/golang.org/x/crypto/ssh/terminal/terminal.go new file mode 100644 index 0000000..2f04ee5 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/terminal.go @@ -0,0 +1,966 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package terminal + +import ( + "bytes" + "io" + "strconv" + "sync" + "unicode/utf8" +) + +// EscapeCodes contains escape sequences that can be written to the terminal in +// order to achieve different styles of text. +type EscapeCodes struct { + // Foreground colors + Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte + + // Reset all attributes + Reset []byte +} + +var vt100EscapeCodes = EscapeCodes{ + Black: []byte{keyEscape, '[', '3', '0', 'm'}, + Red: []byte{keyEscape, '[', '3', '1', 'm'}, + Green: []byte{keyEscape, '[', '3', '2', 'm'}, + Yellow: []byte{keyEscape, '[', '3', '3', 'm'}, + Blue: []byte{keyEscape, '[', '3', '4', 'm'}, + Magenta: []byte{keyEscape, '[', '3', '5', 'm'}, + Cyan: []byte{keyEscape, '[', '3', '6', 'm'}, + White: []byte{keyEscape, '[', '3', '7', 'm'}, + + Reset: []byte{keyEscape, '[', '0', 'm'}, +} + +// Terminal contains the state for running a VT100 terminal that is capable of +// reading lines of input. +type Terminal struct { + // AutoCompleteCallback, if non-null, is called for each keypress with + // the full input line and the current position of the cursor (in + // bytes, as an index into |line|). If it returns ok=false, the key + // press is processed normally. Otherwise it returns a replacement line + // and the new cursor position. + AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool) + + // Escape contains a pointer to the escape codes for this terminal. + // It's always a valid pointer, although the escape codes themselves + // may be empty if the terminal doesn't support them. + Escape *EscapeCodes + + // lock protects the terminal and the state in this object from + // concurrent processing of a key press and a Write() call. + lock sync.Mutex + + c io.ReadWriter + prompt []rune + + // line is the current line being entered. + line []rune + // pos is the logical position of the cursor in line + pos int + // echo is true if local echo is enabled + echo bool + // pasteActive is true iff there is a bracketed paste operation in + // progress. + pasteActive bool + + // cursorX contains the current X value of the cursor where the left + // edge is 0. cursorY contains the row number where the first row of + // the current line is 0. + cursorX, cursorY int + // maxLine is the greatest value of cursorY so far. + maxLine int + + termWidth, termHeight int + + // outBuf contains the terminal data to be sent. + outBuf []byte + // remainder contains the remainder of any partial key sequences after + // a read. It aliases into inBuf. + remainder []byte + inBuf [256]byte + + // history contains previously entered commands so that they can be + // accessed with the up and down keys. + history stRingBuffer + // historyIndex stores the currently accessed history entry, where zero + // means the immediately previous entry. + historyIndex int + // When navigating up and down the history it's possible to return to + // the incomplete, initial line. That value is stored in + // historyPending. + historyPending string +} + +// NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is +// a local terminal, that terminal must first have been put into raw mode. +// prompt is a string that is written at the start of each input line (i.e. +// "> "). +func NewTerminal(c io.ReadWriter, prompt string) *Terminal { + return &Terminal{ + Escape: &vt100EscapeCodes, + c: c, + prompt: []rune(prompt), + termWidth: 80, + termHeight: 24, + echo: true, + historyIndex: -1, + } +} + +const ( + keyCtrlD = 4 + keyCtrlU = 21 + keyEnter = '\r' + keyEscape = 27 + keyBackspace = 127 + keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota + keyUp + keyDown + keyLeft + keyRight + keyAltLeft + keyAltRight + keyHome + keyEnd + keyDeleteWord + keyDeleteLine + keyClearScreen + keyPasteStart + keyPasteEnd +) + +var ( + crlf = []byte{'\r', '\n'} + pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'} + pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'} +) + +// bytesToKey tries to parse a key sequence from b. If successful, it returns +// the key and the remainder of the input. Otherwise it returns utf8.RuneError. +func bytesToKey(b []byte, pasteActive bool) (rune, []byte) { + if len(b) == 0 { + return utf8.RuneError, nil + } + + if !pasteActive { + switch b[0] { + case 1: // ^A + return keyHome, b[1:] + case 5: // ^E + return keyEnd, b[1:] + case 8: // ^H + return keyBackspace, b[1:] + case 11: // ^K + return keyDeleteLine, b[1:] + case 12: // ^L + return keyClearScreen, b[1:] + case 23: // ^W + return keyDeleteWord, b[1:] + case 14: // ^N + return keyDown, b[1:] + case 16: // ^P + return keyUp, b[1:] + } + } + + if b[0] != keyEscape { + if !utf8.FullRune(b) { + return utf8.RuneError, b + } + r, l := utf8.DecodeRune(b) + return r, b[l:] + } + + if !pasteActive && len(b) >= 3 && b[0] == keyEscape && b[1] == '[' { + switch b[2] { + case 'A': + return keyUp, b[3:] + case 'B': + return keyDown, b[3:] + case 'C': + return keyRight, b[3:] + case 'D': + return keyLeft, b[3:] + case 'H': + return keyHome, b[3:] + case 'F': + return keyEnd, b[3:] + } + } + + if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' { + switch b[5] { + case 'C': + return keyAltRight, b[6:] + case 'D': + return keyAltLeft, b[6:] + } + } + + if !pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteStart) { + return keyPasteStart, b[6:] + } + + if pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) { + return keyPasteEnd, b[6:] + } + + // If we get here then we have a key that we don't recognise, or a + // partial sequence. It's not clear how one should find the end of a + // sequence without knowing them all, but it seems that [a-zA-Z~] only + // appears at the end of a sequence. + for i, c := range b[0:] { + if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' { + return keyUnknown, b[i+1:] + } + } + + return utf8.RuneError, b +} + +// queue appends data to the end of t.outBuf +func (t *Terminal) queue(data []rune) { + t.outBuf = append(t.outBuf, []byte(string(data))...) +} + +var eraseUnderCursor = []rune{' ', keyEscape, '[', 'D'} +var space = []rune{' '} + +func isPrintable(key rune) bool { + isInSurrogateArea := key >= 0xd800 && key <= 0xdbff + return key >= 32 && !isInSurrogateArea +} + +// moveCursorToPos appends data to t.outBuf which will move the cursor to the +// given, logical position in the text. +func (t *Terminal) moveCursorToPos(pos int) { + if !t.echo { + return + } + + x := visualLength(t.prompt) + pos + y := x / t.termWidth + x = x % t.termWidth + + up := 0 + if y < t.cursorY { + up = t.cursorY - y + } + + down := 0 + if y > t.cursorY { + down = y - t.cursorY + } + + left := 0 + if x < t.cursorX { + left = t.cursorX - x + } + + right := 0 + if x > t.cursorX { + right = x - t.cursorX + } + + t.cursorX = x + t.cursorY = y + t.move(up, down, left, right) +} + +func (t *Terminal) move(up, down, left, right int) { + m := []rune{} + + // 1 unit up can be expressed as ^[[A or ^[A + // 5 units up can be expressed as ^[[5A + + if up == 1 { + m = append(m, keyEscape, '[', 'A') + } else if up > 1 { + m = append(m, keyEscape, '[') + m = append(m, []rune(strconv.Itoa(up))...) + m = append(m, 'A') + } + + if down == 1 { + m = append(m, keyEscape, '[', 'B') + } else if down > 1 { + m = append(m, keyEscape, '[') + m = append(m, []rune(strconv.Itoa(down))...) + m = append(m, 'B') + } + + if right == 1 { + m = append(m, keyEscape, '[', 'C') + } else if right > 1 { + m = append(m, keyEscape, '[') + m = append(m, []rune(strconv.Itoa(right))...) + m = append(m, 'C') + } + + if left == 1 { + m = append(m, keyEscape, '[', 'D') + } else if left > 1 { + m = append(m, keyEscape, '[') + m = append(m, []rune(strconv.Itoa(left))...) + m = append(m, 'D') + } + + t.queue(m) +} + +func (t *Terminal) clearLineToRight() { + op := []rune{keyEscape, '[', 'K'} + t.queue(op) +} + +const maxLineLength = 4096 + +func (t *Terminal) setLine(newLine []rune, newPos int) { + if t.echo { + t.moveCursorToPos(0) + t.writeLine(newLine) + for i := len(newLine); i < len(t.line); i++ { + t.writeLine(space) + } + t.moveCursorToPos(newPos) + } + t.line = newLine + t.pos = newPos +} + +func (t *Terminal) advanceCursor(places int) { + t.cursorX += places + t.cursorY += t.cursorX / t.termWidth + if t.cursorY > t.maxLine { + t.maxLine = t.cursorY + } + t.cursorX = t.cursorX % t.termWidth + + if places > 0 && t.cursorX == 0 { + // Normally terminals will advance the current position + // when writing a character. But that doesn't happen + // for the last character in a line. However, when + // writing a character (except a new line) that causes + // a line wrap, the position will be advanced two + // places. + // + // So, if we are stopping at the end of a line, we + // need to write a newline so that our cursor can be + // advanced to the next line. + t.outBuf = append(t.outBuf, '\r', '\n') + } +} + +func (t *Terminal) eraseNPreviousChars(n int) { + if n == 0 { + return + } + + if t.pos < n { + n = t.pos + } + t.pos -= n + t.moveCursorToPos(t.pos) + + copy(t.line[t.pos:], t.line[n+t.pos:]) + t.line = t.line[:len(t.line)-n] + if t.echo { + t.writeLine(t.line[t.pos:]) + for i := 0; i < n; i++ { + t.queue(space) + } + t.advanceCursor(n) + t.moveCursorToPos(t.pos) + } +} + +// countToLeftWord returns then number of characters from the cursor to the +// start of the previous word. +func (t *Terminal) countToLeftWord() int { + if t.pos == 0 { + return 0 + } + + pos := t.pos - 1 + for pos > 0 { + if t.line[pos] != ' ' { + break + } + pos-- + } + for pos > 0 { + if t.line[pos] == ' ' { + pos++ + break + } + pos-- + } + + return t.pos - pos +} + +// countToRightWord returns then number of characters from the cursor to the +// start of the next word. +func (t *Terminal) countToRightWord() int { + pos := t.pos + for pos < len(t.line) { + if t.line[pos] == ' ' { + break + } + pos++ + } + for pos < len(t.line) { + if t.line[pos] != ' ' { + break + } + pos++ + } + return pos - t.pos +} + +// visualLength returns the number of visible glyphs in s. +func visualLength(runes []rune) int { + inEscapeSeq := false + length := 0 + + for _, r := range runes { + switch { + case inEscapeSeq: + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { + inEscapeSeq = false + } + case r == '\x1b': + inEscapeSeq = true + default: + length++ + } + } + + return length +} + +// handleKey processes the given key and, optionally, returns a line of text +// that the user has entered. +func (t *Terminal) handleKey(key rune) (line string, ok bool) { + if t.pasteActive && key != keyEnter { + t.addKeyToLine(key) + return + } + + switch key { + case keyBackspace: + if t.pos == 0 { + return + } + t.eraseNPreviousChars(1) + case keyAltLeft: + // move left by a word. + t.pos -= t.countToLeftWord() + t.moveCursorToPos(t.pos) + case keyAltRight: + // move right by a word. + t.pos += t.countToRightWord() + t.moveCursorToPos(t.pos) + case keyLeft: + if t.pos == 0 { + return + } + t.pos-- + t.moveCursorToPos(t.pos) + case keyRight: + if t.pos == len(t.line) { + return + } + t.pos++ + t.moveCursorToPos(t.pos) + case keyHome: + if t.pos == 0 { + return + } + t.pos = 0 + t.moveCursorToPos(t.pos) + case keyEnd: + if t.pos == len(t.line) { + return + } + t.pos = len(t.line) + t.moveCursorToPos(t.pos) + case keyUp: + entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1) + if !ok { + return "", false + } + if t.historyIndex == -1 { + t.historyPending = string(t.line) + } + t.historyIndex++ + runes := []rune(entry) + t.setLine(runes, len(runes)) + case keyDown: + switch t.historyIndex { + case -1: + return + case 0: + runes := []rune(t.historyPending) + t.setLine(runes, len(runes)) + t.historyIndex-- + default: + entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1) + if ok { + t.historyIndex-- + runes := []rune(entry) + t.setLine(runes, len(runes)) + } + } + case keyEnter: + t.moveCursorToPos(len(t.line)) + t.queue([]rune("\r\n")) + line = string(t.line) + ok = true + t.line = t.line[:0] + t.pos = 0 + t.cursorX = 0 + t.cursorY = 0 + t.maxLine = 0 + case keyDeleteWord: + // Delete zero or more spaces and then one or more characters. + t.eraseNPreviousChars(t.countToLeftWord()) + case keyDeleteLine: + // Delete everything from the current cursor position to the + // end of line. + for i := t.pos; i < len(t.line); i++ { + t.queue(space) + t.advanceCursor(1) + } + t.line = t.line[:t.pos] + t.moveCursorToPos(t.pos) + case keyCtrlD: + // Erase the character under the current position. + // The EOF case when the line is empty is handled in + // readLine(). + if t.pos < len(t.line) { + t.pos++ + t.eraseNPreviousChars(1) + } + case keyCtrlU: + t.eraseNPreviousChars(t.pos) + case keyClearScreen: + // Erases the screen and moves the cursor to the home position. + t.queue([]rune("\x1b[2J\x1b[H")) + t.queue(t.prompt) + t.cursorX, t.cursorY = 0, 0 + t.advanceCursor(visualLength(t.prompt)) + t.setLine(t.line, t.pos) + default: + if t.AutoCompleteCallback != nil { + prefix := string(t.line[:t.pos]) + suffix := string(t.line[t.pos:]) + + t.lock.Unlock() + newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key) + t.lock.Lock() + + if completeOk { + t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos])) + return + } + } + if !isPrintable(key) { + return + } + if len(t.line) == maxLineLength { + return + } + t.addKeyToLine(key) + } + return +} + +// addKeyToLine inserts the given key at the current position in the current +// line. +func (t *Terminal) addKeyToLine(key rune) { + if len(t.line) == cap(t.line) { + newLine := make([]rune, len(t.line), 2*(1+len(t.line))) + copy(newLine, t.line) + t.line = newLine + } + t.line = t.line[:len(t.line)+1] + copy(t.line[t.pos+1:], t.line[t.pos:]) + t.line[t.pos] = key + if t.echo { + t.writeLine(t.line[t.pos:]) + } + t.pos++ + t.moveCursorToPos(t.pos) +} + +func (t *Terminal) writeLine(line []rune) { + for len(line) != 0 { + remainingOnLine := t.termWidth - t.cursorX + todo := len(line) + if todo > remainingOnLine { + todo = remainingOnLine + } + t.queue(line[:todo]) + t.advanceCursor(visualLength(line[:todo])) + line = line[todo:] + } +} + +// writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n. +func writeWithCRLF(w io.Writer, buf []byte) (n int, err error) { + for len(buf) > 0 { + i := bytes.IndexByte(buf, '\n') + todo := len(buf) + if i >= 0 { + todo = i + } + + var nn int + nn, err = w.Write(buf[:todo]) + n += nn + if err != nil { + return n, err + } + buf = buf[todo:] + + if i >= 0 { + if _, err = w.Write(crlf); err != nil { + return n, err + } + n++ + buf = buf[1:] + } + } + + return n, nil +} + +func (t *Terminal) Write(buf []byte) (n int, err error) { + t.lock.Lock() + defer t.lock.Unlock() + + if t.cursorX == 0 && t.cursorY == 0 { + // This is the easy case: there's nothing on the screen that we + // have to move out of the way. + return writeWithCRLF(t.c, buf) + } + + // We have a prompt and possibly user input on the screen. We + // have to clear it first. + t.move(0 /* up */, 0 /* down */, t.cursorX /* left */, 0 /* right */) + t.cursorX = 0 + t.clearLineToRight() + + for t.cursorY > 0 { + t.move(1 /* up */, 0, 0, 0) + t.cursorY-- + t.clearLineToRight() + } + + if _, err = t.c.Write(t.outBuf); err != nil { + return + } + t.outBuf = t.outBuf[:0] + + if n, err = writeWithCRLF(t.c, buf); err != nil { + return + } + + t.writeLine(t.prompt) + if t.echo { + t.writeLine(t.line) + } + + t.moveCursorToPos(t.pos) + + if _, err = t.c.Write(t.outBuf); err != nil { + return + } + t.outBuf = t.outBuf[:0] + return +} + +// ReadPassword temporarily changes the prompt and reads a password, without +// echo, from the terminal. +func (t *Terminal) ReadPassword(prompt string) (line string, err error) { + t.lock.Lock() + defer t.lock.Unlock() + + oldPrompt := t.prompt + t.prompt = []rune(prompt) + t.echo = false + + line, err = t.readLine() + + t.prompt = oldPrompt + t.echo = true + + return +} + +// ReadLine returns a line of input from the terminal. +func (t *Terminal) ReadLine() (line string, err error) { + t.lock.Lock() + defer t.lock.Unlock() + + return t.readLine() +} + +func (t *Terminal) readLine() (line string, err error) { + // t.lock must be held at this point + + if t.cursorX == 0 && t.cursorY == 0 { + t.writeLine(t.prompt) + t.c.Write(t.outBuf) + t.outBuf = t.outBuf[:0] + } + + lineIsPasted := t.pasteActive + + for { + rest := t.remainder + lineOk := false + for !lineOk { + var key rune + key, rest = bytesToKey(rest, t.pasteActive) + if key == utf8.RuneError { + break + } + if !t.pasteActive { + if key == keyCtrlD { + if len(t.line) == 0 { + return "", io.EOF + } + } + if key == keyPasteStart { + t.pasteActive = true + if len(t.line) == 0 { + lineIsPasted = true + } + continue + } + } else if key == keyPasteEnd { + t.pasteActive = false + continue + } + if !t.pasteActive { + lineIsPasted = false + } + line, lineOk = t.handleKey(key) + } + if len(rest) > 0 { + n := copy(t.inBuf[:], rest) + t.remainder = t.inBuf[:n] + } else { + t.remainder = nil + } + t.c.Write(t.outBuf) + t.outBuf = t.outBuf[:0] + if lineOk { + if t.echo { + t.historyIndex = -1 + t.history.Add(line) + } + if lineIsPasted { + err = ErrPasteIndicator + } + return + } + + // t.remainder is a slice at the beginning of t.inBuf + // containing a partial key sequence + readBuf := t.inBuf[len(t.remainder):] + var n int + + t.lock.Unlock() + n, err = t.c.Read(readBuf) + t.lock.Lock() + + if err != nil { + return + } + + t.remainder = t.inBuf[:n+len(t.remainder)] + } +} + +// SetPrompt sets the prompt to be used when reading subsequent lines. +func (t *Terminal) SetPrompt(prompt string) { + t.lock.Lock() + defer t.lock.Unlock() + + t.prompt = []rune(prompt) +} + +func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) { + // Move cursor to column zero at the start of the line. + t.move(t.cursorY, 0, t.cursorX, 0) + t.cursorX, t.cursorY = 0, 0 + t.clearLineToRight() + for t.cursorY < numPrevLines { + // Move down a line + t.move(0, 1, 0, 0) + t.cursorY++ + t.clearLineToRight() + } + // Move back to beginning. + t.move(t.cursorY, 0, 0, 0) + t.cursorX, t.cursorY = 0, 0 + + t.queue(t.prompt) + t.advanceCursor(visualLength(t.prompt)) + t.writeLine(t.line) + t.moveCursorToPos(t.pos) +} + +func (t *Terminal) SetSize(width, height int) error { + t.lock.Lock() + defer t.lock.Unlock() + + if width == 0 { + width = 1 + } + + oldWidth := t.termWidth + t.termWidth, t.termHeight = width, height + + switch { + case width == oldWidth: + // If the width didn't change then nothing else needs to be + // done. + return nil + case len(t.line) == 0 && t.cursorX == 0 && t.cursorY == 0: + // If there is nothing on current line and no prompt printed, + // just do nothing + return nil + case width < oldWidth: + // Some terminals (e.g. xterm) will truncate lines that were + // too long when shinking. Others, (e.g. gnome-terminal) will + // attempt to wrap them. For the former, repainting t.maxLine + // works great, but that behaviour goes badly wrong in the case + // of the latter because they have doubled every full line. + + // We assume that we are working on a terminal that wraps lines + // and adjust the cursor position based on every previous line + // wrapping and turning into two. This causes the prompt on + // xterms to move upwards, which isn't great, but it avoids a + // huge mess with gnome-terminal. + if t.cursorX >= t.termWidth { + t.cursorX = t.termWidth - 1 + } + t.cursorY *= 2 + t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2) + case width > oldWidth: + // If the terminal expands then our position calculations will + // be wrong in the future because we think the cursor is + // |t.pos| chars into the string, but there will be a gap at + // the end of any wrapped line. + // + // But the position will actually be correct until we move, so + // we can move back to the beginning and repaint everything. + t.clearAndRepaintLinePlusNPrevious(t.maxLine) + } + + _, err := t.c.Write(t.outBuf) + t.outBuf = t.outBuf[:0] + return err +} + +type pasteIndicatorError struct{} + +func (pasteIndicatorError) Error() string { + return "terminal: ErrPasteIndicator not correctly handled" +} + +// ErrPasteIndicator may be returned from ReadLine as the error, in addition +// to valid line data. It indicates that bracketed paste mode is enabled and +// that the returned line consists only of pasted data. Programs may wish to +// interpret pasted data more literally than typed data. +var ErrPasteIndicator = pasteIndicatorError{} + +// SetBracketedPasteMode requests that the terminal bracket paste operations +// with markers. Not all terminals support this but, if it is supported, then +// enabling this mode will stop any autocomplete callback from running due to +// pastes. Additionally, any lines that are completely pasted will be returned +// from ReadLine with the error set to ErrPasteIndicator. +func (t *Terminal) SetBracketedPasteMode(on bool) { + if on { + io.WriteString(t.c, "\x1b[?2004h") + } else { + io.WriteString(t.c, "\x1b[?2004l") + } +} + +// stRingBuffer is a ring buffer of strings. +type stRingBuffer struct { + // entries contains max elements. + entries []string + max int + // head contains the index of the element most recently added to the ring. + head int + // size contains the number of elements in the ring. + size int +} + +func (s *stRingBuffer) Add(a string) { + if s.entries == nil { + const defaultNumEntries = 100 + s.entries = make([]string, defaultNumEntries) + s.max = defaultNumEntries + } + + s.head = (s.head + 1) % s.max + s.entries[s.head] = a + if s.size < s.max { + s.size++ + } +} + +// NthPreviousEntry returns the value passed to the nth previous call to Add. +// If n is zero then the immediately prior value is returned, if one, then the +// next most recent, and so on. If such an element doesn't exist then ok is +// false. +func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) { + if n >= s.size { + return "", false + } + index := s.head - n + if index < 0 { + index += s.max + } + return s.entries[index], true +} + +// readPasswordLine reads from reader until it finds \n or io.EOF. +// The slice returned does not include the \n. +// readPasswordLine also ignores any \r it finds. +func readPasswordLine(reader io.Reader) ([]byte, error) { + var buf [1]byte + var ret []byte + + for { + n, err := reader.Read(buf[:]) + if n > 0 { + switch buf[0] { + case '\n': + return ret, nil + case '\r': + // remove \r from passwords on Windows + default: + ret = append(ret, buf[0]) + } + continue + } + if err != nil { + if err == io.EOF && len(ret) > 0 { + return ret, nil + } + return ret, err + } + } +} diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util.go b/vendor/golang.org/x/crypto/ssh/terminal/util.go new file mode 100644 index 0000000..3911040 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util.go @@ -0,0 +1,114 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd + +// Package terminal provides support functions for dealing with terminals, as +// commonly found on UNIX systems. +// +// Putting a terminal into raw mode is the most common requirement: +// +// oldState, err := terminal.MakeRaw(0) +// if err != nil { +// panic(err) +// } +// defer terminal.Restore(0, oldState) +package terminal // import "golang.org/x/crypto/ssh/terminal" + +import ( + "golang.org/x/sys/unix" +) + +// State contains the state of a terminal. +type State struct { + termios unix.Termios +} + +// IsTerminal returns whether the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + _, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + return err == nil +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return nil, err + } + + oldState := State{termios: *termios} + + // This attempts to replicate the behaviour documented for cfmakeraw in + // the termios(3) manpage. + termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON + termios.Oflag &^= unix.OPOST + termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN + termios.Cflag &^= unix.CSIZE | unix.PARENB + termios.Cflag |= unix.CS8 + termios.Cc[unix.VMIN] = 1 + termios.Cc[unix.VTIME] = 0 + if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil { + return nil, err + } + + return &oldState, nil +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return nil, err + } + + return &State{termios: *termios}, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios) +} + +// GetSize returns the dimensions of the given terminal. +func GetSize(fd int) (width, height int, err error) { + ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) + if err != nil { + return -1, -1, err + } + return int(ws.Col), int(ws.Row), nil +} + +// passwordReader is an io.Reader that reads from a specific file descriptor. +type passwordReader int + +func (r passwordReader) Read(buf []byte) (int, error) { + return unix.Read(int(r), buf) +} + +// ReadPassword reads a line of input from a terminal without local echo. This +// is commonly used for inputting passwords and other sensitive data. The slice +// returned does not include the \n. +func ReadPassword(fd int) ([]byte, error) { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return nil, err + } + + newState := *termios + newState.Lflag &^= unix.ECHO + newState.Lflag |= unix.ICANON | unix.ISIG + newState.Iflag |= unix.ICRNL + if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil { + return nil, err + } + + defer unix.IoctlSetTermios(fd, ioctlWriteTermios, termios) + + return readPasswordLine(passwordReader(fd)) +} diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_aix.go b/vendor/golang.org/x/crypto/ssh/terminal/util_aix.go new file mode 100644 index 0000000..dfcd627 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_aix.go @@ -0,0 +1,12 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build aix + +package terminal + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETS +const ioctlWriteTermios = unix.TCSETS diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go b/vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go new file mode 100644 index 0000000..cb23a59 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go @@ -0,0 +1,12 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin dragonfly freebsd netbsd openbsd + +package terminal + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TIOCGETA +const ioctlWriteTermios = unix.TIOCSETA diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_linux.go b/vendor/golang.org/x/crypto/ssh/terminal/util_linux.go new file mode 100644 index 0000000..5fadfe8 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_linux.go @@ -0,0 +1,10 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package terminal + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETS +const ioctlWriteTermios = unix.TCSETS diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go b/vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go new file mode 100644 index 0000000..9317ac7 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go @@ -0,0 +1,58 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package terminal provides support functions for dealing with terminals, as +// commonly found on UNIX systems. +// +// Putting a terminal into raw mode is the most common requirement: +// +// oldState, err := terminal.MakeRaw(0) +// if err != nil { +// panic(err) +// } +// defer terminal.Restore(0, oldState) +package terminal + +import ( + "fmt" + "runtime" +) + +type State struct{} + +// IsTerminal returns whether the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + return false +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} + +// GetSize returns the dimensions of the given terminal. +func GetSize(fd int) (width, height int, err error) { + return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} + +// ReadPassword reads a line of input from a terminal without local echo. This +// is commonly used for inputting passwords and other sensitive data. The slice +// returned does not include the \n. +func ReadPassword(fd int) ([]byte, error) { + return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go b/vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go new file mode 100644 index 0000000..3d5f06a --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go @@ -0,0 +1,124 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build solaris + +package terminal // import "golang.org/x/crypto/ssh/terminal" + +import ( + "golang.org/x/sys/unix" + "io" + "syscall" +) + +// State contains the state of a terminal. +type State struct { + termios unix.Termios +} + +// IsTerminal returns whether the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + _, err := unix.IoctlGetTermio(fd, unix.TCGETA) + return err == nil +} + +// ReadPassword reads a line of input from a terminal without local echo. This +// is commonly used for inputting passwords and other sensitive data. The slice +// returned does not include the \n. +func ReadPassword(fd int) ([]byte, error) { + // see also: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libast/common/uwin/getpass.c + val, err := unix.IoctlGetTermios(fd, unix.TCGETS) + if err != nil { + return nil, err + } + oldState := *val + + newState := oldState + newState.Lflag &^= syscall.ECHO + newState.Lflag |= syscall.ICANON | syscall.ISIG + newState.Iflag |= syscall.ICRNL + err = unix.IoctlSetTermios(fd, unix.TCSETS, &newState) + if err != nil { + return nil, err + } + + defer unix.IoctlSetTermios(fd, unix.TCSETS, &oldState) + + var buf [16]byte + var ret []byte + for { + n, err := syscall.Read(fd, buf[:]) + if err != nil { + return nil, err + } + if n == 0 { + if len(ret) == 0 { + return nil, io.EOF + } + break + } + if buf[n-1] == '\n' { + n-- + } + ret = append(ret, buf[:n]...) + if n < len(buf) { + break + } + } + + return ret, nil +} + +// MakeRaw puts the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +// see http://cr.illumos.org/~webrev/andy_js/1060/ +func MakeRaw(fd int) (*State, error) { + termios, err := unix.IoctlGetTermios(fd, unix.TCGETS) + if err != nil { + return nil, err + } + + oldState := State{termios: *termios} + + termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON + termios.Oflag &^= unix.OPOST + termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN + termios.Cflag &^= unix.CSIZE | unix.PARENB + termios.Cflag |= unix.CS8 + termios.Cc[unix.VMIN] = 1 + termios.Cc[unix.VTIME] = 0 + + if err := unix.IoctlSetTermios(fd, unix.TCSETS, termios); err != nil { + return nil, err + } + + return &oldState, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, oldState *State) error { + return unix.IoctlSetTermios(fd, unix.TCSETS, &oldState.termios) +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + termios, err := unix.IoctlGetTermios(fd, unix.TCGETS) + if err != nil { + return nil, err + } + + return &State{termios: *termios}, nil +} + +// GetSize returns the dimensions of the given terminal. +func GetSize(fd int) (width, height int, err error) { + ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) + if err != nil { + return 0, 0, err + } + return int(ws.Col), int(ws.Row), nil +} diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_windows.go b/vendor/golang.org/x/crypto/ssh/terminal/util_windows.go new file mode 100644 index 0000000..5cfdf8f --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_windows.go @@ -0,0 +1,105 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +// Package terminal provides support functions for dealing with terminals, as +// commonly found on UNIX systems. +// +// Putting a terminal into raw mode is the most common requirement: +// +// oldState, err := terminal.MakeRaw(0) +// if err != nil { +// panic(err) +// } +// defer terminal.Restore(0, oldState) +package terminal + +import ( + "os" + + "golang.org/x/sys/windows" +) + +type State struct { + mode uint32 +} + +// IsTerminal returns whether the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + var st uint32 + err := windows.GetConsoleMode(windows.Handle(fd), &st) + return err == nil +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + var st uint32 + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err + } + raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) + if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil { + return nil, err + } + return &State{st}, nil +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + var st uint32 + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err + } + return &State{st}, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + return windows.SetConsoleMode(windows.Handle(fd), state.mode) +} + +// GetSize returns the visible dimensions of the given terminal. +// +// These dimensions don't include any scrollback buffer height. +func GetSize(fd int) (width, height int, err error) { + var info windows.ConsoleScreenBufferInfo + if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil { + return 0, 0, err + } + return int(info.Window.Right - info.Window.Left + 1), int(info.Window.Bottom - info.Window.Top + 1), nil +} + +// ReadPassword reads a line of input from a terminal without local echo. This +// is commonly used for inputting passwords and other sensitive data. The slice +// returned does not include the \n. +func ReadPassword(fd int) ([]byte, error) { + var st uint32 + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err + } + old := st + + st &^= (windows.ENABLE_ECHO_INPUT) + st |= (windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) + if err := windows.SetConsoleMode(windows.Handle(fd), st); err != nil { + return nil, err + } + + defer windows.SetConsoleMode(windows.Handle(fd), old) + + var h windows.Handle + p, _ := windows.GetCurrentProcess() + if err := windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil { + return nil, err + } + + f := os.NewFile(uintptr(h), "stdin") + defer f.Close() + return readPasswordLine(f) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index bfc8880..7c796ba 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -65,6 +65,7 @@ golang.org/x/crypto/poly1305 golang.org/x/crypto/ssh golang.org/x/crypto/ssh/agent golang.org/x/crypto/ssh/knownhosts +golang.org/x/crypto/ssh/terminal # golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 golang.org/x/net/context golang.org/x/net/internal/socks -- 2.40.1