@ -18,21 +18,24 @@ 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"
|
||||
)
|
||||
|
||||
// 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
|
||||
@ -187,11 +190,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 +227,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())
|
||||
}
|
||||
|
33
cmd/flags.go
33
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
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
|
||||
@ -53,15 +52,10 @@ 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)
|
||||
idx, err := argToIndex(index)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issue, err := login.Client().GetIssue(owner, repo, idx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
36
cmd/login.go
36
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)
|
||||
|
@ -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
|
||||
|
187
cmd/pulls.go
187
cmd/pulls.go
@ -5,17 +5,23 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
local_git "code.gitea.io/tea/modules/git"
|
||||
|
||||
"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 represents to login a gitea server.
|
||||
// CmdPulls is the main command to operate on PRs
|
||||
6543
commented
good catch! good catch!
|
||||
var CmdPulls = cli.Command{
|
||||
Name: "pulls",
|
||||
Aliases: []string{"pull", "pr"},
|
||||
Usage: "List open pull requests",
|
||||
Description: `List open pull requests`,
|
||||
Action: runPulls,
|
||||
@ -26,6 +32,10 @@ var CmdPulls = cli.Command{
|
||||
DefaultText: "open",
|
||||
},
|
||||
}, AllDefaultFlags...),
|
||||
Subcommands: []*cli.Command{
|
||||
&CmdPullsCheckout,
|
||||
&CmdPullsClean,
|
||||
},
|
||||
}
|
||||
|
||||
func runPulls(ctx *cli.Context) error {
|
||||
@ -88,3 +98,176 @@ 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: "<pull index>",
|
||||
Flags: AllDefaultFlags,
|
||||
}
|
||||
|
||||
func runPullsCheckout(ctx *cli.Context) error {
|
||||
login, owner, repo := initCommand()
|
||||
if ctx.Args().Len() != 1 {
|
||||
log.Fatal("Must specify a PR index")
|
||||
}
|
||||
|
||||
// fetch PR source-repo & -branch from gitea
|
||||
idx, err := argToIndex(ctx.Args().First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pr, err := login.Client().GetPullRequest(owner, repo, idx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
remoteURL := pr.Head.Repository.CloneURL
|
||||
remoteBranchName := pr.Head.Ref
|
||||
|
||||
// open local git repo
|
||||
localRepo, err := local_git.RepoForWorkdir()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// verify related remote is in local repo, otherwise add it
|
||||
newRemoteName := fmt.Sprintf("pulls/%v", pr.Head.Repository.Owner.UserName)
|
||||
6543
commented
future impruvement: define prefix via config ... (for examle i sue but this can be added after this pull :) future impruvement: define prefix via config ... (for examle i sue `user/`)
but this can be added after this pull :)
|
||||
localRemote, err := localRepo.GetOrCreateRemote(remoteURL, newRemoteName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localRemoteName := localRemote.Config().Name
|
||||
localBranchName := fmt.Sprintf("pulls/%v-%v", idx, remoteBranchName)
|
||||
|
||||
// fetch remote
|
||||
fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n",
|
||||
idx, remoteURL, remoteBranchName, localRemoteName)
|
||||
|
||||
url, err := local_git.ParseURL(localRemote.Config().URLs[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
auth, err := local_git.GetAuthForURL(url, login.User, login.SSHKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = localRemote.Fetch(&git.FetchOptions{Auth: auth})
|
||||
if err == git.NoErrAlreadyUpToDate {
|
||||
fmt.Println(err)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// checkout local branch
|
||||
fmt.Printf("Creating branch '%s'\n", localBranchName)
|
||||
err = localRepo.TeaCreateBranch(localBranchName, remoteBranchName, localRemoteName)
|
||||
if err == git.ErrBranchExists {
|
||||
fmt.Println(err)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Checking out PR %v\n", idx)
|
||||
err = localRepo.TeaCheckout(localBranchName)
|
||||
|
||||
lunny
commented
User may still want to clean it, but we can give a confirmation. User may still want to clean it, but we can give a confirmation.
noerw
commented
But we can't safely remove the remote branch in this case, right? But we can't safely remove the remote branch in this case, right?
Effectively tea wouldn't do anything different than `git branch -D <featurebranch>`, so I don't see the point.
We could guide the user to call `tea pulls close 1` first if he really wants to, thats more expressive than a `--force` flag
|
||||
return err
|
||||
}
|
||||
|
||||
// CmdPullsClean removes the remote and local feature branches, if a PR is merged.
|
||||
var CmdPullsClean = cli.Command{
|
||||
Name: "clean",
|
||||
Usage: "Deletes local & remote feature-branches for a closed pull request",
|
||||
Description: `Deletes local & remote feature-branches for a closed pull request`,
|
||||
ArgsUsage: "<pull index>",
|
||||
Action: runPullsClean,
|
||||
Flags: append([]cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "ignore-sha",
|
||||
Usage: "Find the local branch by name instead of commit hash (less precise)",
|
||||
},
|
||||
}, AllDefaultFlags...),
|
||||
}
|
||||
|
||||
func runPullsClean(ctx *cli.Context) error {
|
||||
login, owner, repo := initCommand()
|
||||
if ctx.Args().Len() != 1 {
|
||||
return fmt.Errorf("Must specify a PR index")
|
||||
}
|
||||
|
||||
// fetch PR source-repo & -branch from gitea
|
||||
idx, err := argToIndex(ctx.Args().First())
|
||||
if err != nil {
|
||||
return err
|
||||
6543
commented
can you move L205-207 too current 213 ...? can you move L205-207 too current 213 ...?
|
||||
}
|
||||
pr, err := login.Client().GetPullRequest(owner, repo, idx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pr.State == gitea.StateOpen {
|
||||
return fmt.Errorf("PR is still open, won't delete branches")
|
||||
}
|
||||
|
||||
// IDEA: abort if PR.Head.Repository.CloneURL does not match login.URL?
|
||||
|
||||
r, err := local_git.RepoForWorkdir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find a branch with matching sha or name, that has a remote matching the repo url
|
||||
var branch *git_config.Branch
|
||||
if ctx.Bool("ignore-sha") {
|
||||
branch, err = r.TeaFindBranchByName(pr.Head.Ref, pr.Head.Repository.CloneURL)
|
||||
} else {
|
||||
branch, err = r.TeaFindBranchBySha(pr.Head.Sha, pr.Head.Repository.CloneURL)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if branch == nil {
|
||||
if ctx.Bool("ignore-sha") {
|
||||
return fmt.Errorf("Remote branch %s not found in local repo", pr.Head.Ref)
|
||||
}
|
||||
return fmt.Errorf(`Remote branch %s not found in local repo.
|
||||
Either you don't track this PR, or the local branch has diverged from the remote.
|
||||
If you still want to continue & are sure you don't loose any important commits,
|
||||
call me again with the --ignore-sha flag`, pr.Head.Ref)
|
||||
}
|
||||
|
||||
// prepare deletion of local branch:
|
||||
headRef, err := r.Head()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if headRef.Name().Short() == branch.Name {
|
||||
fmt.Printf("Checking out 'master' to delete local branch '%s'\n", branch.Name)
|
||||
err = r.TeaCheckout("master")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// remove local & remote branch
|
||||
fmt.Printf("Deleting local branch %s and remote branch %s\n", branch.Name, pr.Head.Ref)
|
||||
url, err := r.TeaRemoteURL(branch.Remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
auth, err := local_git.GetAuthForURL(url, login.User, login.SSHKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.TeaDeleteBranch(branch, pr.Head.Ref, auth)
|
||||
}
|
||||
|
||||
func argToIndex(arg string) (int64, error) {
|
||||
if strings.HasPrefix(arg, "#") {
|
||||
arg = arg[1:]
|
||||
}
|
||||
return strconv.ParseInt(arg, 10, 64)
|
||||
}
|
||||
|
24
cmd/times.go
24
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)
|
||||
}
|
||||
|
1
go.mod
1
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
|
||||
)
|
||||
|
117
modules/git/auth.go
Normal file
117
modules/git/auth.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
177
modules/git/branch.go
Normal file
177
modules/git/branch.go
Normal file
@ -0,0 +1,177 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
lunny
commented
copyright head. copyright head.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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.
|
||||
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),
|
||||
6543
commented
Whats the meaning of the FIXME? Whats the meaning of the FIXME?
|
||||
Remote: remoteName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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())
|
||||
return r.Storer.SetReference(localHashRef)
|
||||
}
|
||||
|
||||
// TeaCheckout checks out the given branch in the worktree.
|
||||
func (r TeaRepo) TeaCheckout(branchName string) error {
|
||||
tree, err := r.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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, 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)
|
||||
if err != nil && err.Error() != "branch not found" {
|
||||
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,
|
||||
Auth: auth,
|
||||
})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// TeaFindBranchBySha returns a branch that is at the the given SHA and syncs to the
|
||||
// given remote repo.
|
||||
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 {
|
||||
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/<remoteName>/*)
|
||||
iter, err := r.References()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 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 == "" || localRefName == "" {
|
||||
// no remote tracking branch found, so a potential local branch
|
||||
// can't be a match either
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b = &git_config.Branch{
|
||||
Remote: remoteName,
|
||||
Name: localRefName.Short(),
|
||||
Merge: localRefName,
|
||||
}
|
||||
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/<remoteName>/*)
|
||||
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()
|
||||
}
|
@ -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()
|
||||
}
|
76
modules/git/remote.go
Normal file
76
modules/git/remote.go
Normal file
@ -0,0 +1,76 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"gopkg.in/src-d/go-git.v4"
|
||||
git_config "gopkg.in/src-d/go-git.v4/config"
|
||||
)
|
||||
|
||||
// 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) GetRemote(remoteURL string) (*git.Remote, error) {
|
||||
repoURL, err := ParseURL(remoteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
remotes, err := r.Remotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range remotes {
|
||||
for _, u := range r.Config().URLs {
|
||||
remoteURL, err := ParseURL(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if remoteURL.Host == repoURL.Host && remoteURL.Path == repoURL.Path {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 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{
|
||||
Name: newRemoteName,
|
||||
URLs: []string{remoteURL},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
27
modules/git/repo.go
Normal file
27
modules/git/repo.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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 (
|
||||
"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
|
||||
}
|
@ -40,6 +40,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
|
||||
}
|
||||
|
||||
|
966
vendor/golang.org/x/crypto/ssh/terminal/terminal.go
generated
vendored
Normal file
966
vendor/golang.org/x/crypto/ssh/terminal/terminal.go
generated
vendored
Normal file
@ -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.
|
||||
no err check
thanks, resolved