github.com/git-lfs/git-lfs@v2.5.2+incompatible/git/filter_process_scanner.go (about)

     1  // Package git contains various commands that shell out to git
     2  // NOTE: Subject to change, do not rely on this package from outside git-lfs source
     3  package git
     4  
     5  import (
     6  	"fmt"
     7  	"io"
     8  	"strings"
     9  
    10  	"github.com/git-lfs/git-lfs/errors"
    11  	"github.com/rubyist/tracerx"
    12  )
    13  
    14  // FilterProcessScanner provides a scanner-like interface capable of
    15  // initializing the filter process with the Git parent, and scanning for
    16  // requests across the protocol.
    17  //
    18  // Reading a request (and errors) is as follows:
    19  //
    20  //     s := NewFilterProcessScanner(os.Stdin, os.Stderr)
    21  //     for s.Scan() {
    22  //             req := s.Request()
    23  //     	       // ...
    24  //     }
    25  //
    26  //     if err := s.Err(); err != nil {
    27  //             // ...
    28  //     }
    29  type FilterProcessScanner struct {
    30  	// pl is the *pktline instance used to read and write packets back and
    31  	// forth between Git.
    32  	pl *pktline
    33  
    34  	// req is a temporary variable used to hold the value accessible by the
    35  	// `Request()` function. It is cleared at the beginning of each `Scan()`
    36  	// invocation, and written to at the end of each `Scan()` invocation.
    37  	req *Request
    38  	// err is a temporary variable used to hold the value accessible by the
    39  	// `Request()` function. It is cleared at the beginning of each `Scan()`
    40  	// invocation, and written to at the end of each `Scan()` invocation.
    41  	err error
    42  }
    43  
    44  // NewFilterProcessScanner constructs a new instance of the
    45  // `*FilterProcessScanner` type which reads packets from the `io.Reader` "r",
    46  // and writes packets to the `io.Writer`, "w".
    47  //
    48  // Both reader and writers SHOULD NOT be `*git.PacketReader` or
    49  // `*git.PacketWriter`s, they will be transparently treated as such. In other
    50  // words, it is safe (and recommended) to pass `os.Stdin` and `os.Stdout`
    51  // directly.
    52  func NewFilterProcessScanner(r io.Reader, w io.Writer) *FilterProcessScanner {
    53  	return &FilterProcessScanner{
    54  		pl: newPktline(r, w),
    55  	}
    56  }
    57  
    58  // Init initializes the filter and ACKs back and forth between the Git LFS
    59  // subprocess and the Git parent process that each is a git-filter-server and
    60  // client respectively.
    61  //
    62  // If either side wrote an invalid sequence of data, or did not meet
    63  // expectations, an error will be returned. If the filter type is not supported,
    64  // an error will be returned. If the pkt-line welcome message was invalid, an
    65  // error will be returned.
    66  //
    67  // If there was an error reading or writing any of the packets below, an error
    68  // will be returned.
    69  func (o *FilterProcessScanner) Init() error {
    70  	tracerx.Printf("Initialize filter-process")
    71  	reqVer := "version=2"
    72  
    73  	initMsg, err := o.pl.readPacketText()
    74  	if err != nil {
    75  		return errors.Wrap(err, "reading filter-process initialization")
    76  	}
    77  	if initMsg != "git-filter-client" {
    78  		return fmt.Errorf("invalid filter-process pkt-line welcome message: %s", initMsg)
    79  	}
    80  
    81  	supVers, err := o.pl.readPacketList()
    82  	if err != nil {
    83  		return errors.Wrap(err, "reading filter-process versions")
    84  	}
    85  	if !isStringInSlice(supVers, reqVer) {
    86  		return fmt.Errorf("filter '%s' not supported (your Git supports: %s)", reqVer, supVers)
    87  	}
    88  
    89  	err = o.pl.writePacketList([]string{"git-filter-server", reqVer})
    90  	if err != nil {
    91  		return errors.Wrap(err, "writing filter-process initialization failed")
    92  	}
    93  	return nil
    94  }
    95  
    96  // NegotiateCapabilities executes the process of negotiating capabilities
    97  // between the filter client and server. If we don't support any of the
    98  // capabilities given to LFS by Git, an error will be returned. If there was an
    99  // error reading or writing capabilities between the two, an error will be
   100  // returned.
   101  func (o *FilterProcessScanner) NegotiateCapabilities() ([]string, error) {
   102  	reqCaps := []string{"capability=clean", "capability=smudge"}
   103  
   104  	supCaps, err := o.pl.readPacketList()
   105  	if err != nil {
   106  		return nil, fmt.Errorf("reading filter-process capabilities failed with %s", err)
   107  	}
   108  
   109  	for _, sup := range supCaps {
   110  		if sup == "capability=delay" {
   111  			reqCaps = append(reqCaps, "capability=delay")
   112  			break
   113  		}
   114  	}
   115  
   116  	for _, reqCap := range reqCaps {
   117  		if !isStringInSlice(supCaps, reqCap) {
   118  			return nil, fmt.Errorf("filter '%s' not supported (your Git supports: %s)", reqCap, supCaps)
   119  		}
   120  	}
   121  
   122  	err = o.pl.writePacketList(reqCaps)
   123  	if err != nil {
   124  		return nil, fmt.Errorf("writing filter-process capabilities failed with %s", err)
   125  	}
   126  
   127  	return supCaps, nil
   128  }
   129  
   130  // Request represents a single command sent to LFS from the parent Git process.
   131  type Request struct {
   132  	// Header maps header strings to values, and is encoded as the first
   133  	// part of the packet.
   134  	Header map[string]string
   135  	// Payload represents the body of the packet, and contains the contents
   136  	// of the file in the index.
   137  	Payload io.Reader
   138  }
   139  
   140  // Scan scans for the next request, or error and returns whether or not the scan
   141  // was successful, indicating the presence of a valid request. If the Scan
   142  // failed, there was either an error reading the next request (and the results
   143  // of calling `Err()` should be inspected), or the pipe was closed and no more
   144  // requests are present.
   145  //
   146  // Closing the pipe is Git's way to communicate that no more files will be
   147  // filtered. Git expects that the LFS process exits after this event.
   148  func (o *FilterProcessScanner) Scan() bool {
   149  	o.req, o.err = nil, nil
   150  
   151  	req, err := o.readRequest()
   152  	if err != nil {
   153  		o.err = err
   154  		return false
   155  	}
   156  
   157  	o.req = req
   158  	return true
   159  }
   160  
   161  // Request returns the request read from a call to Scan(). It is available only
   162  // after a call to `Scan()` has completed, and is re-initialized to nil at the
   163  // beginning of the subsequent `Scan()` call.
   164  func (o *FilterProcessScanner) Request() *Request { return o.req }
   165  
   166  // Err returns any error encountered from the last call to Scan(). It is available only
   167  // after a call to `Scan()` has completed, and is re-initialized to nil at the
   168  // beginning of the subsequent `Scan()` call.
   169  func (o *FilterProcessScanner) Err() error { return o.err }
   170  
   171  // readRequest reads the headers of a request and yields an `io.Reader` which
   172  // will read the body of the request. Since the body is _not_ offset, one
   173  // request should be read in its entirety before consuming the next request.
   174  func (o *FilterProcessScanner) readRequest() (*Request, error) {
   175  	requestList, err := o.pl.readPacketList()
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	req := &Request{
   181  		Header:  make(map[string]string),
   182  		Payload: &pktlineReader{pl: o.pl},
   183  	}
   184  
   185  	for _, pair := range requestList {
   186  		v := strings.SplitN(pair, "=", 2)
   187  		req.Header[v[0]] = v[1]
   188  	}
   189  
   190  	return req, nil
   191  }
   192  
   193  // WriteList writes a list of strings to the underlying pktline data stream in
   194  // pktline format.
   195  func (o *FilterProcessScanner) WriteList(list []string) error {
   196  	return o.pl.writePacketList(list)
   197  }
   198  
   199  func (o *FilterProcessScanner) WriteStatus(status FilterProcessStatus) error {
   200  	return o.pl.writePacketList([]string{"status=" + status.String()})
   201  }
   202  
   203  // isStringInSlice returns whether a given string "what" is contained in a
   204  // slice, "s".
   205  //
   206  // isStringInSlice is copied from "github.com/xeipuuv/gojsonschema/utils.go"
   207  func isStringInSlice(s []string, what string) bool {
   208  	for i := range s {
   209  		if s[i] == what {
   210  			return true
   211  		}
   212  	}
   213  	return false
   214  }