Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(237)

Side by Side Diff: dashboard/watcher/watcher.go

Issue 155730043: code review 155730043: go.tools/dashboard/watcher: commit watcher rewrite (Closed)
Patch Set: Created 10 years, 6 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b