LEFT | RIGHT |
1 // Copyright 2011 The Go Authors. All rights reserved. | 1 // Copyright 2011 The Go Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style | 2 // Use of this source code is governed by a BSD-style |
3 // license that can be found in the LICENSE file. | 3 // license that can be found in the LICENSE file. |
4 | 4 |
5 package build | 5 package build |
6 | 6 |
7 import ( | 7 import ( |
8 "appengine" | 8 "appengine" |
9 "appengine/datastore" | 9 "appengine/datastore" |
10 "bytes" | 10 "bytes" |
11 "compress/gzip" | 11 "compress/gzip" |
12 "crypto/sha1" | 12 "crypto/sha1" |
13 "fmt" | 13 "fmt" |
14 "http" | 14 "http" |
15 "io" | 15 "io" |
16 "json" | 16 "json" |
17 "os" | 17 "os" |
18 "sort" | |
19 "strings" | 18 "strings" |
20 "template" | |
21 ) | 19 ) |
22 | 20 |
23 const commitsPerPage = 20 | 21 const commitsPerPage = 20 |
24 | 22 |
25 // A Package describes a package that is listed on the dashboard. | 23 // A Package describes a package that is listed on the dashboard. |
26 type Package struct { | 24 type Package struct { |
27 Name string | 25 Name string |
28 Path string // (empty for the main Go tree) | 26 Path string // (empty for the main Go tree) |
29 NextNum int // Num of the next head Commit | 27 NextNum int // Num of the next head Commit |
30 } | 28 } |
31 | 29 |
| 30 func (p *Package) String() string { |
| 31 return fmt.Sprintf("%s: %q", p.Path, p.Name) |
| 32 } |
| 33 |
32 func (p *Package) Key(c appengine.Context) *datastore.Key { | 34 func (p *Package) Key(c appengine.Context) *datastore.Key { |
33 key := p.Path | 35 key := p.Path |
34 if key == "" { | 36 if key == "" { |
35 key = "go" | 37 key = "go" |
36 } | 38 } |
37 return datastore.NewKey(c, "Package", key, 0, nil) | 39 return datastore.NewKey(c, "Package", key, 0, nil) |
38 } | 40 } |
39 | 41 |
| 42 // LastCommit returns the most recent Commit for this Package. |
| 43 func (p *Package) LastCommit(c appengine.Context) (*Commit, os.Error) { |
| 44 var commits []*Commit |
| 45 _, err := datastore.NewQuery("Commit"). |
| 46 Ancestor(p.Key(c)). |
| 47 Order("-Time"). |
| 48 Limit(1). |
| 49 GetAll(c, &commits) |
| 50 if err != nil { |
| 51 return nil, err |
| 52 } |
| 53 if len(commits) != 1 { |
| 54 return nil, datastore.ErrNoSuchEntity |
| 55 } |
| 56 return commits[0], nil |
| 57 } |
| 58 |
| 59 // GetPackage fetches a Package by path from the datastore. |
40 func GetPackage(c appengine.Context, path string) (*Package, os.Error) { | 60 func GetPackage(c appengine.Context, path string) (*Package, os.Error) { |
41 p := &Package{Path: path} | 61 p := &Package{Path: path} |
42 err := datastore.Get(c, p.Key(c), p) | 62 err := datastore.Get(c, p.Key(c), p) |
43 if err == datastore.ErrNoSuchEntity { | 63 if err == datastore.ErrNoSuchEntity { |
44 return nil, fmt.Errorf("package %q not found", path) | 64 return nil, fmt.Errorf("package %q not found", path) |
45 } | 65 } |
46 return p, err | 66 return p, err |
47 } | 67 } |
48 | 68 |
49 // A Commit describes an individual commit in a package. | 69 // A Commit describes an individual commit in a package. |
50 // | 70 // |
51 // Each Commit entity is a descendant of its associated Package entity. | 71 // Each Commit entity is a descendant of its associated Package entity. |
52 // In other words, all Commits with the same PackagePath belong to the same | 72 // In other words, all Commits with the same PackagePath belong to the same |
53 // datastore entity group. | 73 // datastore entity group. |
54 type Commit struct { | 74 type Commit struct { |
55 PackagePath string // (empty for Go commits) | 75 PackagePath string // (empty for Go commits) |
56 Hash string | 76 Hash string |
57 ParentHash string | 77 ParentHash string |
58 Num int // Internal monotonic counter unique to this package. | 78 Num int // Internal monotonic counter unique to this package. |
59 | 79 |
60 User string | 80 User string |
61 Desc string `datastore:",noindex"` | 81 Desc string `datastore:",noindex"` |
62 Time datastore.Time | 82 Time datastore.Time |
63 | 83 |
64 » // Result is the Data string of each build Result for this Commit. | 84 » // ResultData is the Data string of each build Result for this Commit. |
65 // For non-Go commits, only the Results for the current Go tip, weekly, | 85 // For non-Go commits, only the Results for the current Go tip, weekly, |
66 // and release Tags are stored here. This is purely de-normalized data. | 86 // and release Tags are stored here. This is purely de-normalized data. |
67 // The complete data set is stored in Result entities. | 87 // The complete data set is stored in Result entities. |
68 » Result []string `datastore:",noindex"` | 88 » ResultData []string `datastore:",noindex"` |
69 } | 89 } |
70 | 90 |
71 func (com *Commit) Key(c appengine.Context) *datastore.Key { | 91 func (com *Commit) Key(c appengine.Context) *datastore.Key { |
72 if com.Hash == "" { | 92 if com.Hash == "" { |
73 panic("tried Key on Commit with empty Hash") | 93 panic("tried Key on Commit with empty Hash") |
74 } | 94 } |
75 p := Package{Path: com.PackagePath} | 95 p := Package{Path: com.PackagePath} |
76 key := com.PackagePath + "|" + com.Hash | 96 key := com.PackagePath + "|" + com.Hash |
77 return datastore.NewKey(c, "Commit", key, 0, p.Key(c)) | 97 return datastore.NewKey(c, "Commit", key, 0, p.Key(c)) |
78 } | 98 } |
79 | 99 |
80 func (c *Commit) Valid() os.Error { | 100 func (c *Commit) Valid() os.Error { |
81 if !validHash(c.Hash) { | 101 if !validHash(c.Hash) { |
82 return os.NewError("invalid Hash") | 102 return os.NewError("invalid Hash") |
83 } | 103 } |
84 if c.ParentHash != "" && !validHash(c.ParentHash) { // empty is OK | 104 if c.ParentHash != "" && !validHash(c.ParentHash) { // empty is OK |
85 return os.NewError("invalid ParentHash") | 105 return os.NewError("invalid ParentHash") |
86 } | 106 } |
87 return nil | 107 return nil |
88 } | 108 } |
89 | 109 |
90 // AddResult adds the denormalized Reuslt data to the Commit's Result field. | 110 // AddResult adds the denormalized Reuslt data to the Commit's Result field. |
91 // It must be called from inside a datastore transaction. | 111 // It must be called from inside a datastore transaction. |
92 func (com *Commit) AddResult(c appengine.Context, r *Result) os.Error { | 112 func (com *Commit) AddResult(c appengine.Context, r *Result) os.Error { |
93 if err := datastore.Get(c, com.Key(c), com); err != nil { | 113 if err := datastore.Get(c, com.Key(c), com); err != nil { |
94 return err | 114 return err |
95 } | 115 } |
96 » com.Result = append(com.Result, r.Data()) | 116 » com.ResultData = append(com.ResultData, r.Data()) |
97 _, err := datastore.Put(c, com.Key(c), com) | 117 _, err := datastore.Put(c, com.Key(c), com) |
98 return err | 118 return err |
99 } | 119 } |
100 | 120 |
101 func (c *Commit) GetResult(builder, goHash string) *Result { | 121 // Result returns the build Result for this Commit for the given builder/goHash. |
102 » for _, r := range c.Result { | 122 func (c *Commit) Result(builder, goHash string) *Result { |
| 123 » for _, r := range c.ResultData { |
103 p := strings.SplitN(r, "|", 4) | 124 p := strings.SplitN(r, "|", 4) |
104 if len(p) != 4 || p[0] != builder || p[3] != goHash { | 125 if len(p) != 4 || p[0] != builder || p[3] != goHash { |
105 continue | 126 continue |
106 } | 127 } |
107 » » return &Result{ | 128 » » return partsToHash(c, p) |
108 » » » Builder: p[0], | |
109 » » » Hash: c.Hash, | |
110 » » » PackagePath: c.PackagePath, | |
111 » » » GoHash: p[3], | |
112 » » » OK: p[1] == "true", | |
113 » » » LogHash: p[2], | |
114 » » } | |
115 } | 129 } |
116 return nil | 130 return nil |
| 131 } |
| 132 |
| 133 // Results returns the build Results for this Commit for the given goHash. |
| 134 func (c *Commit) Results(goHash string) (results []*Result) { |
| 135 for _, r := range c.ResultData { |
| 136 p := strings.SplitN(r, "|", 4) |
| 137 if len(p) != 4 || p[3] != goHash { |
| 138 continue |
| 139 } |
| 140 results = append(results, partsToHash(c, p)) |
| 141 } |
| 142 return |
| 143 } |
| 144 |
| 145 // partsToHash converts a Commit and ResultData substrings to a Result. |
| 146 func partsToHash(c *Commit, p []string) *Result { |
| 147 return &Result{ |
| 148 Builder: p[0], |
| 149 Hash: c.Hash, |
| 150 PackagePath: c.PackagePath, |
| 151 GoHash: p[3], |
| 152 OK: p[1] == "true", |
| 153 LogHash: p[2], |
| 154 } |
117 } | 155 } |
118 | 156 |
119 // A Result describes a build result for a Commit on an OS/architecture. | 157 // A Result describes a build result for a Commit on an OS/architecture. |
120 // | 158 // |
121 // Each Result entity is a descendant of its associated Commit entity. | 159 // Each Result entity is a descendant of its associated Commit entity. |
122 type Result struct { | 160 type Result struct { |
123 Builder string // "arch-os[-note]" | 161 Builder string // "arch-os[-note]" |
124 Hash string | 162 Hash string |
125 PackagePath string // (empty for Go commits) | 163 PackagePath string // (empty for Go commits) |
126 | 164 |
127 // The Go Commit this was built against (empty for Go commits). | 165 // The Go Commit this was built against (empty for Go commits). |
128 GoHash string | 166 GoHash string |
129 | 167 |
130 OK bool | 168 OK bool |
131 Log []byte `datastore:"-"` // for JSON unmarshaling | 169 Log []byte `datastore:"-"` // for JSON unmarshaling |
132 LogHash string `datastore:",noindex"` // Key to the Log record. | 170 LogHash string `datastore:",noindex"` // Key to the Log record. |
133 } | 171 } |
134 | 172 |
135 func (r *Result) Key(c appengine.Context) *datastore.Key { | 173 func (r *Result) Key(c appengine.Context) *datastore.Key { |
136 p := Package{Path: r.PackagePath} | 174 p := Package{Path: r.PackagePath} |
137 key := r.Builder + "|" + r.PackagePath + "|" + r.Hash + "|" + r.GoHash | 175 key := r.Builder + "|" + r.PackagePath + "|" + r.Hash + "|" + r.GoHash |
138 return datastore.NewKey(c, "Result", key, 0, p.Key(c)) | 176 return datastore.NewKey(c, "Result", key, 0, p.Key(c)) |
139 } | 177 } |
140 | 178 |
141 func (r *Result) Data() string { | |
142 return fmt.Sprintf("%v|%v|%v|%v", r.Builder, r.OK, r.LogHash, r.GoHash) | |
143 } | |
144 | |
145 func (r *Result) Valid() os.Error { | 179 func (r *Result) Valid() os.Error { |
146 if !validHash(r.Hash) { | 180 if !validHash(r.Hash) { |
147 return os.NewError("invalid Hash") | 181 return os.NewError("invalid Hash") |
148 } | 182 } |
149 if r.PackagePath != "" && !validHash(r.GoHash) { | 183 if r.PackagePath != "" && !validHash(r.GoHash) { |
150 return os.NewError("invalid GoHash") | 184 return os.NewError("invalid GoHash") |
151 } | 185 } |
152 return nil | 186 return nil |
| 187 } |
| 188 |
| 189 // Data returns the Result in string format |
| 190 // to be stored in Commit's ResultData field. |
| 191 func (r *Result) Data() string { |
| 192 return fmt.Sprintf("%v|%v|%v|%v", r.Builder, r.OK, r.LogHash, r.GoHash) |
153 } | 193 } |
154 | 194 |
155 // A Log is a gzip-compressed log file stored under the SHA1 hash of the | 195 // A Log is a gzip-compressed log file stored under the SHA1 hash of the |
156 // uncompressed log text. | 196 // uncompressed log text. |
157 type Log struct { | 197 type Log struct { |
158 CompressedLog []byte | 198 CompressedLog []byte |
159 } | 199 } |
160 | 200 |
161 func PutLog(c appengine.Context, text []byte) (hash string, err os.Error) { | 201 func PutLog(c appengine.Context, text []byte) (hash string, err os.Error) { |
162 h := sha1.New() | 202 h := sha1.New() |
(...skipping 15 matching lines...) Expand all Loading... |
178 Name string // the tag itself (for example: "release.r60") | 218 Name string // the tag itself (for example: "release.r60") |
179 Hash string | 219 Hash string |
180 } | 220 } |
181 | 221 |
182 func (t *Tag) Key(c appengine.Context) *datastore.Key { | 222 func (t *Tag) Key(c appengine.Context) *datastore.Key { |
183 p := &Package{} | 223 p := &Package{} |
184 return datastore.NewKey(c, "Tag", t.Kind, 0, p.Key(c)) | 224 return datastore.NewKey(c, "Tag", t.Kind, 0, p.Key(c)) |
185 } | 225 } |
186 | 226 |
187 func (t *Tag) Valid() os.Error { | 227 func (t *Tag) Valid() os.Error { |
188 » if t.Kind != "weekly" || t.Kind != "release" || t.Kind != "tip" { | 228 » if t.Kind != "weekly" && t.Kind != "release" && t.Kind != "tip" { |
189 return os.NewError("invalid Kind") | 229 return os.NewError("invalid Kind") |
190 } | 230 } |
191 if !validHash(t.Hash) { | 231 if !validHash(t.Hash) { |
192 return os.NewError("invalid Hash") | 232 return os.NewError("invalid Hash") |
193 } | 233 } |
194 return nil | 234 return nil |
| 235 } |
| 236 |
| 237 // GetTag fetches a Tag by name from the datastore. |
| 238 func GetTag(c appengine.Context, tag string) (*Tag, os.Error) { |
| 239 t := &Tag{Kind: tag} |
| 240 if err := datastore.Get(c, t.Key(c), t); err != nil { |
| 241 if err == datastore.ErrNoSuchEntity { |
| 242 return nil, os.NewError("tag not found: " + tag) |
| 243 } |
| 244 return nil, err |
| 245 } |
| 246 if err := t.Valid(); err != nil { |
| 247 return nil, err |
| 248 } |
| 249 return t, nil |
195 } | 250 } |
196 | 251 |
197 // commitHandler retrieves commit data or records a new commit. | 252 // commitHandler retrieves commit data or records a new commit. |
198 // | 253 // |
199 // For GET requests it returns a Commit value for the specified | 254 // For GET requests it returns a Commit value for the specified |
200 // packagePath and hash. | 255 // packagePath and hash. |
201 // | 256 // |
202 // For POST requests it reads a JSON-encoded Commit value from the request | 257 // For POST requests it reads a JSON-encoded Commit value from the request |
203 // body and creates a new Commit entity. It also updates the "tip" Tag for | 258 // body and creates a new Commit entity. It also updates the "tip" Tag for |
204 // each new commit at tip. | 259 // each new commit at tip. |
(...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
326 Order("-Num"). | 381 Order("-Num"). |
327 Run(c) | 382 Run(c) |
328 for { | 383 for { |
329 com := new(Commit) | 384 com := new(Commit) |
330 if _, err := t.Next(com); err != nil { | 385 if _, err := t.Next(com); err != nil { |
331 if err == datastore.Done { | 386 if err == datastore.Done { |
332 err = nil | 387 err = nil |
333 } | 388 } |
334 return nil, err | 389 return nil, err |
335 } | 390 } |
336 » » if com.GetResult(builder, goHash) == nil { | 391 » » if com.Result(builder, goHash) == nil { |
337 return com.Hash, nil | 392 return com.Hash, nil |
338 } | 393 } |
339 } | 394 } |
340 panic("unreachable") | 395 panic("unreachable") |
341 } | 396 } |
342 | 397 |
343 // packagesHandler returns a list of the non-Go Packages monitored | 398 // packagesHandler returns a list of the non-Go Packages monitored |
344 // by the dashboard. | 399 // by the dashboard. |
345 func packagesHandler(r *http.Request) (interface{}, os.Error) { | 400 func packagesHandler(r *http.Request) (interface{}, os.Error) { |
346 » c := appengine.NewContext(r) | 401 » return Packages(appengine.NewContext(r)) |
| 402 } |
| 403 |
| 404 // Packages returns all non-Go packages. |
| 405 func Packages(c appengine.Context) ([]*Package, os.Error) { |
347 var pkgs []*Package | 406 var pkgs []*Package |
348 for t := datastore.NewQuery("Package").Run(c); ; { | 407 for t := datastore.NewQuery("Package").Run(c); ; { |
349 pkg := new(Package) | 408 pkg := new(Package) |
350 if _, err := t.Next(pkg); err == datastore.Done { | 409 if _, err := t.Next(pkg); err == datastore.Done { |
351 break | 410 break |
352 } else if err != nil { | 411 } else if err != nil { |
353 return nil, err | 412 return nil, err |
354 } | 413 } |
355 if pkg.Path != "" { | 414 if pkg.Path != "" { |
356 pkgs = append(pkgs, pkg) | 415 pkgs = append(pkgs, pkg) |
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
395 if _, err := datastore.Put(c, res.Key(c), res); err != nil { | 454 if _, err := datastore.Put(c, res.Key(c), res); err != nil { |
396 return err | 455 return err |
397 } | 456 } |
398 // add Result to Commit | 457 // add Result to Commit |
399 com := &Commit{PackagePath: res.PackagePath, Hash: res.Hash} | 458 com := &Commit{PackagePath: res.PackagePath, Hash: res.Hash} |
400 return com.AddResult(c, res) | 459 return com.AddResult(c, res) |
401 } | 460 } |
402 return nil, datastore.RunInTransaction(c, tx, nil) | 461 return nil, datastore.RunInTransaction(c, tx, nil) |
403 } | 462 } |
404 | 463 |
| 464 // logHandler displays log text for a given hash. |
| 465 // It handles paths like "/log/hash". |
405 func logHandler(w http.ResponseWriter, r *http.Request) { | 466 func logHandler(w http.ResponseWriter, r *http.Request) { |
406 c := appengine.NewContext(r) | 467 c := appengine.NewContext(r) |
407 h := r.URL.Path[len("/log/"):] | 468 h := r.URL.Path[len("/log/"):] |
408 k := datastore.NewKey(c, "Log", h, 0, nil) | 469 k := datastore.NewKey(c, "Log", h, 0, nil) |
409 l := new(Log) | 470 l := new(Log) |
410 if err := datastore.Get(c, k, l); err != nil { | 471 if err := datastore.Get(c, k, l); err != nil { |
411 logErr(w, r, err) | 472 logErr(w, r, err) |
412 return | 473 return |
413 } | 474 } |
414 d, err := gzip.NewReader(bytes.NewBuffer(l.CompressedLog)) | 475 d, err := gzip.NewReader(bytes.NewBuffer(l.CompressedLog)) |
415 if err != nil { | 476 if err != nil { |
416 logErr(w, r, err) | 477 logErr(w, r, err) |
417 return | 478 return |
418 } | 479 } |
419 if _, err := io.Copy(w, d); err != nil { | 480 if _, err := io.Copy(w, d); err != nil { |
420 logErr(w, r, err) | 481 logErr(w, r, err) |
421 } | 482 } |
422 } | 483 } |
423 | 484 |
424 var uiTemplate = template.Must(template.ParseFile("build/ui.html")) | |
425 | |
426 func uiHandler(w http.ResponseWriter, r *http.Request) { | |
427 c := appengine.NewContext(r) | |
428 | |
429 p := &Package{} | |
430 q := datastore.NewQuery("Commit"). | |
431 Ancestor(p.Key(c)). | |
432 Order("-Time"). | |
433 Limit(commitsPerPage) | |
434 var commits []*Commit | |
435 if _, err := q.GetAll(c, &commits); err != nil { | |
436 logErr(w, r, err) | |
437 return | |
438 } | |
439 | |
440 builders := make(map[string]bool) | |
441 for _, commit := range commits { | |
442 for _, r := range commit.Result { | |
443 p := strings.SplitN(r, "|", 4) | |
444 if len(p) != 4 { | |
445 continue | |
446 } | |
447 builders[p[0]] = true | |
448 } | |
449 } | |
450 | |
451 c.Infof("\ncommits: %v\nq: %v\nkey: %v", commits, q, p.Key(c)) | |
452 | |
453 err := uiTemplate.Execute(w, struct { | |
454 Commits []*Commit | |
455 Builders []string | |
456 }{ | |
457 commits, | |
458 keys(builders), | |
459 }) | |
460 if err != nil { | |
461 logErr(w, r, err) | |
462 } | |
463 } | |
464 | |
465 func keys(m map[string]bool) (s []string) { | |
466 for k := range m { | |
467 s = append(s, k) | |
468 } | |
469 sort.Strings(s) | |
470 return | |
471 } | |
472 | |
473 type errBadMethod string | |
474 | |
475 func (e errBadMethod) String() string { | |
476 return "bad method: " + string(e) | |
477 } | |
478 | |
479 type dashHandler func(*http.Request) (interface{}, os.Error) | 485 type dashHandler func(*http.Request) (interface{}, os.Error) |
480 | 486 |
481 type dashResponse struct { | 487 type dashResponse struct { |
482 Response interface{} | 488 Response interface{} |
483 Error string | 489 Error string |
| 490 } |
| 491 |
| 492 // errBadMethod is returned by a dashHandler when |
| 493 // the request has an unsuitable method. |
| 494 type errBadMethod string |
| 495 |
| 496 func (e errBadMethod) String() string { |
| 497 return "bad method: " + string(e) |
484 } | 498 } |
485 | 499 |
486 // AuthHandler wraps a http.HandlerFunc with a handler that validates the | 500 // AuthHandler wraps a http.HandlerFunc with a handler that validates the |
487 // supplied key and builder query parameters. | 501 // supplied key and builder query parameters. |
488 func AuthHandler(h dashHandler) http.HandlerFunc { | 502 func AuthHandler(h dashHandler) http.HandlerFunc { |
489 return func(w http.ResponseWriter, r *http.Request) { | 503 return func(w http.ResponseWriter, r *http.Request) { |
490 // Put the URL Query values into r.Form to avoid parsing the | 504 // Put the URL Query values into r.Form to avoid parsing the |
491 // request body when calling r.FormValue. | 505 // request body when calling r.FormValue. |
492 r.Form = r.URL.Query() | 506 r.Form = r.URL.Query() |
493 | 507 |
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
540 http.HandleFunc("/init", initHandler) | 554 http.HandleFunc("/init", initHandler) |
541 | 555 |
542 // authenticated handlers | 556 // authenticated handlers |
543 http.HandleFunc("/commit", AuthHandler(commitHandler)) | 557 http.HandleFunc("/commit", AuthHandler(commitHandler)) |
544 http.HandleFunc("/packages", AuthHandler(packagesHandler)) | 558 http.HandleFunc("/packages", AuthHandler(packagesHandler)) |
545 http.HandleFunc("/result", AuthHandler(resultHandler)) | 559 http.HandleFunc("/result", AuthHandler(resultHandler)) |
546 http.HandleFunc("/tag", AuthHandler(tagHandler)) | 560 http.HandleFunc("/tag", AuthHandler(tagHandler)) |
547 http.HandleFunc("/todo", AuthHandler(todoHandler)) | 561 http.HandleFunc("/todo", AuthHandler(todoHandler)) |
548 | 562 |
549 // public handlers | 563 // public handlers |
550 http.HandleFunc("/", uiHandler) | |
551 http.HandleFunc("/log/", logHandler) | 564 http.HandleFunc("/log/", logHandler) |
552 } | 565 } |
553 | 566 |
554 func validHash(hash string) bool { | 567 func validHash(hash string) bool { |
555 // TODO(adg): correctly validate a hash | 568 // TODO(adg): correctly validate a hash |
556 return hash != "" | 569 return hash != "" |
557 } | 570 } |
558 | 571 |
559 func logErr(w http.ResponseWriter, r *http.Request, err os.Error) { | 572 func logErr(w http.ResponseWriter, r *http.Request, err os.Error) { |
560 appengine.NewContext(r).Errorf("Error: %v", err) | 573 appengine.NewContext(r).Errorf("Error: %v", err) |
561 w.WriteHeader(http.StatusInternalServerError) | 574 w.WriteHeader(http.StatusInternalServerError) |
562 fmt.Fprint(w, "Error: ", err) | 575 fmt.Fprint(w, "Error: ", err) |
563 } | 576 } |
LEFT | RIGHT |