1package git23import (4 "bytes"5 "context"6 "strings"7)89// LsRemoteOptions contains arguments for listing references in a remote10// repository.11//12// Docs: https://git-scm.com/docs/git-ls-remote13type LsRemoteOptions struct {14 // Indicates whether include heads.15 Heads bool16 // Indicates whether include tags.17 Tags bool18 // Indicates whether to not show peeled tags or pseudo refs.19 Refs bool20 // The list of patterns to filter results.21 Patterns []string22 CommandOptions23}2425// LsRemote returns a list of references in the remote repository.26func LsRemote(ctx context.Context, url string, opts ...LsRemoteOptions) ([]*Reference, error) {27 var opt LsRemoteOptions28 if len(opts) > 0 {29 opt = opts[0]30 }3132 args := []string{"ls-remote", "--quiet"}33 if opt.Heads {34 args = append(args, "--heads")35 }36 if opt.Tags {37 args = append(args, "--tags")38 }39 if opt.Refs {40 args = append(args, "--refs")41 }42 args = append(args, "--end-of-options", url)43 if len(opt.Patterns) > 0 {44 args = append(args, opt.Patterns...)45 }4647 stdout, err := exec(ctx, "", args, opt.Envs)48 if err != nil {49 return nil, err50 }5152 lines := bytes.Split(stdout, []byte("\n"))53 refs := make([]*Reference, 0, len(lines))54 for i := range lines {55 fields := bytes.Fields(lines[i])56 if len(fields) < 2 {57 continue58 }5960 refs = append(refs, &Reference{61 ID: string(fields[0]),62 Refspec: string(fields[1]),63 })64 }65 return refs, nil66}6768// IsURLAccessible returns true if given remote URL is accessible via Git. The69// caller should use context.WithTimeout to control the timeout.70func IsURLAccessible(ctx context.Context, url string) bool {71 _, err := LsRemote(ctx, url, LsRemoteOptions{72 Patterns: []string{"HEAD"},73 })74 return err == nil75}7677// RemoteAddOptions contains options to add a remote address.78//79// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emaddem80type RemoteAddOptions struct {81 // Indicates whether to execute git fetch after the remote information is set82 // up.83 Fetch bool84 // Indicates whether to add remote as mirror with --mirror=fetch.85 MirrorFetch bool86 CommandOptions87}8889// RemoteAdd adds a new remote to the repository.90func (r *Repository) RemoteAdd(ctx context.Context, name, url string, opts ...RemoteAddOptions) error {91 var opt RemoteAddOptions92 if len(opts) > 0 {93 opt = opts[0]94 }9596 args := []string{"remote", "add"}97 if opt.Fetch {98 args = append(args, "-f")99 }100 if opt.MirrorFetch {101 args = append(args, "--mirror=fetch")102 }103 args = append(args, "--end-of-options", name, url)104105 _, err := exec(ctx, r.path, args, opt.Envs)106 return err107}108109// RemoteRemoveOptions contains arguments for removing a remote from the110// repository.111//112// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emremoveem113type RemoteRemoveOptions struct {114 CommandOptions115}116117// RemoteRemove removes a remote from the repository.118func (r *Repository) RemoteRemove(ctx context.Context, name string, opts ...RemoteRemoveOptions) error {119 var opt RemoteRemoveOptions120 if len(opts) > 0 {121 opt = opts[0]122 }123124 args := []string{"remote", "remove", "--end-of-options", name}125 _, err := exec(ctx, r.path, args, opt.Envs)126 if err != nil {127 if strings.Contains(err.Error(), "error: No such remote") ||128 strings.Contains(err.Error(), "fatal: No such remote") {129 return ErrRemoteNotExist130 }131 return err132 }133 return nil134}135136// RemotesOptions contains arguments for listing remotes of the repository.137// /138// Docs: https://git-scm.com/docs/git-remote#_commands139type RemotesOptions struct {140 CommandOptions141}142143// Remotes lists remotes of the repository.144func (r *Repository) Remotes(ctx context.Context, opts ...RemotesOptions) ([]string, error) {145 var opt RemotesOptions146 if len(opts) > 0 {147 opt = opts[0]148 }149150 args := []string{"remote", "--end-of-options"}151 stdout, err := exec(ctx, r.path, args, opt.Envs)152 if err != nil {153 return nil, err154 }155156 return bytesToStrings(stdout), nil157}158159// RemoteGetURLOptions contains arguments for retrieving URL(s) of a remote of160// the repository.161//162// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emget-urlem163type RemoteGetURLOptions struct {164 // Indicates whether to get push URLs instead of fetch URLs.165 Push bool166 // Indicates whether to get all URLs, including lists that are not part of main167 // URLs. This option is independent of the Push option.168 All bool169 CommandOptions170}171172// RemoteGetURL retrieves URL(s) of a remote of the repository.173func (r *Repository) RemoteGetURL(ctx context.Context, name string, opts ...RemoteGetURLOptions) ([]string, error) {174 var opt RemoteGetURLOptions175 if len(opts) > 0 {176 opt = opts[0]177 }178179 args := []string{"remote", "get-url"}180 if opt.Push {181 args = append(args, "--push")182 }183 if opt.All {184 args = append(args, "--all")185 }186 args = append(args, "--end-of-options", name)187188 stdout, err := exec(ctx, r.path, args, opt.Envs)189 if err != nil {190 return nil, err191 }192 return bytesToStrings(stdout), nil193}194195// RemoteSetURLOptions contains arguments for setting an URL of a remote of the196// repository.197//198// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emset-urlem199type RemoteSetURLOptions struct {200 // Indicates whether to get push URLs instead of fetch URLs.201 Push bool202 // The regex to match existing URLs to replace (instead of first).203 Regex string204 CommandOptions205}206207// RemoteSetURL sets the first URL of the remote with given name of the208// repository.209func (r *Repository) RemoteSetURL(ctx context.Context, name, newurl string, opts ...RemoteSetURLOptions) error {210 var opt RemoteSetURLOptions211 if len(opts) > 0 {212 opt = opts[0]213 }214215 args := []string{"remote", "set-url"}216 if opt.Push {217 args = append(args, "--push")218 }219 args = append(args, "--end-of-options", name, newurl)220 if opt.Regex != "" {221 args = append(args, opt.Regex)222 }223224 _, err := exec(ctx, r.path, args, opt.Envs)225 if err != nil {226 if strings.Contains(err.Error(), "No such URL found") {227 return ErrURLNotExist228 } else if strings.Contains(err.Error(), "No such remote") {229 return ErrRemoteNotExist230 }231 return err232 }233 return nil234}235236// RemoteSetURLAddOptions contains arguments for appending an URL to a remote237// of the repository.238//239// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emset-urlem240type RemoteSetURLAddOptions struct {241 // Indicates whether to get push URLs instead of fetch URLs.242 Push bool243 CommandOptions244}245246// RemoteSetURLAdd appends an URL to the remote with given name of the247// repository. Use RemoteSetURL to overwrite the URL(s) instead.248func (r *Repository) RemoteSetURLAdd(ctx context.Context, name, newurl string, opts ...RemoteSetURLAddOptions) error {249 var opt RemoteSetURLAddOptions250 if len(opts) > 0 {251 opt = opts[0]252 }253254 args := []string{"remote", "set-url", "--add"}255 if opt.Push {256 args = append(args, "--push")257 }258 args = append(args, "--end-of-options", name, newurl)259260 _, err := exec(ctx, r.path, args, opt.Envs)261 if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") {262 return ErrNotDeleteNonPushURLs263 }264 return err265}266267// RemoteSetURLDeleteOptions contains arguments for deleting an URL of a remote268// of the repository.269//270// Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emset-urlem271type RemoteSetURLDeleteOptions struct {272 // Indicates whether to get push URLs instead of fetch URLs.273 Push bool274 CommandOptions275}276277// RemoteSetURLDelete deletes all URLs matching regex of the remote with given278// name of the repository.279func (r *Repository) RemoteSetURLDelete(ctx context.Context, name, regex string, opts ...RemoteSetURLDeleteOptions) error {280 var opt RemoteSetURLDeleteOptions281 if len(opts) > 0 {282 opt = opts[0]283 }284285 args := []string{"remote", "set-url", "--delete"}286 if opt.Push {287 args = append(args, "--push")288 }289 args = append(args, "--end-of-options", name, regex)290291 _, err := exec(ctx, r.path, args, opt.Envs)292 if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") {293 return ErrNotDeleteNonPushURLs294 }295 return err296}