OLD | NEW |
(Empty) | |
| 1 // Command watcher watches the specified repository for new commits |
| 2 // and reports them to the build dashboard. |
| 3 package main |
| 4 |
| 5 import ( |
| 6 "bytes" |
| 7 "encoding/json" |
| 8 "encoding/xml" |
| 9 "flag" |
| 10 "fmt" |
| 11 "io/ioutil" |
| 12 "log" |
| 13 "net/http" |
| 14 "os" |
| 15 "os/exec" |
| 16 "path/filepath" |
| 17 "runtime" |
| 18 "strings" |
| 19 "time" |
| 20 ) |
| 21 |
| 22 var ( |
| 23 pkgPath = flag.String("pkg", "", "Package path (empty for main repo
)") |
| 24 repo = flag.String("repo", "https://code.google.com/p/go", "Repo
sitory URL") |
| 25 local = flag.String("local", "", "Local repo (for testing only)") |
| 26 dashboard = flag.String("dash", "https://build.golang.org/", "Dashboa
rd URL") |
| 27 keyFile = flag.String("key", defaultKeyFile, "Build dashboard key f
ile") |
| 28 pollInterval = flag.Duration("poll", 10*time.Second, "Remote repo poll i
nterval") |
| 29 ) |
| 30 |
| 31 var ( |
| 32 defaultKeyFile = filepath.Join(homeDir(), ".gobuildkey") |
| 33 dashboardStart = "" // we'll ignore commits before this hash |
| 34 dashboardKey = "" |
| 35 ) |
| 36 |
| 37 func main() { |
| 38 flag.Parse() |
| 39 if *pkgPath == "" { |
| 40 // We only started building the main repo at this hash. |
| 41 dashboardStart = "2f970046e1ba96f32de62f5639b7141cda2e977c" |
| 42 } |
| 43 if err := run(); err != nil { |
| 44 fmt.Fprintln(os.Stderr, err) |
| 45 os.Exit(1) |
| 46 } |
| 47 } |
| 48 |
| 49 func run() error { |
| 50 if k, err := readKey(); err != nil { |
| 51 return err |
| 52 } else { |
| 53 dashboardKey = k |
| 54 } |
| 55 |
| 56 var goroot string |
| 57 if *local != "" { |
| 58 goroot = *local |
| 59 } else { |
| 60 dir, err := ioutil.TempDir("", "watcher") |
| 61 if err != nil { |
| 62 return err |
| 63 } |
| 64 defer os.RemoveAll(dir) |
| 65 goroot = filepath.Join(dir, "go") |
| 66 cmd := exec.Command("hg", "clone", *repo, goroot) |
| 67 if out, err := cmd.CombinedOutput(); err != nil { |
| 68 return fmt.Errorf("%v\n\n%s", err, out) |
| 69 } |
| 70 } |
| 71 |
| 72 commits, err := allCommits(goroot) |
| 73 if err != nil { |
| 74 return err |
| 75 } |
| 76 |
| 77 branches := make(map[string]*Branch) |
| 78 for _, c := range commits { |
| 79 if c.children == nil { |
| 80 if !validHead(c) { |
| 81 continue |
| 82 } |
| 83 seen, err := lastSeen(commits, c.Hash) |
| 84 if err != nil { |
| 85 return err |
| 86 } |
| 87 b := &Branch{Name: c.Branch, Head: c, LastSeen: seen} |
| 88 branches[c.Branch] = b |
| 89 log.Printf("found branch: %v", b) |
| 90 } |
| 91 } |
| 92 log.Printf("found %v branches among %v commits\n", len(branches), len(co
mmits)) |
| 93 |
| 94 for { |
| 95 if err := update(goroot, commits, branches); err != nil { |
| 96 return err |
| 97 } |
| 98 for _, b := range branches { |
| 99 if b.Head == b.LastSeen { |
| 100 continue |
| 101 } |
| 102 c := b.LastSeen |
| 103 if c == nil { |
| 104 // Haven't seen any: find the earliest commit on
this branch. |
| 105 for c := b.Head; c.Branch == b.Name; c = c.paren
t { |
| 106 } |
| 107 } |
| 108 // Add unseen commits on this branch, working forward fr
om last seen. |
| 109 for c.children != nil { |
| 110 var next *Commit |
| 111 for _, c2 := range c.children { |
| 112 if c2.Branch != b.Name { |
| 113 continue |
| 114 } |
| 115 if next != nil { |
| 116 // Shouldn't happen, but be para
noid. |
| 117 return fmt.Errorf("found multipl
e children on branch %q: %v and %v", b.Name, next, c2) |
| 118 } |
| 119 next = c2 |
| 120 } |
| 121 if next == nil { |
| 122 // Hit the head commit, nothing more to
do. |
| 123 continue |
| 124 } |
| 125 c = next |
| 126 |
| 127 if err := postCommit(c); err != nil { |
| 128 return err |
| 129 } |
| 130 b.LastSeen = c |
| 131 } |
| 132 } |
| 133 time.Sleep(*pollInterval) |
| 134 } |
| 135 } |
| 136 |
| 137 // validHead reports whether the specified commit should be considered a branch |
| 138 // head. It considers pre-go1 branches and certain specific commits as invalid. |
| 139 func validHead(c *Commit) bool { |
| 140 if strings.HasPrefix(c.Branch, "release-branch.r") { |
| 141 return true |
| 142 } |
| 143 // Not sure why these revisions have no child commits, |
| 144 // but they're old so let's just ignore them. |
| 145 return c.Hash == "b59f4ff1b51094314f735a4d57a2b8f06cfadf15" || |
| 146 c.Hash == "fc75f13840b896e82b9fa6165cf705fbacaf019c" |
| 147 } |
| 148 |
| 149 // lastSeen finds the most recent commit the dashboard has seen, |
| 150 // starting at the specified head. |
| 151 func lastSeen(commits map[string]*Commit, head string) (*Commit, error) { |
| 152 h, ok := commits[head] |
| 153 if !ok { |
| 154 return nil, fmt.Errorf("lastSeen: can't find %q in commits", hea
d) |
| 155 } |
| 156 |
| 157 var s []*Commit |
| 158 for c := h; c != nil; c = c.parent { |
| 159 s = append(s, c) |
| 160 if c.Hash == dashboardStart { |
| 161 break |
| 162 } |
| 163 } |
| 164 |
| 165 for _, c := range s { |
| 166 u := *dashboard + "commit?hash=" + c.Hash |
| 167 r, err := http.Get(u) |
| 168 if err != nil { |
| 169 return nil, err |
| 170 } |
| 171 var resp struct { |
| 172 Error string |
| 173 } |
| 174 err = json.NewDecoder(r.Body).Decode(&resp) |
| 175 r.Body.Close() |
| 176 if err != nil { |
| 177 return nil, err |
| 178 } |
| 179 if resp.Error == "" { |
| 180 return c, nil |
| 181 } |
| 182 } |
| 183 return nil, nil |
| 184 } |
| 185 |
| 186 // postCommit sends a commit to the build dashboard. |
| 187 func postCommit(c *Commit) error { |
| 188 log.Printf("sending commit to dashboard: %v", c) |
| 189 |
| 190 t, err := time.Parse(time.RFC3339, c.Date) |
| 191 if err != nil { |
| 192 return err |
| 193 } |
| 194 dc := struct { |
| 195 PackagePath string // (empty for main repo commits) |
| 196 Hash string |
| 197 ParentHash string |
| 198 |
| 199 User string |
| 200 Desc string |
| 201 Time time.Time |
| 202 |
| 203 NeedsBenchmarking bool |
| 204 }{ |
| 205 *pkgPath, |
| 206 c.Hash, |
| 207 c.Parent, |
| 208 c.Author, |
| 209 c.Desc, |
| 210 t, |
| 211 true, |
| 212 } |
| 213 b, err := json.Marshal(dc) |
| 214 if err != nil { |
| 215 return err |
| 216 } |
| 217 |
| 218 u := *dashboard + "commit?key=" + dashboardKey |
| 219 r, err := http.Post(u, "text/json", bytes.NewReader(b)) |
| 220 if err != nil { |
| 221 return err |
| 222 } |
| 223 if r.StatusCode != 200 { |
| 224 return fmt.Errorf("status: %v", r.Status) |
| 225 } |
| 226 return nil |
| 227 } |
| 228 |
| 229 // allCommits runs "hg log" in goroot and returns a map of all its commits. |
| 230 func allCommits(goroot string) (map[string]*Commit, error) { |
| 231 log, err := hgLog(goroot) |
| 232 if err != nil { |
| 233 return nil, err |
| 234 } |
| 235 commits := make(map[string]*Commit) |
| 236 for _, c := range log { |
| 237 commits[c.Hash] = c |
| 238 } |
| 239 for _, c := range commits { |
| 240 if p, ok := commits[c.Parent]; ok { |
| 241 c.parent = p |
| 242 p.children = append(p.children, c) |
| 243 } |
| 244 } |
| 245 return commits, nil |
| 246 } |
| 247 |
| 248 // update runs "hg pull" in the specified goroot, |
| 249 // looks for new commits and branches, |
| 250 // and updates the comits and branches maps. |
| 251 func update(goroot string, commits map[string]*Commit, branches map[string]*Bran
ch) error { |
| 252 if err := hgPull(goroot); err != nil { |
| 253 return err |
| 254 } |
| 255 |
| 256 newBranches := make(map[string]*Branch) |
| 257 |
| 258 // Check each branch for new commits. |
| 259 for _, b := range branches { |
| 260 // Find all commits from known head. |
| 261 args := []string{"-r", b.Head.Hash + ":"} |
| 262 if b.Name != "" { |
| 263 // Specify -b flag only for non-default branches. |
| 264 args = append(args, "-b", b.Name) |
| 265 } |
| 266 l, err := hgLog(goroot, args...) |
| 267 if err != nil { |
| 268 return err |
| 269 } |
| 270 |
| 271 // Add unknown commits to commits, and create new branches. |
| 272 for _, c := range l { |
| 273 if _, ok := commits[c.Hash]; ok { |
| 274 continue |
| 275 } |
| 276 |
| 277 log.Printf("found new commit %v", c) |
| 278 commits[c.Hash] = c |
| 279 |
| 280 p, ok := commits[c.Parent] |
| 281 if !ok { |
| 282 return fmt.Errorf("can't find parent hash %q for
%v", c.Parent, c) |
| 283 } |
| 284 |
| 285 if c.Branch != b.Name { |
| 286 // The commit isn't on this branch. |
| 287 |
| 288 // If it's a new branch we've already seen this
loop, update the new branch head. |
| 289 if b2, ok := newBranches[c.Branch]; ok { |
| 290 log.Printf("branch %q head updated by: %
v", c.Branch, c) |
| 291 b2.Head = c |
| 292 continue |
| 293 } |
| 294 |
| 295 // Sanity checks: |
| 296 if _, ok := branches[c.Branch]; ok && p.Branch !
= c.Branch { |
| 297 // Shouldn't happen, but be paranoid abo
ut it. |
| 298 return fmt.Errorf("commit branches into
existing branch %q: %v", c.Branch, c) |
| 299 } |
| 300 if p.Branch == c.Branch { |
| 301 // Shouldn't happen, but be paranoid. |
| 302 return fmt.Errorf("commit on branch we h
aven't seen: %v", c) |
| 303 } |
| 304 |
| 305 // A new branch we haven't seen yet; add it. |
| 306 log.Printf("new branch %q created by %v", c.Bran
ch, c) |
| 307 newBranches[c.Branch] = &Branch{Name: c.Branch,
Head: c} |
| 308 continue |
| 309 } |
| 310 |
| 311 // Just a commit to a known branch, add it and update th
e branch head. |
| 312 c.parent = p |
| 313 p.children = append(p.children, c) |
| 314 b.Head = c |
| 315 } |
| 316 } |
| 317 |
| 318 // Find latest dashboard commits for new branches. |
| 319 for _, b := range newBranches { |
| 320 seen, err := lastSeen(commits, b.Head.Hash) |
| 321 if err != nil { |
| 322 return err |
| 323 } |
| 324 b.LastSeen = seen |
| 325 branches[b.Name] = b |
| 326 } |
| 327 |
| 328 return nil |
| 329 } |
| 330 |
| 331 // hgLog runs "hg log" with the supplied arguments |
| 332 // and parses the output into Commit values. |
| 333 func hgLog(dir string, args ...string) ([]*Commit, error) { |
| 334 args = append([]string{"log", "--template", xmlLogTemplate}, args...) |
| 335 cmd := exec.Command("hg", args...) |
| 336 cmd.Dir = dir |
| 337 out, err := cmd.CombinedOutput() |
| 338 if err != nil { |
| 339 return nil, err |
| 340 } |
| 341 |
| 342 // We have a commit with description that contains 0x1b byte. |
| 343 // Mercurial does not escape it, but xml.Unmarshal does not accept it. |
| 344 out = bytes.Replace(out, []byte{0x1b}, []byte{'?'}, -1) |
| 345 |
| 346 var logStruct struct { |
| 347 Log []*Commit |
| 348 } |
| 349 err = xml.Unmarshal([]byte("<Top>"+string(out)+"</Top>"), &logStruct) |
| 350 if err != nil { |
| 351 return nil, err |
| 352 } |
| 353 return logStruct.Log, nil |
| 354 } |
| 355 |
| 356 // hgPull runs "hg pull" in the specified directory. |
| 357 func hgPull(dir string) error { |
| 358 cmd := exec.Command("hg", "pull") |
| 359 cmd.Dir = dir |
| 360 if out, err := cmd.CombinedOutput(); err != nil { |
| 361 return fmt.Errorf("%v\n\n%s", err, out) |
| 362 } |
| 363 return nil |
| 364 } |
| 365 |
| 366 // Branch represents a Mercurial branch. |
| 367 type Branch struct { |
| 368 Name string |
| 369 Head *Commit |
| 370 LastSeen *Commit // the last commit posted to the dashboard |
| 371 } |
| 372 |
| 373 func (b *Branch) String() string { |
| 374 return fmt.Sprintf("%q(Head: %v LastSeen: %v)", b.Name, b.Head, b.LastSe
en) |
| 375 } |
| 376 |
| 377 // Commit represents a single Mercurial revision. |
| 378 type Commit struct { |
| 379 Hash string |
| 380 Author string |
| 381 Date string |
| 382 Desc string |
| 383 Parent string |
| 384 Branch string |
| 385 Files string |
| 386 |
| 387 // For walking the graph. |
| 388 parent *Commit |
| 389 children []*Commit |
| 390 } |
| 391 |
| 392 func (c *Commit) String() string { |
| 393 return fmt.Sprintf("%v(%q)", c.Hash, strings.SplitN(c.Desc, "\n", 2)[0]) |
| 394 } |
| 395 |
| 396 // xmlLogTemplate is a template to pass to Mercurial to make |
| 397 // hg log print the log in valid XML for parsing with xml.Unmarshal. |
| 398 // Can not escape branches and files, because it crashes python with: |
| 399 // AttributeError: 'NoneType' object has no attribute 'replace' |
| 400 const xmlLogTemplate = ` |
| 401 <Log> |
| 402 <Hash>{node|escape}</Hash> |
| 403 <Parent>{p1node}</Parent> |
| 404 <Author>{author|escape}</Author> |
| 405 <Date>{date|rfc3339date}</Date> |
| 406 <Desc>{desc|escape}</Desc> |
| 407 <Branch>{branches}</Branch> |
| 408 <Files>{files}</Files> |
| 409 </Log> |
| 410 ` |
| 411 |
| 412 func homeDir() string { |
| 413 switch runtime.GOOS { |
| 414 case "plan9": |
| 415 return os.Getenv("home") |
| 416 case "windows": |
| 417 return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") |
| 418 } |
| 419 return os.Getenv("HOME") |
| 420 } |
| 421 |
| 422 func readKey() (string, error) { |
| 423 c, err := ioutil.ReadFile(*keyFile) |
| 424 if err != nil { |
| 425 return "", err |
| 426 } |
| 427 return string(bytes.TrimSpace(bytes.SplitN(c, []byte("\n"), 2)[0])), nil |
| 428 } |
OLD | NEW |