github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/review/git-codereview/mail.go (about)

     1  // Copyright 2014 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  package main
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"regexp"
    11  	"sort"
    12  	"strings"
    13  )
    14  
    15  func cmdMail(args []string) {
    16  	var (
    17  		diff   = flags.Bool("diff", false, "show change commit diff and don't upload or mail")
    18  		force  = flags.Bool("f", false, "mail even if there are staged changes")
    19  		topic  = flags.String("topic", "", "set Gerrit topic")
    20  		trybot = flags.Bool("trybot", false, "run trybots on the uploaded CLs")
    21  		rList  = new(stringList) // installed below
    22  		ccList = new(stringList) // installed below
    23  	)
    24  	flags.Var(rList, "r", "comma-separated list of reviewers")
    25  	flags.Var(ccList, "cc", "comma-separated list of people to CC:")
    26  
    27  	flags.Usage = func() {
    28  		fmt.Fprintf(stderr(), "Usage: %s mail %s [-r reviewer,...] [-cc mail,...] [-topic topic] [-trybot] [commit-hash]\n", os.Args[0], globalFlags)
    29  	}
    30  	flags.Parse(args)
    31  	if len(flags.Args()) > 1 {
    32  		flags.Usage()
    33  		os.Exit(2)
    34  	}
    35  
    36  	b := CurrentBranch()
    37  
    38  	var c *Commit
    39  	if len(flags.Args()) == 1 {
    40  		c = b.CommitByHash("mail", flags.Arg(0))
    41  	} else {
    42  		c = b.DefaultCommit("mail")
    43  	}
    44  
    45  	if *diff {
    46  		run("git", "diff", b.Branchpoint()[:7]+".."+c.ShortHash, "--")
    47  		return
    48  	}
    49  
    50  	if !*force && HasStagedChanges() {
    51  		dief("there are staged changes; aborting.\n"+
    52  			"Use '%s change' to include them or '%s mail -f' to force it.", os.Args[0], os.Args[0])
    53  	}
    54  
    55  	// for side effect of dying with a good message if origin is GitHub
    56  	loadGerritOrigin()
    57  
    58  	refSpec := b.PushSpec(c)
    59  	start := "%"
    60  	if *rList != "" {
    61  		refSpec += mailList(start, "r", string(*rList))
    62  		start = ","
    63  	}
    64  	if *ccList != "" {
    65  		refSpec += mailList(start, "cc", string(*ccList))
    66  		start = ","
    67  	}
    68  	if *topic != "" {
    69  		// There's no way to escape the topic, but the only
    70  		// ambiguous character is ',' (though other characters
    71  		// like ' ' will be rejected outright by git).
    72  		if strings.Contains(*topic, ",") {
    73  			dief("topic may not contain a comma")
    74  		}
    75  		refSpec += start + "topic=" + *topic
    76  		start = ","
    77  	}
    78  	if *trybot {
    79  		refSpec += start + "l=Run-TryBot"
    80  		start = ","
    81  	}
    82  	run("git", "push", "-q", "origin", refSpec)
    83  
    84  	// Create local tag for mailed change.
    85  	// If in the 'work' branch, this creates or updates work.mailed.
    86  	// Older mailings are in the reflog, so work.mailed is newest,
    87  	// work.mailed@{1} is the one before that, work.mailed@{2} before that,
    88  	// and so on.
    89  	// Git doesn't actually have a concept of a local tag,
    90  	// but Gerrit won't let people push tags to it, so the tag
    91  	// can't propagate out of the local client into the official repo.
    92  	// There is no conflict with the branch names people are using
    93  	// for work, because git change rejects any name containing a dot.
    94  	// The space of names with dots is ours (the Go team's) to define.
    95  	run("git", "tag", "-f", b.Name+".mailed", c.ShortHash)
    96  }
    97  
    98  // PushSpec returns the spec for a Gerrit push command to publish the change c in b.
    99  // If c is nil, PushSpec returns a spec for pushing all changes in b.
   100  func (b *Branch) PushSpec(c *Commit) string {
   101  	local := "HEAD"
   102  	if c != nil && (len(b.Pending()) == 0 || b.Pending()[0].Hash != c.Hash) {
   103  		local = c.ShortHash
   104  	}
   105  	return local + ":refs/for/" + strings.TrimPrefix(b.OriginBranch(), "origin/")
   106  }
   107  
   108  // mailAddressRE matches the mail addresses we admit. It's restrictive but admits
   109  // all the addresses in the Go CONTRIBUTORS file at time of writing (tested separately).
   110  var mailAddressRE = regexp.MustCompile(`^([a-zA-Z0-9][-_.a-zA-Z0-9]*)(@[-_.a-zA-Z0-9]+)?$`)
   111  
   112  // mailList turns the list of mail addresses from the flag value into the format
   113  // expected by gerrit. The start argument is a % or , depending on where we
   114  // are in the processing sequence.
   115  func mailList(start, tag string, flagList string) string {
   116  	errors := false
   117  	spec := start
   118  	short := ""
   119  	long := ""
   120  	for i, addr := range strings.Split(flagList, ",") {
   121  		m := mailAddressRE.FindStringSubmatch(addr)
   122  		if m == nil {
   123  			printf("invalid reviewer mail address: %s", addr)
   124  			errors = true
   125  			continue
   126  		}
   127  		if m[2] == "" {
   128  			email := mailLookup(addr)
   129  			if email == "" {
   130  				printf("unknown reviewer: %s", addr)
   131  				errors = true
   132  				continue
   133  			}
   134  			short += "," + addr
   135  			long += "," + email
   136  			addr = email
   137  		}
   138  		if i > 0 {
   139  			spec += ","
   140  		}
   141  		spec += tag + "=" + addr
   142  	}
   143  	if short != "" {
   144  		verbosef("expanded %s to %s", short[1:], long[1:])
   145  	}
   146  	if errors {
   147  		die()
   148  	}
   149  	return spec
   150  }
   151  
   152  // reviewers is the list of reviewers for the current repository,
   153  // sorted by how many reviews each has done.
   154  var reviewers []reviewer
   155  
   156  type reviewer struct {
   157  	addr  string
   158  	count int
   159  }
   160  
   161  // mailLookup translates the short name (like adg) into a full
   162  // email address (like adg@golang.org).
   163  // It returns "" if no translation is found.
   164  // The algorithm for expanding short user names is as follows:
   165  // Look at the git commit log for the current repository,
   166  // extracting all the email addresses in Reviewed-By lines
   167  // and sorting by how many times each address appears.
   168  // For each short user name, walk the list, most common
   169  // address first, and use the first address found that has
   170  // the short user name on the left side of the @.
   171  func mailLookup(short string) string {
   172  	loadReviewers()
   173  
   174  	short += "@"
   175  	for _, r := range reviewers {
   176  		if strings.HasPrefix(r.addr, short) {
   177  			return r.addr
   178  		}
   179  	}
   180  	return ""
   181  }
   182  
   183  // loadReviewers reads the reviewer list from the current git repo
   184  // and leaves it in the global variable reviewers.
   185  // See the comment on mailLookup for a description of how the
   186  // list is generated and used.
   187  func loadReviewers() {
   188  	if reviewers != nil {
   189  		return
   190  	}
   191  	countByAddr := map[string]int{}
   192  	for _, line := range nonBlankLines(cmdOutput("git", "log", "--format=format:%B")) {
   193  		if strings.HasPrefix(line, "Reviewed-by:") {
   194  			f := strings.Fields(line)
   195  			addr := f[len(f)-1]
   196  			if strings.HasPrefix(addr, "<") && strings.Contains(addr, "@") && strings.HasSuffix(addr, ">") {
   197  				countByAddr[addr[1:len(addr)-1]]++
   198  			}
   199  		}
   200  	}
   201  
   202  	reviewers = []reviewer{}
   203  	for addr, count := range countByAddr {
   204  		reviewers = append(reviewers, reviewer{addr, count})
   205  	}
   206  	sort.Sort(reviewersByCount(reviewers))
   207  }
   208  
   209  type reviewersByCount []reviewer
   210  
   211  func (x reviewersByCount) Len() int      { return len(x) }
   212  func (x reviewersByCount) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
   213  func (x reviewersByCount) Less(i, j int) bool {
   214  	if x[i].count != x[j].count {
   215  		return x[i].count > x[j].count
   216  	}
   217  	return x[i].addr < x[j].addr
   218  }
   219  
   220  // stringList is a flag.Value that is like flag.String, but if repeated
   221  // keeps appending to the old value, inserting commas as separators.
   222  // This allows people to write -r rsc,adg (like the old hg command)
   223  // but also -r rsc -r adg (like standard git commands).
   224  // This does change the meaning of -r rsc -r adg (it used to mean just adg).
   225  type stringList string
   226  
   227  func (x *stringList) String() string {
   228  	return string(*x)
   229  }
   230  
   231  func (x *stringList) Set(s string) error {
   232  	if *x != "" && s != "" {
   233  		*x += ","
   234  	}
   235  	*x += stringList(s)
   236  	return nil
   237  }