mirror of
https://gitea.com/gitea/tea.git
synced 2025-09-02 01:48:30 +02:00
Merge branch 'master' into issue-97/pulls-clean vendor terminal dependency pull/push: provide authentication method automatically select an AuthMethod according to the remote url type. If required, credentials are prompted for login: store username & optional keyfile refactor refactor GetRemote Merge branch 'master' into issue-97/pulls-clean adress code review 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. add missing error check fix branch-not-found case Merge branch 'master' into issue-97/pulls-clean use directory namespaces for branches & remotes fix TeaCreateBranch() improve method of TeaFindBranch() now only checking .git/refs instead of looking up .git/config which may not list the branch add `tea pulls clean` fixes #97 add copyright to new files make linter happy refactor: use new git functions for old code add `tea pulls checkout` Co-authored-by: Norwin Roosen <git@nroo.de> Co-authored-by: Norwin <git@nroo.de> Reviewed-on: https://gitea.com/gitea/tea/pulls/105 Reviewed-by: 6543 <6543@noreply.gitea.io> Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
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.
|
||||
// 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),
|
||||
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
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user