Skip to content

Commit 9a01961

Browse files
jtagcatunknwon
andauthored
repo: remotes family: implement list/get-url/set-url (#67)
Co-authored-by: Joe Chen <[email protected]>
1 parent e4129b1 commit 9a01961

File tree

4 files changed

+294
-7
lines changed

4 files changed

+294
-7
lines changed

error.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import (
99
)
1010

1111
var (
12-
ErrParentNotExist = errors.New("parent does not exist")
13-
ErrSubmoduleNotExist = errors.New("submodule does not exist")
14-
ErrRevisionNotExist = errors.New("revision does not exist")
15-
ErrRemoteNotExist = errors.New("remote does not exist")
16-
ErrExecTimeout = errors.New("execution was timed out")
17-
ErrNoMergeBase = errors.New("no merge based was found")
18-
ErrNotBlob = errors.New("the entry is not a blob")
12+
ErrParentNotExist = errors.New("parent does not exist")
13+
ErrSubmoduleNotExist = errors.New("submodule does not exist")
14+
ErrRevisionNotExist = errors.New("revision does not exist")
15+
ErrRemoteNotExist = errors.New("remote does not exist")
16+
ErrURLNotExist = errors.New("URL does not exist")
17+
ErrExecTimeout = errors.New("execution was timed out")
18+
ErrNoMergeBase = errors.New("no merge based was found")
19+
ErrNotBlob = errors.New("the entry is not a blob")
20+
ErrNotDeleteNonPushURLs = errors.New("will not delete all non-push URLs")
1921
)

repo_remote.go

+199
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,202 @@ func RepoRemoveRemote(repoPath, name string, opts ...RemoveRemoteOptions) error
146146
func (r *Repository) RemoveRemote(name string, opts ...RemoveRemoteOptions) error {
147147
return RepoRemoveRemote(r.path, name, opts...)
148148
}
149+
150+
// RemotesOptions contains arguments for listing remotes of the repository.
151+
// Docs: https://git-scm.com/docs/git-remote#_commands
152+
type RemotesOptions struct {
153+
// The timeout duration before giving up for each shell command execution.
154+
// The default timeout duration will be used when not supplied.
155+
Timeout time.Duration
156+
}
157+
158+
// Remotes lists remotes of the repository in given path.
159+
func Remotes(repoPath string, opts ...RemotesOptions) ([]string, error) {
160+
var opt RemotesOptions
161+
if len(opts) > 0 {
162+
opt = opts[0]
163+
}
164+
165+
stdout, err := NewCommand("remote").RunInDirWithTimeout(opt.Timeout, repoPath)
166+
if err != nil {
167+
return nil, err
168+
}
169+
170+
return bytesToStrings(stdout), nil
171+
}
172+
173+
// Remotes lists remotes of the repository.
174+
func (r *Repository) Remotes(opts ...RemotesOptions) ([]string, error) {
175+
return Remotes(r.path, opts...)
176+
}
177+
178+
// RemoteGetURLOptions contains arguments for retrieving URL(s) of a remote of
179+
// the repository.
180+
//
181+
// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emget-urlem
182+
type RemoteGetURLOptions struct {
183+
// Indicates whether to get push URLs instead of fetch URLs.
184+
Push bool
185+
// Indicates whether to get all URLs, including lists that are not part of main
186+
// URLs. This option is independent of the Push option.
187+
All bool
188+
// The timeout duration before giving up for each shell command execution.
189+
// The default timeout duration will be used when not supplied.
190+
Timeout time.Duration
191+
}
192+
193+
// RemoteGetURL retrieves URL(s) of a remote of the repository in given path.
194+
func RemoteGetURL(repoPath, name string, opts ...RemoteGetURLOptions) ([]string, error) {
195+
var opt RemoteGetURLOptions
196+
if len(opts) > 0 {
197+
opt = opts[0]
198+
}
199+
200+
cmd := NewCommand("remote", "get-url")
201+
if opt.Push {
202+
cmd.AddArgs("--push")
203+
}
204+
if opt.All {
205+
cmd.AddArgs("--all")
206+
}
207+
208+
stdout, err := cmd.AddArgs(name).RunInDirWithTimeout(opt.Timeout, repoPath)
209+
if err != nil {
210+
return nil, err
211+
}
212+
return bytesToStrings(stdout), nil
213+
}
214+
215+
// RemoteGetURL retrieves URL(s) of a remote of the repository in given path.
216+
func (r *Repository) RemoteGetURL(name string, opts ...RemoteGetURLOptions) ([]string, error) {
217+
return RemoteGetURL(r.path, name, opts...)
218+
}
219+
220+
// RemoteSetURLOptions contains arguments for setting an URL of a remote of the
221+
// repository.
222+
//
223+
// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emset-urlem
224+
type RemoteSetURLOptions struct {
225+
// Indicates whether to get push URLs instead of fetch URLs.
226+
Push bool
227+
// The regex to match existing URLs to replace (instead of first).
228+
Regex string
229+
// The timeout duration before giving up for each shell command execution.
230+
// The default timeout duration will be used when not supplied.
231+
Timeout time.Duration
232+
}
233+
234+
// RemoteSetURL sets first URL of the remote with given name of the repository in given path.
235+
func RemoteSetURL(repoPath, name, newurl string, opts ...RemoteSetURLOptions) error {
236+
var opt RemoteSetURLOptions
237+
if len(opts) > 0 {
238+
opt = opts[0]
239+
}
240+
241+
cmd := NewCommand("remote", "set-url")
242+
if opt.Push {
243+
cmd.AddArgs("--push")
244+
}
245+
246+
cmd.AddArgs(name, newurl)
247+
248+
if opt.Regex != "" {
249+
cmd.AddArgs(opt.Regex)
250+
}
251+
252+
_, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath)
253+
if err != nil {
254+
if strings.Contains(err.Error(), "No such URL found") {
255+
return ErrURLNotExist
256+
} else if strings.Contains(err.Error(), "No such remote") {
257+
return ErrRemoteNotExist
258+
}
259+
return err
260+
}
261+
return nil
262+
}
263+
264+
// RemoteSetURL sets the first URL of the remote with given name of the repository.
265+
func (r *Repository) RemoteSetURL(name, newurl string, opts ...RemoteSetURLOptions) error {
266+
return RemoteSetURL(r.path, name, newurl, opts...)
267+
}
268+
269+
// RemoteSetURLAddOptions contains arguments for appending an URL to a remote
270+
// of the repository.
271+
//
272+
// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emset-urlem
273+
type RemoteSetURLAddOptions struct {
274+
// Indicates whether to get push URLs instead of fetch URLs.
275+
Push bool
276+
// The timeout duration before giving up for each shell command execution.
277+
// The default timeout duration will be used when not supplied.
278+
Timeout time.Duration
279+
}
280+
281+
// RemoteSetURLAdd appends an URL to the remote with given name of the repository in
282+
// given path. Use RemoteSetURL to overwrite the URL(s) instead.
283+
func RemoteSetURLAdd(repoPath, name, newurl string, opts ...RemoteSetURLAddOptions) error {
284+
var opt RemoteSetURLAddOptions
285+
if len(opts) > 0 {
286+
opt = opts[0]
287+
}
288+
289+
cmd := NewCommand("remote", "set-url", "--add")
290+
if opt.Push {
291+
cmd.AddArgs("--push")
292+
}
293+
294+
cmd.AddArgs(name, newurl)
295+
296+
_, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath)
297+
if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") {
298+
return ErrNotDeleteNonPushURLs
299+
}
300+
return err
301+
}
302+
303+
// RemoteSetURLAdd appends an URL to the remote with given name of the repository.
304+
// Use RemoteSetURL to overwrite the URL(s) instead.
305+
func (r *Repository) RemoteSetURLAdd(name, newurl string, opts ...RemoteSetURLAddOptions) error {
306+
return RemoteSetURLAdd(r.path, name, newurl, opts...)
307+
}
308+
309+
// RemoteSetURLDeleteOptions contains arguments for deleting an URL of a remote
310+
// of the repository.
311+
//
312+
// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emset-urlem
313+
type RemoteSetURLDeleteOptions struct {
314+
// Indicates whether to get push URLs instead of fetch URLs.
315+
Push bool
316+
// The timeout duration before giving up for each shell command execution.
317+
// The default timeout duration will be used when not supplied.
318+
Timeout time.Duration
319+
}
320+
321+
// RemoteSetURLDelete deletes the remote with given name of the repository in
322+
// given path.
323+
func RemoteSetURLDelete(repoPath, name, regex string, opts ...RemoteSetURLDeleteOptions) error {
324+
var opt RemoteSetURLDeleteOptions
325+
if len(opts) > 0 {
326+
opt = opts[0]
327+
}
328+
329+
cmd := NewCommand("remote", "set-url", "--delete")
330+
if opt.Push {
331+
cmd.AddArgs("--push")
332+
}
333+
334+
cmd.AddArgs(name, regex)
335+
336+
_, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath)
337+
if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") {
338+
return ErrNotDeleteNonPushURLs
339+
}
340+
return err
341+
}
342+
343+
// RemoteSetURLDelete deletes all URLs matching regex of the remote with given
344+
// name of the repository.
345+
func (r *Repository) RemoteSetURLDelete(name, regex string, opts ...RemoteSetURLDeleteOptions) error {
346+
return RemoteSetURLDelete(r.path, name, regex, opts...)
347+
}

