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 }