add tea pulls checkout
command (#93) #103
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
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)
|
||||
}
|
||||
|
|
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)
|
||||
}
|
||||
|
|
46
modules/git/branch.go
Normal file
46
modules/git/branch.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
// 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"
|
||||
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})
|
||||
}
|
51
modules/git/remote.go
Normal file
51
modules/git/remote.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
// 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"
|
||||
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
|
||||
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
|
||||
}
|
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
|
||||
}
|
|
@ -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