github.com/sdboyer/gps@v0.16.3/version_queue.go (about)

     1  package gps
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  )
     7  
     8  type failedVersion struct {
     9  	v Version
    10  	f error
    11  }
    12  
    13  type versionQueue struct {
    14  	id           ProjectIdentifier
    15  	pi           []Version
    16  	lockv, prefv Version
    17  	fails        []failedVersion
    18  	b            sourceBridge
    19  	failed       bool
    20  	allLoaded    bool
    21  	adverr       error
    22  }
    23  
    24  func newVersionQueue(id ProjectIdentifier, lockv, prefv Version, b sourceBridge) (*versionQueue, error) {
    25  	vq := &versionQueue{
    26  		id: id,
    27  		b:  b,
    28  	}
    29  
    30  	// Lock goes in first, if present
    31  	if lockv != nil {
    32  		vq.lockv = lockv
    33  		vq.pi = append(vq.pi, lockv)
    34  	}
    35  
    36  	// Preferred version next
    37  	if prefv != nil {
    38  		vq.prefv = prefv
    39  		vq.pi = append(vq.pi, prefv)
    40  	}
    41  
    42  	if len(vq.pi) == 0 {
    43  		var err error
    44  		vq.pi, err = vq.b.listVersions(vq.id)
    45  		if err != nil {
    46  			// TODO(sdboyer) pushing this error this early entails that we
    47  			// unconditionally deep scan (e.g. vendor), as well as hitting the
    48  			// network.
    49  			return nil, err
    50  		}
    51  		vq.allLoaded = true
    52  	}
    53  
    54  	return vq, nil
    55  }
    56  
    57  func (vq *versionQueue) current() Version {
    58  	if len(vq.pi) > 0 {
    59  		return vq.pi[0]
    60  	}
    61  
    62  	return nil
    63  }
    64  
    65  // advance moves the versionQueue forward to the next available version,
    66  // recording the failure that eliminated the current version.
    67  func (vq *versionQueue) advance(fail error) error {
    68  	// Nothing in the queue means...nothing in the queue, nicely enough
    69  	if vq.adverr != nil || len(vq.pi) == 0 { // should be a redundant check, but just in case
    70  		return vq.adverr
    71  	}
    72  
    73  	// Record the fail reason and pop the queue
    74  	vq.fails = append(vq.fails, failedVersion{
    75  		v: vq.pi[0],
    76  		f: fail,
    77  	})
    78  	vq.pi = vq.pi[1:]
    79  
    80  	// *now*, if the queue is empty, ensure all versions have been loaded
    81  	if len(vq.pi) == 0 {
    82  		if vq.allLoaded {
    83  			// This branch gets hit when the queue is first fully exhausted,
    84  			// after a previous advance() already called ListVersions().
    85  			return nil
    86  		}
    87  		vq.allLoaded = true
    88  
    89  		var vltmp []Version
    90  		vltmp, vq.adverr = vq.b.listVersions(vq.id)
    91  		if vq.adverr != nil {
    92  			return vq.adverr
    93  		}
    94  		// defensive copy - calling listVersions here means slice contents may
    95  		// be modified when removing prefv/lockv.
    96  		vq.pi = make([]Version, len(vltmp))
    97  		copy(vq.pi, vltmp)
    98  
    99  		// search for and remove lockv and prefv, in a pointer GC-safe manner
   100  		//
   101  		// could use the version comparator for binary search here to avoid
   102  		// O(n) each time...if it matters
   103  		var delkeys []int
   104  		for k, pi := range vq.pi {
   105  			if pi == vq.lockv || pi == vq.prefv {
   106  				delkeys = append(delkeys, k)
   107  			}
   108  		}
   109  
   110  		for k, dk := range delkeys {
   111  			dk -= k
   112  			copy(vq.pi[dk:], vq.pi[dk+1:])
   113  			// write nil to final position for GC safety
   114  			vq.pi[len(vq.pi)-1] = nil
   115  			vq.pi = vq.pi[:len(vq.pi)-1]
   116  		}
   117  
   118  		if len(vq.pi) == 0 {
   119  			// If listing versions added nothing (new), then return now
   120  			return nil
   121  		}
   122  	}
   123  
   124  	// We're finally sure that there's something in the queue. Remove the
   125  	// failure marker, as the current version may have failed, but the next one
   126  	// hasn't yet
   127  	vq.failed = false
   128  
   129  	// If all have been loaded and the queue is empty, we're definitely out
   130  	// of things to try. Return empty, though, because vq semantics dictate
   131  	// that we don't explicitly indicate the end of the queue here.
   132  	return nil
   133  }
   134  
   135  // isExhausted indicates whether or not the queue has definitely been exhausted,
   136  // in which case it will return true.
   137  //
   138  // It may return false negatives - suggesting that there is more in the queue
   139  // when a subsequent call to current() will be empty. Plan accordingly.
   140  func (vq *versionQueue) isExhausted() bool {
   141  	if !vq.allLoaded {
   142  		return false
   143  	}
   144  	return len(vq.pi) == 0
   145  }
   146  
   147  func (vq *versionQueue) String() string {
   148  	var vs []string
   149  
   150  	for _, v := range vq.pi {
   151  		vs = append(vs, v.String())
   152  	}
   153  	return fmt.Sprintf("[%s]", strings.Join(vs, ", "))
   154  }