Skip to content

repo: remotes family: implement list/get-url/set-url #67

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
}