github.com/driusan/dgit@v0.0.0-20221118233547-f39f0c15edbb/git/remote.go (about) 1 package git 2 3 import ( 4 "fmt" 5 "io" 6 "net/url" 7 "path/filepath" 8 "strings" 9 ) 10 11 type Remote string 12 13 func (r Remote) RemoteURL(c *Client) (string, error) { 14 config, err := LoadLocalConfig(c) 15 if err != nil { 16 return "", err 17 } 18 if strings.Index(r.String(), "://") != -1 { 19 // It's already a URL 20 return string(r), nil 21 } 22 if File(r.String()).Exists() { 23 // It's a known file path, so convert it to a file url 24 // and let localConn handle it. 25 // It needs to be absolute for the file:// url to work. 26 abs, err := filepath.Abs(string(r)) 27 if err != nil { 28 return "", err 29 } 30 return "file://" + abs, nil 31 } 32 // If it might be a remote name, look it up in the config. 33 cfg, _ := config.GetConfig(fmt.Sprintf("remote.%v.url", r)) 34 if cfg == "" { 35 return "", fmt.Errorf("Unknown remote") 36 } 37 if File(cfg).Exists() { 38 // The config pointed to a path but we already handled 39 // that case above, so now try it with the config setting 40 abs, err := filepath.Abs(string(cfg)) 41 if err != nil { 42 return "", err 43 } 44 return "file://" + abs, nil 45 } 46 return cfg, nil 47 } 48 49 // Returns the URL used to fetch from remote. 50 func (r Remote) FetchURL(c *Client) (string, error) { 51 // FIXME: Handle fetch url config settings if they don't match url 52 return r.RemoteURL(c) 53 } 54 55 // Returns the URL used to push to a remote. 56 func (r Remote) PushURL(c *Client) (string, error) { 57 if config := c.GetConfig("remote." + r.String() + ".pushurl"); config != "" { 58 return config, nil 59 } 60 if config := c.GetConfig("remote." + r.String() + ".url"); config != "" { 61 return config, nil 62 } 63 return r.RemoteURL(c) 64 } 65 66 func (r Remote) String() string { 67 return string(r) 68 } 69 70 func (r Remote) Name() string { 71 return string(r) 72 } 73 74 func (r Remote) IsStateless(c *Client) (bool, error) { 75 url, err := r.RemoteURL(c) 76 if err != nil { 77 return false, err 78 } 79 url = strings.ToLower(url) 80 return strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://"), nil 81 } 82 83 // Returns true if the remote points to a local filesystem path. 84 func (r Remote) IsFile() bool { 85 return strings.HasPrefix(r.String(), "file://") || File(r.String()).Exists() 86 } 87 88 // Gets a list of local references cached for this Remote 89 func (r Remote) GetLocalRefs(c *Client) ([]Ref, error) { 90 allrefs, err := ShowRef(c, ShowRefOptions{}, nil) 91 if err != nil { 92 return nil, err 93 } 94 ourrefs := make([]Ref, 0, len(allrefs)) 95 for _, rf := range allrefs { 96 if strings.HasPrefix(rf.Name, "refs/remotes/"+r.String()) { 97 ourrefs = append(ourrefs, rf) 98 } 99 } 100 return ourrefs, nil 101 } 102 103 type GitService uint8 104 105 const ( 106 UploadPackService = GitService(iota) 107 ReceivePackService 108 ) 109 110 // A RemoteConn represends a connection to a remote which communicates 111 // with the remote. 112 type RemoteConn interface { 113 // Opens a connection to the remote. This requires at least one round 114 // trip to the service and may mutate the state of this RemoteConn. 115 // 116 // After calling this, the RemoteConn should be in a useable state. 117 OpenConn(GitService) error 118 119 // Gets a list of references on the remote. If patterns is specified, 120 // restrict to refs which match the pattern. If not, return all 121 // refs 122 GetRefs(opts LsRemoteOptions, patterns []string) ([]Ref, error) 123 124 // Close the underlying connection to this service. 125 Close() error 126 127 // Sets the name of git-upload-pack to use for this remote, where 128 // applicable. This must be called before OpenConn. 129 // When called on a transport type that does not support it (such 130 // as the git transport protocol), it will return a nil error. An 131 // error indicates that the protocol *should* support the operation 132 // but was unable to set the variable. 133 SetService(string) error 134 135 // Gets the protocol version that was negotiated during connection 136 // opening. Only valid after calling OpenConn. 137 ProtocolVersion() uint8 138 139 // Returns the capabilities determined during the initial protocol 140 // connection. 141 // 142 // The first index is the capability, the second is the arguments 143 // defined for it. 144 Capabilities() map[string]map[string]struct{} 145 146 // Tells the connection to print any sideband data to w 147 SetSideband(w io.Writer) 148 149 // A RemoteConn should act as a writter. When written to, it should 150 // write to the underlying connection in pkt-line format or directly 151 // as per SetWriteMode. 152 io.Writer 153 154 // Reading from a RemoteConn should return the data after decoding 155 // the line length from a pktline. 156 // The behaviour of the read depends on the PackProtocolMode set 157 // by SetReadMode 158 io.Reader 159 160 // Determines how reading from the connection returns data to the 161 // caller. 162 SetReadMode(mode PackProtocolMode) 163 164 // Determines how reading from the connection returns data to the 165 // caller. 166 SetWriteMode(mode PackProtocolMode) 167 168 // Send a flush packet to the connection 169 Flush() error 170 171 // Sends a Delimiter packet in protocol V2 172 Delim() error 173 } 174 175 func NewRemoteConn(c *Client, r Remote) (RemoteConn, error) { 176 urls, err := r.RemoteURL(c) 177 if err != nil { 178 return nil, err 179 } 180 uri, err := url.Parse(urls) 181 if err != nil { 182 return nil, err 183 } 184 switch uri.Scheme { 185 case "http", "https": 186 conn := &smartHTTPConn{ 187 sharedRemoteConn: &sharedRemoteConn{uri: uri}, 188 giturl: urls, 189 } 190 return conn, nil 191 case "git": 192 conn := &gitConn{ 193 sharedRemoteConn: &sharedRemoteConn{uri: uri}, 194 } 195 return conn, nil 196 case "ssh": 197 return &sshConn{ 198 sharedRemoteConn: &sharedRemoteConn{uri: uri}, 199 }, nil 200 case "file": 201 return &localConn{ 202 sharedRemoteConn: &sharedRemoteConn{uri: uri}, 203 }, nil 204 default: 205 return nil, fmt.Errorf("Unsupported remote type for: %v", r) 206 } 207 } 208 209 // Helper for implenting things which are shared across all RemoteConn 210 // implementations 211 type sharedRemoteConn struct { 212 uri *url.URL 213 protocolversion uint8 214 capabilities map[string]map[string]struct{} 215 216 // The remote service to be invoked 217 218 service string 219 // References advertised during opening of connection. Only valid 220 // for protocol v1 221 refs []Ref 222 223 *packProtocolReader 224 225 writemode PackProtocolMode 226 } 227 228 func (r *sharedRemoteConn) SetService(s string) error { 229 r.service = s 230 return nil 231 } 232 233 func (r *sharedRemoteConn) SetWriteMode(m PackProtocolMode) { 234 r.writemode = m 235 } 236 237 func (r sharedRemoteConn) Capabilities() map[string]map[string]struct{} { 238 return r.capabilities 239 } 240 241 func (r sharedRemoteConn) ProtocolVersion() uint8 { 242 return r.protocolversion 243 } 244 245 type RemoteOptions struct { 246 Verbose bool 247 } 248 249 type RemoteAddOptions struct { 250 RemoteOptions 251 252 Fetch bool 253 // Fetch remote branches (not implemented) 254 255 // Import all tags and associated objects when fetching 256 Tags bool 257 258 // Branch(es) to track (not implemented) 259 Track string 260 261 // Master branch (not implemented) 262 Master string 263 264 // Mirror=[push|fetch] 265 // Set up remote as a mirror to push to or fetch from 266 // (not implemented) 267 Mirror string 268 } 269 type RemoteShowOptions struct { 270 RemoteOptions 271 272 // Do not query the remote with ls-remote, only show the local cache. 273 NoQuery bool 274 } 275 276 type RemoteGetURLOptions struct { 277 RemoteOptions 278 Push bool 279 All bool 280 } 281 282 func RemoteAdd(c *Client, opts RemoteAddOptions, name, url string) error { 283 if name == "" { 284 return fmt.Errorf("Missing remote name") 285 } 286 if url == "" { 287 return fmt.Errorf("Missing remote URL") 288 } 289 290 configname := fmt.Sprintf("remote.%v.url", name) 291 if c.GetConfig(configname) != "" { 292 return fmt.Errorf("fatal: remote %v already exists.", name) 293 } 294 295 config, err := LoadLocalConfig(c) 296 if err != nil { 297 return err 298 } 299 config.SetConfig(configname, url) 300 config.SetConfig( 301 fmt.Sprintf("remote.%v.fetch", name), 302 fmt.Sprintf("+refs/heads/*:refs/remotes/%v/*", name), 303 ) 304 return config.WriteConfig() 305 } 306 307 // Retrieves a list of remotes set up in the local git repository 308 // for Client c. 309 func RemoteList(c *Client, opts RemoteOptions) ([]Remote, error) { 310 config, err := LoadLocalConfig(c) 311 if err != nil { 312 return nil, err 313 } 314 configs := config.GetConfigSections("remote", "") 315 remotes := make([]Remote, 0, len(configs)) 316 for _, cfg := range configs { 317 remotes = append(remotes, Remote(cfg.subsection)) 318 } 319 return remotes, nil 320 } 321 322 // Prints the remote named r in the format of "git remote show r" to destination 323 // w. 324 func RemoteShow(c *Client, opts RemoteShowOptions, r Remote, w io.Writer) error { 325 if !opts.NoQuery { 326 return fmt.Errorf("NoQuery is currently required") 327 } 328 fetchurl, err := r.FetchURL(c) 329 if err != nil { 330 return err 331 } 332 pushurl, err := r.PushURL(c) 333 if err != nil { 334 return err 335 } 336 headref := "(not queried)" 337 if !opts.NoQuery { 338 // FIXME: ls-remote needs to parse this properly 339 headref = "(not implemented)" 340 } 341 fmt.Fprintf(w, 342 `* remote %v 343 Fetch URL: %v 344 Push URL: %v 345 HEAD branch: %v 346 Remote branches:`, r, fetchurl, pushurl, headref) 347 if opts.NoQuery { 348 fmt.Fprintf(w, " (status not queried)\n") 349 } else { 350 fmt.Fprintf(w, "\n") 351 } 352 refbase := fmt.Sprintf("refs/remotes/%v/", r.Name()) 353 ForEachRefCallback(c, refbase, func(c *Client, ref Ref) error { 354 if opts.NoQuery { 355 fmt.Fprintf(w, "\t%v\n", strings.TrimPrefix(string(ref.Name), refbase)) 356 } else { 357 // FIXME: Implement this. 358 // This needs to add a status after the ref name and 359 // merge with ls-remote too to print new ones 360 } 361 return nil 362 }) 363 364 config, err := LoadLocalConfig(c) 365 if err != nil { 366 return err 367 } 368 fmt.Fprintln(w, `Local branches configured for 'git pull':`) 369 // We go through all branches in the local config file, then print them. 370 // We need to know what the longest name is in order to format the 371 // printing. 372 branchconfigs := config.GetConfigSections("branch", "") 373 374 var branches []struct { 375 local, remote string 376 } 377 var longest int 378 for _, branch := range branchconfigs { 379 if branch.values["remote"] == r.Name() { 380 bname := struct { 381 local, remote string 382 }{local: branch.subsection} 383 if remote, ok := branch.values["merge"]; ok { 384 bname.remote = branch.subsection 385 } else { 386 bname.remote = strings.TrimPrefix(remote, "refs/heads/") 387 } 388 branches = append(branches, bname) 389 if lname := len(bname.local); lname > longest { 390 longest = lname 391 } 392 } 393 } 394 for _, branch := range branches { 395 fmt.Fprintf(w, "\t%-*s\tmerges with remote %s\n", longest, branch.local, branch.remote) 396 } 397 // FIXME: Figure out where the "Local ref configured for git push" line 398 // comes from and add it here. 399 return nil 400 401 } 402 403 // Implements the "git remote get-url" command. 404 func RemoteGetURL(c *Client, opts RemoteGetURLOptions, r Remote) ([]string, error) { 405 if opts.Push { 406 u, err := r.PushURL(c) 407 if err != nil { 408 return nil, err 409 } 410 return []string{u}, nil 411 } 412 u, err := r.FetchURL(c) 413 if err != nil { 414 return nil, err 415 } 416 return []string{u}, nil 417 }