LEFT | RIGHT |
| 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 |
1 // Command watcher watches the specified repository for new commits | 5 // Command watcher watches the specified repository for new commits |
2 // and reports them to the build dashboard. | 6 // and reports them to the build dashboard. |
3 package main | 7 package main |
4 | 8 |
5 import ( | 9 import ( |
6 "bytes" | 10 "bytes" |
7 "encoding/json" | 11 "encoding/json" |
8 "encoding/xml" | 12 "encoding/xml" |
| 13 "errors" |
9 "flag" | 14 "flag" |
10 "fmt" | 15 "fmt" |
| 16 "io" |
11 "io/ioutil" | 17 "io/ioutil" |
12 "log" | 18 "log" |
13 "net/http" | 19 "net/http" |
| 20 "net/url" |
14 "os" | 21 "os" |
15 "os/exec" | 22 "os/exec" |
| 23 "path" |
16 "path/filepath" | 24 "path/filepath" |
17 "runtime" | 25 "runtime" |
18 "strings" | 26 "strings" |
19 "time" | 27 "time" |
20 ) | 28 ) |
21 | 29 |
22 var ( | 30 var ( |
23 » pkgPath = flag.String("pkg", "", "Package path (empty for main repo
)") | 31 » repoURL = flag.String("repo", "https://code.google.com/p/go", "Repo
sitory URL") |
24 » repo = flag.String("repo", "https://code.google.com/p/go", "Repo
sitory URL") | 32 » dashboard = flag.String("dash", "https://build.golang.org/", "Dashboa
rd URL (must end in /)") |
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") | 33 keyFile = flag.String("key", defaultKeyFile, "Build dashboard key f
ile") |
28 pollInterval = flag.Duration("poll", 10*time.Second, "Remote repo poll i
nterval") | 34 pollInterval = flag.Duration("poll", 10*time.Second, "Remote repo poll i
nterval") |
29 ) | 35 ) |
30 | 36 |
31 var ( | 37 var ( |
32 defaultKeyFile = filepath.Join(homeDir(), ".gobuildkey") | 38 defaultKeyFile = filepath.Join(homeDir(), ".gobuildkey") |
33 dashboardStart = "" // we'll ignore commits before this hash | |
34 dashboardKey = "" | 39 dashboardKey = "" |
35 ) | 40 ) |
36 | 41 |
| 42 // The first main repo commit on the dashboard; ignore commits before this. |
| 43 // This is for the main Go repo only. |
| 44 const dashboardStart = "2f970046e1ba96f32de62f5639b7141cda2e977c" |
| 45 |
37 func main() { | 46 func main() { |
38 flag.Parse() | 47 flag.Parse() |
39 » if *pkgPath == "" { | 48 |
40 » » // We only started building the main repo at this hash. | 49 » err := run() |
41 » » dashboardStart = "2f970046e1ba96f32de62f5639b7141cda2e977c" | 50 » fmt.Fprintln(os.Stderr, err) |
42 » } | 51 » os.Exit(1) |
43 » if err := run(); err != nil { | 52 } |
44 » » fmt.Fprintln(os.Stderr, err) | 53 |
45 » » os.Exit(1) | 54 // run is a little wrapper so we can use defer and return to signal |
46 » } | 55 // errors. It should only return a non-nil error. |
47 } | |
48 | |
49 func run() error { | 56 func run() error { |
| 57 if !strings.HasSuffix(*dashboard, "/") { |
| 58 return errors.New("dashboard URL (-dashboard) must end in /") |
| 59 } |
| 60 if err := checkHgVersion(); err != nil { |
| 61 return err |
| 62 } |
| 63 |
50 if k, err := readKey(); err != nil { | 64 if k, err := readKey(); err != nil { |
51 return err | 65 return err |
52 } else { | 66 } else { |
53 dashboardKey = k | 67 dashboardKey = k |
54 } | 68 } |
55 | 69 |
56 » var goroot string | 70 » dir, err := ioutil.TempDir("", "watcher") |
57 » if *local != "" { | 71 » if err != nil { |
58 » » goroot = *local | 72 » » return err |
59 » } else { | 73 » } |
60 » » dir, err := ioutil.TempDir("", "watcher") | 74 » defer os.RemoveAll(dir) |
| 75 |
| 76 » errc := make(chan error) |
| 77 |
| 78 » go func() { |
| 79 » » r, err := NewRepo(dir, *repoURL, "") |
61 if err != nil { | 80 if err != nil { |
| 81 errc <- err |
| 82 return |
| 83 } |
| 84 errc <- r.Watch() |
| 85 }() |
| 86 |
| 87 subrepos, err := subrepoList() |
| 88 if err != nil { |
| 89 return err |
| 90 } |
| 91 for _, path := range subrepos { |
| 92 go func(path string) { |
| 93 url := "https://" + path |
| 94 r, err := NewRepo(dir, url, path) |
| 95 if err != nil { |
| 96 errc <- err |
| 97 return |
| 98 } |
| 99 errc <- r.Watch() |
| 100 }(path) |
| 101 } |
| 102 |
| 103 // Must be non-nil. |
| 104 return <-errc |
| 105 } |
| 106 |
| 107 // Repo represents a repository to be watched. |
| 108 type Repo struct { |
| 109 root string // on-disk location of the hg repo |
| 110 path string // base import path for repo (blank for main
repo) |
| 111 commits map[string]*Commit // keyed by full commit hash (40 lowercase h
ex digits) |
| 112 branches map[string]*Branch // keyed by branch name, eg "release-branch.
go1.3" (or empty for default) |
| 113 } |
| 114 |
| 115 // NewRepo checks out a new instance of the Mercurial repository |
| 116 // specified by url to a new directory inside dir. |
| 117 // The path argument is the base import path of the repository, |
| 118 // and should be empty for the main Go repo. |
| 119 func NewRepo(dir, url, path string) (*Repo, error) { |
| 120 r := &Repo{ |
| 121 path: path, |
| 122 root: filepath.Join(dir, filepath.Base(path)), |
| 123 } |
| 124 |
| 125 r.logf("cloning %v", url) |
| 126 cmd := exec.Command("hg", "clone", url, r.root) |
| 127 if out, err := cmd.CombinedOutput(); err != nil { |
| 128 return nil, fmt.Errorf("%v\n\n%s", err, out) |
| 129 } |
| 130 |
| 131 r.logf("loading commit log") |
| 132 if err := r.loadCommits(); err != nil { |
| 133 return nil, err |
| 134 } |
| 135 if err := r.findBranches(); err != nil { |
| 136 return nil, err |
| 137 } |
| 138 |
| 139 r.logf("found %v branches among %v commits\n", len(r.branches), len(r.co
mmits)) |
| 140 return r, nil |
| 141 } |
| 142 |
| 143 // Watch continuously runs "hg pull" in the repo, checks for |
| 144 // new commits, and posts any new commits to the dashboard. |
| 145 // It only returns a non-nil error. |
| 146 func (r *Repo) Watch() error { |
| 147 for { |
| 148 if err := hgPull(r.root); err != nil { |
62 return err | 149 return err |
63 } | 150 } |
64 » » defer os.RemoveAll(dir) | 151 » » if err := r.update(); err != nil { |
65 » » goroot = filepath.Join(dir, "go") | 152 » » » return err |
66 » » cmd := exec.Command("hg", "clone", *repo, goroot) | 153 » » } |
67 » » if out, err := cmd.CombinedOutput(); err != nil { | 154 » » for _, b := range r.branches { |
68 » » » return fmt.Errorf("%v\n\n%s", err, out) | 155 » » » if err := r.postNewCommits(b); err != nil { |
69 » » } | 156 » » » » return err |
70 » } | 157 » » » } |
71 | 158 » » } |
72 » commits, err := allCommits(goroot) | 159 » » time.Sleep(*pollInterval) |
73 » if err != nil { | 160 » } |
74 » » return err | 161 } |
75 » } | 162 |
76 | 163 func (r *Repo) logf(format string, args ...interface{}) { |
77 » branches := make(map[string]*Branch) | 164 » p := "go" |
78 » for _, c := range commits { | 165 » if r.path != "" { |
| 166 » » p = path.Base(r.path) |
| 167 » } |
| 168 » log.Printf(p+": "+format, args...) |
| 169 } |
| 170 |
| 171 // postNewCommits looks for unseen commits on the specified branch and |
| 172 // posts them to the dashboard. |
| 173 func (r *Repo) postNewCommits(b *Branch) error { |
| 174 » if b.Head == b.LastSeen { |
| 175 » » return nil |
| 176 » } |
| 177 » c := b.LastSeen |
| 178 » if c == nil { |
| 179 » » // Haven't seen any: find the commit that this branch forked fro
m. |
| 180 » » for c := b.Head; c.Branch == b.Name; c = c.parent { |
| 181 » » } |
| 182 » } |
| 183 » // Add unseen commits on this branch, working forward from last seen. |
| 184 » for c.children != nil { |
| 185 » » // Find the next commit on this branch. |
| 186 » » var next *Commit |
| 187 » » for _, c2 := range c.children { |
| 188 » » » if c2.Branch != b.Name { |
| 189 » » » » continue |
| 190 » » » } |
| 191 » » » if next != nil { |
| 192 » » » » // Shouldn't happen, but be paranoid. |
| 193 » » » » return fmt.Errorf("found multiple children of %v
on branch %q: %v and %v", c, b.Name, next, c2) |
| 194 » » » } |
| 195 » » » next = c2 |
| 196 » » } |
| 197 » » if next == nil { |
| 198 » » » // No more children on this branch, bail. |
| 199 » » » break |
| 200 » » } |
| 201 » » // Found it. |
| 202 » » c = next |
| 203 |
| 204 » » if err := r.postCommit(c); err != nil { |
| 205 » » » return err |
| 206 » » } |
| 207 » » b.LastSeen = c |
| 208 » } |
| 209 » return nil |
| 210 } |
| 211 |
| 212 // postCommit sends a commit to the build dashboard. |
| 213 func (r *Repo) postCommit(c *Commit) error { |
| 214 » r.logf("sending commit to dashboard: %v", c) |
| 215 |
| 216 » t, err := time.Parse(time.RFC3339, c.Date) |
| 217 » if err != nil { |
| 218 » » return err |
| 219 » } |
| 220 » dc := struct { |
| 221 » » PackagePath string // (empty for main repo commits) |
| 222 » » Hash string |
| 223 » » ParentHash string |
| 224 |
| 225 » » User string |
| 226 » » Desc string |
| 227 » » Time time.Time |
| 228 |
| 229 » » NeedsBenchmarking bool |
| 230 » }{ |
| 231 » » PackagePath: r.path, |
| 232 » » Hash: c.Hash, |
| 233 » » ParentHash: c.Parent, |
| 234 |
| 235 » » User: c.Author, |
| 236 » » Desc: c.Desc, |
| 237 » » Time: t, |
| 238 |
| 239 » » NeedsBenchmarking: c.NeedsBenchmarking(), |
| 240 » } |
| 241 » b, err := json.Marshal(dc) |
| 242 » if err != nil { |
| 243 » » return err |
| 244 » } |
| 245 |
| 246 » u := *dashboard + "commit?version=2&key=" + dashboardKey |
| 247 » resp, err := http.Post(u, "text/json", bytes.NewReader(b)) |
| 248 » if err != nil { |
| 249 » » return err |
| 250 » } |
| 251 » if resp.StatusCode != 200 { |
| 252 » » return fmt.Errorf("status: %v", resp.Status) |
| 253 » } |
| 254 » return nil |
| 255 } |
| 256 |
| 257 // loadCommits runs "hg log" and populates the Repo's commit map. |
| 258 func (r *Repo) loadCommits() error { |
| 259 » log, err := hgLog(r.root) |
| 260 » if err != nil { |
| 261 » » return err |
| 262 » } |
| 263 » r.commits = make(map[string]*Commit) |
| 264 » for _, c := range log { |
| 265 » » r.commits[c.Hash] = c |
| 266 » } |
| 267 » for _, c := range r.commits { |
| 268 » » if p, ok := r.commits[c.Parent]; ok { |
| 269 » » » c.parent = p |
| 270 » » » p.children = append(p.children, c) |
| 271 » » } |
| 272 » } |
| 273 » return nil |
| 274 } |
| 275 |
| 276 // findBranches finds branch heads in the Repo's commit map |
| 277 // and populates its branch map. |
| 278 func (r *Repo) findBranches() error { |
| 279 » r.branches = make(map[string]*Branch) |
| 280 » for _, c := range r.commits { |
79 if c.children == nil { | 281 if c.children == nil { |
80 if !validHead(c) { | 282 if !validHead(c) { |
81 continue | 283 continue |
82 } | 284 } |
83 » » » seen, err := lastSeen(commits, c.Hash) | 285 » » » seen, err := r.lastSeen(c.Hash) |
84 if err != nil { | 286 if err != nil { |
85 return err | 287 return err |
86 } | 288 } |
87 b := &Branch{Name: c.Branch, Head: c, LastSeen: seen} | 289 b := &Branch{Name: c.Branch, Head: c, LastSeen: seen} |
88 » » » branches[c.Branch] = b | 290 » » » r.branches[c.Branch] = b |
89 » » » log.Printf("found branch: %v", b) | 291 » » » r.logf("found branch: %v", b) |
90 » » } | 292 » » } |
91 » } | 293 » } |
92 » log.Printf("found %v branches among %v commits\n", len(branches), len(co
mmits)) | 294 » return nil |
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 } | 295 } |
136 | 296 |
137 // validHead reports whether the specified commit should be considered a branch | 297 // validHead reports whether the specified commit should be considered a branch |
138 // head. It considers pre-go1 branches and certain specific commits as invalid. | 298 // head. It considers pre-go1 branches and certain specific commits as invalid. |
139 func validHead(c *Commit) bool { | 299 func validHead(c *Commit) bool { |
| 300 // Pre Go-1 releases branches are irrelevant. |
140 if strings.HasPrefix(c.Branch, "release-branch.r") { | 301 if strings.HasPrefix(c.Branch, "release-branch.r") { |
141 » » return true | 302 » » return false |
142 } | 303 } |
143 // Not sure why these revisions have no child commits, | 304 // Not sure why these revisions have no child commits, |
144 // but they're old so let's just ignore them. | 305 // but they're old so let's just ignore them. |
145 » return c.Hash == "b59f4ff1b51094314f735a4d57a2b8f06cfadf15" || | 306 » if c.Hash == "b59f4ff1b51094314f735a4d57a2b8f06cfadf15" || |
146 » » c.Hash == "fc75f13840b896e82b9fa6165cf705fbacaf019c" | 307 » » c.Hash == "fc75f13840b896e82b9fa6165cf705fbacaf019c" { |
| 308 » » return false |
| 309 » } |
| 310 » // All other branches are valid. |
| 311 » return true |
| 312 } |
| 313 |
| 314 // update runs "hg pull" in the specified reporoot, |
| 315 // looks for new commits and branches, |
| 316 // and updates the comits and branches maps. |
| 317 func (r *Repo) update() error { |
| 318 » // TODO(adg): detect new branches with "hg branches". |
| 319 |
| 320 » // Check each branch for new commits. |
| 321 » for _, b := range r.branches { |
| 322 |
| 323 » » // Find all commits on this branch from known head. |
| 324 » » // The logic of this function assumes that "hg log $HASH:" |
| 325 » » // returns hashes in the order they were committed (parent first
). |
| 326 » » bname := b.Name |
| 327 » » if bname == "" { |
| 328 » » » bname = "default" |
| 329 » » } |
| 330 » » log, err := hgLog(r.root, "-r", b.Head.Hash+":", "-b", bname) |
| 331 » » if err != nil { |
| 332 » » » return err |
| 333 » » } |
| 334 |
| 335 » » // Add unknown commits to r.commits, and update branch head. |
| 336 » » for _, c := range log { |
| 337 » » » // Ignore if we already know this commit. |
| 338 » » » if _, ok := r.commits[c.Hash]; ok { |
| 339 » » » » continue |
| 340 » » » } |
| 341 » » » r.logf("found new commit %v", c) |
| 342 |
| 343 » » » // Sanity check that we're looking at a commit on this b
ranch. |
| 344 » » » if c.Branch != b.Name { |
| 345 » » » » return fmt.Errorf("hg log gave us a commit from
wrong branch: want %q, got %q", b.Name, c.Branch) |
| 346 » » » } |
| 347 |
| 348 » » » // Find parent commit. |
| 349 » » » p, ok := r.commits[c.Parent] |
| 350 » » » if !ok { |
| 351 » » » » return fmt.Errorf("can't find parent hash %q for
%v", c.Parent, c) |
| 352 » » » } |
| 353 |
| 354 » » » // Link parent and child Commits. |
| 355 » » » c.parent = p |
| 356 » » » p.children = append(p.children, c) |
| 357 |
| 358 » » » // Update branch head. |
| 359 » » » b.Head = c |
| 360 |
| 361 » » » // Add new commit to map. |
| 362 » » » r.commits[c.Hash] = c |
| 363 » » } |
| 364 » } |
| 365 |
| 366 » return nil |
147 } | 367 } |
148 | 368 |
149 // lastSeen finds the most recent commit the dashboard has seen, | 369 // lastSeen finds the most recent commit the dashboard has seen, |
150 // starting at the specified head. | 370 // starting at the specified head. If the dashboard hasn't seen |
151 func lastSeen(commits map[string]*Commit, head string) (*Commit, error) { | 371 // any of the commits from head to the beginning, it returns nil. |
152 » h, ok := commits[head] | 372 func (r *Repo) lastSeen(head string) (*Commit, error) { |
| 373 » h, ok := r.commits[head] |
153 if !ok { | 374 if !ok { |
154 return nil, fmt.Errorf("lastSeen: can't find %q in commits", hea
d) | 375 return nil, fmt.Errorf("lastSeen: can't find %q in commits", hea
d) |
155 } | 376 } |
156 | 377 |
157 var s []*Commit | 378 var s []*Commit |
158 for c := h; c != nil; c = c.parent { | 379 for c := h; c != nil; c = c.parent { |
159 s = append(s, c) | 380 s = append(s, c) |
160 » » if c.Hash == dashboardStart { | 381 » » if r.path == "" && c.Hash == dashboardStart { |
161 break | 382 break |
162 } | 383 } |
163 } | 384 } |
164 | 385 |
165 for _, c := range s { | 386 for _, c := range s { |
166 » » u := *dashboard + "commit?hash=" + c.Hash | 387 » » v := url.Values{"hash": {c.Hash}, "packagePath": {r.path}} |
| 388 » » u := *dashboard + "commit?" + v.Encode() |
167 r, err := http.Get(u) | 389 r, err := http.Get(u) |
168 if err != nil { | 390 if err != nil { |
169 return nil, err | 391 return nil, err |
170 } | 392 } |
171 var resp struct { | 393 var resp struct { |
172 Error string | 394 Error string |
173 } | 395 } |
174 err = json.NewDecoder(r.Body).Decode(&resp) | 396 err = json.NewDecoder(r.Body).Decode(&resp) |
175 r.Body.Close() | 397 r.Body.Close() |
176 if err != nil { | 398 if err != nil { |
177 return nil, err | 399 return nil, err |
178 } | 400 } |
179 » » if resp.Error == "" { | 401 » » switch resp.Error { |
| 402 » » case "": |
| 403 » » » // Found one. |
180 return c, nil | 404 return c, nil |
181 » » } | 405 » » case "Commit not found": |
182 » } | 406 » » » // Commit not found, keep looking for earlier commits. |
| 407 » » » continue |
| 408 » » default: |
| 409 » » » return nil, fmt.Errorf("dashboard: %v", resp.Error) |
| 410 » » } |
| 411 » } |
| 412 |
| 413 » // Dashboard saw no commits. |
183 return nil, nil | 414 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 } | 415 } |
330 | 416 |
331 // hgLog runs "hg log" with the supplied arguments | 417 // hgLog runs "hg log" with the supplied arguments |
332 // and parses the output into Commit values. | 418 // and parses the output into Commit values. |
333 func hgLog(dir string, args ...string) ([]*Commit, error) { | 419 func hgLog(dir string, args ...string) ([]*Commit, error) { |
334 args = append([]string{"log", "--template", xmlLogTemplate}, args...) | 420 args = append([]string{"log", "--template", xmlLogTemplate}, args...) |
335 cmd := exec.Command("hg", args...) | 421 cmd := exec.Command("hg", args...) |
336 cmd.Dir = dir | 422 cmd.Dir = dir |
337 out, err := cmd.CombinedOutput() | 423 out, err := cmd.CombinedOutput() |
338 if err != nil { | 424 if err != nil { |
339 return nil, err | 425 return nil, err |
340 } | 426 } |
341 | 427 |
342 // We have a commit with description that contains 0x1b byte. | 428 // We have a commit with description that contains 0x1b byte. |
343 // Mercurial does not escape it, but xml.Unmarshal does not accept it. | 429 // Mercurial does not escape it, but xml.Unmarshal does not accept it. |
344 out = bytes.Replace(out, []byte{0x1b}, []byte{'?'}, -1) | 430 out = bytes.Replace(out, []byte{0x1b}, []byte{'?'}, -1) |
345 | 431 |
| 432 xr := io.MultiReader( |
| 433 strings.NewReader("<Top>"), |
| 434 bytes.NewReader(out), |
| 435 strings.NewReader("</Top>"), |
| 436 ) |
346 var logStruct struct { | 437 var logStruct struct { |
347 Log []*Commit | 438 Log []*Commit |
348 } | 439 } |
349 » err = xml.Unmarshal([]byte("<Top>"+string(out)+"</Top>"), &logStruct) | 440 » err = xml.NewDecoder(xr).Decode(&logStruct) |
350 if err != nil { | 441 if err != nil { |
351 return nil, err | 442 return nil, err |
352 } | 443 } |
353 return logStruct.Log, nil | 444 return logStruct.Log, nil |
354 } | 445 } |
355 | 446 |
356 // hgPull runs "hg pull" in the specified directory. | 447 // hgPull runs "hg pull" in the specified directory. |
| 448 // It tries three times, just in case it failed because of a transient error. |
357 func hgPull(dir string) error { | 449 func hgPull(dir string) error { |
358 » cmd := exec.Command("hg", "pull") | 450 » var err error |
359 » cmd.Dir = dir | 451 » for tries := 0; tries < 3; tries++ { |
360 » if out, err := cmd.CombinedOutput(); err != nil { | 452 » » time.Sleep(time.Duration(tries) * 5 * time.Second) // Linear bac
k-off. |
361 » » return fmt.Errorf("%v\n\n%s", err, out) | 453 » » cmd := exec.Command("hg", "pull") |
362 » } | 454 » » cmd.Dir = dir |
363 » return nil | 455 » » if out, e := cmd.CombinedOutput(); err != nil { |
| 456 » » » e = fmt.Errorf("%v\n\n%s", e, out) |
| 457 » » » log.Printf("hg pull error %v: %v", dir, e) |
| 458 » » » if err == nil { |
| 459 » » » » err = e |
| 460 » » » } |
| 461 » » » continue |
| 462 » » } |
| 463 » » return nil |
| 464 » } |
| 465 » return err |
364 } | 466 } |
365 | 467 |
366 // Branch represents a Mercurial branch. | 468 // Branch represents a Mercurial branch. |
367 type Branch struct { | 469 type Branch struct { |
368 Name string | 470 Name string |
369 Head *Commit | 471 Head *Commit |
370 LastSeen *Commit // the last commit posted to the dashboard | 472 LastSeen *Commit // the last commit posted to the dashboard |
371 } | 473 } |
372 | 474 |
373 func (b *Branch) String() string { | 475 func (b *Branch) String() string { |
374 return fmt.Sprintf("%q(Head: %v LastSeen: %v)", b.Name, b.Head, b.LastSe
en) | 476 return fmt.Sprintf("%q(Head: %v LastSeen: %v)", b.Name, b.Head, b.LastSe
en) |
375 } | 477 } |
376 | 478 |
377 // Commit represents a single Mercurial revision. | 479 // Commit represents a single Mercurial revision. |
378 type Commit struct { | 480 type Commit struct { |
379 Hash string | 481 Hash string |
380 Author string | 482 Author string |
381 Date string | 483 Date string |
382 » Desc string | 484 » Desc string // Plain text, first linefeed-terminated line is a short d
escription. |
383 Parent string | 485 Parent string |
384 Branch string | 486 Branch string |
385 Files string | 487 Files string |
386 | 488 |
387 // For walking the graph. | 489 // For walking the graph. |
388 parent *Commit | 490 parent *Commit |
389 children []*Commit | 491 children []*Commit |
390 } | 492 } |
391 | 493 |
392 func (c *Commit) String() string { | 494 func (c *Commit) String() string { |
393 return fmt.Sprintf("%v(%q)", c.Hash, strings.SplitN(c.Desc, "\n", 2)[0]) | 495 return fmt.Sprintf("%v(%q)", c.Hash, strings.SplitN(c.Desc, "\n", 2)[0]) |
| 496 } |
| 497 |
| 498 // NeedsBenchmarking reports whether the Commit needs benchmarking. |
| 499 func (c *Commit) NeedsBenchmarking() bool { |
| 500 // Do not benchmark branch commits, they are usually not interesting |
| 501 // and fall out of the trunk succession. |
| 502 if c.Branch != "" { |
| 503 return false |
| 504 } |
| 505 // Do not benchmark commits that do not touch source files (e.g. CONTRIB
UTORS). |
| 506 for _, f := range strings.Split(c.Files, " ") { |
| 507 if (strings.HasPrefix(f, "include") || strings.HasPrefix(f, "src
")) && |
| 508 !strings.HasSuffix(f, "_test.go") && !strings.Contains(f
, "testdata") { |
| 509 return true |
| 510 } |
| 511 } |
| 512 return false |
394 } | 513 } |
395 | 514 |
396 // xmlLogTemplate is a template to pass to Mercurial to make | 515 // xmlLogTemplate is a template to pass to Mercurial to make |
397 // hg log print the log in valid XML for parsing with xml.Unmarshal. | 516 // 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: | 517 // Can not escape branches and files, because it crashes python with: |
399 // AttributeError: 'NoneType' object has no attribute 'replace' | 518 // AttributeError: 'NoneType' object has no attribute 'replace' |
400 const xmlLogTemplate = ` | 519 const xmlLogTemplate = ` |
401 <Log> | 520 <Log> |
402 <Hash>{node|escape}</Hash> | 521 <Hash>{node|escape}</Hash> |
403 <Parent>{p1node}</Parent> | 522 <Parent>{p1node}</Parent> |
(...skipping 15 matching lines...) Expand all Loading... |
419 return os.Getenv("HOME") | 538 return os.Getenv("HOME") |
420 } | 539 } |
421 | 540 |
422 func readKey() (string, error) { | 541 func readKey() (string, error) { |
423 c, err := ioutil.ReadFile(*keyFile) | 542 c, err := ioutil.ReadFile(*keyFile) |
424 if err != nil { | 543 if err != nil { |
425 return "", err | 544 return "", err |
426 } | 545 } |
427 return string(bytes.TrimSpace(bytes.SplitN(c, []byte("\n"), 2)[0])), nil | 546 return string(bytes.TrimSpace(bytes.SplitN(c, []byte("\n"), 2)[0])), nil |
428 } | 547 } |
| 548 |
| 549 // subrepoList fetches a list of sub-repositories from the dashboard |
| 550 // and returns them as a slice of base import paths. |
| 551 // Eg, []string{"code.google.com/p/go.tools", "code.google.com/p/go.net"}. |
| 552 func subrepoList() ([]string, error) { |
| 553 r, err := http.Get(*dashboard + "packages?kind=subrepo") |
| 554 if err != nil { |
| 555 return nil, err |
| 556 } |
| 557 var resp struct { |
| 558 Response []struct { |
| 559 Path string |
| 560 } |
| 561 Error string |
| 562 } |
| 563 err = json.NewDecoder(r.Body).Decode(&resp) |
| 564 r.Body.Close() |
| 565 if err != nil { |
| 566 return nil, err |
| 567 } |
| 568 if resp.Error != "" { |
| 569 return nil, errors.New(resp.Error) |
| 570 } |
| 571 var pkgs []string |
| 572 for _, r := range resp.Response { |
| 573 pkgs = append(pkgs, r.Path) |
| 574 } |
| 575 return pkgs, nil |
| 576 } |
| 577 |
| 578 // checkHgVersion checks whether the installed version of hg supports the |
| 579 // template features we need. (May not be precise.) |
| 580 func checkHgVersion() error { |
| 581 out, err := exec.Command("hg", "help", "templates").CombinedOutput() |
| 582 if err != nil { |
| 583 return fmt.Errorf("error running hg help templates: %v\n\n%s", e
rr, out) |
| 584 } |
| 585 if !bytes.Contains(out, []byte("p1node")) { |
| 586 return errors.New("installed hg doesn't support 'p1node' templat
e keyword; please upgrade") |
| 587 } |
| 588 return nil |
| 589 } |
LEFT | RIGHT |