github.com/xushiwei/go@v0.0.0-20130601165731-2b9d83f45bc9/misc/dashboard/app/build/build.go (about) 1 // Copyright 2011 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 build 6 7 import ( 8 "bytes" 9 "compress/gzip" 10 "crypto/sha1" 11 "errors" 12 "fmt" 13 "io" 14 "io/ioutil" 15 "strings" 16 "time" 17 18 "appengine" 19 "appengine/datastore" 20 ) 21 22 const maxDatastoreStringLen = 500 23 24 // A Package describes a package that is listed on the dashboard. 25 type Package struct { 26 Kind string // "subrepo", "external", or empty for the main Go tree 27 Name string 28 Path string // (empty for the main Go tree) 29 NextNum int // Num of the next head Commit 30 } 31 32 func (p *Package) String() string { 33 return fmt.Sprintf("%s: %q", p.Path, p.Name) 34 } 35 36 func (p *Package) Key(c appengine.Context) *datastore.Key { 37 key := p.Path 38 if key == "" { 39 key = "go" 40 } 41 return datastore.NewKey(c, "Package", key, 0, nil) 42 } 43 44 // LastCommit returns the most recent Commit for this Package. 45 func (p *Package) LastCommit(c appengine.Context) (*Commit, error) { 46 var commits []*Commit 47 _, err := datastore.NewQuery("Commit"). 48 Ancestor(p.Key(c)). 49 Order("-Time"). 50 Limit(1). 51 GetAll(c, &commits) 52 if _, ok := err.(*datastore.ErrFieldMismatch); ok { 53 // Some fields have been removed, so it's okay to ignore this error. 54 err = nil 55 } 56 if err != nil { 57 return nil, err 58 } 59 if len(commits) != 1 { 60 return nil, datastore.ErrNoSuchEntity 61 } 62 return commits[0], nil 63 } 64 65 // GetPackage fetches a Package by path from the datastore. 66 func GetPackage(c appengine.Context, path string) (*Package, error) { 67 p := &Package{Path: path} 68 err := datastore.Get(c, p.Key(c), p) 69 if err == datastore.ErrNoSuchEntity { 70 return nil, fmt.Errorf("package %q not found", path) 71 } 72 if _, ok := err.(*datastore.ErrFieldMismatch); ok { 73 // Some fields have been removed, so it's okay to ignore this error. 74 err = nil 75 } 76 return p, err 77 } 78 79 // A Commit describes an individual commit in a package. 80 // 81 // Each Commit entity is a descendant of its associated Package entity. 82 // In other words, all Commits with the same PackagePath belong to the same 83 // datastore entity group. 84 type Commit struct { 85 PackagePath string // (empty for Go commits) 86 Hash string 87 ParentHash string 88 Num int // Internal monotonic counter unique to this package. 89 90 User string 91 Desc string `datastore:",noindex"` 92 Time time.Time 93 94 // ResultData is the Data string of each build Result for this Commit. 95 // For non-Go commits, only the Results for the current Go tip, weekly, 96 // and release Tags are stored here. This is purely de-normalized data. 97 // The complete data set is stored in Result entities. 98 ResultData []string `datastore:",noindex"` 99 100 FailNotificationSent bool 101 } 102 103 func (com *Commit) Key(c appengine.Context) *datastore.Key { 104 if com.Hash == "" { 105 panic("tried Key on Commit with empty Hash") 106 } 107 p := Package{Path: com.PackagePath} 108 key := com.PackagePath + "|" + com.Hash 109 return datastore.NewKey(c, "Commit", key, 0, p.Key(c)) 110 } 111 112 func (c *Commit) Valid() error { 113 if !validHash(c.Hash) { 114 return errors.New("invalid Hash") 115 } 116 if c.ParentHash != "" && !validHash(c.ParentHash) { // empty is OK 117 return errors.New("invalid ParentHash") 118 } 119 return nil 120 } 121 122 // each result line is approx 105 bytes. This constant is a tradeoff between 123 // build history and the AppEngine datastore limit of 1mb. 124 const maxResults = 1000 125 126 // AddResult adds the denormalized Reuslt data to the Commit's Result field. 127 // It must be called from inside a datastore transaction. 128 func (com *Commit) AddResult(c appengine.Context, r *Result) error { 129 if err := datastore.Get(c, com.Key(c), com); err != nil { 130 return fmt.Errorf("getting Commit: %v", err) 131 } 132 com.ResultData = trim(append(com.ResultData, r.Data()), maxResults) 133 if _, err := datastore.Put(c, com.Key(c), com); err != nil { 134 return fmt.Errorf("putting Commit: %v", err) 135 } 136 return nil 137 } 138 139 func trim(s []string, n int) []string { 140 l := min(len(s), n) 141 return s[len(s)-l:] 142 } 143 144 func min(a, b int) int { 145 if a < b { 146 return a 147 } 148 return b 149 } 150 151 // Result returns the build Result for this Commit for the given builder/goHash. 152 func (c *Commit) Result(builder, goHash string) *Result { 153 for _, r := range c.ResultData { 154 p := strings.SplitN(r, "|", 4) 155 if len(p) != 4 || p[0] != builder || p[3] != goHash { 156 continue 157 } 158 return partsToHash(c, p) 159 } 160 return nil 161 } 162 163 // Results returns the build Results for this Commit for the given goHash. 164 func (c *Commit) Results(goHash string) (results []*Result) { 165 for _, r := range c.ResultData { 166 p := strings.SplitN(r, "|", 4) 167 if len(p) != 4 || p[3] != goHash { 168 continue 169 } 170 results = append(results, partsToHash(c, p)) 171 } 172 return 173 } 174 175 // partsToHash converts a Commit and ResultData substrings to a Result. 176 func partsToHash(c *Commit, p []string) *Result { 177 return &Result{ 178 Builder: p[0], 179 Hash: c.Hash, 180 PackagePath: c.PackagePath, 181 GoHash: p[3], 182 OK: p[1] == "true", 183 LogHash: p[2], 184 } 185 } 186 187 // A Result describes a build result for a Commit on an OS/architecture. 188 // 189 // Each Result entity is a descendant of its associated Commit entity. 190 type Result struct { 191 Builder string // "os-arch[-note]" 192 Hash string 193 PackagePath string // (empty for Go commits) 194 195 // The Go Commit this was built against (empty for Go commits). 196 GoHash string 197 198 OK bool 199 Log string `datastore:"-"` // for JSON unmarshaling only 200 LogHash string `datastore:",noindex"` // Key to the Log record. 201 202 RunTime int64 // time to build+test in nanoseconds 203 } 204 205 func (r *Result) Key(c appengine.Context) *datastore.Key { 206 p := Package{Path: r.PackagePath} 207 key := r.Builder + "|" + r.PackagePath + "|" + r.Hash + "|" + r.GoHash 208 return datastore.NewKey(c, "Result", key, 0, p.Key(c)) 209 } 210 211 func (r *Result) Valid() error { 212 if !validHash(r.Hash) { 213 return errors.New("invalid Hash") 214 } 215 if r.PackagePath != "" && !validHash(r.GoHash) { 216 return errors.New("invalid GoHash") 217 } 218 return nil 219 } 220 221 // Data returns the Result in string format 222 // to be stored in Commit's ResultData field. 223 func (r *Result) Data() string { 224 return fmt.Sprintf("%v|%v|%v|%v", r.Builder, r.OK, r.LogHash, r.GoHash) 225 } 226 227 // A Log is a gzip-compressed log file stored under the SHA1 hash of the 228 // uncompressed log text. 229 type Log struct { 230 CompressedLog []byte 231 } 232 233 func (l *Log) Text() ([]byte, error) { 234 d, err := gzip.NewReader(bytes.NewBuffer(l.CompressedLog)) 235 if err != nil { 236 return nil, fmt.Errorf("reading log data: %v", err) 237 } 238 b, err := ioutil.ReadAll(d) 239 if err != nil { 240 return nil, fmt.Errorf("reading log data: %v", err) 241 } 242 return b, nil 243 } 244 245 func PutLog(c appengine.Context, text string) (hash string, err error) { 246 h := sha1.New() 247 io.WriteString(h, text) 248 b := new(bytes.Buffer) 249 z, _ := gzip.NewWriterLevel(b, gzip.BestCompression) 250 io.WriteString(z, text) 251 z.Close() 252 hash = fmt.Sprintf("%x", h.Sum(nil)) 253 key := datastore.NewKey(c, "Log", hash, 0, nil) 254 _, err = datastore.Put(c, key, &Log{b.Bytes()}) 255 return 256 } 257 258 // A Tag is used to keep track of the most recent Go weekly and release tags. 259 // Typically there will be one Tag entity for each kind of hg tag. 260 type Tag struct { 261 Kind string // "weekly", "release", or "tip" 262 Name string // the tag itself (for example: "release.r60") 263 Hash string 264 } 265 266 func (t *Tag) Key(c appengine.Context) *datastore.Key { 267 p := &Package{} 268 return datastore.NewKey(c, "Tag", t.Kind, 0, p.Key(c)) 269 } 270 271 func (t *Tag) Valid() error { 272 if t.Kind != "weekly" && t.Kind != "release" && t.Kind != "tip" { 273 return errors.New("invalid Kind") 274 } 275 if !validHash(t.Hash) { 276 return errors.New("invalid Hash") 277 } 278 return nil 279 } 280 281 // Commit returns the Commit that corresponds with this Tag. 282 func (t *Tag) Commit(c appengine.Context) (*Commit, error) { 283 com := &Commit{Hash: t.Hash} 284 err := datastore.Get(c, com.Key(c), com) 285 return com, err 286 } 287 288 // GetTag fetches a Tag by name from the datastore. 289 func GetTag(c appengine.Context, tag string) (*Tag, error) { 290 t := &Tag{Kind: tag} 291 if err := datastore.Get(c, t.Key(c), t); err != nil { 292 if err == datastore.ErrNoSuchEntity { 293 return nil, errors.New("tag not found: " + tag) 294 } 295 return nil, err 296 } 297 if err := t.Valid(); err != nil { 298 return nil, err 299 } 300 return t, nil 301 } 302 303 // Packages returns packages of the specified kind. 304 // Kind must be one of "external" or "subrepo". 305 func Packages(c appengine.Context, kind string) ([]*Package, error) { 306 switch kind { 307 case "external", "subrepo": 308 default: 309 return nil, errors.New(`kind must be one of "external" or "subrepo"`) 310 } 311 var pkgs []*Package 312 q := datastore.NewQuery("Package").Filter("Kind=", kind) 313 for t := q.Run(c); ; { 314 pkg := new(Package) 315 _, err := t.Next(pkg) 316 if _, ok := err.(*datastore.ErrFieldMismatch); ok { 317 // Some fields have been removed, so it's okay to ignore this error. 318 err = nil 319 } 320 if err == datastore.Done { 321 break 322 } else if err != nil { 323 return nil, err 324 } 325 if pkg.Path != "" { 326 pkgs = append(pkgs, pkg) 327 } 328 } 329 return pkgs, nil 330 }