diff --git a/BUILD.bazel b/BUILD.bazel index 462bedcd97b..94b9daa069a 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -29,9 +29,11 @@ filegroup( ":package-srcs", "//build/debs:all-srcs", "//cmd/blocking-testgrid-tests:all-srcs", + "//cmd/krel:all-srcs", "//cmd/release-notes:all-srcs", "//lib:all-srcs", "//pkg/notes:all-srcs", + "//pkg/util:all-srcs", ], tags = ["automanaged"], visibility = ["//visibility:public"], diff --git a/cmd/krel/BUILD.bazel b/cmd/krel/BUILD.bazel new file mode 100644 index 00000000000..ae76b9b52a1 --- /dev/null +++ b/cmd/krel/BUILD.bazel @@ -0,0 +1,32 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "go_default_library", + srcs = ["main.go"], + importpath = "k8s.io/release/cmd/krel", + visibility = ["//visibility:private"], + deps = ["//cmd/krel/cmd:go_default_library"], +) + +go_binary( + name = "krel", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//cmd/krel/cmd:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/cmd/krel/cmd/BUILD.bazel b/cmd/krel/cmd/BUILD.bazel new file mode 100644 index 00000000000..612ea338649 --- /dev/null +++ b/cmd/krel/cmd/BUILD.bazel @@ -0,0 +1,31 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "ff.go", + "root.go", + ], + importpath = "k8s.io/release/cmd/krel/cmd", + visibility = ["//visibility:public"], + deps = [ + "//pkg/util:go_default_library", + "@com_github_spf13_cobra//:go_default_library", + "@in_gopkg_src_d_go_git_v4//:go_default_library", + "@in_gopkg_src_d_go_git_v4//plumbing:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/cmd/krel/cmd/ff.go b/cmd/krel/cmd/ff.go new file mode 100644 index 00000000000..cacca0dceec --- /dev/null +++ b/cmd/krel/cmd/ff.go @@ -0,0 +1,316 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "bytes" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing" + + "k8s.io/release/pkg/util" +) + +var cfgFile string + +const defaultMasterRef string = "HEAD" + +type Options struct { + branch string + masterRef string + org string + nomock bool + cleanup bool +} + +var opts = &Options{} + +// ffCmd represents the base command when called without any subcommands +var ffCmd = &cobra.Command{ + Use: "ff --branch [--ref ] [--nomock] [--cleanup]", + Short: "ff fast forwards a Kubernetes release branch", + Long: `ff fast forwards a branch to a specified master object reference +(defaults to HEAD), and then prepares the branch as a Kubernetes release branch: + +- Run hack/update-all.sh to ensure compliance of generated files`, + Example: "krel ff --branch release-1.16 39d0135e --ref HEAD --cleanup", + RunE: func(cmd *cobra.Command, args []string) error { + if err := runFf(opts); err != nil { + return err + } + + return nil + }, +} + +func init() { + cobra.OnInitialize(initConfig) + + ffCmd.PersistentFlags().StringVar(&opts.branch, "branch", "", "branch") + ffCmd.PersistentFlags().StringVar(&opts.masterRef, "ref", defaultMasterRef, "ref on master") + ffCmd.PersistentFlags().StringVar(&opts.org, "org", util.DefaultGithubOrg, "org to run tool against") + + rootCmd.AddCommand(ffCmd) +} + +func runFf(opts *Options) error { + // TODO: Add usage/help + // TODO: Check prerequisites (git, jq, go, make) + // TODO: Set positional args + // TODO: Fail on empty branch + // TODO: Fail on GITHUB_TOKEN not set + + branch := opts.branch + masterRef := opts.masterRef + remote := util.DefaultRemote + remoteMaster := fmt.Sprintf("%s/%s", remote, "master") + + log.Printf("Preparing to fast-forward master@%s onto the %s branch...\n", masterRef, branch) + + nomock := opts.nomock + dryRunFlag := "--dry-run" + if nomock { + // TODO: Set this to empty string once we're ready to turn on the tool + log.Println("Running in no-mock mode!") + dryRunFlag = "--dry-run" + } + + isReleaseBranch := util.IsReleaseBranch(branch) + if !isReleaseBranch { + log.Fatalf("%s is not a release branch\n", branch) + } + + branchExists, err := util.BranchExists(branch) + if err != nil { + return err + } + if !branchExists { + log.Fatalf("the %s branch does not exist\n", branch) + } + + baseDir, err := ioutil.TempDir("", "ff-") + if err != nil { + return err + } + + cleanup := opts.cleanup + if cleanup { + defer cleanupTmpDir(baseDir) + } + + workingDir := filepath.Join(baseDir, branch) + log.Printf("%s", workingDir) + + os.Setenv("GOPATH", workingDir) + log.Printf("GOPATH: %s", os.Getenv("GOPATH")) + + gitRoot := fmt.Sprintf("%s/src/k8s.io/kubernetes", workingDir) + + // TODO: nomock? + if nomock { + log.Printf("nomock mode (from within ff)\n") + } + + // TODO: If workingDir exists, prompt user to delete + // TODO: Tweak file permissions (dir + user rwx) + workingDirErr := os.MkdirAll(workingDir, os.ModePerm) + if workingDirErr != nil { + return err + } + + // TODO: Remove once SyncRepo works + gitRoot = "/tmp/ff-465649664/release-1.16/src/k8s.io/kubernetes" + + syncErr := util.SyncRepo(util.KubernetesGitHubAuthURL, gitRoot) + if syncErr != nil { + return syncErr + } + + chdirErr := os.Chdir(gitRoot) + if chdirErr != nil { + return chdirErr + } + + repo, repoErr := git.PlainOpen(gitRoot) + if repoErr != nil { + return repoErr + } + + mergeBase, err := util.GetMergeBase("master", branch, repo) + if err != nil { + return err + } + + // TODO: Rewrite using go-git + comparedCommits := []string{mergeBase, remoteMaster} + var tags []string + for _, commit := range comparedCommits { + cmd := exec.Command("git", "describe", "--abbrev=0", "--tags", commit) + cmd.Stdin = strings.NewReader("some input") + var out bytes.Buffer + cmd.Stdout = &out + err = cmd.Run() + if err != nil { + log.Fatal(err) + } + log.Printf("in all caps: %q\n", out.String()) + tags = append(tags, strings.TrimSuffix(out.String(), "\n")) + } + + // TODO: This should return an error if it fails + // TODO: Provide more information to debug here + if tags[0] != tags[1] { + log.Printf("%s did not match %s", tags[0], tags[1]) + } + + // TODO: Rewrite using go-git + // --dry-run appears to be unsupported for git push, so we shell out here. + checkPushCmd := exec.Command("git", "push", "-q", "--dry-run", util.KubernetesGitHubAuthURL) + util.Run(checkPushCmd) + + w, err := repo.Worktree() + if err != nil { + return err + } + + // ... checking out to commit + //Info("git checkout %s", commit) + remoteHash, err := repo.ResolveRevision(plumbing.Revision(fmt.Sprintf("%s/%s", remote, branch))) + if err != nil { + return err + } + + err = w.Checkout(&git.CheckoutOptions{ + Hash: plumbing.NewHash(remoteHash.String()), + Branch: plumbing.NewBranchReferenceName(branch), + Create: true, + }) + + if err != nil { + return err + } + + // TODO: Merge and update + mergeCmd := exec.Command("git", "merge", "-X", "ours", remoteMaster) + util.Run(mergeCmd) + + // TODO: Check for deleted files + // TODO: Do we need hack/install-etcd.sh + installEtcd := exec.Command("hack/install-etcd.sh") + util.Run(installEtcd) + + currentPath := os.Getenv("PATH") + etcdDir := filepath.Join(gitRoot, "third_party/etcd") + newPath := fmt.Sprintf("%s:%s", etcdDir, currentPath) + os.Setenv("PATH", newPath) + log.Printf("PATH has been set to %s", os.Getenv("PATH")) + + // TODO: Running update scripts fails with go1.13 + log.Printf("Running hack/update* scripts...") + updateScripts := []string{ + "update-openapi-spec", + } + /* + for _, script := range updateScripts { + scriptPath := fmt.Sprintf("hack/%s.sh", script) + if _, err := os.Stat(scriptPath); os.IsNotExist(err) { + log.Printf("The update script (%s) does not exist. Skipping...", scriptPath) + continue + } + + scriptCmd := exec.Command(scriptPath) + log.Printf("Running %s...", scriptPath) + util.Run(scriptCmd) + } + */ + + status, err := w.Status() + if err != nil { + return err + } + + if !status.IsClean() { + log.Printf("Commit changes:") + // TODO: Rewrite using go-git + gitAdd := exec.Command("git", "add", "-A") + util.Run(gitAdd) + + // TODO: Rewrite using go-git + gitCommit := exec.Command("git", "commit", "-am", fmt.Sprintf("Results of running update scripts: %s", strings.Join(updateScripts, ","))) + util.Run(gitCommit) + } + + releaseRefName := remoteHash.String() + releaseRev, err := util.RevParseShort(releaseRefName, gitRoot) + if err != nil { + return err + } + + headRev, err := util.RevParseShort("HEAD", gitRoot) + if err != nil { + return err + } + + log.Printf("%s", prepushMessage(gitRoot, remote, branch, util.KubernetesGitHubURL, releaseRev, headRev)) + + _, pushUpstream, err := util.Ask("Are you ready to push the local branch fast-forward changes upstream? Please only answer after you have validated the changes.", "yes", 3) + if err != nil { + return err + } + + if pushUpstream { + log.Printf("Pushing %s %s branch upstream: ", dryRunFlag, branch) + //git push $DRYRUN_FLAG origin $RELEASE_BRANCH:$RELEASE_BRANCH + // TODO: Need to handle https and ssh auth sanely + gitPushCmd := exec.Command("git", "push", dryRunFlag, remote, branch) //fmt.Sprintf("%s:%s", branch, branch)) + util.Run(gitPushCmd) + } + + return nil +} + +func prepushMessage(gitRoot, remote, branch, githubURL, releaseRev, headRev string) string { + message := `Go look around in %s to make sure things look okay before pushing... + +Check for files left uncommitted using: + + git status -s + +Validate the fast-forward commit using: + + git show + +Validate the changes pulled in from master using: + + git log %s/%s..HEAD + +Once the branch fast-forward is complete, the diff will be available after push at: + + %s/compare/%s...%s" + +` + + return message +} diff --git a/cmd/krel/cmd/root.go b/cmd/krel/cmd/root.go new file mode 100644 index 00000000000..f88aaf2612f --- /dev/null +++ b/cmd/krel/cmd/root.go @@ -0,0 +1,66 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "log" + "os" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "krel", + Short: "krel", +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + log.Fatal(err) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + rootCmd.PersistentFlags().BoolVar(&opts.nomock, "nomock", false, "nomock flag") + rootCmd.PersistentFlags().BoolVar(&opts.cleanup, "cleanup", false, "cleanup flag") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { +} + +func cleanupTmpDir(dir string) error { + log.Printf("Deleting %s...", dir) + err := os.RemoveAll(dir) + if err != nil { + return err + } + + return nil +} + +func exitWithHelp(cmd *cobra.Command, err string) { + fmt.Fprintln(os.Stderr, err) + cmd.Help() + os.Exit(1) +} diff --git a/cmd/krel/main.go b/cmd/krel/main.go new file mode 100644 index 00000000000..ad8afd62885 --- /dev/null +++ b/cmd/krel/main.go @@ -0,0 +1,23 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import "k8s.io/release/cmd/krel/cmd" + +func main() { + cmd.Execute() +} diff --git a/go.mod b/go.mod index 458f925429d..fa7c79c03bd 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/kolide/kit v0.0.0-20190123023048-c155a91098e3 github.com/pkg/errors v0.8.1 github.com/psampaz/go-mod-outdated v0.4.0 + github.com/spf13/cobra v0.0.5 github.com/stretchr/testify v1.4.0 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 google.golang.org/appengine v1.5.0 // indirect diff --git a/go.sum b/go.sum index db32c10eef7..3507839239a 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,7 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andygrunwald/go-gerrit v0.0.0-20190120104749-174420ebee6c/go.mod h1:0iuRQp6WJ44ts+iihy5E/WlPqfg5RNeQxOmzRkxCdtk= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= @@ -170,6 +171,7 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb v0.0.0-20161215172503-049f9b42e9a5/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -217,7 +219,9 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/mailru/easyjson v0.0.0-20171120080333-32fa128f234d/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/markbates/inflect v1.0.4/go.mod h1:1fR9+pO2KHEO9ZRtto13gDwwZaAKstQzferVeWqbgNs= github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= @@ -304,6 +308,7 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -361,6 +366,7 @@ golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/pkg/util/BUILD.bazel b/pkg/util/BUILD.bazel new file mode 100644 index 00000000000..26597dea924 --- /dev/null +++ b/pkg/util/BUILD.bazel @@ -0,0 +1,33 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "common.go", + "git.go", + ], + importpath = "k8s.io/release/pkg/util", + visibility = ["//visibility:public"], + deps = [ + "@com_github_pkg_errors//:go_default_library", + "@in_gopkg_src_d_go_git_v4//:go_default_library", + "@in_gopkg_src_d_go_git_v4//config:go_default_library", + "@in_gopkg_src_d_go_git_v4//plumbing:go_default_library", + "@in_gopkg_src_d_go_git_v4//plumbing/object:go_default_library", + "@in_gopkg_src_d_go_git_v4//storage/memory:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/util/common.go b/pkg/util/common.go new file mode 100644 index 00000000000..6c3711e08cd --- /dev/null +++ b/pkg/util/common.go @@ -0,0 +1,99 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "bufio" + "errors" + "log" + "os" + "os/exec" + "strings" +) + +// Run wraps the exec.Cmd.Run() command and sets the standard output. +// TODO: Should this take an error code argument/return an error code? +func Run(c *exec.Cmd) { + c.Stdout = os.Stdout + c.Stderr = os.Stderr + if err := c.Run(); err != nil { + log.Fatalf("Command %q failed: %v", strings.Join(c.Args, " "), err) + } +} + +/* +############################################################################# +# Simple yes/no prompt +# +# @optparam default -n(default)/-y/-e (default to n, y or make (e)xplicit) +# @param message +common::askyorn () { + local yorn + local def=n + local msg="y/N" + + case $1 in + -y) # yes default + def="y" msg="Y/n" + shift + ;; + -e) # Explicit + def="" msg="y/n" + shift + ;; + -n) shift + ;; + esac + + while [[ $yorn != [yYnN] ]]; do + logecho -n "$*? ($msg): " + read yorn + : ${yorn:=$def} + done + + # Final test to set return code + [[ $yorn == [yY] ]] +} +*/ + +func Ask(question, expectedResponse string, retries int) (string, bool, error) { + attempts := 1 + answer := "" + + if retries < 0 { + log.Printf("Retries was set to a number less than zero (%d). Please specify a positive number of retries or zero, if you want to ask unconditionally.", retries) + } + + for attempts <= retries { + scanner := bufio.NewScanner(os.Stdin) + log.Printf("%s (%d/%d) ", question, attempts, retries) + + scanner.Scan() + answer = scanner.Text() + + if answer == expectedResponse { + return answer, true, nil + } + + log.Printf("Expected '%s', but got '%s'", expectedResponse, answer) + + attempts++ + } + + log.Printf("Expected response was not provided. Retries exceeded.") + return answer, false, errors.New("Expected response was not input. Retries exceeded.") +} diff --git a/pkg/util/git.go b/pkg/util/git.go new file mode 100644 index 00000000000..635b95cabeb --- /dev/null +++ b/pkg/util/git.go @@ -0,0 +1,212 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "fmt" + "log" + "os" + "regexp" + + "github.com/pkg/errors" + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/config" + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/object" + "gopkg.in/src-d/go-git.v4/storage/memory" +) + +const ( + branchRE = `master|release-([0-9]{1,})\.([0-9]{1,})(\.([0-9]{1,}))*$` + // TODO: Use "kubernetes" as DefaultGithubOrg when this is ready to turn on + DefaultGithubOrg = "justaugustus" + DefaultGithubAuthRoot = "git@github.com:" + DefaultRemote = "origin" +) + +var ( + KubernetesGitHubURL = fmt.Sprintf("https://github.com/%s/kubernetes", DefaultGithubOrg) + KubernetesGitHubAuthURL = fmt.Sprintf("%s%s/kubernetes.git", DefaultGithubAuthRoot, DefaultGithubOrg) +) + +func BranchExists(branch string) (bool, error) { + log.Printf("Verifying %s branch exists on the remote...", branch) + + // Create the remote with repository URL + rem := git.NewRemote( + memory.NewStorage(), + &config.RemoteConfig{ + Name: DefaultRemote, + URLs: []string{KubernetesGitHubURL}, + }, + ) + + log.Print("Fetching tags...") + + // We can then use every Remote functions to retrieve wanted information + refs, err := rem.List(&git.ListOptions{}) + if err != nil { + log.Printf("Could not list references on the remote repository.") + return false, err + } + + // Filters the references list and only keeps branches + for _, ref := range refs { + if ref.Name().IsBranch() { + if ref.Name().Short() == branch { + log.Printf("Found branch %s", ref.Name().Short()) + return true, nil + } + } + } + + log.Printf("Could not find branch %s", branch) + + return false, nil +} + +func IsReleaseBranch(branch string) bool { + re := regexp.MustCompile(branchRE) + if !re.MatchString(branch) { + log.Fatalf("%s is not a release branch\n", branch) + return false + } + + return true +} + +// TODO: Need to handle https and ssh auth sanely +func SyncRepo(repoURL, destination string) error { + log.Printf("Syncing %s to %s:", repoURL, destination) + + if _, err := os.Stat(destination); !os.IsNotExist(err) { + chdirErr := os.Chdir(destination) + if chdirErr != nil { + return chdirErr + } + + repo, repoErr := git.PlainOpen(destination) + if repoErr != nil { + return repoErr + } + + w, err := repo.Worktree() + if err != nil { + return err + } + + // ... checking out to commit + //Info("git checkout %s", commit) + err = w.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName("master"), + }) + if err != nil { + return err + } + + pullOpts := &git.PullOptions{RemoteName: DefaultRemote} + err = w.Pull(pullOpts) + if err != nil && err != git.NoErrAlreadyUpToDate { + return err + } + + return nil + } + + cloneOpts := &git.CloneOptions{ + URL: repoURL, + } + _, repoErr := git.PlainClone(destination, false, cloneOpts) + if repoErr != nil { + return repoErr + } + + return nil +} + +// RevParse parses a git revision and returns a SHA1 on success, otherwise an +// error. +func RevParse(rev, workDir string) (string, error) { + repo, err := git.PlainOpen(workDir) + if err != nil { + return "", err + } + + ref, err := repo.ResolveRevision(plumbing.Revision(rev)) + if err != nil { + return "", err + } + + return ref.String(), nil +} + +// RevParseShort parses a git revision and returns a SHA1 on success, otherwise an +// error. +func RevParseShort(rev, workDir string) (string, error) { + fullRev, err := RevParse(rev, workDir) + if err != nil { + return "", err + } + + shortRev := fullRev[:10] + + return shortRev, nil +} + +func GetMergeBase(masterRefShort, releaseRefShort string, repo *git.Repository) (string, error) { + masterRef := fmt.Sprintf("%s/%s", DefaultRemote, masterRefShort) + releaseRef := fmt.Sprintf("%s/%s", DefaultRemote, releaseRefShort) + + log.Printf("masterRef: %s, releaseRef: %s", masterRef, releaseRef) + + commitRevs := []string{masterRef, releaseRef} + var res []*object.Commit + + var hashes []*plumbing.Hash + for _, rev := range commitRevs { + hash, err := repo.ResolveRevision(plumbing.Revision(rev)) + if err != nil { + return "", err + } + hashes = append(hashes, hash) + } + + var commits []*object.Commit + for _, hash := range hashes { + commit, err := repo.CommitObject(*hash) + if err != nil { + return "", err + } + commits = append(commits, commit) + } + + res, err := commits[0].MergeBase(commits[1]) + if err != nil { + return "", err + } + + if len(res) == 0 { + return "", errors.Errorf("could not find a merge base between %s and %s", masterRefShort, releaseRefShort) + } + + log.Printf("len commits %v\n", len(res)) + + mergeBase := res[0].Hash.String() + log.Printf("merge base is %s", mergeBase) + + return mergeBase, nil +}