diff --git a/go.mod b/go.mod index acd7eaae..29997e6d 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chai2010/jsonv v1.1.3 // indirect github.com/chai2010/protorpc v1.1.4 // indirect + github.com/chainguard-dev/git-urls v1.0.2 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/containerd v1.7.0 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect @@ -45,10 +46,12 @@ require ( github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.10.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/getkin/kin-openapi v0.123.0 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-logr/logr v1.3.0 // indirect @@ -128,11 +131,14 @@ require ( google.golang.org/grpc v1.62.0 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect kcl-lang.io/lib v0.8.0 // indirect ) require ( + github.com/containers/image v3.0.2+incompatible github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/go-git/go-git/v5 v5.11.0 github.com/gofrs/flock v0.8.1 @@ -140,6 +146,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-getter v1.7.3 github.com/hashicorp/go-version v1.6.0 + github.com/kubescape/go-git-url v0.0.30 github.com/moby/term v0.0.0-20221205130635-1aeaba878587 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.10 diff --git a/go.sum b/go.sum index 6b62aa77..7162af86 100644 --- a/go.sum +++ b/go.sum @@ -246,6 +246,8 @@ github.com/chai2010/jsonv v1.1.3 h1:gBIHXn/5mdEPTuWZfjC54fn/yUSRR8OGobXobcc6now= github.com/chai2010/jsonv v1.1.3/go.mod h1:mEoT1dQ9qVF4oP9peVTl0UymTmJwXoTDOh+sNA6+XII= github.com/chai2010/protorpc v1.1.4 h1:CTtFUhzXRoeuR7FtgQ2b2vdT/KgWVpCM+sIus8zJjHs= github.com/chai2010/protorpc v1.1.4/go.mod h1:/wO0kiyVdu7ug8dCMrA2yDr2vLfyhsLEuzLa9J2HJ+I= +github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ= +github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -271,6 +273,8 @@ github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXT github.com/containerd/containerd v1.7.0/go.mod h1:QfR7Efgb/6X2BDpTPJRvPTYDE9rsF0FsXX9J8sIs/sc= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/containers/image v3.0.2+incompatible h1:B1lqAE8MUPCrsBLE86J0gnXleeRq8zJnQryhiiGQNyE= +github.com/containers/image v3.0.2+incompatible/go.mod h1:8Vtij257IWSanUQKe1tAeNOm2sRVkSqQTVQ1IlwI3+M= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -325,6 +329,7 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8= github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= @@ -545,6 +550,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kubescape/go-git-url v0.0.30 h1:PIbg86ae0ftee/p/Tu/6CA1ju6VoJ51G3sQWNHOm6wg= +github.com/kubescape/go-git-url v0.0.30/go.mod h1:3ddc1HEflms1vMhD9owt/3FBES070UaYTUarcjx8jDk= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -1309,6 +1316,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 h1:kmDqav+P+/5e1i9tFfHq1qcF3sOrDp+YEkVDAHu7Jwk= +k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= kcl-lang.io/kcl-go v0.8.0 h1:ep+r4QMiAOeTkOJl9then58D+W8OpX5WHKuL+Tf8+po= kcl-lang.io/kcl-go v0.8.0/go.mod h1:Z+bJWXe5X1Xra7AUOvDpCb4WBcCGNo0sqQG/bNjz6+k= kcl-lang.io/lib v0.8.0 h1:bzMzPpaXaAxWO9JP0B7eI2ZFOYfojdEYUMtNGlUrPx4= diff --git a/pkg/3rdparty/mvs/mvs.go b/pkg/3rdparty/mvs/mvs.go index 08df38a1..4376c9d1 100644 --- a/pkg/3rdparty/mvs/mvs.go +++ b/pkg/3rdparty/mvs/mvs.go @@ -12,9 +12,8 @@ import ( "sort" "sync" - "kcl-lang.io/kpm/pkg/3rdparty/par" - "golang.org/x/mod/module" + "kcl-lang.io/kpm/pkg/3rdparty/par" ) // A Reqs is the requirement graph on which Minimal Version Selection (MVS) operates. diff --git a/pkg/3rdparty/mvs/mvs_test.go b/pkg/3rdparty/mvs/mvs_test.go new file mode 100644 index 00000000..b18ee2de --- /dev/null +++ b/pkg/3rdparty/mvs/mvs_test.go @@ -0,0 +1,635 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mvs + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "golang.org/x/mod/module" +) + +var tests = ` +# Scenario from blog. +name: blog +A: B1 C2 +B1: D3 +C1: D2 +C2: D4 +C3: D5 +C4: G1 +D2: E1 +D3: E2 +D4: E2 F1 +D5: E2 +G1: C4 +A2: B1 C4 D4 +build A: A B1 C2 D4 E2 F1 +upgrade* A: A B1 C4 D5 E2 F1 G1 +upgrade A C4: A B1 C4 D4 E2 F1 G1 +build A2: A2 B1 C4 D4 E2 F1 G1 +downgrade A2 D2: A2 C4 D2 E2 F1 G1 + +name: trim +A: B1 C2 +B1: D3 +C2: B2 +B2: +build A: A B2 C2 D3 + +# Cross-dependency between D and E. +# No matter how it arises, should get result of merging all build lists via max, +# which leads to including both D2 and E2. + +name: cross1 +A: B C +B: D1 +C: D2 +D1: E2 +D2: E1 +build A: A B C D2 E2 + +name: cross1V +A: B2 C D2 E1 +B1: +B2: D1 +C: D2 +D1: E2 +D2: E1 +build A: A B2 C D2 E2 + +name: cross1U +A: B1 C +B1: +B2: D1 +C: D2 +D1: E2 +D2: E1 +build A: A B1 C D2 E1 +upgrade A B2: A B2 C D2 E2 + +name: cross1R +A: B C +B: D2 +C: D1 +D1: E2 +D2: E1 +build A: A B C D2 E2 + +name: cross1X +A: B C +B: D1 E2 +C: D2 +D1: E2 +D2: E1 +build A: A B C D2 E2 + +name: cross2 +A: B D2 +B: D1 +D1: E2 +D2: E1 +build A: A B D2 E2 + +name: cross2X +A: B D2 +B: D1 E2 +C: D2 +D1: E2 +D2: E1 +build A: A B D2 E2 + +name: cross3 +A: B D2 E1 +B: D1 +D1: E2 +D2: E1 +build A: A B D2 E2 + +name: cross3X +A: B D2 E1 +B: D1 E2 +D1: E2 +D2: E1 +build A: A B D2 E2 + +# Should not get E2 here, because B has been updated +# not to depend on D1 anymore. +name: cross4 +A1: B1 D2 +A2: B2 D2 +B1: D1 +B2: D2 +D1: E2 +D2: E1 +build A1: A1 B1 D2 E2 +build A2: A2 B2 D2 E1 + +# But the upgrade from A1 preserves the E2 dep explicitly. +upgrade A1 B2: A1 B2 D2 E2 +upgradereq A1 B2: B2 E2 + +name: cross5 +A: D1 +D1: E2 +D2: E1 +build A: A D1 E2 +upgrade* A: A D2 E2 +upgrade A D2: A D2 E2 +upgradereq A D2: D2 E2 + +name: cross6 +A: D2 +D1: E2 +D2: E1 +build A: A D2 E1 +upgrade* A: A D2 E2 +upgrade A E2: A D2 E2 + +name: cross7 +A: B C +B: D1 +C: E1 +D1: E2 +E1: D2 +build A: A B C D2 E2 + +# golang.org/issue/31248: +# Even though we select X2, the requirement on I1 +# via X1 should be preserved. +name: cross8 +M: A1 B1 +A1: X1 +B1: X2 +X1: I1 +X2: +build M: M A1 B1 I1 X2 + +# Upgrade from B1 to B2 should not drop the transitive dep on D. +name: drop +A: B1 C1 +B1: D1 +B2: +C2: +D2: +build A: A B1 C1 D1 +upgrade* A: A B2 C2 D2 + +name: simplify +A: B1 C1 +B1: C2 +C1: D1 +C2: +build A: A B1 C2 D1 + +name: up1 +A: B1 C1 +B1: +B2: +B3: +B4: +B5.hidden: +C2: +C3: +build A: A B1 C1 +upgrade* A: A B4 C3 + +name: up2 +A: B5.hidden C1 +B1: +B2: +B3: +B4: +B5.hidden: +C2: +C3: +build A: A B5.hidden C1 +upgrade* A: A B5.hidden C3 + +name: down1 +A: B2 +B1: C1 +B2: C2 +build A: A B2 C2 +downgrade A C1: A B1 C1 + +name: down2 +A: B2 E2 +B1: +B2: C2 F2 +C1: +D1: +C2: D2 E2 +D2: B2 +E2: D2 +E1: +F1: +build A: A B2 C2 D2 E2 F2 +downgrade A F1: A B1 C1 D1 E1 F1 + +# https://research.swtch.com/vgo-mvs#algorithm_4: +# “[D]owngrades are constrained to only downgrade packages, not also upgrade +# them; if an upgrade before downgrade is needed, the user must ask for it +# explicitly.” +# +# Here, downgrading B2 to B1 upgrades C1 to C2, and C2 does not depend on D2. +# However, C2 would be an upgrade — not a downgrade — so B1 must also be +# rejected. +name: downcross1 +A: B2 C1 +B1: C2 +B2: C1 +C1: D2 +C2: +D1: +D2: +build A: A B2 C1 D2 +downgrade A D1: A D1 + +# https://research.swtch.com/vgo-mvs#algorithm_4: +# “Unlike upgrades, downgrades must work by removing requirements, not adding +# them.” +# +# However, downgrading a requirement may introduce a new requirement on a +# previously-unrequired module. If each dependency's requirements are complete +# (“tidy”), that can't change the behavior of any other package whose version is +# not also being downgraded, so we should allow it. +name: downcross2 +A: B2 +B1: C1 +B2: D2 +C1: +D1: +D2: +build A: A B2 D2 +downgrade A D1: A B1 C1 D1 + +name: downcycle +A: A B2 +B2: A +B1: +build A: A B2 +downgrade A B1: A B1 + +# Both B3 and C2 require D2. +# If we downgrade D to D1, then in isolation B3 would downgrade to B1, +# because B2 is hidden — B1 is the next-highest version that is not hidden. +# However, if we downgrade D, we will also downgrade C to C1. +# And C1 requires B2.hidden, and B2.hidden also meets our requirements: +# it is compatible with D1 and a strict downgrade from B3. +# +# Since neither the initial nor the final build list includes B1, +# and the nothing in the final downgraded build list requires E at all, +# no dependency on E1 (required by only B1) should be introduced. +# +name: downhiddenartifact +A: B3 C2 +A1: B3 +B1: E1 +B2.hidden: +B3: D2 +C1: B2.hidden +C2: D2 +D1: +D2: +build A1: A1 B3 D2 +downgrade A1 D1: A1 B1 D1 E1 +build A: A B3 C2 D2 +downgrade A D1: A B2.hidden C1 D1 + +# Both B3 and C3 require D2. +# If we downgrade D to D1, then in isolation B3 would downgrade to B1, +# and C3 would downgrade to C1. +# But C1 requires B2.hidden, and B1 requires C2.hidden, so we can't +# downgrade to either of those without pulling the other back up a little. +# +# B2.hidden and C2.hidden are both compatible with D1, so that still +# meets our requirements — but then we're in an odd state in which +# B and C have both been downgraded to hidden versions, without any +# remaining requirements to explain how those hidden versions got there. +# +# TODO(bcmills): Would it be better to force downgrades to land on non-hidden +# versions? +# In this case, that would remove the dependencies on B and C entirely. +# +name: downhiddencross +A: B3 C3 +B1: C2.hidden +B2.hidden: +B3: D2 +C1: B2.hidden +C2.hidden: +C3: D2 +D1: +D2: +build A: A B3 C3 D2 +downgrade A D1: A B2.hidden C2.hidden D1 + +# golang.org/issue/25542. +name: noprev1 +A: B4 C2 +B2.hidden: +C2: +build A: A B4 C2 +downgrade A B2.hidden: A B2.hidden C2 + +name: noprev2 +A: B4 C2 +B2.hidden: +B1: +C2: +build A: A B4 C2 +downgrade A B2.hidden: A B2.hidden C2 + +name: noprev3 +A: B4 C2 +B3: +B2.hidden: +C2: +build A: A B4 C2 +downgrade A B2.hidden: A B2.hidden C2 + +# Cycles involving the target. + +# The target must be the newest version of itself. +name: cycle1 +A: B1 +B1: A1 +B2: A2 +B3: A3 +build A: A B1 +upgrade A B2: A B2 +upgrade* A: A B3 + +# golang.org/issue/29773: +# Requirements of older versions of the target +# must be carried over. +name: cycle2 +A: B1 +A1: C1 +A2: D1 +B1: A1 +B2: A2 +C1: A2 +C2: +D2: +build A: A B1 C1 D1 +upgrade* A: A B2 C2 D2 + +# Cycles with multiple possible solutions. +# (golang.org/issue/34086) +name: cycle3 +M: A1 C2 +A1: B1 +B1: C1 +B2: C2 +C1: +C2: B2 +build M: M A1 B2 C2 +req M: A1 B2 +req M A: A1 B2 +req M C: A1 C2 + +# Requirement minimization. + +name: req1 +A: B1 C1 D1 E1 F1 +B1: C1 E1 F1 +req A: B1 D1 +req A C: B1 C1 D1 + +name: req2 +A: G1 H1 +G1: H1 +H1: G1 +req A: G1 +req A G: G1 +req A H: H1 + +name: req3 +M: A1 B1 +A1: X1 +B1: X2 +X1: I1 +X2: +req M: A1 B1 + +name: reqnone +M: Anone B1 D1 E1 +B1: Cnone D1 +E1: Fnone +build M: M B1 D1 E1 +req M: B1 E1 + +name: reqdup +M: A1 B1 +A1: B1 +B1: +req M A A: A1 + +name: reqcross +M: A1 B1 C1 +A1: B1 C1 +B1: C1 +C1: +req M A B: A1 B1 +` + +func Test(t *testing.T) { + var ( + name string + reqs reqsMap + fns []func(*testing.T) + ) + flush := func() { + if name != "" { + t.Run(name, func(t *testing.T) { + for _, fn := range fns { + fn(t) + } + if len(fns) == 0 { + t.Errorf("no functions tested") + } + }) + } + } + m := func(s string) module.Version { + return module.Version{Path: s[:1], Version: s[1:]} + } + ms := func(list []string) []module.Version { + var mlist []module.Version + for _, s := range list { + mlist = append(mlist, m(s)) + } + return mlist + } + checkList := func(t *testing.T, desc string, list []module.Version, err error, val string) { + if err != nil { + t.Fatalf("%s: %v", desc, err) + } + vs := ms(strings.Fields(val)) + if !reflect.DeepEqual(list, vs) { + t.Errorf("%s = %v, want %v", desc, list, vs) + } + } + + for _, line := range strings.Split(tests, "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "#") || line == "" { + continue + } + i := strings.Index(line, ":") + if i < 0 { + t.Fatalf("missing colon: %q", line) + } + key := strings.TrimSpace(line[:i]) + val := strings.TrimSpace(line[i+1:]) + if key == "" { + t.Fatalf("missing key: %q", line) + } + kf := strings.Fields(key) + switch kf[0] { + case "name": + if len(kf) != 1 { + t.Fatalf("name takes no arguments: %q", line) + } + flush() + reqs = make(reqsMap) + fns = nil + name = val + continue + case "build": + if len(kf) != 2 { + t.Fatalf("build takes one argument: %q", line) + } + fns = append(fns, func(t *testing.T) { + list, err := BuildList([]module.Version{m(kf[1])}, reqs) + checkList(t, key, list, err, val) + }) + continue + case "upgrade*": + if len(kf) != 2 { + t.Fatalf("upgrade* takes one argument: %q", line) + } + fns = append(fns, func(t *testing.T) { + list, err := UpgradeAll(m(kf[1]), reqs) + checkList(t, key, list, err, val) + }) + continue + case "upgradereq": + if len(kf) < 2 { + t.Fatalf("upgrade takes at least one argument: %q", line) + } + fns = append(fns, func(t *testing.T) { + list, err := Upgrade(m(kf[1]), reqs, ms(kf[2:])...) + if err == nil { + // Copy the reqs map, but substitute the upgraded requirements in + // place of the target's original requirements. + upReqs := make(reqsMap, len(reqs)) + for m, r := range reqs { + upReqs[m] = r + } + upReqs[m(kf[1])] = list + + list, err = Req(m(kf[1]), nil, upReqs) + } + checkList(t, key, list, err, val) + }) + continue + case "upgrade": + if len(kf) < 2 { + t.Fatalf("upgrade takes at least one argument: %q", line) + } + fns = append(fns, func(t *testing.T) { + list, err := Upgrade(m(kf[1]), reqs, ms(kf[2:])...) + checkList(t, key, list, err, val) + }) + continue + case "downgrade": + if len(kf) < 2 { + t.Fatalf("downgrade takes at least one argument: %q", line) + } + fns = append(fns, func(t *testing.T) { + list, err := Downgrade(m(kf[1]), reqs, ms(kf[1:])...) + checkList(t, key, list, err, val) + }) + continue + case "req": + if len(kf) < 2 { + t.Fatalf("req takes at least one argument: %q", line) + } + fns = append(fns, func(t *testing.T) { + list, err := Req(m(kf[1]), kf[2:], reqs) + checkList(t, key, list, err, val) + }) + continue + } + if len(kf) == 1 && 'A' <= key[0] && key[0] <= 'Z' { + var rs []module.Version + for _, f := range strings.Fields(val) { + r := m(f) + if reqs[r] == nil { + reqs[r] = []module.Version{} + } + rs = append(rs, r) + } + reqs[m(key)] = rs + continue + } + t.Fatalf("bad line: %q", line) + } + flush() +} + +type reqsMap map[module.Version][]module.Version + +func (r reqsMap) Max(_, v1, v2 string) string { + if v1 == "none" || v2 == "" { + return v2 + } + if v2 == "none" || v1 == "" { + return v1 + } + if v1 < v2 { + return v2 + } + return v1 +} + +func (r reqsMap) Upgrade(m module.Version) (module.Version, error) { + u := module.Version{Version: "none"} + for k := range r { + if k.Path == m.Path && r.Max(k.Path, u.Version, k.Version) == k.Version && !strings.HasSuffix(k.Version, ".hidden") { + u = k + } + } + if u.Path == "" { + return module.Version{}, fmt.Errorf("missing module: %v", module.Version{Path: m.Path}) + } + return u, nil +} + +func (r reqsMap) Previous(m module.Version) (module.Version, error) { + var p module.Version + for k := range r { + if k.Path == m.Path && p.Version < k.Version && k.Version < m.Version && !strings.HasSuffix(k.Version, ".hidden") { + p = k + } + } + if p.Path == "" { + return module.Version{Path: m.Path, Version: "none"}, nil + } + return p, nil +} + +func (r reqsMap) Required(m module.Version) ([]module.Version, error) { + rr, ok := r[m] + if !ok { + return nil, fmt.Errorf("missing module: %v", m) + } + return rr, nil +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 7384b78f..e7bb1d4d 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1255,7 +1255,7 @@ func (c *KpmClient) InitGraphAndDownloadDeps(kclPkg *pkg.KclPkg) (*pkg.Dependenc return nil, nil, err } - changedDeps, err := c.downloadDeps(&kclPkg.ModFile.Dependencies, &kclPkg.Dependencies, depGraph, kclPkg.HomePath, root) + changedDeps, err := c.downloadDeps(kclPkg.ModFile.Dependencies, kclPkg.Dependencies, depGraph, kclPkg.HomePath, root) if err != nil { return nil, nil, err } @@ -1299,7 +1299,7 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie return nil, errors.InvalidDependency } - existDep := c.dependencyExists(&d, lockDeps) + existDep := c.dependencyExists(&d, &lockDeps) if existDep != nil { newDeps.Deps[d.Name] = *existDep continue @@ -1366,7 +1366,7 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie source := module.Version{Path: d.Name, Version: d.Version} - err = depGraph.AddVertex(source) + err = depGraph.AddVertex(source, graph.VertexAttribute(d.GetSourceType(), d.GetDownloadPath())) if err != nil && err != graph.ErrVertexAlreadyExists { return nil, err } @@ -1384,7 +1384,7 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie } // Download the indirect dependencies. - nested, err := c.downloadDeps(&deppkg.ModFile.Dependencies, lockDeps, depGraph, deppkg.HomePath, source) + nested, err := c.downloadDeps(deppkg.ModFile.Dependencies, lockDeps, depGraph, deppkg.HomePath, source) if err != nil { return nil, err } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 9f38ae1f..81a055b3 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -184,7 +184,7 @@ func TestDependencyGraph(t *testing.T) { assert.Equal(t, err, nil) m := func(Path, Version string) module.Version { - return module.Version{Path, Version} + return module.Version{Path: Path, Version: Version} } edgeProp := graph.EdgeProperties{ diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 5ae56535..5ec109c4 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -24,6 +24,9 @@ var InvalidAddOptionsInvalidOciRef = errors.New("invalid 'kpm add' argument, you var InvalidAddOptionsInvalidOciReg = errors.New("invalid 'kpm add' argument, you must provide a Reg for the package.") var InvalidAddOptionsInvalidOciRepo = errors.New("invalid 'kpm add' argument, you must provide a Repo for the package.") +// Invalid 'kpm update' +var MultipleSources = errors.New("multiple sources found, there must be a single source.") + // Invalid 'kpm run' var InvalidRunOptionsWithoutEntryFiles = errors.New("invalid 'kpm run' argument, you must provide an entry file.") var EntryFileNotFound = errors.New("entry file cannot be found, please make sure the '--input' entry file can be found") diff --git a/pkg/git/git.go b/pkg/git/git.go index 0af8db18..667b4338 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -1,12 +1,18 @@ package git import ( + "encoding/json" "errors" + "fmt" "io" + "net/http" + "regexp" + "time" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/hashicorp/go-getter" + giturl "github.com/kubescape/go-git-url" ) // CloneOptions is a struct for specifying options for cloning a git repository @@ -153,3 +159,82 @@ func Clone(repoURL string, tagName string, localPath string, writer io.Writer) ( }) return repo, err } + +type GitHubRelease struct { + TagName string `json:"tag_name"` +} + +// parseNextPageURL extracts the 'next' page URL from the 'Link' header +func parseNextPageURL(linkHeader string) (string, error) { + // Regex to extract 'next' page URL from the link header + r := regexp.MustCompile(`<([^>]+)>;\s*rel="next"`) + matches := r.FindStringSubmatch(linkHeader) + + if len(matches) < 2 { + return "", errors.New("next page URL not found") + } + return matches[1], nil +} + +// GetAllGithubReleases fetches all releases from a GitHub repository +func GetAllGithubReleases(url string) ([]string, error) { + // Initialize and parse the URL to extract owner and repo names + gitURL, err := giturl.NewGitURL(url) + if err != nil { + return nil, err + } + + // Construct initial API URL for the first page + apiBase := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", gitURL.GetOwnerName(), gitURL.GetRepoName()) + apiURL := fmt.Sprintf("%s?per_page=100&page=1", apiBase) + + client := http.Client{ + Timeout: 10 * time.Second, + } + + var releaseTags []string + + for apiURL != "" { + req, err := http.NewRequest("GET", apiURL, nil) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch tags, status code: %d", resp.StatusCode) + } + + // Decode the JSON response into a slice of releases + var releases []GitHubRelease + if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil { + return nil, err + } + + // Extract tag names from the releases + for _, release := range releases { + releaseTags = append(releaseTags, release.TagName) + } + + // Read the `Link` header to get the next page URL, if available + linkHeader := resp.Header.Get("Link") + if linkHeader != "" { + nextURL, err := parseNextPageURL(linkHeader) + if err != nil { + apiURL = "" + } else { + apiURL = nextURL + } + } else { + apiURL = "" + } + fmt.Println(apiURL) + } + + return releaseTags, nil +} diff --git a/pkg/graph/graph.go b/pkg/graph/graph.go index cb0fca1f..e22d40e2 100644 --- a/pkg/graph/graph.go +++ b/pkg/graph/graph.go @@ -5,20 +5,39 @@ import ( "github.com/dominikbraun/graph" "golang.org/x/mod/module" + pkg "kcl-lang.io/kpm/pkg/package" ) -// ToAdjacencyList converts graph to adjacency list -func ToAdjacencyList(g graph.Graph[module.Version, module.Version]) (map[module.Version][]module.Version, error) { +func ChangeGraphType(g graph.Graph[pkg.Dependency, pkg.Dependency]) (graph.Graph[module.Version, module.Version], error) { AdjacencyMap, err := g.AdjacencyMap() if err != nil { return nil, fmt.Errorf("failed to get adjacency map: %w", err) } - adjList := make(map[module.Version][]module.Version) - for from, v := range AdjacencyMap { - for to := range v { - adjList[from] = append(adjList[from], to) + m := func(dep pkg.Dependency) module.Version { + return module.Version{Path: dep.Name, Version: dep.Version} + } + + moduleHash := func(m module.Version) module.Version { + return m + } + + depGraph := graph.New(moduleHash, graph.Directed(), graph.PreventCycles()) + for node, edges := range AdjacencyMap { + err := depGraph.AddVertex(m(node)) + if err != nil && err != graph.ErrVertexAlreadyExists { + return nil, fmt.Errorf("failed to add vertex: %w", err) + } + for edge := range edges { + err := depGraph.AddVertex(m(edge)) + if err != nil && err != graph.ErrVertexAlreadyExists { + return nil, fmt.Errorf("failed to add vertex: %w", err) + } + err = depGraph.AddEdge(m(node), m(edge)) + if err != nil { + return nil, fmt.Errorf("failed to add edge: %w", err) + } } } - return adjList, nil + return depGraph, nil } diff --git a/pkg/mvs/mvs.go b/pkg/mvs/mvs.go index 291b5213..be89bdec 100644 --- a/pkg/mvs/mvs.go +++ b/pkg/mvs/mvs.go @@ -3,36 +3,114 @@ package mvs import ( "fmt" + "github.com/dominikbraun/graph" + "github.com/hashicorp/go-version" "golang.org/x/mod/module" + "kcl-lang.io/kpm/pkg/errors" + "kcl-lang.io/kpm/pkg/git" + "kcl-lang.io/kpm/pkg/oci" + pkg "kcl-lang.io/kpm/pkg/package" + "kcl-lang.io/kpm/pkg/reporter" + "kcl-lang.io/kpm/pkg/semver" ) -type ReqsMap map[module.Version][]module.Version +type ReqsGraph struct { + graph.Graph[module.Version, module.Version] +} -func (r ReqsMap) Max(_, v1, v2 string) string { - if v1 == "none" || v2 == "" { - return v2 +func (r ReqsGraph) Max(_, v1, v2 string) string { + version1, err := version.NewVersion(v1) + if err != nil { + reporter.NewErrorEvent(reporter.FailedParseVersion, err, fmt.Sprintf("failed to parse version %s", v1)) + return "" + } + version2, err := version.NewVersion(v2) + if err != nil { + reporter.NewErrorEvent(reporter.FailedParseVersion, err, fmt.Sprintf("failed to parse version %s", v2)) + return "" } - if v2 == "none" || v1 == "" { + if version1.GreaterThan(version2) { return v1 } - if v1 < v2 { - return v2 + return v2 +} + +func (r ReqsGraph) Upgrade(m module.Version) (module.Version, error) { + _, properties, err := r.VertexWithProperties(m) + if err != nil { + return module.Version{}, err + } + + releases, err := getReleasesFromSource(properties) + if err != nil { + return module.Version{}, err + } + + if releases == nil { + return m, nil + } + + m.Version, err = semver.LatestVersion(releases) + if err != nil { + return module.Version{}, err } - return v1 + return m, nil } -func (r ReqsMap) Upgrade(m module.Version) (module.Version, error) { - panic("unimplemented") +func (r ReqsGraph) Previous(m module.Version) (module.Version, error) { + _, properties, err := r.VertexWithProperties(m) + if err != nil { + return module.Version{}, err + } + + releases, err := getReleasesFromSource(properties) + if err != nil { + return module.Version{}, err + } + + if releases == nil { + return m, nil + } + + m.Version, err = semver.OldestVersion(releases) + if err != nil { + return module.Version{}, err + } + return m, nil } -func (r ReqsMap) Previous(m module.Version) (module.Version, error) { - panic("unimplemented") +func (r ReqsGraph) Required(m module.Version) ([]module.Version, error) { + adjMap, err := r.AdjacencyMap() + if err != nil { + return nil, err + } + var reqs []module.Version + for v := range adjMap[m] { + reqs = append(reqs, v) + } + return reqs, nil } -func (r ReqsMap) Required(m module.Version) ([]module.Version, error) { - rr, ok := r[m] - if !ok { - return nil, fmt.Errorf("missing module: %v", m) +func getReleasesFromSource(properties graph.VertexProperties) ([]string, error) { + var releases []string + var err error + + // there must be only one property depending on the download source type + if len(properties.Attributes) != 1 { + return nil, errors.MultipleSources } - return rr, nil + + for k, v := range properties.Attributes { + switch k { + case pkg.GIT: + releases, err = git.GetAllGithubReleases(v) + case pkg.OCI: + releases, err = oci.GetAllImageTags(v) + } + if err != nil { + return nil, err + } + } + + return releases, nil } diff --git a/pkg/mvs/mvs_test.go b/pkg/mvs/mvs_test.go new file mode 100644 index 00000000..b9cb8006 --- /dev/null +++ b/pkg/mvs/mvs_test.go @@ -0,0 +1,192 @@ +package mvs + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/mod/module" + "kcl-lang.io/kpm/pkg/3rdparty/mvs" + "kcl-lang.io/kpm/pkg/client" + "kcl-lang.io/kpm/pkg/utils" +) + +const testDataDir = "test_data" + +func getTestDir(subDir string) string { + pwd, _ := os.Getwd() + testDir := filepath.Join(pwd, testDataDir) + testDir = filepath.Join(testDir, subDir) + + return testDir +} + +func TestMax(t *testing.T) { + reqs := ReqsGraph{} + assert.Equal(t, reqs.Max("", "1.0.0", "2.0.0"), "2.0.0") + assert.Equal(t, reqs.Max("", "1.2", "2.0"), "2.0") + assert.Equal(t, reqs.Max("", "2.5.0", "2.6"), "2.6") + assert.Equal(t, reqs.Max("", "2.0.0", "v3.0"), "v3.0") +} + +func TestRequired(t *testing.T) { + pkg_path := filepath.Join(getTestDir("test_with_internal_deps"), "aaa") + assert.Equal(t, utils.DirExists(filepath.Join(pkg_path, "kcl.mod")), true) + kpmcli, err := client.NewKpmClient() + assert.Equal(t, err, nil) + kclPkg, err := kpmcli.LoadPkgFromPath(pkg_path) + assert.Equal(t, err, nil) + + _, depGraph, err := kpmcli.InitGraphAndDownloadDeps(kclPkg) + assert.Equal(t, err, nil) + + reqs := ReqsGraph{ + depGraph, + } + + req, err := reqs.Required(module.Version{Path: "aaa", Version: "0.0.1"}) + assert.Equal(t, err, nil) + assert.Equal(t, len(req), 2) + + expectedReqs := []module.Version{ + {Path: "bbb", Version: "0.0.1"}, + {Path: "ccc", Version: "0.0.1"}, + } + assert.Equal(t, req, expectedReqs) +} + +func TestMinBuildList(t *testing.T) { + pkg_path := getTestDir("test_with_external_deps") + assert.Equal(t, utils.DirExists(filepath.Join(pkg_path, "kcl.mod")), true) + kpmcli, err := client.NewKpmClient() + assert.Equal(t, err, nil) + kclPkg, err := kpmcli.LoadPkgFromPath(pkg_path) + assert.Equal(t, err, nil) + + _, depGraph, err := kpmcli.InitGraphAndDownloadDeps(kclPkg) + assert.Equal(t, err, nil) + + reqs := ReqsGraph{ + depGraph, + } + + target := module.Version{Path: kclPkg.GetPkgName(), Version: kclPkg.GetPkgVersion()} + + req, err := mvs.BuildList([]module.Version{target}, reqs) + assert.Equal(t, err, nil) + + expectedReqs := []module.Version{ + {Path: "test_with_external_deps", Version: "0.0.1"}, + {Path: "argo-cd-order", Version: "0.1.2"}, + {Path: "helloworld", Version: "0.1.0"}, + {Path: "json_merge_patch", Version: "0.1.0"}, + {Path: "k8s", Version: "1.29"}, + {Path: "podinfo", Version: "0.1.1"}, + } + assert.Equal(t, req, expectedReqs) + + base := []string{target.Path} + for depName := range kclPkg.Dependencies.Deps { + base = append(base, depName) + } + req, err = mvs.Req(target, base, reqs) + assert.Equal(t, err, nil) + + expectedReqs = []module.Version{ + {Path: "argo-cd-order", Version: "0.1.2"}, + {Path: "helloworld", Version: "0.1.0"}, + {Path: "json_merge_patch", Version: "0.1.0"}, + {Path: "k8s", Version: "1.29"}, + {Path: "podinfo", Version: "0.1.1"}, + {Path: "test_with_external_deps", Version: "0.0.1"}, + } + assert.Equal(t, req, expectedReqs) +} + +func TestUpgradeToLatest(t *testing.T) { + pkg_path := getTestDir("test_with_external_deps") + assert.Equal(t, utils.DirExists(filepath.Join(pkg_path, "kcl.mod")), true) + kpmcli, err := client.NewKpmClient() + assert.Equal(t, err, nil) + kclPkg, err := kpmcli.LoadPkgFromPath(pkg_path) + assert.Equal(t, err, nil) + + _, depGraph, err := kpmcli.InitGraphAndDownloadDeps(kclPkg) + assert.Equal(t, err, nil) + + reqs := ReqsGraph{ + depGraph, + } + + upgrade, err := reqs.Upgrade(module.Version{Path: "k8s", Version: "1.27"}) + assert.Equal(t, err, nil) + assert.Equal(t, upgrade, module.Version{Path: "k8s", Version: "1.29"}) +} + +func TestUpgradeAllToLatest(t *testing.T) { + pkg_path := getTestDir("test_with_external_deps") + assert.Equal(t, utils.DirExists(filepath.Join(pkg_path, "kcl.mod")), true) + kpmcli, err := client.NewKpmClient() + assert.Equal(t, err, nil) + kclPkg, err := kpmcli.LoadPkgFromPath(pkg_path) + assert.Equal(t, err, nil) + + _, depGraph, err := kpmcli.InitGraphAndDownloadDeps(kclPkg) + assert.Equal(t, err, nil) + + reqs := ReqsGraph{ + depGraph, + } + + target := module.Version{Path: kclPkg.GetPkgName(), Version: kclPkg.GetPkgVersion()} + fmt.Println(target) + upgrade, err := mvs.UpgradeAll(target, reqs) + assert.Equal(t, err, nil) + assert.Equal(t, upgrade, module.Version{Path: "k8s", Version: "1.29"}) +} + +func TestPrevious(t *testing.T) { + pkg_path := getTestDir("test_with_external_deps") + assert.Equal(t, utils.DirExists(filepath.Join(pkg_path, "kcl.mod")), true) + kpmcli, err := client.NewKpmClient() + assert.Equal(t, err, nil) + kclPkg, err := kpmcli.LoadPkgFromPath(pkg_path) + assert.Equal(t, err, nil) + + _, depGraph, err := kpmcli.InitGraphAndDownloadDeps(kclPkg) + assert.Equal(t, err, nil) + + reqs := ReqsGraph{ + depGraph, + } + + downgrade, err := reqs.Previous(module.Version{Path: "k8s", Version: "1.27"}) + assert.Equal(t, err, nil) + assert.Equal(t, downgrade, module.Version{Path: "k8s", Version: "1.14"}) +} + +func TestUpgradePreviousOfLocalDependency(t *testing.T) { + pkg_path := filepath.Join(getTestDir("test_with_internal_deps"), "aaa") + assert.Equal(t, utils.DirExists(filepath.Join(pkg_path, "kcl.mod")), true) + kpmcli, err := client.NewKpmClient() + assert.Equal(t, err, nil) + kclPkg, err := kpmcli.LoadPkgFromPath(pkg_path) + assert.Equal(t, err, nil) + + _, depGraph, err := kpmcli.InitGraphAndDownloadDeps(kclPkg) + assert.Equal(t, err, nil) + + reqs := ReqsGraph{ + depGraph, + } + + upgrade, err := reqs.Upgrade(module.Version{Path: "bbb", Version: "0.0.1"}) + assert.Equal(t, err, nil) + assert.Equal(t, upgrade, module.Version{Path: "bbb", Version: "0.0.1"}) + + downgrade, err := reqs.Previous(module.Version{Path: "bbb", Version: "0.0.1"}) + assert.Equal(t, err, nil) + assert.Equal(t, downgrade, module.Version{Path: "bbb", Version: "0.0.1"}) +} diff --git a/pkg/mvs/test_data/test_with_external_deps/kcl.mod b/pkg/mvs/test_data/test_with_external_deps/kcl.mod new file mode 100644 index 00000000..67278fa8 --- /dev/null +++ b/pkg/mvs/test_data/test_with_external_deps/kcl.mod @@ -0,0 +1,10 @@ +[package] +name = "test_with_external_deps" +edition = "0.0.1" +version = "0.0.1" + +[dependencies] +k8s = "1.27" +helloworld = "0.1.0" +argo-cd-order = "0.1.2" +podinfo = "0.1.1" \ No newline at end of file diff --git a/pkg/mvs/test_data/test_with_internal_deps/aaa/kcl.mod b/pkg/mvs/test_data/test_with_internal_deps/aaa/kcl.mod new file mode 100644 index 00000000..fdd2927f --- /dev/null +++ b/pkg/mvs/test_data/test_with_internal_deps/aaa/kcl.mod @@ -0,0 +1,8 @@ +[package] +name = "aaa" +edition = "0.0.1" +version = "0.0.1" + +[dependencies] +bbb = { path = "../bbb" } +ccc = { path = "../ccc" } diff --git a/pkg/mvs/test_data/test_with_internal_deps/bbb/kcl.mod b/pkg/mvs/test_data/test_with_internal_deps/bbb/kcl.mod new file mode 100644 index 00000000..e9ea10a5 --- /dev/null +++ b/pkg/mvs/test_data/test_with_internal_deps/bbb/kcl.mod @@ -0,0 +1,5 @@ +[package] +name = "bbb" +edition = "0.0.1" +version = "0.0.1" + diff --git a/pkg/mvs/test_data/test_with_internal_deps/ccc/kcl.mod b/pkg/mvs/test_data/test_with_internal_deps/ccc/kcl.mod new file mode 100644 index 00000000..9a762a4f --- /dev/null +++ b/pkg/mvs/test_data/test_with_internal_deps/ccc/kcl.mod @@ -0,0 +1,5 @@ +[package] +name = "ccc" +edition = "0.0.1" +version = "0.0.1" + diff --git a/pkg/oci/oci.go b/pkg/oci/oci.go index 989ac88c..c51da2bb 100644 --- a/pkg/oci/oci.go +++ b/pkg/oci/oci.go @@ -4,9 +4,13 @@ import ( "context" "fmt" "io" + "log" "os" "path/filepath" + "strings" + "github.com/containers/image/docker" + "github.com/containers/image/types" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/thoas/go-funk" "oras.land/oras-go/pkg/auth" @@ -385,3 +389,17 @@ func GenOciManifestFromPkg(kclPkg *pkg.KclPkg) (map[string]string, error) { res[constants.DEFAULT_KCL_OCI_MANIFEST_SUM] = sum return res, nil } + +func GetAllImageTags(imageName string) ([]string, error) { + sysCtx := &types.SystemContext{} + ref, err := docker.ParseReference("//" + strings.TrimPrefix(imageName, "oci://")) + if err != nil { + log.Fatalf("Error parsing reference: %v", err) + } + + tags, err := docker.GetRepositoryTags(context.Background(), sysCtx, ref) + if err != nil { + log.Fatalf("Error getting tags: %v", err) + } + return tags, nil +} diff --git a/pkg/package/modfile.go b/pkg/package/modfile.go index 65657ed8..55e6cb2a 100644 --- a/pkg/package/modfile.go +++ b/pkg/package/modfile.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/BurntSushi/toml" + "kcl-lang.io/kcl-go/pkg/kcl" "oras.land/oras-go/v2/registry" @@ -24,6 +25,9 @@ import ( const ( MOD_FILE = "kcl.mod" MOD_LOCK_FILE = "kcl.mod.lock" + GIT = "git" + OCI = "oci" + LOCAL = "local" ) // 'Package' is the kcl package section of 'kcl.mod'. @@ -244,6 +248,31 @@ func (dep *Dependency) GenDepFullName() string { return dep.FullName } +// GetDownloadPath will get the download path of a dependency. +func (dep *Dependency) GetDownloadPath() string { + if dep.Source.Git != nil { + return dep.Source.Git.Url + } + if dep.Source.Oci != nil { + return dep.Source.Oci.IntoOciUrl() + } + return "" +} + +// GetSourceType will get the source type of a dependency. +func (dep *Dependency) GetSourceType() string { + if dep.Source.Git != nil { + return GIT + } + if dep.Source.Oci != nil { + return OCI + } + if dep.Source.Local != nil { + return LOCAL + } + return "" +} + // Source is the package source from registry. type Source struct { *Git diff --git a/pkg/package/package.go b/pkg/package/package.go index 28401265..91dd547d 100644 --- a/pkg/package/package.go +++ b/pkg/package/package.go @@ -6,8 +6,6 @@ import ( "path/filepath" "strings" - "golang.org/x/mod/module" - "kcl-lang.io/kcl-go/pkg/kcl" "kcl-lang.io/kpm/pkg/constants" errors "kcl-lang.io/kpm/pkg/errors" @@ -22,8 +20,6 @@ type KclPkg struct { // The dependencies in the current kcl package are the dependencies of kcl.mod.lock, // not the dependencies in kcl.mod. Dependencies - // BuildList denotes the minimal build list for the current kcl package. - BuildList []module.Version // The flag 'NoSumCheck' is true if the checksum of the current kcl package is not checked. NoSumCheck bool } diff --git a/pkg/semver/semver.go b/pkg/semver/semver.go index 450bf8f9..53e0dd21 100644 --- a/pkg/semver/semver.go +++ b/pkg/semver/semver.go @@ -26,3 +26,22 @@ func LatestVersion(versions []string) (string, error) { return latest.Original(), nil } + +func OldestVersion(versions []string) (string, error) { + var oldest *version.Version + for _, v := range versions { + ver, err := version.NewVersion(v) + if err != nil { + return "", reporter.NewErrorEvent(reporter.FailedParseVersion, err, fmt.Sprintf("failed to parse version %s", v)) + } + if oldest == nil || ver.LessThan(oldest) { + oldest = ver + } + } + + if oldest == nil { + return "", errors.InvalidVersionFormat + } + + return oldest.Original(), nil +} \ No newline at end of file diff --git a/pkg/semver/semver_test.go b/pkg/semver/semver_test.go index 7e211eea..560b4d99 100644 --- a/pkg/semver/semver_test.go +++ b/pkg/semver/semver_test.go @@ -38,3 +38,35 @@ func TestTheLatestTagWithMissingVersion(t *testing.T) { assert.Equal(t, err, nil) assert.Equal(t, latest, "5.5") } + +func TestOldestVersion(t *testing.T) { + oldest, err := OldestVersion([]string{"1.2.3", "1.4.0", "1.3.5", "1.0.0"}) + assert.Equal(t, err, nil) + assert.Equal(t, oldest, "1.0.0") + + oldest, err = OldestVersion([]string{}) + assert.Equal(t, err, errors.InvalidVersionFormat) + assert.Equal(t, oldest, "") + + oldest, err = OldestVersion([]string{"invalid_version"}) + assert.Equal(t, err.Error(), "failed to parse version invalid_version\nMalformed version: invalid_version\n") + assert.Equal(t, oldest, "") + + oldest, err = OldestVersion([]string{"1.2.3", "1.4.0", "1.3.5", "invalid_version"}) + assert.Equal(t, err.Error(), "failed to parse version invalid_version\nMalformed version: invalid_version\n") + assert.Equal(t, oldest, "") +} + +func TestOldestVersionWithVariousFormats(t *testing.T) { + oldest, err := OldestVersion([]string{"2.2", "2.4.5", "2.3.9", "2.1.0", "2.0"}) + assert.Equal(t, err, nil) + assert.Equal(t, oldest, "2.0") + + oldest, err = OldestVersion([]string{"0.1", "0.1.1", "0.1.2-beta", "0.0.9"}) + assert.Equal(t, err, nil) + assert.Equal(t, oldest, "0.0.9") + + oldest, err = OldestVersion([]string{"3.3.3", "3.2", "3.1", "3.0.0"}) + assert.Equal(t, err, nil) + assert.Equal(t, oldest, "3.0.0") +}