| // Copyright 2022 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| package main |
| |
| import ( |
| "bufio" |
| "errors" |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "strings" |
| "time" |
| ) |
| |
| const ( |
| cqCommitText = "\n" + |
| "Cros-Add-Test-Suites: cellular_ota,cellular_ota_flaky\n" + |
| "Cros-Add-TS-Boards-BuildTarget: trogdor,brya,octopus,nissa\n" + |
| "Cros-Add-TS-Pool: cellular\n" |
| colorGreen = "\033[32m" |
| colorRed = "\033[31m" |
| colorReset = "\033[0m" |
| // Constants in the empty CL logic |
| emptyCLRepoName = "modemmanager-next" |
| emptyCLBranchName = "uprev_empty_cl" |
| emptyCLFileName = "src/meson.build" |
| emptyCLMsg = "\n# EMPTY" |
| emptyCLCommitMsg = "EMPTY CL FOR UPREV PURPOSES: **DO NOT MERGE**\n" + |
| "\nBUG=None\nFIXED=None\n\nTEST=NONE\n" + cqCommitText |
| ) |
| |
| func logPanic(s string) { |
| fmt.Print(s) |
| log.Panicln(s) |
| } |
| |
| func runCmd(cmd *exec.Cmd, abortOnFail bool) (string, error) { |
| fmt.Printf("Executing: %q\n", cmd.String()) |
| log.Printf("Executing: %q\n", cmd.String()) |
| out, err := cmd.CombinedOutput() |
| if err != nil { |
| log.Printf("err: %q\n", err) |
| if abortOnFail { |
| logPanic(err.Error()) |
| } |
| log.Printf("Ignoring non-fatal error") |
| } |
| log.Printf("Execution result: %s\n", string(out)) |
| return string(out), err |
| } |
| |
| // mergeUntilConflict merges the last non conflicting merge from the upstream branch |
| func mergeUntilConflict(upstreamBranch string) (string, error) { |
| cmd := exec.Command("git", "log", "HEAD.."+upstreamBranch, "--abbrev", "--oneline", "--format=%h") |
| out, _ := runCmd(cmd, true) |
| if out == "" { |
| return "", nil |
| } |
| |
| commits := strings.Split(out, "\n") |
| // The first element of this array is the most recent commit, and we're going backwards in time until we find one that merges cleanly |
| for i, commitSHA := range commits[:len(commits)-1] { |
| cmd := exec.Command("git", "merge", commitSHA) |
| _, e := runCmd(cmd, false) |
| if e != nil { |
| cmd := exec.Command("git", "merge", "--abort") |
| if _, err := runCmd(cmd, false); err != nil { |
| return "", fmt.Errorf("Could not abort merge: %w", e) |
| } |
| } else { |
| if i != 0 { |
| fmt.Println(colorRed, "Could not merge cros/upstream fully. Merged only until commit: ", commitSHA, colorReset) |
| fmt.Println(colorRed, "Warning: CQ does not support merge commits stacked on top of other commits. Your next commit needs to be a merge of the commit after", commitSHA, "without any commits in between.", colorReset) |
| } else { |
| fmt.Println(colorGreen, "Merged ", upstreamBranch, colorReset) |
| genCommitMsg("cros/main", upstreamBranch, "", "None", upstreamBranch) |
| } |
| return fmt.Sprintf(commitSHA), nil |
| } |
| } |
| fmt.Println(colorRed, "Could not merge any commit in cros/upstream", colorReset) |
| return "", nil |
| } |
| |
| // uprevRepo creates a new branch and attempts to git merge |
| func uprevRepo(rootDir string, repoName string, upstreamBranch string, lastTagFlag bool, commitSHA string, forceFlag bool) { |
| uprevDate := time.Now().Format("01-02-2006") |
| branchName := "merge-upstream-" + uprevDate |
| fmt.Println(rootDir, repoName, upstreamBranch, uprevDate) |
| if err := os.Chdir(filepath.Join(rootDir, repoName)); err != nil { |
| logPanic(err.Error()) |
| } |
| newDir, _ := os.Getwd() |
| fmt.Printf(colorGreen+"Working on : %s\n"+colorReset, newDir) |
| var cmd *exec.Cmd |
| |
| cmd = exec.Command("git", "status", "--porcelain") |
| gitStatus, _ := runCmd(cmd, true) |
| if gitStatus != "" { |
| if forceFlag { |
| cmd = exec.Command("git", "stash", "--include-untracked") |
| runCmd(cmd, false) |
| cmd = exec.Command("git", "reset", "--hard", "cros/main") |
| runCmd(cmd, false) |
| } else { |
| logPanic("Please ensure that there are no uncommitted changes. Or run with --force=true") |
| } |
| } |
| |
| // Repo start a clean branch with HEAD cros/main and merge cros/upstream |
| cmd = exec.Command("repo", "sync", "-d", ".") |
| runCmd(cmd, true) |
| if forceFlag { |
| cmd = exec.Command("git", "branch", "-D", branchName) |
| } |
| runCmd(cmd, false) |
| cmd = exec.Command("repo", "start", branchName) |
| runCmd(cmd, true) |
| cmd = exec.Command("git", "fetch", "cros", upstreamBranch) |
| runCmd(cmd, true) |
| targetSHA := "" |
| if lastTagFlag { |
| cmd = exec.Command("git", "describe", "--abbrev=0", commitSHA) |
| gitOut, _ := runCmd(cmd, true) |
| // git may throw warnings, but still have the last tag as the last but one line. |
| gitOutArr := strings.Split(gitOut, "\n") |
| targetSHA = gitOutArr[len(gitOutArr)-2] |
| } else { |
| targetSHA = commitSHA |
| } |
| _, err := mergeUntilConflict(targetSHA) |
| if err != nil { |
| logPanic(fmt.Errorf("Could not merge: %w", err).Error()) |
| } |
| } |
| |
| // squash squashes all commits between the HEAD commit and base commit into the HEAD commit. The HEAD commit must be a merge. |
| func squash(baseSHA string) { |
| if getWD() != "modemmanager-next" { |
| logPanic("squash-merges are supported for MM only. Please run uprev_script inside ~/chromiumos/src/third_party/modemmanager-next/") |
| } |
| |
| bkupDir, err := os.MkdirTemp("", "") |
| if err != nil { |
| log.Fatal(err) |
| } |
| var cmd *exec.Cmd |
| // store the merge conflict resolution |
| cmd = exec.Command("cp", "-R", "../modemmanager-next", bkupDir) |
| runCmd(cmd, true) |
| cmd = exec.Command("rm", "-rf", bkupDir+"/modemmanager-next/.git") |
| runCmd(cmd, true) |
| |
| // get the SHA of the parent commit from cros/upstream, and attempt to merge it again. |
| cmd = exec.Command("git", "log", "--pretty=%p", "-n", "1", "HEAD") |
| out, _ := runCmd(cmd, true) |
| |
| parentCommits := strings.Split(out, " ") |
| if len(parentCommits) != 2 { |
| logPanic("Cannot squash-merge since top commit is not a 2-way merge") |
| } |
| mergeSHA := strings.TrimSuffix(parentCommits[1], "\n") |
| |
| cmd = exec.Command("git", "reset", "--hard", baseSHA) |
| runCmd(cmd, true) |
| cmd = exec.Command("git", "merge", mergeSHA) |
| // there will be merge conflicts, but we already have the resolution |
| runCmd(cmd, false) |
| fmt.Println("Please run the following commnand and press Enter") |
| // TODO(pholla): Figure out why cmd = exec.Command("cp", "-R", logsDir+"/modemmanager-next/*", "./") fails |
| fmt.Println("cp", "-r", "-f", bkupDir+"/modemmanager-next/*", "~/chromiumos/src/third_party/modemmanager-next/") |
| input := bufio.NewScanner(os.Stdin) |
| input.Scan() |
| cmd = exec.Command("git", "add", "-u") |
| runCmd(cmd, true) |
| cmd = exec.Command("git", "commit", "--no-edit") |
| runCmd(cmd, true) |
| genCommitMsg(baseSHA, mergeSHA, "", "None", mergeSHA) |
| } |
| |
| // returns current working directory |
| func getWD() string { |
| d, err := os.Getwd() |
| if err != nil { |
| logPanic(err.Error()) |
| } |
| return filepath.Base(d) |
| } |
| |
| func genCommitMsg(baseSHA string, mergeSHA string, cqDepend string, bugFlag string, lastTag string) { |
| var cmd *exec.Cmd |
| |
| cmd = exec.Command("git", "log", "cros/main.."+mergeSHA, "--abbrev", "--oneline", "--no-merges", "--format=\"%C(auto) %h %s (%an)\"") |
| out, _ := runCmd(cmd, false) |
| commitMsg := "Merge cros/upstream to cros/main - " + lastTag + "\n\nPart of an uprev that contains the following commits:\n\n" + out + "\n\nBUG=" + bugFlag + "\nFIXED=" + bugFlag + "\n\nTEST=None" |
| if cqDepend != "" { |
| commitMsg = commitMsg + "\n\nCq-Depend: " + cqDepend |
| } |
| if getWD() == "modemmanager-next" { |
| commitMsg += cqCommitText |
| } |
| if err := os.WriteFile("/tmp/commit-msg.log", []byte(commitMsg), 0644); err != nil { |
| logPanic("Cannot write commits.log") |
| } |
| cmd = exec.Command("git", "commit", "--amend", "-F", "/tmp/commit-msg.log") |
| runCmd(cmd, true) |
| } |
| |
| func compile(compileBoardFlag string) { |
| repoNames := []string{"libqrtr-glib", "libmbim", "libqmi", "modemmanager-next"} |
| for _, repoName := range repoNames { |
| cBoards := strings.Split(compileBoardFlag, ",") |
| for _, c := range cBoards { |
| runCmd(exec.Command("cros_sdk", "cros_workon", "--board="+c, "start", repoName), true) |
| runCmd(exec.Command("cros_sdk", "emerge-"+c, repoName), true) |
| defer runCmd(exec.Command("cros_sdk", "cros_workon", "--board="+c, "stop", repoName), false) |
| } |
| } |
| } |
| |
| // returns gerrit cl |
| func postMerge(rootDir string, repoName string, upstreamBranch string, cqDepend string, uploadFlag bool, cqP1Flag bool, prettyMsg bool, bugFlag string) string { |
| uprevDate := time.Now().Format("01-02-2006") |
| branchName := "merge-upstream-" + uprevDate |
| fmt.Println(rootDir, repoName, upstreamBranch, uprevDate) |
| if err := os.Chdir(filepath.Join(rootDir, repoName)); err != nil { |
| logPanic(err.Error()) |
| } |
| newDir, _ := os.Getwd() |
| fmt.Printf(colorGreen+"Working on : %s\n"+colorReset, newDir) |
| |
| var cmd *exec.Cmd |
| cmd = exec.Command("git", "log", "cros/main..HEAD", "--abbrev", "--oneline", "--format=%h") |
| out, _ := runCmd(cmd, true) |
| if out == "" { |
| return "" |
| } |
| |
| cmd = exec.Command("git", "describe", "--abbrev=0", "cros/"+upstreamBranch) |
| gitOut, _ := runCmd(cmd, true) |
| // git may throw warnings, but still have the last tag as the last but one line. |
| gitOutArr := strings.Split(gitOut, "\n") |
| lastTag := gitOutArr[len(gitOutArr)-2] |
| fmt.Printf(colorGreen+"Last tag : %s\n"+colorReset, lastTag) |
| |
| if prettyMsg { |
| genCommitMsg("cros/main", "HEAD", cqDepend, bugFlag, lastTag) |
| } |
| |
| if !uploadFlag { |
| return "" |
| } |
| //TODO: check if -o uploadvalidator~skip is needed whenever AUTHORS change. (Florence_ is a banned word) |
| cmd = exec.Command("repo", "upload", "--cbr", ".", "--no-verify", "-o", "topic="+branchName, "-y") |
| cmd.Stdin = strings.NewReader("yes") |
| out, _ = runCmd(cmd, true) |
| re := regexp.MustCompile(`\+/(.*) Merge`) |
| res := re.FindStringSubmatch(out) |
| fmt.Printf("%sUploaded crrev.com/c/%s\n%s", colorGreen, res[1], colorReset) |
| |
| if cqP1Flag { |
| runCmd(exec.Command("gerrit", "label-v", res[1], "1"), false) |
| runCmd(exec.Command("gerrit", "label-cq", res[1], "1"), false) |
| } |
| |
| return "chromium:" + res[1] + " " |
| } |
| |
| func uploadEmptyCL() { |
| fmt.Printf("%sUploading an empty CL before the merge...\n%s", colorGreen, colorReset) |
| checkoutDir := lookupCheckout() |
| rootDir := filepath.Join(checkoutDir, "src/third_party") |
| if err := os.Chdir(filepath.Join(rootDir, emptyCLRepoName)); err != nil { |
| logPanic(err.Error()) |
| } |
| newDir, _ := os.Getwd() |
| fmt.Printf("%sWorking on: %s\n%s", colorGreen, newDir, colorReset) |
| |
| runCmd(exec.Command("repo", "sync", "-d", "."), true) |
| runCmd(exec.Command("git", "branch", "-D", emptyCLBranchName), false) |
| runCmd(exec.Command("repo", "start", emptyCLBranchName, "."), true) |
| defer runCmd(exec.Command("repo", "abandon", emptyCLBranchName, "."), false) |
| |
| f, err := os.OpenFile(emptyCLFileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) |
| if err != nil { |
| panic(err) |
| } |
| defer f.Close() |
| if _, err = f.WriteString(emptyCLMsg); err != nil { |
| panic(err) |
| } |
| |
| runCmd(exec.Command("git", "add", "."), true) |
| runCmd(exec.Command("git", "commit", "-m", emptyCLCommitMsg), true) |
| |
| cmd := exec.Command("repo", "upload", "--cbr", ".", "--no-verify", "-o", "topic="+emptyCLBranchName, "-y") |
| cmd.Stdin = strings.NewReader("yes") |
| out, _ := runCmd(cmd, true) |
| re := regexp.MustCompile(`\+/(.*) EMPTY`) |
| res := re.FindStringSubmatch(out) |
| |
| runCmd(exec.Command("gerrit", "label-v", res[1], "1"), false) |
| runCmd(exec.Command("gerrit", "label-cq", res[1], "1"), false) |
| fmt.Printf("%sUploaded an empty CL: crrev.com/c/%s\n%s", colorGreen, res[1], colorReset) |
| } |
| |
| // Returns the absolute path of the chromeos source checkout. |
| func lookupCheckout() string { |
| workingDirectory, err := os.Getwd() |
| if err != nil { |
| logPanic("Failed to retrieve working directory: " + err.Error()) |
| } |
| |
| currentDirectory := workingDirectory |
| for { |
| repoDirectory := filepath.Join(currentDirectory, ".repo") |
| if _, err := os.Stat(repoDirectory); err == nil { |
| return currentDirectory |
| } |
| |
| currentDirectory = filepath.Dir(currentDirectory) |
| if currentDirectory == "/" { |
| logPanic("Please run within a chromeos source checkout.") |
| } |
| } |
| } |
| |
| func printUsageAndExit(exitcode int) { |
| fmt.Printf("Usage: %s [ACTION] ...\n", filepath.Base(os.Args[0])) |
| fmt.Printf(" ACTION is one of 'empty-cl', 'merge', 'post-merge' or 'help'\n") |
| fmt.Printf(" Run 'ACTION -help' for details\n") |
| fmt.Printf("\n") |
| os.Exit(exitcode) |
| } |
| |
| func main() { |
| file, err := ioutil.TempFile("/tmp", "uprev") |
| if err != nil { |
| logPanic(err.Error()) |
| } |
| log.SetOutput(file) |
| defer fmt.Println("\nUprev logs in ", file.Name()) |
| |
| if len(os.Args) < 2 { |
| fmt.Fprintf(os.Stderr, "error: action not specified\n\n") |
| printUsageAndExit(1) |
| } |
| |
| if _, err := os.Stat("/etc/cros_chroot_version"); !errors.Is(err, os.ErrNotExist) { |
| logPanic("Please run out of the chroot.") |
| } |
| |
| switch os.Args[1] { |
| case "empty-cl": |
| uploadEmptyCL() |
| |
| case "merge": |
| mergeSubCmd() |
| |
| case "compile": |
| compileSubCmd() |
| |
| case "post-merge": |
| postMergeSubCmd() |
| |
| case "help": |
| printUsageAndExit(0) |
| |
| default: |
| fmt.Fprintf(os.Stderr, "error: invalid action: %s\n\n", os.Args[1]) |
| printUsageAndExit(2) |
| } |
| } |
| |
| func mergeSubCmd() { |
| mergeFlagSet := flag.NewFlagSet("merge", flag.ExitOnError) |
| mergeUntilConflictFlag := mergeFlagSet.Bool("merge-until-conflict", false, "Merge the last non conflicting commit between current branch and cros/upstream") |
| forceFlag := mergeFlagSet.Bool("force", false, "force a reset to cros/main. You will lose all changes on the current branch. Prefer stashing your changes instead") |
| createAndMergeFlag := mergeFlagSet.Bool("create-branch-and-merge", false, "Creates a new branch and performs a git merge of cros/upstream* until a conflict occurs") |
| squashMergeFlag := mergeFlagSet.String("squash-merge", "", "squashes all commits between provided SHA and HEAD. Use to overcome CQ uncertainity with stacked merge commits") |
| libmbimCommitFlag := mergeFlagSet.String("libmbim-commit", "cros/upstream", "libmbim SHA that needs to be merged") |
| libqmiCommitFlag := mergeFlagSet.String("libqmi-commit", "cros/upstream", "libqmi SHA that needs to be merged") |
| libqrtrCommitFlag := mergeFlagSet.String("libqrtr-commit", "cros/upstream/main", "libqrtr-glib SHA that needs to be merged") |
| mmCommitFlag := mergeFlagSet.String("mm-commit", "cros/upstream", "modemmanager SHA that needs to be merged") |
| lastTagFlag := mergeFlagSet.Bool("last-tag", false, "merges until the last tag in upstream") |
| |
| mergeFlagSet.Parse(os.Args[2:]) |
| flagCount := 0 |
| if *mergeUntilConflictFlag { |
| flagCount++ |
| } |
| if *squashMergeFlag != "" { |
| flagCount++ |
| } |
| if *createAndMergeFlag { |
| flagCount++ |
| } |
| if flagCount != 1 { |
| pre := "Only one" |
| if flagCount == 0 { |
| pre = "One" |
| } |
| logPanic(pre + " of create-branch-and-merge, squash-merge, merge-until-conflict should be provided") |
| } |
| if *lastTagFlag && !*createAndMergeFlag { |
| logPanic("--last-tag needs to be used with create-and-merge") |
| } |
| |
| if *mergeUntilConflictFlag { |
| commitSHA, err := mergeUntilConflict("cros/upstream") |
| if err != nil { |
| logPanic(err.Error()) |
| return |
| } |
| fmt.Println(commitSHA, "is the last commit that can be merged without conflicts") |
| return |
| } |
| |
| if *squashMergeFlag != "" { |
| squash(*squashMergeFlag) |
| return |
| } |
| |
| checkoutDir := lookupCheckout() |
| rootDir := filepath.Join(checkoutDir, "src/third_party") |
| |
| if *createAndMergeFlag { |
| uprevRepo(rootDir, "libqrtr-glib", "upstream/main", *lastTagFlag, *libqrtrCommitFlag, *forceFlag) |
| uprevRepo(rootDir, "libqmi", "upstream", *lastTagFlag, *libqmiCommitFlag, *forceFlag) |
| uprevRepo(rootDir, "libmbim", "upstream", *lastTagFlag, *libmbimCommitFlag, *forceFlag) |
| uprevRepo(rootDir, "modemmanager-next", "upstream", *lastTagFlag, *mmCommitFlag, *forceFlag) |
| return |
| } |
| } |
| |
| func compileSubCmd() { |
| compileFlagSet := flag.NewFlagSet("compile", flag.ExitOnError) |
| compileBoardFlag := compileFlagSet.String("board", "trogdor,dedede", "boards to be used in the compile process (default:trogdor,dedede)") |
| skipSetupBoardFlag := compileFlagSet.Bool("skip-setup-board", false, "Skip executing setup_board. You will have to ensure that it's already been run before.") |
| |
| compileFlagSet.Parse(os.Args[2:]) |
| |
| if !*skipSetupBoardFlag { |
| cBoards := strings.Split(*compileBoardFlag, ",") |
| for _, c := range cBoards { |
| cmd := exec.Command("cros_sdk", "setup_board", "--board="+c) |
| runCmd(cmd, true) |
| } |
| } |
| |
| compile(*compileBoardFlag) |
| } |
| |
| func postMergeSubCmd() { |
| postmergeFlagSet := flag.NewFlagSet("post-merge", flag.ExitOnError) |
| prettyMsgFlag := postmergeFlagSet.Bool("pretty-msg", true, "prettify the commit message of the head commit.") |
| uploadFlag := postmergeFlagSet.Bool("upload", false, "upload to gerrit") |
| cqP1Flag := postmergeFlagSet.Bool("cq", false, "V+1, CQ+1 on gerrit") |
| bugFlag := postmergeFlagSet.String("bug", "None", "Bug number in commit msg") |
| postmergeFlagSet.Parse(os.Args[2:]) |
| if !*uploadFlag && !*prettyMsgFlag { |
| logPanic("Atleast one of --upload, --cq, --pretty-msg needs to be set") |
| } |
| if *cqP1Flag && !*uploadFlag { |
| logPanic("--cq needs to be used with --upload") |
| } |
| |
| checkoutDir := lookupCheckout() |
| rootDir := filepath.Join(checkoutDir, "src/third_party") |
| cqDepend := "" |
| |
| libqrtrCL := postMerge(rootDir, "libqrtr-glib", "upstream/main", cqDepend, *uploadFlag, *cqP1Flag, *prettyMsgFlag, *bugFlag) |
| libqmiCL := postMerge(rootDir, "libqmi", "upstream", cqDepend, *uploadFlag, *cqP1Flag, *prettyMsgFlag, *bugFlag) |
| libmbimCL := postMerge(rootDir, "libmbim", "upstream", cqDepend, *uploadFlag, *cqP1Flag, *prettyMsgFlag, *bugFlag) |
| |
| cqDepend = libqrtrCL + libqmiCL + libmbimCL |
| cqDepend = strings.ReplaceAll(strings.TrimSpace(cqDepend), " ", ",") |
| postMerge(rootDir, "modemmanager-next", "upstream", cqDepend, *uploadFlag, *cqP1Flag, *prettyMsgFlag, *bugFlag) |
| } |