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 3 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
2 changes: 2 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ var (
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")
ErrDelAllNonPushURL = errors.New("will not delete all non-push URLs")
)
177 changes: 177 additions & 0 deletions repo_remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,180 @@ func RepoRemoveRemote(repoPath, name string, opts ...RemoveRemoteOptions) error
func (r *Repository) RemoveRemote(name string, opts ...RemoveRemoteOptions) error {
return RepoRemoveRemote(r.path, name, opts...)
}

// RemotesListOptions contains arguments for listing remotes of the repository.
// Docs: https://git-scm.com/docs/git-remote#_commands
type RemotesListOptions 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
}

// RepoRemotes lists remotes of the repository in given path.
func RepoRemotes(repoPath string, opts ...RemotesListOptions) ([]string, error) {
var opt RemotesListOptions
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 ...RemotesListOptions) ([]string, error) {
return RepoRemotes(r.path, opts...)
}

// RemoteURLGetOptions 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 RemoteURLGetOptions struct {
// Indicates whether to get push URLs instead of fetch URLs.
Push bool
// True: get all URLs (lists also non-main URLs; not related with Push)
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
}

// RepoRemoteURLGet retrieves URL(s) of a remote of the repository in given path.
func RepoRemoteURLGet(repoPath, name string, opts ...RemoteURLGetOptions) ([]string, error) {
var opt RemoteURLGetOptions
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
}

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

// RemoteURLSetOptions contains arguments for setting an URL of a remote of the repository.
// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emget-urlem
type RemoteURLSetOptions struct {
// False: set fetch URLs
// True: set push 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
}

// RepoRemoteURLSetFirst sets first URL of the remote with given name of the repository in given path.
func RepoRemoteURLSetFirst(repoPath, name, newurl string, opts ...RemoteURLSetOptions) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is the "first" come into play? Not seeing "first" is used as git args?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uhh yeah so.. repo can have like
origin urls:

  • type fetch url: foobar1
  • type push url: foobar2
  • type fetch url: foobar3
  • type fetch url: foobar4

^^^ in this case, the first fetch url is foobar1, and first push url is foobar3

In reality, this almost never happens, and even if it does, the 'first' is used elsewhere as well.

It might be a good idea to add a short 'it is almost always first, and if not, you probably don't have to worry', but don't know on how to word it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the context!

Either way I think we do not need explicitly put "first" in our function name because we are basically pass-on the value from Git CLI, and whatever it returns is what we return to the caller.

var opt RemoteURLSetOptions
if len(opts) > 0 {
opt = opts[0]
}

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

_, err := cmd.AddArgs(name, newurl).RunInDirWithTimeout(opt.Timeout, repoPath)
if err != nil && strings.Contains(err.Error(), "No such remote") {
return ErrRemoteNotExist
}
return err
}

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

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

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

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

// RemoteURLSetRegex sets the first URL of the remote with given name and URL regex match of the repository.
func (r *Repository) RemoteURLSetRegex(name, urlregex, newurl string, opts ...RemoteURLSetOptions) error {
return RepoRemoteURLSetRegex(r.path, name, urlregex, newurl, opts...)
}

// RepoRemoteURLAdd adds an URL to the remote with given name of the repository in given path.
func RepoRemoteURLAdd(repoPath, name, newurl string, opts ...RemoteURLSetOptions) error {
var opt RemoteURLSetOptions
if len(opts) > 0 {
opt = opts[0]
}

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

_, err := cmd.AddArgs(name, newurl).RunInDirWithTimeout(opt.Timeout, repoPath)
return err
}

// RemoteURLAdd adds an URL to the remote with given name of the repository.
func (r *Repository) RemoteURLAdd(name, newvalue string, opts ...RemoteURLSetOptions) error {
return RepoRemoteURLAdd(r.path, name, newvalue, opts...)
}

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

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

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

// RemoteURLDelRegex deletes all URLs matching regex of the remote with given name of the repository.
func (r *Repository) RemoteURLDelRegex(name, urlregex string, opts ...RemoteURLSetOptions) error {
return RepoRemoteURLDelRegex(r.path, name, urlregex, 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.RemoteURLDelRegex("origin", ".*")
assert.Equal(t, ErrDelAllNonPushURL, err)

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

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

// default origin URL is not easily testable
err = r.RemoteURLSetFirst("origin", "t")
assert.Nil(t, err)
URLs, err := r.RemoteURLGet("origin")
assert.Nil(t, err)
assert.Equal(t, []string{"t"}, URLs)

err = r.RemoteURLAdd("origin", "e")
assert.Nil(t, err)
URLs, err = r.RemoteURLGet("origin", RemoteURLGetOptions{All: true})
assert.Nil(t, err)
assert.Equal(t, []string{"t", "e"}, URLs)

err = r.RemoteURLSetRegex("origin", "e", "s")
assert.Nil(t, err)
URLs, err = r.RemoteURLGet("origin", RemoteURLGetOptions{All: true})
assert.Nil(t, err)
assert.Equal(t, []string{"t", "s"}, URLs)

err = r.RemoteURLDelRegex("origin", "t")
assert.Nil(t, err)
URLs, err = r.RemoteURLGet("origin", RemoteURLGetOptions{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")
}