From 034dfcd90fe7dc353fe0a4d8588d5ed4f40aae85 Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Sat, 3 Mar 2018 08:14:11 -0500 Subject: Updating vendored dependencies Signed-off-by: Dave Henderson --- vendor/github.com/google/go-cmp/cmp/compare.go | 42 +++++++++--- .../go-cmp/cmp/internal/diff/debug_enable.go | 2 +- .../google/go-cmp/cmp/internal/diff/diff.go | 18 ++--- .../google/go-cmp/cmp/internal/value/format.go | 78 +++++++++++++--------- .../google/go-cmp/cmp/internal/value/sort.go | 2 +- vendor/github.com/google/go-cmp/cmp/options.go | 37 +++++----- vendor/github.com/google/go-cmp/cmp/path.go | 36 +++++++--- vendor/github.com/google/go-cmp/cmp/reporter.go | 12 ++-- .../github.com/google/go-cmp/cmp/unsafe_panic.go | 2 +- .../github.com/google/go-cmp/cmp/unsafe_reflect.go | 2 +- 10 files changed, 143 insertions(+), 88 deletions(-) (limited to 'vendor/github.com/google') diff --git a/vendor/github.com/google/go-cmp/cmp/compare.go b/vendor/github.com/google/go-cmp/cmp/compare.go index 5527f014..7e215f22 100644 --- a/vendor/github.com/google/go-cmp/cmp/compare.go +++ b/vendor/github.com/google/go-cmp/cmp/compare.go @@ -22,7 +22,7 @@ // equality is determined by recursively comparing the primitive kinds on both // values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported // fields are not compared by default; they result in panics unless suppressed -// by using an Ignore option (see cmpopts.IgnoreUnexported) or explictly compared +// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared // using the AllowUnexported option. package cmp @@ -35,7 +35,7 @@ import ( "github.com/google/go-cmp/cmp/internal/value" ) -// BUG: Maps with keys containing NaN values cannot be properly compared due to +// BUG(dsnet): Maps with keys containing NaN values cannot be properly compared due to // the reflection package's inability to retrieve such entries. Equal will panic // anytime it comes across a NaN key, but this behavior may change. // @@ -61,8 +61,8 @@ var nothing = reflect.Value{} // // • If the values have an Equal method of the form "(T) Equal(T) bool" or // "(T) Equal(I) bool" where T is assignable to I, then use the result of -// x.Equal(y). Otherwise, no such method exists and evaluation proceeds to -// the next rule. +// x.Equal(y) even if x or y is nil. +// Otherwise, no such method exists and evaluation proceeds to the next rule. // // • Lastly, try to compare x and y based on their basic kinds. // Simple kinds like booleans, integers, floats, complex numbers, strings, and @@ -304,7 +304,8 @@ func (s *state) tryOptions(vx, vy reflect.Value, t reflect.Type) bool { // Evaluate all filters and apply the remaining options. if opt := opts.filter(s, vx, vy, t); opt != nil { - return opt.apply(s, vx, vy) + opt.apply(s, vx, vy) + return true } return false } @@ -322,6 +323,7 @@ func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool { } func (s *state) callTRFunc(f, v reflect.Value) reflect.Value { + v = sanitizeValue(v, f.Type().In(0)) if !s.dynChecker.Next() { return f.Call([]reflect.Value{v})[0] } @@ -345,6 +347,8 @@ func (s *state) callTRFunc(f, v reflect.Value) reflect.Value { } func (s *state) callTTBFunc(f, x, y reflect.Value) bool { + x = sanitizeValue(x, f.Type().In(0)) + y = sanitizeValue(y, f.Type().In(1)) if !s.dynChecker.Next() { return f.Call([]reflect.Value{x, y})[0].Bool() } @@ -372,20 +376,40 @@ func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) { ret = f.Call(vs)[0] } +// sanitizeValue converts nil interfaces of type T to those of type R, +// assuming that T is assignable to R. +// Otherwise, it returns the input value as is. +func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value { + // TODO(dsnet): Remove this hacky workaround. + // See https://golang.org/issue/22143 + if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t { + return reflect.New(t).Elem() + } + return v +} + func (s *state) compareArray(vx, vy reflect.Value, t reflect.Type) { step := &sliceIndex{pathStep{t.Elem()}, 0, 0} s.curPath.push(step) // Compute an edit-script for slices vx and vy. - eq, es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { + es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { step.xkey, step.ykey = ix, iy return s.statelessCompare(vx.Index(ix), vy.Index(iy)) }) - // Equal or no edit-script, so report entire slices as is. - if eq || es == nil { + // Report the entire slice as is if the arrays are of primitive kind, + // and the arrays are different enough. + isPrimitive := false + switch t.Elem().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: + isPrimitive = true + } + if isPrimitive && es.Dist() > (vx.Len()+vy.Len())/4 { s.curPath.pop() // Pop first since we are reporting the whole slice - s.report(eq, vx, vy) + s.report(false, vx, vy) return } diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go index ba46c62e..fd9f7f17 100644 --- a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go +++ b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go @@ -50,7 +50,7 @@ import ( // // The series of '.', 'X', 'Y', and 'M' characters at the bottom represents // the currently established path from the forward and reverse searches, -// seperated by a '|' character. +// separated by a '|' character. const ( updateDelay = 100 * time.Millisecond diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go index baa41fd2..260befea 100644 --- a/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go +++ b/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go @@ -106,9 +106,9 @@ func (r Result) Similar() bool { // Difference reports whether two lists of lengths nx and ny are equal // given the definition of equality provided as f. // -// This function may return a edit-script, which is a sequence of operations -// needed to convert one list into the other. If non-nil, the following -// invariants for the edit-script are maintained: +// This function returns an edit-script, which is a sequence of operations +// needed to convert one list into the other. The following invariants for +// the edit-script are maintained: // • eq == (es.Dist()==0) // • nx == es.LenX() // • ny == es.LenY() @@ -117,17 +117,7 @@ func (r Result) Similar() bool { // produces an edit-script with a minimal Levenshtein distance). This algorithm // favors performance over optimality. The exact output is not guaranteed to // be stable and may change over time. -func Difference(nx, ny int, f EqualFunc) (eq bool, es EditScript) { - es = searchGraph(nx, ny, f) - st := es.stats() - eq = len(es) == st.NI - if !eq && st.NI < (nx+ny)/4 { - return eq, nil // Edit-script more distracting than helpful - } - return eq, es -} - -func searchGraph(nx, ny int, f EqualFunc) EditScript { +func Difference(nx, ny int, f EqualFunc) (es EditScript) { // This algorithm is based on traversing what is known as an "edit-graph". // See Figure 1 from "An O(ND) Difference Algorithm and Its Variations" // by Eugene W. Myers. Since D can be as large as N itself, this is diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/format.go b/vendor/github.com/google/go-cmp/cmp/internal/value/format.go index abaeca89..657e5087 100644 --- a/vendor/github.com/google/go-cmp/cmp/internal/value/format.go +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/format.go @@ -8,15 +8,11 @@ package value import ( "fmt" "reflect" + "strconv" "strings" "unicode" - "unicode/utf8" ) -// formatFakePointers controls whether to substitute pointer addresses with nil. -// This is used for deterministic testing. -var formatFakePointers = false - var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() // Format formats the value v as a string. @@ -26,28 +22,35 @@ var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() // * Avoids printing struct fields that are zero // * Prints a nil-slice as being nil, not empty // * Prints map entries in deterministic order -func Format(v reflect.Value, useStringer bool) string { - return formatAny(v, formatConfig{useStringer, true, true, !formatFakePointers}, nil) +func Format(v reflect.Value, conf FormatConfig) string { + conf.printType = true + conf.followPointers = true + conf.realPointers = true + return formatAny(v, conf, nil) } -type formatConfig struct { - useStringer bool // Should the String method be used if available? - printType bool // Should we print the type before the value? - followPointers bool // Should we recursively follow pointers? - realPointers bool // Should we print the real address of pointers? +type FormatConfig struct { + UseStringer bool // Should the String method be used if available? + printType bool // Should we print the type before the value? + PrintPrimitiveType bool // Should we print the type of primitives? + followPointers bool // Should we recursively follow pointers? + realPointers bool // Should we print the real address of pointers? } -func formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) string { +func formatAny(v reflect.Value, conf FormatConfig, visited map[uintptr]bool) string { // TODO: Should this be a multi-line printout in certain situations? if !v.IsValid() { return "" } - if conf.useStringer && v.Type().Implements(stringerIface) && v.CanInterface() { + if conf.UseStringer && v.Type().Implements(stringerIface) && v.CanInterface() { if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() { return "" } - return fmt.Sprintf("%q", v.Interface().(fmt.Stringer).String()) + + const stringerPrefix = "s" // Indicates that the String method was used + s := v.Interface().(fmt.Stringer).String() + return stringerPrefix + formatString(s) } switch v.Kind() { @@ -66,7 +69,7 @@ func formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) str case reflect.Complex64, reflect.Complex128: return formatPrimitive(v.Type(), v.Complex(), conf) case reflect.String: - return formatPrimitive(v.Type(), fmt.Sprintf("%q", v), conf) + return formatPrimitive(v.Type(), formatString(v.String()), conf) case reflect.UnsafePointer, reflect.Chan, reflect.Func: return formatPointer(v, conf) case reflect.Ptr: @@ -127,11 +130,13 @@ func formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) str visited = insertPointer(visited, v.Pointer()) var ss []string - subConf := conf - subConf.printType = v.Type().Elem().Kind() == reflect.Interface + keyConf, valConf := conf, conf + keyConf.printType = v.Type().Key().Kind() == reflect.Interface + keyConf.followPointers = false + valConf.printType = v.Type().Elem().Kind() == reflect.Interface for _, k := range SortKeys(v.MapKeys()) { - sk := formatAny(k, formatConfig{realPointers: conf.realPointers}, visited) - sv := formatAny(v.MapIndex(k), subConf, visited) + sk := formatAny(k, keyConf, visited) + sv := formatAny(v.MapIndex(k), valConf, visited) ss = append(ss, fmt.Sprintf("%s: %s", sk, sv)) } s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) @@ -149,7 +154,7 @@ func formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) str continue // Elide zero value fields } name := v.Type().Field(i).Name - subConf.useStringer = conf.useStringer && isExported(name) + subConf.UseStringer = conf.UseStringer s := formatAny(vv, subConf, visited) ss = append(ss, fmt.Sprintf("%s: %s", name, s)) } @@ -163,14 +168,33 @@ func formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) str } } -func formatPrimitive(t reflect.Type, v interface{}, conf formatConfig) string { - if conf.printType && t.PkgPath() != "" { +func formatString(s string) string { + // Use quoted string if it the same length as a raw string literal. + // Otherwise, attempt to use the raw string form. + qs := strconv.Quote(s) + if len(qs) == 1+len(s)+1 { + return qs + } + + // Disallow newlines to ensure output is a single line. + // Only allow printable runes for readability purposes. + rawInvalid := func(r rune) bool { + return r == '`' || r == '\n' || !unicode.IsPrint(r) + } + if strings.IndexFunc(s, rawInvalid) < 0 { + return "`" + s + "`" + } + return qs +} + +func formatPrimitive(t reflect.Type, v interface{}, conf FormatConfig) string { + if conf.printType && (conf.PrintPrimitiveType || t.PkgPath() != "") { return fmt.Sprintf("%v(%v)", t, v) } return fmt.Sprintf("%v", v) } -func formatPointer(v reflect.Value, conf formatConfig) string { +func formatPointer(v reflect.Value, conf FormatConfig) string { p := v.Pointer() if !conf.realPointers { p = 0 // For deterministic printing purposes @@ -251,9 +275,3 @@ func isZero(v reflect.Value) bool { } return false } - -// isExported reports whether the identifier is exported. -func isExported(id string) bool { - r, _ := utf8.DecodeRuneInString(id) - return unicode.IsUpper(r) -} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go b/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go index ea73cf14..fe8aa27a 100644 --- a/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go @@ -24,7 +24,7 @@ func SortKeys(vs []reflect.Value) []reflect.Value { // Deduplicate keys (fails for NaNs). vs2 := vs[:1] for _, v := range vs[1:] { - if v.Interface() != vs2[len(vs2)-1].Interface() { + if isLess(vs2[len(vs2)-1], v) { vs2 = append(vs2, v) } } diff --git a/vendor/github.com/google/go-cmp/cmp/options.go b/vendor/github.com/google/go-cmp/cmp/options.go index a4e159ac..91d4b066 100644 --- a/vendor/github.com/google/go-cmp/cmp/options.go +++ b/vendor/github.com/google/go-cmp/cmp/options.go @@ -38,9 +38,8 @@ type Option interface { type applicableOption interface { Option - // apply executes the option and reports whether the option was applied. - // Each option may mutate s. - apply(s *state, vx, vy reflect.Value) bool + // apply executes the option, which may mutate s or panic. + apply(s *state, vx, vy reflect.Value) } // coreOption represents the following types: @@ -85,7 +84,7 @@ func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out return out } -func (opts Options) apply(s *state, _, _ reflect.Value) bool { +func (opts Options) apply(s *state, _, _ reflect.Value) { const warning = "ambiguous set of applicable options" const help = "consider using filters to ensure at most one Comparer or Transformer may apply" var ss []string @@ -196,7 +195,7 @@ type ignore struct{ core } func (ignore) isFiltered() bool { return false } func (ignore) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return ignore{} } -func (ignore) apply(_ *state, _, _ reflect.Value) bool { return true } +func (ignore) apply(_ *state, _, _ reflect.Value) { return } func (ignore) String() string { return "Ignore()" } // invalid is a sentinel Option type to indicate that some options could not @@ -204,7 +203,7 @@ func (ignore) String() string type invalid struct{ core } func (invalid) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return invalid{} } -func (invalid) apply(s *state, _, _ reflect.Value) bool { +func (invalid) apply(s *state, _, _ reflect.Value) { const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported" panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help)) } @@ -215,9 +214,12 @@ func (invalid) apply(s *state, _, _ reflect.Value) bool { // The transformer f must be a function "func(T) R" that converts values of // type T to those of type R and is implicitly filtered to input values // assignable to T. The transformer must not mutate T in any way. -// If T and R are the same type, an additional filter must be applied to -// act as the base case to prevent an infinite recursion applying the same -// transform to itself (see the SortedSlice example). +// +// To help prevent some cases of infinite recursive cycles applying the +// same transform to the output of itself (e.g., in the case where the +// input and output types are the same), an implicit filter is added such that +// a transformer is applicable only if that exact transformer is not already +// in the tail of the Path since the last non-Transform step. // // The name is a user provided label that is used as the Transform.Name in the // transformation PathStep. If empty, an arbitrary name is used. @@ -248,14 +250,21 @@ type transformer struct { func (tr *transformer) isFiltered() bool { return tr.typ != nil } -func (tr *transformer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applicableOption { +func (tr *transformer) filter(s *state, _, _ reflect.Value, t reflect.Type) applicableOption { + for i := len(s.curPath) - 1; i >= 0; i-- { + if t, ok := s.curPath[i].(*transform); !ok { + break // Hit most recent non-Transform step + } else if tr == t.trans { + return nil // Cannot directly use same Transform + } + } if tr.typ == nil || t.AssignableTo(tr.typ) { return tr } return nil } -func (tr *transformer) apply(s *state, vx, vy reflect.Value) bool { +func (tr *transformer) apply(s *state, vx, vy reflect.Value) { // Update path before calling the Transformer so that dynamic checks // will use the updated path. s.curPath.push(&transform{pathStep{tr.fnc.Type().Out(0)}, tr}) @@ -264,7 +273,6 @@ func (tr *transformer) apply(s *state, vx, vy reflect.Value) bool { vx = s.callTRFunc(tr.fnc, vx) vy = s.callTRFunc(tr.fnc, vy) s.compareAny(vx, vy) - return true } func (tr transformer) String() string { @@ -310,10 +318,9 @@ func (cm *comparer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applica return nil } -func (cm *comparer) apply(s *state, vx, vy reflect.Value) bool { +func (cm *comparer) apply(s *state, vx, vy reflect.Value) { eq := s.callTTBFunc(cm.fnc, vx, vy) s.report(eq, vx, vy) - return true } func (cm comparer) String() string { @@ -348,7 +355,7 @@ func (cm comparer) String() string { // all unexported fields on specified struct types. func AllowUnexported(types ...interface{}) Option { if !supportAllowUnexported { - panic("AllowUnexported is not supported on App Engine Classic or GopherJS") + panic("AllowUnexported is not supported on purego builds, Google App Engine Standard, or GopherJS") } m := make(map[reflect.Type]bool) for _, typ := range types { diff --git a/vendor/github.com/google/go-cmp/cmp/path.go b/vendor/github.com/google/go-cmp/cmp/path.go index 0c2eb333..c08a3cf8 100644 --- a/vendor/github.com/google/go-cmp/cmp/path.go +++ b/vendor/github.com/google/go-cmp/cmp/path.go @@ -79,6 +79,11 @@ type ( PathStep Name() string Func() reflect.Value + + // Option returns the originally constructed Transformer option. + // The == operator can be used to detect the exact option used. + Option() Option + isTransform() } ) @@ -94,10 +99,21 @@ func (pa *Path) pop() { // Last returns the last PathStep in the Path. // If the path is empty, this returns a non-nil PathStep that reports a nil Type. func (pa Path) Last() PathStep { - if len(pa) > 0 { - return pa[len(pa)-1] + return pa.Index(-1) +} + +// Index returns the ith step in the Path and supports negative indexing. +// A negative index starts counting from the tail of the Path such that -1 +// refers to the last step, -2 refers to the second-to-last step, and so on. +// If index is invalid, this returns a non-nil PathStep that reports a nil Type. +func (pa Path) Index(i int) PathStep { + if i < 0 { + i = len(pa) + i + } + if i < 0 || i >= len(pa) { + return pathStep{} } - return pathStep{} + return pa[i] } // String returns the simplified path to a node. @@ -150,13 +166,12 @@ func (pa Path) GoString() string { ssPost = append(ssPost, ")") continue case *typeAssertion: - // Elide type assertions immediately following a transform to - // prevent overly verbose path printouts. - // Some transforms return interface{} because of Go's lack of - // generics, but typically take in and return the exact same - // concrete type. Other times, the transform creates an anonymous - // struct, which will be very verbose to print. - if _, ok := nextStep.(*transform); ok { + // As a special-case, elide type assertions on anonymous types + // since they are typically generated dynamically and can be very + // verbose. For example, some transforms return interface{} because + // of Go's lack of generics, but typically take in and return the + // exact same concrete type. + if s.Type().PkgPath() == "" { continue } } @@ -250,6 +265,7 @@ func (sf structField) Name() string { return sf.name } func (sf structField) Index() int { return sf.idx } func (tf transform) Name() string { return tf.trans.name } func (tf transform) Func() reflect.Value { return tf.trans.fnc } +func (tf transform) Option() Option { return tf.trans } func (pathStep) isPathStep() {} func (sliceIndex) isSliceIndex() {} diff --git a/vendor/github.com/google/go-cmp/cmp/reporter.go b/vendor/github.com/google/go-cmp/cmp/reporter.go index a21d0cde..20e9f18e 100644 --- a/vendor/github.com/google/go-cmp/cmp/reporter.go +++ b/vendor/github.com/google/go-cmp/cmp/reporter.go @@ -30,12 +30,12 @@ func (r *defaultReporter) Report(x, y reflect.Value, eq bool, p Path) { const maxLines = 256 r.ndiffs++ if r.nbytes < maxBytes && r.nlines < maxLines { - sx := value.Format(x, true) - sy := value.Format(y, true) + sx := value.Format(x, value.FormatConfig{UseStringer: true}) + sy := value.Format(y, value.FormatConfig{UseStringer: true}) if sx == sy { - // Stringer is not helpful, so rely on more exact formatting. - sx = value.Format(x, false) - sy = value.Format(y, false) + // Unhelpful output, so use more exact formatting. + sx = value.Format(x, value.FormatConfig{PrintPrimitiveType: true}) + sy = value.Format(y, value.FormatConfig{PrintPrimitiveType: true}) } s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy) r.diffs = append(r.diffs, s) @@ -49,5 +49,5 @@ func (r *defaultReporter) String() string { if r.ndiffs == len(r.diffs) { return s } - return fmt.Sprintf("%s... %d more differences ...", s, len(r.diffs)-r.ndiffs) + return fmt.Sprintf("%s... %d more differences ...", s, r.ndiffs-len(r.diffs)) } diff --git a/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go b/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go index 0d44987f..d1518eb3 100644 --- a/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go +++ b/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE.md file. -// +build appengine js +// +build purego appengine js package cmp diff --git a/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go b/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go index 81fb8263..579b6550 100644 --- a/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go +++ b/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE.md file. -// +build !appengine,!js +// +build !purego,!appengine,!js package cmp -- cgit v1.2.3