github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/cl-fetch/main.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // cl-fetch fetches and tags CLs from Gerrit. 6 package main 7 8 import ( 9 "context" 10 "encoding/json" 11 "flag" 12 "fmt" 13 "log" 14 "os" 15 "os/exec" 16 "regexp" 17 "strings" 18 19 "golang.org/x/build/gerrit" 20 ) 21 22 var ( 23 flagOutgoing = flag.Bool("outgoing", false, "fetch outgoing CLs") 24 flagIncoming = flag.Bool("incoming", false, "fetch incoming CLs") 25 flagQuery = flag.String("q", "", "fetch CLs matching `query`") 26 flagVerbose = flag.Bool("v", false, "verbose output") 27 flagDry = flag.Bool("dry-run", false, "print but do not execute commands") 28 ) 29 30 var clRe = regexp.MustCompile("^[0-9]+$|^I[0-9a-f]{40}$") 31 32 type Tag struct { 33 tag string 34 commit *gerrit.CommitInfo 35 } 36 37 func main() { 38 flag.Usage = func() { 39 fmt.Fprintf(os.Stderr, "Usage: %s [flags] [CLs...]\n", os.Args[0]) 40 flag.PrintDefaults() 41 } 42 flag.Parse() 43 44 queryParts := []string{} 45 if *flagOutgoing { 46 queryParts = append(queryParts, "is:open owner:self") 47 } 48 if *flagIncoming { 49 queryParts = append(queryParts, "is:open reviewer:self -owner:self") 50 } 51 if *flagQuery != "" { 52 queryParts = append(queryParts, *flagQuery) 53 } 54 for _, arg := range flag.Args() { 55 if !clRe.MatchString(arg) { 56 fmt.Fprintf(os.Stderr, "CL must be a CL number or Change-Id\n") 57 os.Exit(2) 58 } 59 queryParts = append(queryParts, "change:"+arg) 60 } 61 if len(queryParts) == 0 { 62 fmt.Fprintf(os.Stderr, "must specify something to fetch\n") 63 os.Exit(2) 64 } 65 query := "(" + strings.Join(queryParts, ") OR (") + ")" 66 67 if *flagVerbose { 68 log.Printf("query: %s", query) 69 } 70 71 // Get the origin so we don't pull CLs for other repositories 72 // in to this one. 73 origin := gitOutput("config", "remote.origin.url") 74 75 // Get the existing CL tags. 76 haveTags := map[string]bool{} 77 for _, tag := range strings.Split(gitOutput("tag"), "\n") { 78 haveTags[tag] = true 79 } 80 81 c := gerrit.NewClient("https://go-review.googlesource.com", gerrit.GitCookiesAuth()) 82 83 cls, err := c.QueryChanges(context.Background(), query, gerrit.QueryChangesOpt{ 84 Fields: []string{"CURRENT_REVISION", "CURRENT_COMMIT"}, 85 }) 86 if err != nil { 87 log.Fatal(err) 88 } 89 90 if *flagVerbose { 91 v, _ := json.MarshalIndent(cls, "", " ") 92 log.Printf("Query response:\n%s\n", v) 93 } 94 95 // Collect git fetch and tag commands. 96 fetchCmd := []string{"fetch", "--", origin} 97 tags := make(map[string]*Tag) 98 hashOrder := []string{} 99 for _, cl := range cls { 100 for commitID, rev := range cl.Revisions { 101 tag := fmt.Sprintf("cl/%d/%d", cl.ChangeNumber, rev.PatchSetNumber) 102 if !haveTags[tag] { 103 any := false 104 for _, fetch := range rev.Fetch { 105 if fetch.URL == origin { 106 fetchCmd = append(fetchCmd, fetch.Ref) 107 any = true 108 break 109 } 110 } 111 if !any { 112 continue 113 } 114 } 115 116 tags[commitID] = &Tag{ 117 tag: tag, 118 commit: rev.Commit, 119 } 120 121 hashOrder = append(hashOrder, commitID) 122 } 123 } 124 125 // Execute git fetch and tag commands. 126 if len(fetchCmd) != 3 { 127 git(fetchCmd...) 128 fmt.Println() 129 } 130 for commitID, tag := range tags { 131 if !haveTags[tag.tag] { 132 git("tag", tag.tag, commitID) 133 } 134 } 135 if *flagDry { 136 // Separate command from printed tags. 137 fmt.Println() 138 } 139 140 // Print tags. 141 leafs := make(map[string]bool) 142 for commitID, _ := range tags { 143 leafs[commitID] = true 144 } 145 for _, tag := range tags { 146 for _, parent := range tag.commit.Parents { 147 leafs[parent.CommitID] = false 148 } 149 } 150 151 printed := make(map[string]bool) 152 needBlank := false 153 for i := range hashOrder { 154 commitID := hashOrder[len(hashOrder)-i-1] 155 if !leafs[commitID] { 156 continue 157 } 158 if needBlank { 159 fmt.Println() 160 } 161 needBlank = printChain(tags, commitID, printed) 162 } 163 } 164 165 func git(args ...string) { 166 if *flagDry { 167 fmt.Printf("git %s\n", strings.Join(args, " ")) 168 return 169 } 170 171 cmd := exec.Command("git", args...) 172 cmd.Stdout = os.Stdout 173 cmd.Stderr = os.Stderr 174 if err := cmd.Run(); err != nil { 175 log.Fatalf("git %s failed: %s", strings.Join(args, " "), err) 176 } 177 } 178 179 func gitOutput(args ...string) string { 180 if *flagDry { 181 fmt.Printf("git %s\n", strings.Join(args, " ")) 182 } 183 184 cmd := exec.Command("git", args...) 185 cmd.Stderr = os.Stderr 186 out, err := cmd.Output() 187 if err != nil { 188 log.Fatalf("git %s failed: %s", strings.Join(args, " "), err) 189 } 190 return strings.TrimRight(string(out), "\n") 191 } 192 193 func printChain(tags map[string]*Tag, commitID string, printed map[string]bool) bool { 194 if printed[commitID] { 195 return false 196 } 197 printed[commitID] = true 198 199 tag := tags[commitID] 200 for _, parent := range tag.commit.Parents { 201 if tags[parent.CommitID] != nil { 202 printChain(tags, parent.CommitID, printed) 203 } 204 } 205 fmt.Printf("%s %s\n", tag.tag, tag.commit.Subject) 206 return true 207 }