github.com/psexton/git-lfs@v2.1.1-0.20170517224304-289a18b2bc53+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 the parent, an error will be returned. If there
    99  // was an error reading or writing capabilities between the two, an error will
   100  // be returned.
   101  func (o *FilterProcessScanner) NegotiateCapabilities() error {
   102  	reqCaps := []string{"capability=clean", "capability=smudge"}
   103  
   104  	supCaps, err := o.pl.readPacketList()
   105  	if err != nil {
   106  		return fmt.Errorf("reading filter-process capabilities failed with %s", err)
   107  	}
   108  	for _, reqCap := range reqCaps {
   109  		if !isStringInSlice(supCaps, reqCap) {
   110  			return fmt.Errorf("filter '%s' not supported (your Git supports: %s)", reqCap, supCaps)
   111  		}
   112  	}
   113  
   114  	err = o.pl.writePacketList(reqCaps)
   115  	if err != nil {
   116  		return fmt.Errorf("writing filter-process capabilities failed with %s", err)
   117  	}
   118  
   119  	return nil
   120  }
   121  
   122  // Request represents a single command sent to LFS from the parent Git process.
   123  type Request struct {
   124  	// Header maps header strings to values, and is encoded as the first
   125  	// part of the packet.
   126  	Header map[string]string
   127  	// Payload represents the body of the packet, and contains the contents
   128  	// of the file in the index.
   129  	Payload io.Reader
   130  }
   131  
   132  // Scan scans for the next request, or error and returns whether or not the scan
   133  // was successful, indicating the presence of a valid request. If the Scan
   134  // failed, there was either an error reading the next request (and the results
   135  // of calling `Err()` should be inspected), or the pipe was closed and no more
   136  // requests are present.
   137  //
   138  // Closing the pipe is Git's way to communicate that no more files will be
   139  // filtered. Git expects that the LFS process exits after this event.
   140  func (o *FilterProcessScanner) Scan() bool {
   141  	o.req, o.err = nil, nil
   142  
   143  	req, err := o.readRequest()
   144  	if err != nil {
   145  		o.err = err
   146  		return false
   147  	}
   148  
   149  	o.req = req
   150  	return true
   151  }
   152  
   153  // Request returns the request read from a call to Scan(). It is available only
   154  // after a call to `Scan()` has completed, and is re-initialized to nil at the
   155  // beginning of the subsequent `Scan()` call.
   156  func (o *FilterProcessScanner) Request() *Request { return o.req }
   157  
   158  // Err returns any error encountered from the last call to Scan(). It is available only
   159  // after a call to `Scan()` has completed, and is re-initialized to nil at the
   160  // beginning of the subsequent `Scan()` call.
   161  func (o *FilterProcessScanner) Err() error { return o.err }
   162  
   163  // readRequest reads the headers of a request and yields an `io.Reader` which
   164  // will read the body of the request. Since the body is _not_ offset, one
   165  // request should be read in its entirety before consuming the next request.
   166  func (o *FilterProcessScanner) readRequest() (*Request, error) {
   167  	tracerx.Printf("Read filter-process request.")
   168  
   169  	requestList, err := o.pl.readPacketList()
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  
   174  	req := &Request{
   175  		Header:  make(map[string]string),
   176  		Payload: &pktlineReader{pl: o.pl},
   177  	}
   178  
   179  	for _, pair := range requestList {
   180  		v := strings.SplitN(pair, "=", 2)
   181  		req.Header[v[0]] = v[1]
   182  	}
   183  
   184  	return req, nil
   185  }
   186  
   187  func (o *FilterProcessScanner) WriteStatus(status string) error {
   188  	return o.pl.writePacketList([]string{"status=" + status})
   189  }
   190  
   191  // isStringInSlice returns whether a given string "what" is contained in a
   192  // slice, "s".
   193  //
   194  // isStringInSlice is copied from "github.com/xeipuuv/gojsonschema/utils.go"
   195  func isStringInSlice(s []string, what string) bool {
   196  	for i := range s {
   197  		if s[i] == what {
   198  			return true
   199  		}
   200  	}
   201  	return false
   202  }