repo_remote_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,78 @@ func TestRepository_RemoveRemote(t *testing.T) {
131131
err = r.RemoveRemote("origin", RemoveRemoteOptions{})
132132
assert.Equal(t, ErrRemoteNotExist, err)
133133
}
134+
135+
func TestRepository_RemotesList(t *testing.T) {
136+
r, cleanup, err := setupTempRepo()
137+
if err != nil {
138+
t.Fatal(err)
139+
}
140+
defer cleanup()
141+
142+
// 1 remote
143+
remotes, err := r.Remotes()
144+
assert.Nil(t, err)
145+
assert.Equal(t, []string{"origin"}, remotes)
146+
147+
// 2 remotes
148+
err = r.AddRemote("t", "t")
149+
assert.Nil(t, err)
150+
151+
remotes, err = r.Remotes()
152+
assert.Nil(t, err)
153+
assert.Equal(t, []string{"origin", "t"}, remotes)
154+
assert.Len(t, remotes, 2)
155+
156+
// 0 remotes
157+
err = r.RemoveRemote("t")
158+
assert.Nil(t, err)
159+
err = r.RemoveRemote("origin")
160+
assert.Nil(t, err)
161+
162+
remotes, err = r.Remotes()
163+
assert.Nil(t, err)
164+
assert.Equal(t, []string{}, remotes)
165+
assert.Len(t, remotes, 0)
166+
}
167+
168+
func TestRepository_RemoteURLFamily(t *testing.T) {
169+
r, cleanup, err := setupTempRepo()
170+
if err != nil {
171+
t.Fatal(err)
172+
}
173+
defer cleanup()
174+
175+
err = r.RemoteSetURLDelete("origin", ".*")
176+
assert.Equal(t, ErrNotDeleteNonPushURLs, err)
177+
178+
err = r.RemoteSetURL("notexist", "t")
179+
assert.Equal(t, ErrRemoteNotExist, err)
180+
181+
err = r.RemoteSetURL("notexist", "t", RemoteSetURLOptions{Regex: "t"})
182+
assert.Equal(t, ErrRemoteNotExist, err)
183+
184+
// Default origin URL is not easily testable
185+
err = r.RemoteSetURL("origin", "t")
186+
assert.Nil(t, err)
187+
urls, err := r.RemoteGetURL("origin")
188+
assert.Nil(t, err)
189+
assert.Equal(t, []string{"t"}, urls)
190+
191+
err = r.RemoteSetURLAdd("origin", "e")
192+
assert.Nil(t, err)
193+
urls, err = r.RemoteGetURL("origin", RemoteGetURLOptions{All: true})
194+
assert.Nil(t, err)
195+
assert.Equal(t, []string{"t", "e"}, urls)
196+
197+
err = r.RemoteSetURL("origin", "s", RemoteSetURLOptions{Regex: "e"})
198+
assert.Nil(t, err)
199+
urls, err = r.RemoteGetURL("origin", RemoteGetURLOptions{All: true})
200+
assert.Nil(t, err)
201+
assert.Equal(t, []string{"t", "s"}, urls)
202+
203+
err = r.RemoteSetURLDelete("origin", "t")
204+
assert.Nil(t, err)
205+
urls, err = r.RemoteGetURL("origin", RemoteGetURLOptions{All: true})
206+
assert.Nil(t, err)
207+
assert.Equal(t, []string{"s"}, urls)
208+
}

utils.go

+11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package git
77
import (
88
"fmt"
99
"os"
10+
"strings"
1011
"sync"
1112
)
1213

@@ -71,3 +72,13 @@ func concatenateError(err error, stderr string) error {
7172
}
7273
return fmt.Errorf("%v - %s", err, stderr)
7374
}
75+
76+
// bytesToStrings splits given bytes into strings by line separator ("\n").
77+
// It returns empty slice if the given bytes only contains line separators.
78+
func bytesToStrings(in []byte) []string {
79+
s := strings.TrimRight(string(in), "\n")
80+
if s == "" { // empty (not {""}, len=1)
81+
return []string{}
82+
}
83+
return strings.Split(s, "\n")
84+
}

0 commit comments

Comments
 (0)