Skip to content

Commit

Permalink
repo: remotes family: implement list/get-url/set-url (#67)
Browse files Browse the repository at this point in the history
Co-authored-by: Joe Chen <[email protected]>
  • Loading branch information
jtagcat and unknwon authored Feb 25, 2022
1 parent e4129b1 commit 9a01961
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 7 deletions.
16 changes: 9 additions & 7 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import (
)

var (
ErrParentNotExist = errors.New("parent does not exist")
ErrSubmoduleNotExist = errors.New("submodule does not exist")
ErrRevisionNotExist = errors.New("revision does not exist")
ErrRemoteNotExist = errors.New("remote does not exist")
ErrExecTimeout = errors.New("execution was timed out")
ErrNoMergeBase = errors.New("no merge based was found")
ErrNotBlob = errors.New("the entry is not a blob")
ErrParentNotExist = errors.New("parent does not exist")
ErrSubmoduleNotExist = errors.New("submodule does not exist")
ErrRevisionNotExist = errors.New("revision does not exist")
ErrRemoteNotExist = errors.New("remote does not exist")
ErrURLNotExist = errors.New("URL does not exist")
ErrExecTimeout = errors.New("execution was timed out")
ErrNoMergeBase = errors.New("no merge based was found")
ErrNotBlob = errors.New("the entry is not a blob")
ErrNotDeleteNonPushURLs = errors.New("will not delete all non-push URLs")
)
199 changes: 199 additions & 0 deletions repo_remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,202 @@ func RepoRemoveRemote(repoPath, name string, opts ...RemoveRemoteOptions) error
func (r *Repository) RemoveRemote(name string, opts ...RemoveRemoteOptions) error {
return RepoRemoveRemote(r.path, name, opts...)
}

// RemotesOptions contains arguments for listing remotes of the repository.
// Docs: https://git-scm.com/docs/git-remote#_commands
type RemotesOptions struct {
// The timeout duration before giving up for each shell command execution.
// The default timeout duration will be used when not supplied.
Timeout time.Duration
}

// Remotes lists remotes of the repository in given path.
func Remotes(repoPath string, opts ...RemotesOptions) ([]string, error) {
var opt RemotesOptions
if len(opts) > 0 {
opt = opts[0]
}

stdout, err := NewCommand("remote").RunInDirWithTimeout(opt.Timeout, repoPath)
if err != nil {
return nil, err
}

return bytesToStrings(stdout), nil
}

// Remotes lists remotes of the repository.
func (r *Repository) Remotes(opts ...RemotesOptions) ([]string, error) {
return Remotes(r.path, opts...)
}

// RemoteGetURLOptions contains arguments for retrieving URL(s) of a remote of
// the repository.
//
// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emget-urlem
type RemoteGetURLOptions struct {
// Indicates whether to get push URLs instead of fetch URLs.
Push bool
// Indicates whether to get all URLs, including lists that are not part of main
// URLs. This option is independent of the Push option.
All bool
// The timeout duration before giving up for each shell command execution.
// The default timeout duration will be used when not supplied.
Timeout time.Duration
}

// RemoteGetURL retrieves URL(s) of a remote of the repository in given path.
func RemoteGetURL(repoPath, name string, opts ...RemoteGetURLOptions) ([]string, error) {
var opt RemoteGetURLOptions
if len(opts) > 0 {
opt = opts[0]
}

cmd := NewCommand("remote", "get-url")
if opt.Push {
cmd.AddArgs("--push")
}
if opt.All {
cmd.AddArgs("--all")
}

stdout, err := cmd.AddArgs(name).RunInDirWithTimeout(opt.Timeout, repoPath)
if err != nil {
return nil, err
}
return bytesToStrings(stdout), nil
}

// RemoteGetURL retrieves URL(s) of a remote of the repository in given path.
func (r *Repository) RemoteGetURL(name string, opts ...RemoteGetURLOptions) ([]string, error) {
return RemoteGetURL(r.path, name, opts...)
}

// RemoteSetURLOptions contains arguments for setting an URL of a remote of the
// repository.
//
// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emset-urlem
type RemoteSetURLOptions struct {
// Indicates whether to get push URLs instead of fetch URLs.
Push bool
// The regex to match existing URLs to replace (instead of first).
Regex string
// The timeout duration before giving up for each shell command execution.
// The default timeout duration will be used when not supplied.
Timeout time.Duration
}

// RemoteSetURL sets first URL of the remote with given name of the repository in given path.
func RemoteSetURL(repoPath, name, newurl string, opts ...RemoteSetURLOptions) error {
var opt RemoteSetURLOptions
if len(opts) > 0 {
opt = opts[0]
}

cmd := NewCommand("remote", "set-url")
if opt.Push {
cmd.AddArgs("--push")
}

cmd.AddArgs(name, newurl)

if opt.Regex != "" {
cmd.AddArgs(opt.Regex)
}

_, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath)
if err != nil {
if strings.Contains(err.Error(), "No such URL found") {
return ErrURLNotExist
} else if strings.Contains(err.Error(), "No such remote") {
return ErrRemoteNotExist
}
return err
}
return nil
}

// RemoteSetURL sets the first URL of the remote with given name of the repository.
func (r *Repository) RemoteSetURL(name, newurl string, opts ...RemoteSetURLOptions) error {
return RemoteSetURL(r.path, name, newurl, opts...)
}

// RemoteSetURLAddOptions contains arguments for appending an URL to a remote
// of the repository.
//
// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emset-urlem
type RemoteSetURLAddOptions struct {
// Indicates whether to get push URLs instead of fetch URLs.
Push bool
// The timeout duration before giving up for each shell command execution.
// The default timeout duration will be used when not supplied.
Timeout time.Duration
}

