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  }