add tea pulls checkout
command (#93) #103
86
cmd/pulls.go
86
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: "<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 := 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)
|
||||
}
|
||||
|
|
42
modules/git/branch.go
Normal file
42
modules/git/branch.go
Normal file
|
@ -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})
|
||||
}
|
47
modules/git/remote.go
Normal file
47
modules/git/remote.go
Normal file
|
@ -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
|
||||
}
|
23
modules/git/repo.go
Normal file
23
modules/git/repo.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user