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  }