diff --git a/.github/workflows/gen-docs-migration.yml b/.github/workflows/gen-docs-migration.yml new file mode 100644 index 0000000000..7ee442396d --- /dev/null +++ b/.github/workflows/gen-docs-migration.yml @@ -0,0 +1,34 @@ +name: Generate Migration Docs +on: + release: + types: [ published ] + +jobs: + cli: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: 'stable' + + - name: Generate Scaffold Migration Docs + run: ./scripts/gen-mig-diffs + + - name: Create Pull Request + id: cpr + uses: peter-evans/create-pull-request@v6 + with: + title: "docs(migration): update generated docs" + commit-message: "docs(migration): update generated docs" + body: "" + branch: feat/gen-migration-docs + add-paths: | + docs/ + + - name: Check outputs + run: | + echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" + echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" + diff --git a/changelog.md b/changelog.md index 369a2c0cb6..4bdfddd697 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ - [#4001](https://github.com/ignite/cli/pull/4001) Improve `xgenny` dry run - [#3967](https://github.com/ignite/cli/issues/3967) Add HD wallet parameters `address index` and `account number` to the chain account config - [#4004](https://github.com/ignite/cli/pull/4004) Remove all import placeholders using the `xast` pkg +- [#3718](https://github.com/ignite/cli/pull/3718) Add `gen-mig-diffs` tool app to compare scaffold output of two versions of ignite - [#4077](https://github.com/ignite/cli/pull/4077) Merge the swagger files manually instead use nodetime `swagger-combine` ### Changes diff --git a/ignite/internal/tools/gen-mig-diffs/cmd/root.go b/ignite/internal/tools/gen-mig-diffs/cmd/root.go new file mode 100644 index 0000000000..70c9aea268 --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/cmd/root.go @@ -0,0 +1,183 @@ +package cmd + +import ( + "fmt" + "path/filepath" + + "github.com/Masterminds/semver/v3" + "github.com/spf13/cobra" + + "github.com/ignite/cli/v29/ignite/pkg/cliui" + "github.com/ignite/cli/v29/ignite/pkg/errors" + "github.com/ignite/cli/v29/ignite/pkg/xgenny" + + "github.com/ignite/cli/ignite/internal/tools/gen-mig-diffs/pkg/diff" + "github.com/ignite/cli/ignite/internal/tools/gen-mig-diffs/pkg/repo" + "github.com/ignite/cli/ignite/internal/tools/gen-mig-diffs/pkg/scaffold" + "github.com/ignite/cli/ignite/internal/tools/gen-mig-diffs/templates/doc" +) + +const ( + flagFrom = "from" + flagTo = "to" + flagOutput = "output" + flagSource = "repo-source" + flagRepoURL = "repo-url" + flagRepoOutput = "repo-output" + flagScaffoldOutput = "scaffold-output" + flagScaffoldCache = "scaffold-cache" + + defaultDocPath = "docs/docs/06-migration" +) + +// NewRootCmd creates a new root command. +func NewRootCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "gen-mig-diffs", + Short: "GenerateBinaries migration diffs", + Long: "This tool is used to generate migration diff files for each of ignites scaffold commands", + RunE: func(cmd *cobra.Command, args []string) error { + session := cliui.New() + defer session.End() + + var ( + from, _ = cmd.Flags().GetString(flagFrom) + to, _ = cmd.Flags().GetString(flagTo) + repoSource, _ = cmd.Flags().GetString(flagSource) + output, _ = cmd.Flags().GetString(flagOutput) + repoURL, _ = cmd.Flags().GetString(flagRepoURL) + repoOutput, _ = cmd.Flags().GetString(flagRepoOutput) + scaffoldOutput, _ = cmd.Flags().GetString(flagScaffoldOutput) + scaffoldCache, _ = cmd.Flags().GetString(flagScaffoldCache) + ) + fromVer, err := semver.NewVersion(from) + if err != nil && from != "" { + return errors.Wrapf(err, "failed to parse from version %s", from) + } + toVer, err := semver.NewVersion(to) + if err != nil && to != "" { + return errors.Wrapf(err, "failed to parse to version %s", to) + } + + // Check or download the source and generate the binaries for each version. + repoOptions := make([]repo.Options, 0) + if repoSource != "" { + repoOptions = append(repoOptions, repo.WithSource(repoSource)) + } + if repoURL != "" { + repoOptions = append(repoOptions, repo.WithRepoURL(repoURL)) + } + if repoOutput != "" { + repoOptions = append(repoOptions, repo.WithRepoOutput(repoOutput)) + } + + igniteRepo, err := repo.New(fromVer, toVer, session, repoOptions...) + if err != nil { + return err + } + defer igniteRepo.Cleanup() + + releaseDescription, err := igniteRepo.ReleaseDescription() + if err != nil { + return errors.Wrapf(err, "failed to fetch the release tag %s description", igniteRepo.To.Original()) + } + + fromBin, toBin, err := igniteRepo.GenerateBinaries(cmd.Context()) + if err != nil { + return err + } + + // Scaffold the default commands for each version. + scaffoldOptions := make([]scaffold.Options, 0) + if scaffoldOutput != "" { + scaffoldOptions = append(scaffoldOptions, scaffold.WithOutput(scaffoldOutput)) + } + if scaffoldCache != "" { + scaffoldOptions = append(scaffoldOptions, scaffold.WithCachePath(scaffoldCache)) + } + + session.StartSpinner(fmt.Sprintf("Running scaffold commands for %s...", igniteRepo.From.Original())) + sFrom, err := scaffold.New(fromBin, igniteRepo.From, scaffoldOptions...) + if err != nil { + return err + } + defer sFrom.Cleanup() + + if err := sFrom.Run(cmd.Context()); err != nil { + return err + } + session.StopSpinner() + session.EventBus().SendInfo(fmt.Sprintf("Scaffolded code for %s at %s", igniteRepo.From.Original(), sFrom.Output)) + + session.StartSpinner(fmt.Sprintf("Running scaffold commands for %s...", igniteRepo.To.Original())) + sTo, err := scaffold.New(toBin, igniteRepo.To, scaffoldOptions...) + if err != nil { + return err + } + defer sTo.Cleanup() + + if err := sTo.Run(cmd.Context()); err != nil { + return err + } + session.StopSpinner() + session.EventBus().SendInfo(fmt.Sprintf("Scaffolded code for %s at %s", igniteRepo.To.Original(), sTo.Output)) + + // Calculate and save the diffs from the scaffolded code. + session.StartSpinner("Calculating diff...") + diffs, err := diff.CalculateDiffs(sFrom.Output, sTo.Output) + if err != nil { + return errors.Wrap(err, "failed to calculate diff") + } + + formatedDiffs, err := diff.FormatDiffs(diffs) + if err != nil { + return errors.Wrap(err, "failed to save diff map") + } + session.StopSpinner() + session.EventBus().SendInfo("Diff calculated successfully") + + output, err = filepath.Abs(output) + if err != nil { + return errors.Wrap(err, "failed to find the abs path") + } + + // Generate the docs file. + g, err := doc.NewGenerator(doc.Options{ + Path: output, + FromVersion: igniteRepo.From, + ToVersion: igniteRepo.To, + Diffs: string(formatedDiffs), + Description: releaseDescription, + }) + if err != nil { + return errors.Wrap(err, "failed to create the doc generator object") + } + + sm, err := xgenny.NewRunner(cmd.Context(), output).RunAndApply(g) + if err != nil { + return err + } + + files := append(sm.CreatedFiles(), sm.ModifiedFiles()...) + if len(files) == 0 { + return errors.Errorf("migration doc not created at %s", output) + } + session.EventBus().SendInfo( + fmt.Sprintf("Migration doc generated successfully at %s", files[0]), + ) + + return nil + }, + } + + cmd.Flags().StringP(flagFrom, "f", "", "Version of Ignite or path to Ignite source code to generate the diff from") + cmd.Flags().StringP(flagTo, "t", "", "Version of Ignite or path to Ignite source code to generate the diff to") + cmd.Flags().StringP(flagOutput, "o", defaultDocPath, "Output directory to save the migration document") + cmd.Flags().StringP(flagSource, "s", "", "Path to Ignite source code repository. Set the source automatically set the cleanup to false") + cmd.Flags().String(flagRepoURL, repo.DefaultRepoURL, "Git URL for the Ignite repository") + cmd.Flags().String(flagRepoOutput, "", "Output path to clone the Ignite repository") + cmd.Flags().String(flagScaffoldOutput, "", "Output path to clone the Ignite repository") + cmd.Flags().String(flagScaffoldCache, "", "Path to cache directory") + + return cmd +} diff --git a/ignite/internal/tools/gen-mig-diffs/go.mod b/ignite/internal/tools/gen-mig-diffs/go.mod new file mode 100644 index 0000000000..931888d888 --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/go.mod @@ -0,0 +1,93 @@ +module github.com/ignite/cli/ignite/internal/tools/gen-mig-diffs + +go 1.22.2 + +replace github.com/ignite/cli/v29 => ../../../../ + +require ( + github.com/Masterminds/semver/v3 v3.2.1 + github.com/go-git/go-git/v5 v5.12.0 + github.com/gobuffalo/genny/v2 v2.1.0 + github.com/gobuffalo/plush/v4 v4.1.19 + github.com/gobwas/glob v0.2.3 + github.com/hexops/gotextdiff v1.0.3 + github.com/ignite/cli/v29 v29.0.0-00010101000000-000000000000 + github.com/spf13/cobra v1.8.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/AlecAivazis/survey/v2 v2.3.7 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/briandowns/spinner v1.23.0 // indirect + github.com/charmbracelet/lipgloss v0.10.0 // indirect + github.com/chzyer/readline v1.5.1 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cockroachdb/errors v1.11.1 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/redact v1.1.5 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/proto v1.13.2 // indirect + github.com/emicklei/proto-contrib v0.16.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/getsentry/sentry-go v0.27.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/gobuffalo/flect v1.0.2 // indirect + github.com/gobuffalo/github_flavored_markdown v1.1.4 // indirect + github.com/gobuffalo/helpers v0.6.7 // indirect + github.com/gobuffalo/logger v1.0.7 // indirect + github.com/gobuffalo/packd v1.0.2 // indirect + github.com/gobuffalo/tags/v3 v3.1.4 // indirect + github.com/gobuffalo/validate/v3 v3.3.3 // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/microcosm-cc/bluemonday v1.0.26 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect + github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.20.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/ignite/internal/tools/gen-mig-diffs/go.sum b/ignite/internal/tools/gen-mig-diffs/go.sum new file mode 100644 index 0000000000..0587461407 --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/go.sum @@ -0,0 +1,323 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= +github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= +github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= +github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= +github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/emicklei/proto-contrib v0.16.0 h1:I0rAHhIVfI6j6GC3ojlFHXH3wNH8gvxtBOjlp64UQ0o= +github.com/emicklei/proto-contrib v0.16.0/go.mod h1:fzL/hybgHb6PA60EnSS2WnYoR4SPTD8sF5FpUa+Ay/Q= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/gobuffalo/flect v0.3.0/go.mod h1:5pf3aGnsvqvCj50AVni7mJJF8ICxGZ8HomberC3pXLE= +github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= +github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gobuffalo/genny/v2 v2.1.0 h1:cCRBbqzo3GfNvj3UetD16zRgUvWFEyyl0qTqquuIqOM= +github.com/gobuffalo/genny/v2 v2.1.0/go.mod h1:4yoTNk4bYuP3BMM6uQKYPvtP6WsXFGm2w2EFYZdRls8= +github.com/gobuffalo/github_flavored_markdown v1.1.3/go.mod h1:IzgO5xS6hqkDmUh91BW/+Qxo/qYnvfzoz3A7uLkg77I= +github.com/gobuffalo/github_flavored_markdown v1.1.4 h1:WacrEGPXUDX+BpU1GM/Y0ADgMzESKNWls9hOTG1MHVs= +github.com/gobuffalo/github_flavored_markdown v1.1.4/go.mod h1:Vl9686qrVVQou4GrHRK/KOG3jCZOKLUqV8MMOAYtlso= +github.com/gobuffalo/helpers v0.6.7 h1:C9CedoRSfgWg2ZoIkVXgjI5kgmSpL34Z3qdnzpfNVd8= +github.com/gobuffalo/helpers v0.6.7/go.mod h1:j0u1iC1VqlCaJEEVkZN8Ia3TEzfj/zoXANqyJExTMTA= +github.com/gobuffalo/logger v1.0.7 h1:LTLwWelETXDYyqF/ASf0nxaIcdEOIJNxRokPcfI/xbU= +github.com/gobuffalo/logger v1.0.7/go.mod h1:u40u6Bq3VVvaMcy5sRBclD8SXhBYPS0Qk95ubt+1xJM= +github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw= +github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8= +github.com/gobuffalo/plush/v4 v4.1.16/go.mod h1:6t7swVsarJ8qSLw1qyAH/KbrcSTwdun2ASEQkOznakg= +github.com/gobuffalo/plush/v4 v4.1.19 h1:o0E5gEJw+ozkAwQoCeiaWC6VOU2lEmX+GhtGkwpqZ8o= +github.com/gobuffalo/plush/v4 v4.1.19/go.mod h1:WiKHJx3qBvfaDVlrv8zT7NCd3dEMaVR/fVxW4wqV17M= +github.com/gobuffalo/tags/v3 v3.1.4 h1:X/ydLLPhgXV4h04Hp2xlbI2oc5MDaa7eub6zw8oHjsM= +github.com/gobuffalo/tags/v3 v3.1.4/go.mod h1:ArRNo3ErlHO8BtdA0REaZxijuWnWzF6PUXngmMXd2I0= +github.com/gobuffalo/validate/v3 v3.3.3 h1:o7wkIGSvZBYBd6ChQoLxkz2y1pfmhbI4jNJYh6PuNJ4= +github.com/gobuffalo/validate/v3 v3.3.3/go.mod h1:YC7FsbJ/9hW/VjQdmXPvFqvRis4vrRYFxr69WiNZw6g= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50= +github.com/microcosm-cc/bluemonday v1.0.22/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ignite/internal/tools/gen-mig-diffs/main.go b/ignite/internal/tools/gen-mig-diffs/main.go new file mode 100644 index 0000000000..11de0ee9fb --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "os" + + "github.com/ignite/cli/ignite/internal/tools/gen-mig-diffs/cmd" +) + +func main() { + if err := cmd.NewRootCmd().Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/cache/cache.go b/ignite/internal/tools/gen-mig-diffs/pkg/cache/cache.go new file mode 100644 index 0000000000..3804d5c766 --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/cache/cache.go @@ -0,0 +1,77 @@ +package cache + +import ( + "os" + "path/filepath" + "sync" + + "github.com/ignite/cli/v29/ignite/pkg/errors" + "github.com/ignite/cli/v29/ignite/pkg/xos" +) + +// Cache represents a cache for executed scaffold command. +type Cache struct { + cachePath string + cachesPath map[string]string + mu sync.RWMutex +} + +// New initializes a new Cache instance. +func New(path string) (*Cache, error) { + return &Cache{ + cachePath: path, + cachesPath: make(map[string]string), + }, os.MkdirAll(path, os.ModePerm) +} + +// Save creates a new cache. +func (c *Cache) Save(name, path string) error { + c.mu.Lock() + defer c.mu.Unlock() + + dstPath := filepath.Join(c.cachePath, name) + if err := xos.CopyFolder(path, dstPath); err != nil { + return err + } + + c.cachesPath[name] = dstPath + return nil +} + +// Has return if the cache exist. +func (c *Cache) Has(name string) bool { + c.mu.RLock() + defer c.mu.RUnlock() + + cachePath, ok := c.cachesPath[name] + if !ok { + return false + } + if _, err := os.Stat(cachePath); os.IsNotExist(err) { + return false + } + + return true +} + +// Get return the cache path and copy all files to the destination path. +func (c *Cache) Get(name, dstPath string) error { + c.mu.RLock() + defer c.mu.RUnlock() + + cachePath, ok := c.cachesPath[name] + if !ok { + return errors.Errorf("command %s not exist in the cache list", name) + } + if _, err := os.Stat(cachePath); os.IsNotExist(err) { + return errors.Wrapf(err, "cache %s not exist in the path", name) + } + dstPath, err := filepath.Abs(dstPath) + if err != nil { + return err + } + if err := xos.CopyFolder(cachePath, dstPath); err != nil { + return errors.Wrapf(err, "error to copy cache from %s to %s", cachePath, dstPath) + } + return nil +} diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/diff/compute.go b/ignite/internal/tools/gen-mig-diffs/pkg/diff/compute.go new file mode 100644 index 0000000000..b1ae925e03 --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/diff/compute.go @@ -0,0 +1,119 @@ +package diff + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/gobwas/glob" + "github.com/hexops/gotextdiff" + "github.com/hexops/gotextdiff/myers" + "github.com/hexops/gotextdiff/span" + + "github.com/ignite/cli/v29/ignite/pkg/errors" +) + +// computeFS computes the unified diffs between the origin and modified filesystems. +// but ignores files that match the given globs. +func computeFS(origin, modified fs.FS, ignoreGlobs ...string) ([]gotextdiff.Unified, error) { + compiledGlobs, err := compileGlobs(ignoreGlobs) + if err != nil { + return nil, err + } + + marked := make(map[string]struct{}) + unified := make([]gotextdiff.Unified, 0) + err = fs.WalkDir(origin, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return errors.Errorf("failed to walk origin: %w", err) + } + + if d.IsDir() { + return nil + } + + if matchGlobs(compiledGlobs, path) { + return nil + } + + marked[path] = struct{}{} + data, err := fs.ReadFile(origin, path) + if err != nil { + return errors.Errorf("failed to read file %q from origin: %w", path, err) + } + originFile := string(data) + + data, err = fs.ReadFile(modified, path) + if !os.IsNotExist(err) && err != nil { + return errors.Errorf("failed to read file %q from modified: %w", path, err) + } + modifiedFile := string(data) + + edits := myers.ComputeEdits(span.URIFromURI(fmt.Sprintf("file://%s", path)), originFile, modifiedFile) + if len(edits) > 0 { + unified = append(unified, gotextdiff.ToUnified(path, path, originFile, edits)) + } + return nil + }) + if err != nil { + return nil, err + } + + err = fs.WalkDir(modified, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return errors.Errorf("failed to walk modified: %w", err) + } + + if d.IsDir() { + return nil + } + + if _, ok := marked[path]; ok { + return nil + } + + if matchGlobs(compiledGlobs, path) { + return nil + } + + originFile := "" + data, err := fs.ReadFile(modified, path) + if err != nil { + return errors.Errorf("failed to read file %q from modified: %w", path, err) + } + modifiedFile := string(data) + + edits := myers.ComputeEdits(span.URIFromURI(fmt.Sprintf("file://%s", path)), originFile, modifiedFile) + if len(edits) > 0 { + unified = append(unified, gotextdiff.ToUnified(path, path, originFile, edits)) + } + return nil + }) + if err != nil { + return nil, err + } + + return unified, nil +} + +func compileGlobs(globs []string) ([]glob.Glob, error) { + var compiledGlobs []glob.Glob + for _, g := range globs { + compiledGlob, err := glob.Compile(g, filepath.Separator) + if err != nil { + return nil, errors.Errorf("failed to compile glob %q: %w", g, err) + } + compiledGlobs = append(compiledGlobs, compiledGlob) + } + return compiledGlobs, nil +} + +func matchGlobs(globs []glob.Glob, path string) bool { + for _, g := range globs { + if g.Match(path) { + return true + } + } + return false +} diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/diff/compute_test.go b/ignite/internal/tools/gen-mig-diffs/pkg/diff/compute_test.go new file mode 100644 index 0000000000..01b3968180 --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/diff/compute_test.go @@ -0,0 +1,55 @@ +package diff + +import ( + "testing" + "testing/fstest" + + "github.com/stretchr/testify/require" +) + +func TestComputeFS(t *testing.T) { + require := require.New(t) + + origin := fstest.MapFS{ + "foo.txt": &fstest.MapFile{ + Data: []byte("hello"), + }, + "bar.txt": &fstest.MapFile{ + Data: []byte("unmodified"), + }, + "pkg/main.go": &fstest.MapFile{ + Data: []byte("package main"), + }, + } + modified := fstest.MapFS{ + "foo.txt": &fstest.MapFile{ + Data: []byte("world"), + }, + "bar.txt": &fstest.MapFile{ + Data: []byte("unmodified"), + }, + "new.txt": &fstest.MapFile{ + Data: []byte("new file"), + }, + "pkg/main.go": &fstest.MapFile{ + Data: []byte("package main\nfunc main() {}"), + }, + } + + unified, err := computeFS(origin, modified) + require.NoError(err) + require.Len(unified, 3) + expectedFiles := []string{"foo.txt", "new.txt", "pkg/main.go"} + for _, u := range unified { + require.Contains(expectedFiles, u.From, "unexpected file in diff: %s", u.From) + } + + // Test ignoring files + unified, err = computeFS(origin, modified, "**.go") + require.NoError(err) + require.Len(unified, 2) + expectedFiles = []string{"foo.txt", "new.txt"} + for _, u := range unified { + require.Contains(expectedFiles, u.From, "unexpected file in diff: %s", u.From) + } +} diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/diff/diff.go b/ignite/internal/tools/gen-mig-diffs/pkg/diff/diff.go new file mode 100644 index 0000000000..b66b7a1ced --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/diff/diff.go @@ -0,0 +1,154 @@ +package diff + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + + "github.com/hexops/gotextdiff" + + "github.com/ignite/cli/v29/ignite/pkg/xstrings" +) + +type Diffs map[string][]gotextdiff.Unified + +var diffIgnoreGlobs = []string{ + ".git/**", + "**.md", + "go.sum", + "**_test.go", + "**.pb.go", + "**.pb.gw.go", + "**.pulsar.go", + "**/node_modules/**", + "**/openapi.yml", + ".gitignore", + ".github/**", + "**.html", + "**.css", + "**.js", + "**.ts", + "**.json", +} + +// CalculateDiffs calculate the diff from two directories. +func CalculateDiffs(fromDir, toDir string) (Diffs, error) { + paths, err := readRootFolders(fromDir) + if err != nil { + return nil, err + } + toPaths, err := readRootFolders(toDir) + if err != nil { + return nil, err + } + for key, value := range toPaths { + paths[key] = value + } + + diffs := make(Diffs) + for path := range paths { + from := filepath.Join(fromDir, path) + if err := os.MkdirAll(from, os.ModePerm); err != nil { + return nil, err + } + to := filepath.Join(toDir, path) + if err := os.MkdirAll(to, os.ModePerm); err != nil { + return nil, err + } + + computedDiff, err := computeFS( + os.DirFS(from), + os.DirFS(to), + diffIgnoreGlobs..., + ) + if err != nil { + return nil, err + } + + diffs[path] = computedDiff + } + return subtractBaseDiffs(diffs), nil +} + +// SaveDiffs save all migration diffs to the output path. +func SaveDiffs(diffs Diffs, outputPath string) error { + if err := os.MkdirAll(outputPath, os.ModePerm); err != nil { + return err + } + + for name, diffs := range diffs { + output, err := os.Create(filepath.Join(outputPath, name+".diff")) + if err != nil { + return err + } + for _, d := range diffs { + output.WriteString(fmt.Sprint(d)) + output.WriteString("\n") + } + if err := output.Close(); err != nil { + return err + } + } + + return nil +} + +// FormatDiffs format all diffs in a single markdown byte array. +func FormatDiffs(diffs Diffs) ([]byte, error) { + if len(diffs) == 0 { + return []byte{}, nil + } + buffer := &bytes.Buffer{} + for name, diffs := range diffs { + if len(diffs) == 0 { + continue + } + buffer.WriteString(fmt.Sprintf("#### **%s diff**\n\n", xstrings.ToUpperFirst(name))) + buffer.WriteString("```diff\n") + for _, d := range diffs { + buffer.WriteString(fmt.Sprint(d)) + } + buffer.WriteString("```\n\n") + } + return buffer.Bytes(), nil +} + +// readRootFolders return a map of all root folders from a directory. +func readRootFolders(dir string) (map[string]struct{}, error) { + paths := make(map[string]struct{}) + dirEntries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + for _, entry := range dirEntries { + if entry.IsDir() { + paths[entry.Name()] = struct{}{} + } + } + return paths, nil +} + +// subtractBaseDiffs removes chain and module diffs from other diffs. +func subtractBaseDiffs(diffs Diffs) Diffs { + chainDiff := diffs["chain"] + moduleDiff := diffs["module"] + for name, d := range diffs { + if name != "chain" && name != "module" { + diffs[name] = subtractUnifieds(d, moduleDiff) + } + } + diffs["module"] = subtractUnifieds(moduleDiff, chainDiff) + return diffs +} + +func subtractUnifieds(a, b []gotextdiff.Unified) []gotextdiff.Unified { + for i, ad := range a { + for _, bd := range b { + if ad.From == bd.From && ad.To == bd.To { + a[i] = subtract(ad, bd) + } + } + } + return a +} diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/diff/subtract.go b/ignite/internal/tools/gen-mig-diffs/pkg/diff/subtract.go new file mode 100644 index 0000000000..f80edcd513 --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/diff/subtract.go @@ -0,0 +1,165 @@ +package diff + +import ( + "sort" + + "github.com/hexops/gotextdiff" +) + +// subtract two unified diffs from each other. +func subtract(a, b gotextdiff.Unified) gotextdiff.Unified { + return gotextdiff.Unified{ + From: a.From, + To: a.To, + Hunks: subtractHunks(a.Hunks, b.Hunks), + } +} + +func subtractHunks(src, base []*gotextdiff.Hunk) []*gotextdiff.Hunk { + sortHunks(src) + sortHunks(base) + + res := make([]*gotextdiff.Hunk, 0, len(src)) + offset := 0 + for i, j := 0, 0; i < len(src) || j < len(base); { + if i >= len(src) { + break + } + if j >= len(base) { + res = append(res, src[i]) + i++ + continue + } + + s := src[i] + b := base[j] + + switch { + case beforeHunk(s, b, offset): + res = append(res, s) + offset += calculateHunkOffsetChange(s.Lines) + i++ + case beforeHunk(b, s, -offset): + j++ + case hunksOverlap(s, b, offset): + if s.FromLine < b.FromLine { + res = append(res, s) + offset += calculateHunkOffsetChange(s.Lines) - calculateHunkOffsetChange(b.Lines) + i++ + } else { + offset += calculateHunkOffsetChange(s.Lines) - calculateHunkOffsetChange(b.Lines) + j++ + } + default: + h := subtractHunk(s, b) + if !isHunkEmpty(h) { + res = append(res, subtractHunk(s, b)) + } + offset += calculateHunkOffsetChange(s.Lines) - calculateHunkOffsetChange(b.Lines) + i++ + j++ + } + + } + + return res +} + +func sortHunks(hunks []*gotextdiff.Hunk) { + sort.Slice(hunks, func(i, j int) bool { + return hunks[i].FromLine < hunks[j].FromLine + }) +} + +// beforeHunk returns true if a comes before b. +func beforeHunk(a, b *gotextdiff.Hunk, offset int) bool { + return a.ToLine-calculateEndEqualLines(a) < b.FromLine+calculateStartEqualLines(b)+offset +} + +func calculateStartEqualLines(h *gotextdiff.Hunk) int { + lines := 0 + for _, l := range h.Lines { + if l.Kind == gotextdiff.Equal { + lines++ + } else { + break + } + } + return lines +} + +func calculateEndEqualLines(h *gotextdiff.Hunk) int { + lines := 0 + for i := len(h.Lines) - 1; i >= 0; i-- { + if h.Lines[i].Kind == gotextdiff.Equal { + lines++ + } else { + break + } + } + return lines +} + +func calculateHunkOffsetChange(lines []gotextdiff.Line) int { + offset := 0 + for _, l := range lines { + if l.Kind == gotextdiff.Insert { + offset++ + } else if l.Kind == gotextdiff.Delete { + offset-- + } + } + return offset +} + +func hunksOverlap(a, b *gotextdiff.Hunk, offset int) bool { + if !isLineInHunk(a.FromLine, b, offset) && isLineInHunk(a.ToLine, b, offset) { + return true + } + if isLineInHunk(a.FromLine, b, offset) && !isLineInHunk(a.ToLine, b, offset) { + return true + } + return false +} + +func isLineInHunk(line int, h *gotextdiff.Hunk, offset int) bool { + return line-calculateStartEqualLines(h) > h.FromLine+offset && line+calculateEndEqualLines(h) < h.ToLine+offset +} + +func subtractHunk(a, b *gotextdiff.Hunk) *gotextdiff.Hunk { + lines := subtractLines(a.Lines, b.Lines) + return &gotextdiff.Hunk{ + FromLine: a.FromLine, + ToLine: a.ToLine + calculateHunkOffsetChange(a.Lines) - calculateHunkOffsetChange(lines), + Lines: lines, + } +} + +func subtractLines(a, b []gotextdiff.Line) []gotextdiff.Line { + res := make([]gotextdiff.Line, 0, len(a)) + for _, la := range a { + rep := false + for _, lb := range b { + if la.Kind != gotextdiff.Equal && la.Kind == lb.Kind && la.Content == lb.Content { + rep = true + break + } + } + + if !rep { + res = append(res, la) + } + } + + return res +} + +func isHunkEmpty(h *gotextdiff.Hunk) bool { + effectiveLines := 0 + for _, l := range h.Lines { + if l.Kind != gotextdiff.Equal { + effectiveLines++ + } + } + return effectiveLines == 0 +} diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/diff/subtract_test.go b/ignite/internal/tools/gen-mig-diffs/pkg/diff/subtract_test.go new file mode 100644 index 0000000000..78d2a64426 --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/diff/subtract_test.go @@ -0,0 +1,178 @@ +package diff + +import ( + "reflect" + "testing" + + "github.com/hexops/gotextdiff" +) + +func TestSubtract(t *testing.T) { + type args struct { + a gotextdiff.Unified + b gotextdiff.Unified + } + tests := []struct { + name string + args args + want gotextdiff.Unified + }{ + { + name: "Equal diffs", + args: args{ + a: gotextdiff.Unified{ + Hunks: []*gotextdiff.Hunk{ + { + FromLine: 1, + ToLine: 3, + Lines: []gotextdiff.Line{ + {Kind: gotextdiff.Equal, Content: "equal1\n"}, + {Kind: gotextdiff.Insert, Content: "insert1\n"}, + {Kind: gotextdiff.Equal, Content: "equal2\n"}, + {Kind: gotextdiff.Delete, Content: "delete1\n"}, + }, + }, + }, + }, + b: gotextdiff.Unified{ + Hunks: []*gotextdiff.Hunk{ + { + FromLine: 1, + ToLine: 3, + Lines: []gotextdiff.Line{ + {Kind: gotextdiff.Equal, Content: "equal1\n"}, + {Kind: gotextdiff.Insert, Content: "insert1\n"}, + {Kind: gotextdiff.Equal, Content: "equal2\n"}, + {Kind: gotextdiff.Delete, Content: "delete1\n"}, + }, + }, + }, + }, + }, + want: gotextdiff.Unified{ + Hunks: []*gotextdiff.Hunk{}, + }, + }, + { + name: "Add hunk at the beginning", + args: args{ + a: gotextdiff.Unified{ + Hunks: []*gotextdiff.Hunk{ + { + FromLine: 1, + ToLine: 3, + Lines: []gotextdiff.Line{ + {Kind: gotextdiff.Equal, Content: "equal1\n"}, + {Kind: gotextdiff.Insert, Content: "insert1\n"}, + {Kind: gotextdiff.Equal, Content: "equal2\n"}, + {Kind: gotextdiff.Delete, Content: "delete1\n"}, + }, + }, + { + FromLine: 4, + ToLine: 6, + Lines: []gotextdiff.Line{ + {Kind: gotextdiff.Equal, Content: "equal3\n"}, + {Kind: gotextdiff.Insert, Content: "insert2\n"}, + {Kind: gotextdiff.Equal, Content: "equal4\n"}, + {Kind: gotextdiff.Delete, Content: "delete2\n"}, + }, + }, + }, + }, + b: gotextdiff.Unified{ + Hunks: []*gotextdiff.Hunk{ + { + FromLine: 4, + ToLine: 6, + Lines: []gotextdiff.Line{ + {Kind: gotextdiff.Equal, Content: "equal3\n"}, + {Kind: gotextdiff.Insert, Content: "insert2\n"}, + {Kind: gotextdiff.Equal, Content: "equal4\n"}, + {Kind: gotextdiff.Delete, Content: "delete2\n"}, + }, + }, + }, + }, + }, + want: gotextdiff.Unified{ + Hunks: []*gotextdiff.Hunk{ + { + FromLine: 1, + ToLine: 3, + Lines: []gotextdiff.Line{ + {Kind: gotextdiff.Equal, Content: "equal1\n"}, + {Kind: gotextdiff.Insert, Content: "insert1\n"}, + {Kind: gotextdiff.Equal, Content: "equal2\n"}, + {Kind: gotextdiff.Delete, Content: "delete1\n"}, + }, + }, + }, + }, + }, + { + name: "Add hunk at the end", + args: args{ + a: gotextdiff.Unified{ + Hunks: []*gotextdiff.Hunk{ + { + FromLine: 1, + ToLine: 3, + Lines: []gotextdiff.Line{ + {Kind: gotextdiff.Equal, Content: "equal1"}, + {Kind: gotextdiff.Insert, Content: "insert1"}, + {Kind: gotextdiff.Equal, Content: "equal2"}, + {Kind: gotextdiff.Delete, Content: "delete1"}, + }, + }, + { + FromLine: 4, + ToLine: 6, + Lines: []gotextdiff.Line{ + {Kind: gotextdiff.Equal, Content: "equal3"}, + {Kind: gotextdiff.Insert, Content: "insert2"}, + {Kind: gotextdiff.Equal, Content: "equal4"}, + {Kind: gotextdiff.Delete, Content: "delete2"}, + }, + }, + }, + }, + b: gotextdiff.Unified{ + Hunks: []*gotextdiff.Hunk{ + { + FromLine: 1, + ToLine: 3, + Lines: []gotextdiff.Line{ + {Kind: gotextdiff.Equal, Content: "equal1"}, + {Kind: gotextdiff.Insert, Content: "insert1"}, + {Kind: gotextdiff.Equal, Content: "equal2"}, + {Kind: gotextdiff.Delete, Content: "delete1"}, + }, + }, + }, + }, + }, + want: gotextdiff.Unified{ + Hunks: []*gotextdiff.Hunk{ + { + FromLine: 4, + ToLine: 6, + Lines: []gotextdiff.Line{ + {Kind: gotextdiff.Equal, Content: "equal3"}, + {Kind: gotextdiff.Insert, Content: "insert2"}, + {Kind: gotextdiff.Equal, Content: "equal4"}, + {Kind: gotextdiff.Delete, Content: "delete2"}, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := subtract(tt.args.a, tt.args.b); !reflect.DeepEqual(got, tt.want) { + t.Errorf("subtract() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/modified/bar.txt b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/modified/bar.txt new file mode 100644 index 0000000000..bd03c683ce --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/modified/bar.txt @@ -0,0 +1 @@ +unmodified \ No newline at end of file diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/modified/foo.txt b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/modified/foo.txt new file mode 100644 index 0000000000..04fea06420 --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/modified/foo.txt @@ -0,0 +1 @@ +world \ No newline at end of file diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/modified/new.txt b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/modified/new.txt new file mode 100644 index 0000000000..1271944b7e --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/modified/new.txt @@ -0,0 +1 @@ +new file \ No newline at end of file diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/modified/pkg/main.go b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/modified/pkg/main.go new file mode 100644 index 0000000000..38dd16da61 --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/modified/pkg/main.go @@ -0,0 +1,3 @@ +package main + +func main() {} diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/origin/bar.txt b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/origin/bar.txt new file mode 100644 index 0000000000..bd03c683ce --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/origin/bar.txt @@ -0,0 +1 @@ +unmodified \ No newline at end of file diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/origin/foo.txt b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/origin/foo.txt new file mode 100644 index 0000000000..b6fc4c620b --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/origin/foo.txt @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/origin/pkg/main.go b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/origin/pkg/main.go new file mode 100644 index 0000000000..06ab7d0f9a --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/diff/testdata/origin/pkg/main.go @@ -0,0 +1 @@ +package main diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/repo/repo.go b/ignite/internal/tools/gen-mig-diffs/pkg/repo/repo.go new file mode 100644 index 0000000000..000eab5f52 --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/repo/repo.go @@ -0,0 +1,426 @@ +package repo + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + + "github.com/ignite/cli/v29/ignite/pkg/cliui" + "github.com/ignite/cli/v29/ignite/pkg/cmdrunner/exec" + "github.com/ignite/cli/v29/ignite/pkg/cmdrunner/step" + "github.com/ignite/cli/v29/ignite/pkg/errors" +) + +const ( + DefaultRepoURL = "https://github.com/ignite/cli.git" + defaultBinaryPath = "dist/ignite" +) + +type ( + // Generator is used to generate migration diffs. + Generator struct { + From, To *semver.Version + source string + binPath string + repo *git.Repository + session *cliui.Session + cleanup bool + } + + // options represents configuration for the generator. + options struct { + source string + output string + repoURL string + binPath string + } + // Options configures the generator. + Options func(*options) +) + +// newOptions returns a options with default options. +func newOptions() (options, error) { + var ( + tmpDir = os.TempDir() + binPath = filepath.Join(tmpDir, "bin") + output = filepath.Join(tmpDir, "migration-source") + ) + if err := os.RemoveAll(binPath); err != nil { + return options{}, errors.Wrap(err, "failed to clean the output directory") + } + if err := os.RemoveAll(output); err != nil { + return options{}, errors.Wrap(err, "failed to clean the output directory") + } + return options{ + source: "", + binPath: filepath.Join(tmpDir, "bin"), + output: filepath.Join(tmpDir, "migration-source"), + repoURL: DefaultRepoURL, + }, nil +} + +// WithSource set the repo source Options. +func WithSource(source string) Options { + return func(o *options) { + o.source = source + } +} + +// WithRepoURL set the repo URL Options. +func WithRepoURL(repoURL string) Options { + return func(o *options) { + o.repoURL = repoURL + } +} + +// WithRepoOutput set the repo output Options. +func WithRepoOutput(output string) Options { + return func(o *options) { + o.output = output + } +} + +// WithBinPath set the binary path to build the source. +func WithBinPath(binPath string) Options { + return func(o *options) { + o.binPath = binPath + } +} + +// validate options. +func (o options) validate() error { + if o.source != "" && (o.repoURL != DefaultRepoURL) { + return errors.New("cannot set source and repo URL at the same time") + } + if o.source != "" { + return errors.New("cannot set source and cleanup the repo") + } + return nil +} + +// New creates a new generator for migration diffs between from and to versions of ignite cli +// If source is empty, then it clones the ignite cli repository to a temporary directory and uses it as the source. +func New(from, to *semver.Version, session *cliui.Session, options ...Options) (*Generator, error) { + opts, err := newOptions() + if err != nil { + return nil, err + } + + for _, apply := range options { + apply(&opts) + } + if err := opts.validate(); err != nil { + return nil, err + } + + var ( + source = opts.source + repo *git.Repository + ) + if source != "" { + repo, err = verifyRepoSource(source, opts.repoURL) + if err != nil { + return nil, errors.Wrap(err, "failed to open ignite repository") + } + + session.StopSpinner() + session.EventBus().SendInfo(fmt.Sprintf("Using ignite repository at: %s", source)) + } else { + session.StartSpinner("Cloning ignite repository...") + + source = opts.output + repo, err = git.PlainClone(source, false, &git.CloneOptions{URL: opts.repoURL, Depth: 1}) + if errors.Is(err, git.ErrRepositoryAlreadyExists) { + repo, err = verifyRepoSource(source, opts.repoURL) + } + if err != nil { + return nil, errors.Wrap(err, "failed to clone ignite repository") + } + + session.StopSpinner() + session.EventBus().SendInfo(fmt.Sprintf("Cloned ignite repository to: %s", source)) + } + + versions, err := getRepoVersionTags(repo) + if err != nil { + return nil, err + } + + from, to, err = validateVersionRange(from, to, versions) + if err != nil { + return nil, err + } + + binPath, err := filepath.Abs(opts.binPath) + if err != nil { + return nil, err + } + + return &Generator{ + From: from, + To: to, + source: source, + repo: repo, + session: session, + binPath: binPath, + }, nil +} + +// ReleaseDescription generate the release description based in the tag data, if not exist, from the commit data. +func (g *Generator) ReleaseDescription() (string, error) { + tag, err := g.repo.Tag(g.To.Original()) + if err != nil { + return "", errors.Wrapf(err, "failed to get tag %s", g.To.Original()) + } + + var ( + author string + date time.Time + msg string + ) + tagObj, err := g.repo.TagObject(tag.Hash()) + switch { + case errors.Is(err, plumbing.ErrObjectNotFound): + commit, err := g.repo.CommitObject(tag.Hash()) + if err != nil { + return "", errors.Wrapf(err, "failed to get commit %s", g.To.Original()) + } + author = commit.Author.String() + date = commit.Author.When + msg = commit.Message + case err != nil: + return "", errors.Wrapf(err, "failed to get tag object %s", tag.Hash().String()) + default: + author = tagObj.Tagger.String() + date = tagObj.Tagger.When + msg = tagObj.Message + } + + description := fmt.Sprintf(`Tag: %[1]v +Commit: %[2]v +Author: %[3]v +Date: %[4]v + +%[5]v`, + g.To.Original(), + tag.Hash().String(), + author, + msg, + date.Format("Jan 2 15:04:05 2006"), + ) + return description, nil +} + +// Cleanup cleanup all temporary directories. +func (g *Generator) Cleanup() { + if !g.cleanup { + return + } + if err := os.RemoveAll(g.source); err != nil { + g.session.EventBus().SendError(err) + return + } + g.session.EventBus().SendInfo(fmt.Sprintf("Removed temporary directory: %s", g.source)) +} + +func (g *Generator) GenerateBinaries(ctx context.Context) (string, string, error) { + fromBinPath, err := g.buildIgniteCli(ctx, g.From) + if err != nil { + return "", "", errors.Wrapf(err, "failed to run scaffolds for 'FROM' version %s", g.From) + } + toBinPath, err := g.buildIgniteCli(ctx, g.To) + if err != nil { + return "", "", errors.Wrapf(err, "failed to run scaffolds for 'TO' version %s", g.To) + } + return fromBinPath, toBinPath, nil +} + +// buildIgniteCli build the ignite CLI from version. +func (g *Generator) buildIgniteCli(ctx context.Context, ver *semver.Version) (string, error) { + g.session.StartSpinner(fmt.Sprintf("Building binary for version v%s...", ver)) + + if err := g.checkoutToTag(ver.Original()); err != nil { + return "", err + } + + err := exec.Exec(ctx, []string{"make", "build"}, exec.StepOption(step.Workdir(g.source))) + if err != nil { + return "", errors.Wrap(err, "failed to build ignite cli using make build") + } + + // Copy the built binary to the binary path. + genBinaryPath := filepath.Join(g.source, defaultBinaryPath) + binPath := filepath.Join(g.binPath, ver.Original(), "ignite") + if err := copyFile(genBinaryPath, binPath); err != nil { + return "", err + } + + g.session.StopSpinner() + g.session.EventBus().SendInfo(fmt.Sprintf("Built ignite cli for %s at %s", ver.Original(), binPath)) + + return binPath, nil +} + +// checkoutToTag checkout the repository from a specific git tag. +func (g *Generator) checkoutToTag(tag string) error { + wt, err := g.repo.Worktree() + if err != nil { + return err + } + // Reset and clean the git directory before the checkout to avoid conflicts. + if err := wt.Reset(&git.ResetOptions{Mode: git.HardReset}); err != nil { + return errors.Wrapf(err, "failed to reset %s", g.source) + } + if err := wt.Clean(&git.CleanOptions{Dir: true}); err != nil { + return errors.Wrapf(err, "failed to reset %s", g.source) + } + if err = wt.Checkout(&git.CheckoutOptions{Branch: plumbing.NewTagReferenceName(tag)}); err != nil { + return errors.Wrapf(err, "failed to checkout tag %s", tag) + } + return nil +} + +// getRepoVersionTags returns a sorted collection of semver tags from the ignite cli repository. +func getRepoVersionTags(repo *git.Repository) (semver.Collection, error) { + tags, err := repo.Tags() + if err != nil { + return nil, errors.Wrap(err, "failed to get tags") + } + + // Iterate over all tags in the repository and pick valid semver tags + var versions semver.Collection + err = tags.ForEach(func(ref *plumbing.Reference) error { + name := ref.Name() + if name.IsTag() { + ver, err := semver.NewVersion(name.Short()) + if err != nil { + // Do nothing as it's not a semver tag + return nil + } + versions = append(versions, ver) + } + return nil + }) + if err != nil { + return nil, errors.Wrap(err, "failed to iterate over tags") + } + + sort.Sort(versions) + + return versions, nil +} + +// validateVersionRange checks if the provided fromVer and toVer exist in the versions and if any of them is nil, then it picks default values. +func validateVersionRange(fromVer, toVer *semver.Version, versions semver.Collection) (*semver.Version, *semver.Version, error) { + // Unable to generate migration document if there are less than two releases! + if versions.Len() < 2 { + return nil, nil, errors.New("At least two semver tags are required") + } + + versionMap := make(map[string]*semver.Version) + for _, ver := range versions { + versionMap[ver.String()] = ver + } + + // Picking default values for fromVer and toVer such that: + // If both fromVer and toVer are not provided, then generate migration document for second last and last semver tags + // If only fromVer is not provided, then use the tag before toVer as fromVer + // If only toVer is not provided, then use the last tag as toVer + if toVer != nil { + if _, found := versionMap[toVer.String()]; !found { + return nil, nil, errors.Errorf("tag %s not found", toVer) + } + } else { + toVer = versions[versions.Len()-1] + } + + // Replace fromVer and toVer with equivalent semver tags from versions + if fromVer != nil { + if _, found := versionMap[fromVer.String()]; !found { + return nil, nil, errors.Errorf("tag %s not found", fromVer) + } + } else { + sort.Search(versions.Len(), func(i int) bool { + if versions[i].LessThan(toVer) { + fromVer = versions[i] + return false + } + return true + }) + } + + // Unable to generate migration document if fromVer is greater or equal to toVer + if fromVer.GreaterThan(toVer) || fromVer.Equal(toVer) { + return nil, nil, errors.Errorf("from version %s should be less than to version %s", fromVer, toVer) + } + + return fromVer, toVer, nil +} + +// copyFile copy a file to a destination directory. Creates the directory if not exist. +func copyFile(srcPath, dstPath string) error { + dstDir := filepath.Dir(dstPath) + if err := os.RemoveAll(dstDir); err != nil { + return err + } + if err := os.MkdirAll(dstDir, os.ModePerm); err != nil { + return err + } + + src, err := os.Open(srcPath) + if err != nil { + return errors.Wrap(err, "failed to open source file") + } + defer src.Close() + + dst, err := os.Create(dstPath) + if err != nil { + return errors.Wrap(err, "failed to create destination file") + } + defer dst.Close() + + _, err = io.Copy(dst, src) + if err != nil { + return errors.Wrap(err, "failed to copy data: %s") + } + // Sync to ensure data is flushed to disk. + err = dst.Sync() + if err != nil { + return errors.Wrap(err, "failed to sync destination file") + } + + // Set executable permissions on the destination file. + err = os.Chmod(dstPath, 0o755) + if err != nil { + return errors.Wrap(err, "failed to set executable permissions") + } + return err +} + +// verifyRepoSource checks if the repose source path is the same from the provider URL +// and returns the *git.Repository object. +func verifyRepoSource(source, url string) (*git.Repository, error) { + repo, err := git.PlainOpen(source) + if err != nil { + return nil, errors.Wrap(err, "failed to open ignite repository") + } + remote, err := repo.Remote("origin") + if err != nil { + return nil, errors.Wrap(err, "failed to open ignite repository") + } + for _, remoteURL := range remote.Config().URLs { + if strings.TrimSuffix(url, ".git") == strings.TrimSuffix(remoteURL, ".git") { + return repo, nil + } + } + return nil, errors.Wrap(err, "repository folder does not match the repo URL") +} diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/scaffold/commands.go b/ignite/internal/tools/gen-mig-diffs/pkg/scaffold/commands.go new file mode 100644 index 0000000000..69725e899a --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/scaffold/commands.go @@ -0,0 +1,119 @@ +package scaffold + +import "github.com/ignite/cli/v29/ignite/pkg/errors" + +type ( + // Command represents a set of command and prerequisites scaffold command that are required to run before them. + Command struct { + // Name is the unique identifier of the command + Name string + // Prerequisite is the name of command that need to be run before this command set + Prerequisite string + // Commands is the list of scaffold command that are going to be run + // The command will be prefixed with "ignite scaffold" and executed in order + Commands []string + } + + Commands []Command +) + +func (c Commands) Get(name string) (Command, error) { + for _, cmd := range c { + if cmd.Name == name { + return cmd, nil + } + } + return Command{}, errors.Errorf("command %s not exist", name) +} + +func (c Commands) Has(name string) bool { + for _, cmd := range c { + if cmd.Name == name { + return true + } + } + return false +} + +func (c Commands) Validate() error { + cmdMap := make(map[string]bool) + for i, command := range c { + if cmdMap[command.Name] { + return errors.Errorf("duplicate command name found: %s", command.Name) + } + cmdMap[command.Name] = true + if command.Name == "" { + return errors.Errorf("empty command name at index %d: %v", i, command) + } + if len(command.Commands) == 0 { + return errors.Errorf("empty command list at index %d: %v", i, command) + } + } + for _, command := range c { + if command.Prerequisite != "" && !cmdMap[command.Prerequisite] { + return errors.Errorf("command %s pre-requisete %s not found", command.Name, command.Prerequisite) + } + } + return nil +} + +var defaultCommands = Commands{ + Command{ + Name: "chain", + Commands: []string{"chain example --no-module"}, + }, + Command{ + Name: "module", + Prerequisite: "chain", + Commands: []string{"module example --ibc"}, + }, + Command{ + Name: "list", + Prerequisite: "module", + Commands: []string{ + "list list1 f1:string f2:strings f3:bool f4:int f5:ints f6:uint f7:uints f8:coin f9:coins --module example --yes", + }, + }, + Command{ + Name: "map", + Prerequisite: "module", + Commands: []string{ + "map map1 f1:string f2:strings f3:bool f4:int f5:ints f6:uint f7:uints f8:coin f9:coins --index i1:string --module example --yes", + }, + }, + Command{ + Name: "single", + Prerequisite: "module", + Commands: []string{ + "single single1 f1:string f2:strings f3:bool f4:int f5:ints f6:uint f7:uints f8:coin f9:coins --module example --yes", + }, + }, + Command{ + Name: "type", + Prerequisite: "module", + Commands: []string{ + "type type1 f1:string f2:strings f3:bool f4:int f5:ints f6:uint f7:uints f8:coin f9:coins --module example --yes", + }, + }, + Command{ + Name: "message", + Prerequisite: "module", + Commands: []string{ + "message message1 f1:string f2:strings f3:bool f4:int f5:ints f6:uint f7:uints f8:coin f9:coins --module example --yes", + }, + }, + Command{ + Name: "query", + Prerequisite: "module", + Commands: []string{ + "query query1 f1:string f2:strings f3:bool f4:int f5:ints f6:uint f7:uints --module example --yes", + }, + }, + Command{ + Name: "packet", + Prerequisite: "module", + Commands: []string{ + "packet packet1 f1:string f2:strings f3:bool f4:int f5:ints f6:uint f7:uints f8:coin f9:coins --ack f1:string,f2:strings,f3:bool,f4:int,f5:ints,f6:uint,f7:uints,f8:coin,f9:coins --module example --yes", + }, + }, +} diff --git a/ignite/internal/tools/gen-mig-diffs/pkg/scaffold/scaffold.go b/ignite/internal/tools/gen-mig-diffs/pkg/scaffold/scaffold.go new file mode 100644 index 0000000000..db7affe7eb --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/pkg/scaffold/scaffold.go @@ -0,0 +1,195 @@ +package scaffold + +import ( + "context" + "os" + "path/filepath" + "strings" + + "github.com/Masterminds/semver/v3" + + "github.com/ignite/cli/v29/ignite/pkg/cmdrunner/exec" + "github.com/ignite/cli/v29/ignite/pkg/errors" + "github.com/ignite/cli/v29/ignite/pkg/randstr" + + "github.com/ignite/cli/ignite/internal/tools/gen-mig-diffs/pkg/cache" +) + +var v027 = semver.MustParse("v0.27.0") + +type ( + // Scaffold holder the Scaffold logic. + Scaffold struct { + Output string + binary string + version *semver.Version + cache *cache.Cache + cachePath string + commandList Commands + } + + // options represents configuration for the generator. + options struct { + cachePath string + output string + commands Commands + } + // Options configures the generator. + Options func(*options) +) + +// newOptions returns a options with default options. +func newOptions() options { + tmpDir := filepath.Join(os.TempDir(), randstr.Runes(4)) + return options{ + cachePath: filepath.Join(tmpDir, "migration-cache"), + output: filepath.Join(tmpDir, "migration"), + commands: defaultCommands, + } +} + +// WithOutput set the ignite scaffold Output. +func WithOutput(output string) Options { + return func(o *options) { + o.output = output + } +} + +// WithCachePath set the ignite scaffold cache path. +func WithCachePath(cachePath string) Options { + return func(o *options) { + o.cachePath = cachePath + } +} + +// WithCommandList set the migration docs Output. +func WithCommandList(commands Commands) Options { + return func(o *options) { + o.commands = commands + } +} + +// New returns a new Scaffold. +func New(binary string, ver *semver.Version, options ...Options) (*Scaffold, error) { + opts := newOptions() + for _, apply := range options { + apply(&opts) + } + + output, err := filepath.Abs(opts.output) + if err != nil { + return nil, err + } + + c, err := cache.New(opts.cachePath) + if err != nil { + return nil, err + } + + if err := opts.commands.Validate(); err != nil { + return nil, err + } + + return &Scaffold{ + binary: binary, + version: ver, + cache: c, + cachePath: opts.cachePath, + Output: filepath.Join(output, ver.Original()), + commandList: opts.commands, + }, nil +} + +// Run execute the scaffold command based in the binary semantic version. +func (s *Scaffold) Run(ctx context.Context) error { + if err := os.RemoveAll(s.Output); err != nil { + return errors.Wrapf(err, "failed to remove the scaffold output directory: %s", s.Output) + } + + for _, command := range s.commandList { + if err := s.runCommand(ctx, command.Name, command); err != nil { + return err + } + if err := applyPostScaffoldExceptions(s.version, command.Name, s.Output); err != nil { + return err + } + } + return nil +} + +// Cleanup cleanup all temporary directories. +func (s *Scaffold) Cleanup() error { + if err := os.RemoveAll(s.cachePath); err != nil { + return err + } + return os.RemoveAll(s.Output) +} + +func (s *Scaffold) runCommand(ctx context.Context, name string, command Command) error { + path := filepath.Join(s.Output, name) + if command.Prerequisite != "" { + reqCmd, err := s.commandList.Get(command.Prerequisite) + if err != nil { + return errors.Wrapf(err, "pre-requisite command %s from %s not found", command.Prerequisite, name) + } + + if s.cache.Has(command.Prerequisite) { + if err := s.cache.Get(command.Prerequisite, path); err != nil { + return errors.Wrapf(err, "failed to get cache key %s", command.Prerequisite) + } + } else { + if err := s.runCommand(ctx, name, reqCmd); err != nil { + return err + } + } + } + + for _, cmd := range command.Commands { + if err := s.executeScaffold(ctx, cmd, path); err != nil { + return err + } + } + return s.cache.Save(command.Name, path) +} + +func (s *Scaffold) executeScaffold(ctx context.Context, cmd, path string) error { + args := append([]string{s.binary, "scaffold"}, strings.Fields(cmd)...) + args = append(args, "--path", path) + args = applyPreExecuteExceptions(s.version, args) + + if err := exec.Exec(ctx, args); err != nil { + return errors.Wrapf(err, "failed to execute ignite scaffold command: %s", cmd) + } + return nil +} + +// applyPreExecuteExceptions this function we can manipulate command arguments before executing it in +// order to compensate for differences in versions. +func applyPreExecuteExceptions(ver *semver.Version, args []string) []string { + // In versions <0.27.0, "scaffold chain" command always creates a new directory with the + // name of chain at the given '--path', so we need to append "example" to the path if the + // command is not "chain". + if ver.LessThan(v027) && args[2] != "chain" { + args[len(args)-1] = filepath.Join(args[len(args)-1], "example") + } + return args +} + +// applyPostScaffoldExceptions this function we can manipulate the Output of scaffold command after +// they have been executed in order to compensate for differences in versions. +func applyPostScaffoldExceptions(ver *semver.Version, name string, output string) error { + // In versions <0.27.0, "scaffold chain" command always creates a new directory with the name of + // chain at the given '--path', so we need to move the directory to the parent directory. + if ver.LessThan(v027) { + if err := os.Rename(filepath.Join(output, name, "example"), filepath.Join(output, "example_tmp")); err != nil { + return errors.Wrapf(err, "failed to move %s directory to tmp directory", name) + } + if err := os.RemoveAll(filepath.Join(output, name)); err != nil { + return errors.Wrapf(err, "failed to remove %s directory", name) + } + if err := os.Rename(filepath.Join(output, "example_tmp"), filepath.Join(output, name)); err != nil { + return errors.Wrapf(err, "failed to move tmp directory to %s directory", name) + } + } + return nil +} diff --git a/ignite/internal/tools/gen-mig-diffs/templates/doc/doc.go b/ignite/internal/tools/gen-mig-diffs/templates/doc/doc.go new file mode 100644 index 0000000000..707ec95fb1 --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/templates/doc/doc.go @@ -0,0 +1,69 @@ +package doc + +import ( + "embed" + "fmt" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/gobuffalo/genny/v2" + "github.com/gobuffalo/plush/v4" + + "github.com/ignite/cli/v29/ignite/pkg/xgenny" + "github.com/ignite/cli/v29/ignite/templates/field/plushhelpers" +) + +//go:embed files/* +var fsFiles embed.FS + +// Options represents the options to scaffold a migration document. +type Options struct { + Path string + FromVersion *semver.Version + ToVersion *semver.Version + Diffs string + Description string +} + +func (o Options) position() string { + return fmt.Sprintf("%02d%02d%02d", o.ToVersion.Major(), o.ToVersion.Minor(), o.ToVersion.Patch()) +} + +func (o Options) shortDescription() string { + return fmt.Sprintf("Release %s", o.ToVersion.Original()) +} + +func (o Options) date() string { + return time.Now().Format("Jan 2 15:04:05 2006") +} + +// NewGenerator returns the generator to scaffold a migration doc. +func NewGenerator(opts Options) (*genny.Generator, error) { + var ( + g = genny.New() + docTemplate = xgenny.NewEmbedWalker( + fsFiles, + "files/", + opts.Path, + ) + ) + + if err := g.Box(docTemplate); err != nil { + return g, err + } + + ctx := plush.NewContext() + ctx.Set("Position", opts.position()) + ctx.Set("FromVersion", opts.FromVersion.Original()) + ctx.Set("ToVersion", opts.ToVersion.Original()) + ctx.Set("Diffs", opts.Diffs) + ctx.Set("Description", opts.Description) + ctx.Set("ShortDescription", opts.shortDescription()) + ctx.Set("Date", opts.date()) + + plushhelpers.ExtendPlushContext(ctx) + g.Transformer(xgenny.Transformer(ctx)) + g.Transformer(genny.Replace("{{Version}}", opts.ToVersion.Original())) + + return g, nil +} diff --git a/ignite/internal/tools/gen-mig-diffs/templates/doc/files/{{Version}}.md.plush b/ignite/internal/tools/gen-mig-diffs/templates/doc/files/{{Version}}.md.plush new file mode 100644 index 0000000000..e250845bef --- /dev/null +++ b/ignite/internal/tools/gen-mig-diffs/templates/doc/files/{{Version}}.md.plush @@ -0,0 +1,17 @@ +--- +sidebar_position: <%= Position %> +title: <%= ToVersion %> +description: <%= ShortDescription %> +--- + +## **<%= ToVersion %>** + +<%= Description %> +### **Chain migration diffs** + +<%= Diffs %> +### **Details** + +- **The CLI tools automatically generated this file**; +- Generated from <%= FromVersion %> to <%= ToVersion %>; +- Generated <%= Date %>; diff --git a/ignite/pkg/xstrings/xstrings_test.go b/ignite/pkg/xstrings/xstrings_test.go index 1637119c92..9c98a1f10a 100644 --- a/ignite/pkg/xstrings/xstrings_test.go +++ b/ignite/pkg/xstrings/xstrings_test.go @@ -8,6 +8,18 @@ import ( "github.com/ignite/cli/v29/ignite/pkg/xstrings" ) +func TestTitle(t *testing.T) { + require.Equal(t, "Foo", xstrings.Title("foo")) + require.Equal(t, "Foo", xstrings.Title("FOO")) + require.Equal(t, "Foobar", xstrings.Title("fooBar")) +} + +func TestToUpperFirst(t *testing.T) { + require.Equal(t, "Foo", xstrings.ToUpperFirst("foo")) + require.Equal(t, "FOO", xstrings.ToUpperFirst("FOO")) + require.Equal(t, "FooBar", xstrings.ToUpperFirst("fooBar")) +} + func TestNoDash(t *testing.T) { require.Equal(t, "foo", xstrings.NoDash("foo")) require.Equal(t, "foo", xstrings.NoDash("-f-o-o---")) diff --git a/scripts/gen-mig-diffs b/scripts/gen-mig-diffs new file mode 100755 index 0000000000..d6e2bfb305 --- /dev/null +++ b/scripts/gen-mig-diffs @@ -0,0 +1,3 @@ +#!/bin/bash + +go run ignite/internal/tools/gen-mig-diffs/*.go