github.com/psexton/git-lfs@v2.1.1-0.20170517224304-289a18b2bc53+incompatible/git/object_scanner.go (about)

     1  package git
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os/exec"
    10  	"strconv"
    11  
    12  	"github.com/git-lfs/git-lfs/errors"
    13  	"github.com/rubyist/tracerx"
    14  )
    15  
    16  // object represents a generic Git object of any type.
    17  type object struct {
    18  	// Contents reads Git's internal object representation.
    19  	Contents *io.LimitedReader
    20  	// Oid is the ID of the object.
    21  	Oid string
    22  	// Size is the size in bytes of the object.
    23  	Size int64
    24  	// Type is the type of the object being held.
    25  	Type string
    26  }
    27  
    28  // ObjectScanner is a scanner type that scans for Git objects reference-able in
    29  // Git's object database by their unique OID.
    30  type ObjectScanner struct {
    31  	// object is the object that the ObjectScanner last scanned, or nil.
    32  	object *object
    33  	// err is the error (if any) that the ObjectScanner encountered during
    34  	// its last scan, or nil.
    35  	err error
    36  
    37  	// from is the buffered source of input to the *ObjectScanner. It
    38  	// expects input in the form described by
    39  	// https://git-scm.com/docs/git-cat-file.
    40  	from *bufio.Reader
    41  	// to is a writer which accepts the object's OID to be scanned.
    42  	to io.Writer
    43  	// closeFn is an optional function that is run before the ObjectScanner
    44  	// is closed. It is designated to clean up and close any resources held
    45  	// by the ObjectScanner during runtime.
    46  	closeFn func() error
    47  }
    48  
    49  // NewObjectScanner constructs a new instance of the `*ObjectScanner` type and
    50  // returns it. It backs the ObjectScanner with an invocation of the `git
    51  // cat-file --batch` command. If any errors were encountered while starting that
    52  // command, they will be returned immediately.
    53  //
    54  // Otherwise, an `*ObjectScanner` is returned with no error.
    55  func NewObjectScanner() (*ObjectScanner, error) {
    56  	cmd := exec.Command("git", "cat-file", "--batch")
    57  	stdout, err := cmd.StdoutPipe()
    58  	if err != nil {
    59  		return nil, errors.Wrap(err, "open stdout")
    60  	}
    61  	stdin, err := cmd.StdinPipe()
    62  	if err != nil {
    63  		return nil, errors.Wrap(err, "open stdin")
    64  	}
    65  
    66  	stderr, err := cmd.StderrPipe()
    67  	if err != nil {
    68  		return nil, errors.Wrap(err, "open stderr")
    69  	}
    70  
    71  	closeFn := func() error {
    72  		if err := stdin.Close(); err != nil {
    73  			return err
    74  		}
    75  
    76  		msg, _ := ioutil.ReadAll(stderr)
    77  		if err = cmd.Wait(); err != nil {
    78  			return errors.Errorf("Error in git cat-file --batch: %v %v", err, string(msg))
    79  		}
    80  
    81  		return nil
    82  	}
    83  
    84  	tracerx.Printf("run_command: git cat-file --batch")
    85  	if err := cmd.Start(); err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	return &ObjectScanner{
    90  		from: bufio.NewReaderSize(stdout, 16384),
    91  		to:   stdin,
    92  
    93  		closeFn: closeFn,
    94  	}, nil
    95  }
    96  
    97  // NewObjectScannerFrom returns a new `*ObjectScanner` populated with data from
    98  // the given `io.Reader`, "r". It supplies no close function, and discards any
    99  // input given to the Scan() function.
   100  func NewObjectScannerFrom(r io.Reader) *ObjectScanner {
   101  	return &ObjectScanner{
   102  		from: bufio.NewReader(r),
   103  		to:   ioutil.Discard,
   104  	}
   105  }
   106  
   107  // Scan scans for a particular object given by the "oid" parameter. Once the
   108  // scan is complete, the Contents(), Sha1(), Size() and Type() functions may be
   109  // called and will return data corresponding to the given OID.
   110  //
   111  // Scan() returns whether the scan was successful, or in other words, whether or
   112  // not the scanner can continue to progress.
   113  func (s *ObjectScanner) Scan(oid string) bool {
   114  	if err := s.reset(); err != nil {
   115  		s.err = err
   116  		return false
   117  	}
   118  
   119  	obj, err := s.scan(oid)
   120  	s.object = obj
   121  
   122  	if err != nil {
   123  		if err != io.EOF {
   124  			s.err = err
   125  		}
   126  		return false
   127  	}
   128  	return true
   129  }
   130  
   131  // Close closes and frees any resources owned by the *ObjectScanner that it is
   132  // called upon. If there were any errors in freeing that (those) resource(s), it
   133  // it will be returned, otherwise nil.
   134  func (s *ObjectScanner) Close() error {
   135  	if s.closeFn != nil {
   136  		return s.closeFn()
   137  	}
   138  	return nil
   139  }
   140  
   141  // Contents returns an io.Reader which reads Git's representation of the object
   142  // that was last scanned for.
   143  func (s *ObjectScanner) Contents() io.Reader {
   144  	return s.object.Contents
   145  }
   146  
   147  // Sha1 returns the SHA1 object ID of the object that was last scanned for.
   148  func (s *ObjectScanner) Sha1() string {
   149  	return s.object.Oid
   150  }
   151  
   152  // Size returns the size in bytes of the object that was last scanned for.
   153  func (s *ObjectScanner) Size() int64 {
   154  	return s.object.Size
   155  }
   156  
   157  // Type returns the type of the object that was last scanned for.
   158  func (s *ObjectScanner) Type() string {
   159  	return s.object.Type
   160  }
   161  
   162  // Err returns the error (if any) that was encountered during the last Scan()
   163  // operation.
   164  func (s *ObjectScanner) Err() error { return s.err }
   165  
   166  // reset resets the `*ObjectScanner` to scan again by advancing the reader (if
   167  // necessary) and clearing both the object and error fields on the
   168  // `*ObjectScanner` instance.
   169  func (s *ObjectScanner) reset() error {
   170  	if s.object != nil {
   171  		if s.object.Contents != nil {
   172  			remaining := s.object.Contents.N
   173  			if _, err := io.CopyN(ioutil.Discard, s.object.Contents, remaining); err != nil {
   174  				return errors.Wrap(err, "unwind contents")
   175  			}
   176  		}
   177  
   178  		// Consume extra LF inserted by cat-file
   179  		if _, err := s.from.ReadByte(); err != nil {
   180  			return err
   181  		}
   182  	}
   183  
   184  	s.object, s.err = nil, nil
   185  
   186  	return nil
   187  }
   188  
   189  // scan scans for and populates a new Git object given an OID.
   190  func (s *ObjectScanner) scan(oid string) (*object, error) {
   191  	if _, err := fmt.Fprintln(s.to, oid); err != nil {
   192  		return nil, err
   193  	}
   194  
   195  	l, err := s.from.ReadBytes('\n')
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  
   200  	fields := bytes.Fields(l)
   201  	if len(fields) < 3 {
   202  		return nil, errors.Errorf("invalid line: %q", l)
   203  	}
   204  
   205  	oid = string(fields[0])
   206  	typ := string(fields[1])
   207  	size, _ := strconv.Atoi(string(fields[2]))
   208  	contents := io.LimitReader(s.from, int64(size))
   209  
   210  	return &object{
   211  		Contents: contents.(*io.LimitedReader),
   212  		Oid:      oid,
   213  		Size:     int64(size),
   214  		Type:     typ,
   215  	}, nil
   216  }