// RemoteSetURLAdd appends an URL to the remote with given name of the repository in
// given path. Use RemoteSetURL to overwrite the URL(s) instead.
func RemoteSetURLAdd(repoPath, name, newurl string, opts ...RemoteSetURLAddOptions) error {
var opt RemoteSetURLAddOptions
if len(opts) > 0 {
opt = opts[0]
}

cmd := NewCommand("remote", "set-url", "--add")
if opt.Push {
cmd.AddArgs("--push")
}

cmd.AddArgs(name, newurl)

_, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath)
if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") {
return ErrNotDeleteNonPushURLs
}
return err
}

// RemoteSetURLAdd appends an URL to the remote with given name of the repository.
// Use RemoteSetURL to overwrite the URL(s) instead.
func (r *Repository) RemoteSetURLAdd(name, newurl string, opts ...RemoteSetURLAddOptions) error {
return RemoteSetURLAdd(r.path, name, newurl, opts...)
}

// RemoteSetURLDeleteOptions contains arguments for deleting an URL of a remote
// of the repository.
//
// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emset-urlem
type RemoteSetURLDeleteOptions struct {
// Indicates whether to get push URLs instead of fetch URLs.
Push bool
// The timeout duration before giving up for each shell command execution.
// The default timeout duration will be used when not supplied.
Timeout time.Duration
}

// RemoteSetURLDelete deletes the remote with given name of the repository in
// given path.
func RemoteSetURLDelete(repoPath, name, regex string, opts ...RemoteSetURLDeleteOptions) error {
var opt RemoteSetURLDeleteOptions
if len(opts) > 0 {
opt = opts[0]
}

cmd := NewCommand("remote", "set-url", "--delete")
if opt.Push {
cmd.AddArgs("--push")
}

cmd.AddArgs(name, regex)

_, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath)
if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") {
return ErrNotDeleteNonPushURLs
}
return err
}

// RemoteSetURLDelete deletes all URLs matching regex of the remote with given
// name of the repository.
func (r *Repository) RemoteSetURLDelete(name, regex string, opts ...RemoteSetURLDeleteOptions) error {
return RemoteSetURLDelete(r.path, name, regex, opts...)
}
75 changes: 75 additions & 0 deletions repo_remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,78 @@ func TestRepository_RemoveRemote(t *testing.T) {
err = r.RemoveRemote("origin", RemoveRemoteOptions{})
assert.Equal(t, ErrRemoteNotExist, err)
}

func TestRepository_RemotesList(t *testing.T) {
r, cleanup, err := setupTempRepo()
if err != nil {
t.Fatal(err)
}
defer cleanup()

// 1 remote
remotes, err := r.Remotes()
assert.Nil(t, err)
assert.Equal(t, []string{"origin"}, remotes)

// 2 remotes
err = r.AddRemote("t", "t")
assert.Nil(t, err)

remotes, err = r.Remotes()
assert.Nil(t, err)
assert.Equal(t, []string{"origin", "t"}, remotes)
assert.Len(t, remotes, 2)

// 0 remotes
err = r.RemoveRemote("t")
assert.Nil(t, err)
err = r.RemoveRemote("origin")
assert.Nil(t, err)

remotes, err = r.Remotes()
assert.Nil(t, err)
assert.Equal(t, []string{}, remotes)
assert.Len(t, remotes, 0)
}

func TestRepository_RemoteURLFamily(t *testing.T) {
r, cleanup, err := setupTempRepo()
if err != nil {
t.Fatal(err)
}
defer cleanup()

err = r.RemoteSetURLDelete("origin", ".*")
assert.Equal(t, ErrNotDeleteNonPushURLs, err)

err = r.RemoteSetURL("notexist", "t")
assert.Equal(t, ErrRemoteNotExist, err)

err = r.RemoteSetURL("notexist", "t", RemoteSetURLOptions{Regex: "t"})
assert.Equal(t, ErrRemoteNotExist, err)

// Default origin URL is not easily testable
err = r.RemoteSetURL("origin", "t")
assert.Nil(t, err)
urls, err := r.RemoteGetURL("origin")
assert.Nil(t, err)
assert.Equal(t, []string{"t"}, urls)

err = r.RemoteSetURLAdd("origin", "e")
assert.Nil(t, err)
urls, err = r.RemoteGetURL("origin", RemoteGetURLOptions{All: true})
assert.Nil(t, err)
assert.Equal(t, []string{"t", "e"}, urls)

err = r.RemoteSetURL("origin", "s", RemoteSetURLOptions{Regex: "e"})
assert.Nil(t, err)
urls, err = r.RemoteGetURL("origin", RemoteGetURLOptions{All: true})
assert.Nil(t, err)
assert.Equal(t, []string{"t", "s"}, urls)

err = r.RemoteSetURLDelete("origin", "t")
assert.Nil(t, err)
urls, err = r.RemoteGetURL("origin", RemoteGetURLOptions{All: true})
assert.Nil(t, err)
assert.Equal(t, []string{"s"}, urls)
}
11 changes: 11 additions & 0 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package git
import (
"fmt"
"os"
"strings"
"sync"
)

Expand Down Expand Up @@ -71,3 +72,13 @@ func concatenateError(err error, stderr string) error {
}
return fmt.Errorf("%v - %s", err, stderr)
}

// bytesToStrings splits given bytes into strings by line separator ("\n").
// It returns empty slice if the given bytes only contains line separators.
func bytesToStrings(in []byte) []string {
s := strings.TrimRight(string(in), "\n")
if s == "" { // empty (not {""}, len=1)
return []string{}
}
return strings.Split(s, "\n")
}

0 comments on commit 9a01961

Please sign in to comment.