// Copyright © 2023 OpenIM. All rights reserved. // // 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 ( "fmt" "log" "os" "os/exec" "regexp" "sort" "strings" ) var ( mergeRequest = regexp.MustCompile(`Merge pull request #([\d]+)`) webconsoleBump = regexp.MustCompile(regexp.QuoteMeta("bump(github.com/openshift/origin-web-console): ") + `([\w]+)`) upstreamKube = regexp.MustCompile(`^UPSTREAM: (\d+)+:(.+)`) upstreamRepo = regexp.MustCompile(`^UPSTREAM: ([\w/-]+): (\d+)+:(.+)`) prefix = regexp.MustCompile(`^[\w-]: `) assignments = []prefixAssignment{ {"cluster up", "cluster"}, {" pv ", "storage"}, {"haproxy", "router"}, {"router", "router"}, {"route", "route"}, {"authoriz", "auth"}, {"rbac", "auth"}, {"authent", "auth"}, {"reconcil", "auth"}, {"auth", "auth"}, {"role", "auth"}, {" dc ", "deploy"}, {"deployment", "deploy"}, {"rolling", "deploy"}, {"security context constr", "security"}, {"scc", "security"}, {"pipeline", "build"}, {"build", "build"}, {"registry", "registry"}, {"registries", "image"}, {"image", "image"}, {" arp ", "network"}, {" cni ", "network"}, {"egress", "network"}, {"network", "network"}, {"oc ", "cli"}, {"template", "template"}, {"etcd", "server"}, {"pod", "node"}, {"scripts/", "hack"}, {"e2e", "test"}, {"integration", "test"}, {"cluster", "cluster"}, {"master", "server"}, {"packages", "hack"}, {"api", "server"}, } ) type prefixAssignment struct { term string prefix string } type commit struct { short string parents []string message string } func contains(arr []string, value string) bool { for _, s := range arr { if s == value { return true } } return false } func main() { log.SetFlags(0) if len(os.Args) != 3 { log.Fatalf("Must specify two arguments, FROM and TO") } from := os.Args[1] to := os.Args[2] out, err := exec.Command("git", "log", "--topo-order", "--pretty=tformat:%h %p|%s", "--reverse", fmt.Sprintf("%s..%s", from, to)).CombinedOutput() if err != nil { log.Fatal(err) } hide := make(map[string]struct{}) var apiChanges []string var webconsole []string var commits []commit var upstreams []commit var bumps []commit for _, line := range strings.Split(string(out), "\n") { if len(strings.TrimSpace(line)) == 0 { continue } parts := strings.SplitN(line, "|", 2) hashes := strings.Split(parts[0], " ") c := commit{short: hashes[0], parents: hashes[1:], message: parts[1]} if strings.HasPrefix(c.message, "UPSTREAM: ") { hide[c.short] = struct{}{} upstreams = append(upstreams, c) } if strings.HasPrefix(c.message, "bump(") { hide[c.short] = struct{}{} bumps = append(bumps, c) } if len(c.parents) == 1 { commits = append(commits, c) continue } matches := mergeRequest.FindStringSubmatch(line) if len(matches) == 0 { // this may have been a human pressing the merge button, we'll just record this as a direct push continue } // split the accumulated commits into any that are force merges (assumed to be the initial set due // to --topo-order) from the PR commits as soon as we see any of our merge parents. Then print // any of the force merges var first int for i := range commits { first = i if contains(c.parents, commits[i].short) { first++ break } } individual := commits[:first] merged := commits[first:] for _, commit := range individual { if len(commit.parents) > 1 { continue } if _, ok := hide[commit.short]; ok { continue } fmt.Printf("force-merge: %s %s\n", commit.message, commit.short) } // try to find either the PR title or the first commit title from the merge commit out, err := exec.Command("git", "show", "--pretty=tformat:%b", c.short).CombinedOutput() if err != nil { log.Fatal(err) } var message string para := strings.Split(string(out), "\n\n") if len(para) > 0 && strings.HasPrefix(para[0], "Automatic merge from submit-queue") { para = para[1:] } // this is no longer necessary with the submit queue in place if len(para) > 0 && strings.HasPrefix(para[0], "Merged by ") { para = para[1:] } // post submit-queue, the merge bot will add the PR title, which is usually pretty good if len(para) > 0 { message = strings.Split(para[0], "\n")[0] } if len(message) == 0 && len(merged) > 0 { message = merged[0].message } if len(message) > 0 && len(merged) == 1 && message == merged[0].message { merged = nil } // try to calculate a prefix based on the diff if len(message) > 0 && !prefix.MatchString(message) { prefix, ok := findPrefixFor(message, merged) if ok { message = prefix + ": " + message } } // github merge // has api changes display := fmt.Sprintf("%s [\\#%s](https://github.com/openimsdk/Open-IM-Server/pull/%s)", message, matches[1], matches[1]) if hasFileChanges(c.short, "pkg/apistruct/") { apiChanges = append(apiChanges, display) } var filtered []commit for _, commit := range merged { if _, ok := hide[commit.short]; ok { continue } filtered = append(filtered, commit) } if len(filtered) > 0 { fmt.Printf("- %s\n", display) for _, commit := range filtered { fmt.Printf(" - %s (%s)\n", commit.message, commit.short) } } // stick the merge commit in at the beginning of the next list so we can anchor the previous parent commits = []commit{c} } // chunk the bumps var lines []string for _, commit := range bumps { if m := webconsoleBump.FindStringSubmatch(commit.message); len(m) > 0 { webconsole = append(webconsole, m[1]) continue } lines = append(lines, commit.message) } lines = sortAndUniq(lines) for _, line := range lines { fmt.Printf("- %s\n", line) } // chunk the upstreams lines = nil for _, commit := range upstreams { lines = append(lines, commit.message) } lines = sortAndUniq(lines) for _, line := range lines { fmt.Printf("- %s\n", upstreamLinkify(line)) } if len(webconsole) > 0 { fmt.Printf("- web: from %s^..%s\n", webconsole[0], webconsole[len(webconsole)-1]) } for _, apiChange := range apiChanges { fmt.Printf(" - %s\n", apiChange) } } func findPrefixFor(message string, commits []commit) (string, bool) { message = strings.ToLower(message) for _, m := range assignments { if strings.Contains(message, m.term) { return m.prefix, true } } for _, c := range commits { if prefix, ok := findPrefixFor(c.message, nil); ok { return prefix, ok } } return "", false } func hasFileChanges(commit string, prefixes ...string) bool { out, err := exec.Command("git", "diff", "--name-only", fmt.Sprintf("%s^..%s", commit, commit)).CombinedOutput() if err != nil { log.Fatal(err) } for _, file := range strings.Split(string(out), "\n") { for _, prefix := range prefixes { if strings.HasPrefix(file, prefix) { return true } } } return false } func sortAndUniq(lines []string) []string { sort.Strings(lines) out := make([]string, 0, len(lines)) last := "" for _, s := range lines { if last == s { continue } last = s out = append(out, s) } return out } func upstreamLinkify(line string) string { if m := upstreamKube.FindStringSubmatch(line); len(m) > 0 { return fmt.Sprintf("UPSTREAM: [#%s](https://github.com/openimsdk/open-im-server/pull/%s):%s", m[1], m[1], m[2]) } if m := upstreamRepo.FindStringSubmatch(line); len(m) > 0 { return fmt.Sprintf("UPSTREAM: [%s#%s](https://github.com/%s/pull/%s):%s", m[1], m[2], m[1], m[2], m[3]) } return line }