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  }