| Index: go/src/infra/libs/git/commit.go
|
| diff --git a/go/src/infra/libs/git/commit.go b/go/src/infra/libs/git/commit.go
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..6e168fa183eb029738ae4aa09dd20d0937a07aab
|
| --- /dev/null
|
| +++ b/go/src/infra/libs/git/commit.go
|
| @@ -0,0 +1,242 @@
|
| +// Copyright 2014 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +package git
|
| +
|
| +import (
|
| + "bytes"
|
| + "fmt"
|
| + "strings"
|
| +
|
| + "infra/libs/infra_util"
|
| +)
|
| +
|
| +// Types ///////////////////////////////////////////////////////////////////////
|
| +
|
| +// Footer represents the Key/Value pair of a single git commit footer.
|
| +type Footer struct {
|
| + Key string
|
| + Value string
|
| +}
|
| +
|
| +// Commit represents an immutable git commit.
|
| +//
|
| +// It also will lazily parse the message for footers.
|
| +type Commit struct {
|
| + id *ObjectID
|
| +
|
| + // TODO(iannucci): Make these real Object's
|
| + tree *ObjectID
|
| + parents []*ObjectID
|
| +
|
| + author User
|
| + committer User
|
| + extraHeaders []string
|
| + messageRaw string
|
| +
|
| + // private cache fields
|
| + messageRawLines []string
|
| + messageLines []string
|
| + footerPairs []Footer
|
| + footers map[string][]string
|
| +}
|
| +
|
| +// Constructors ///////////////////////////////////////////////////////////////
|
| +
|
| +// CommitFromRaw returns a Commit parsed from the hash-object compatible commit
|
| +// text format.
|
| +//
|
| +// This will calculate and fill in the Commit.ID from the actual commit data
|
| +// provided.
|
| +func NewCommitFromRaw(data []byte) (*Commit, error) {
|
| + return NewCommitFromRawWithID(MakeObjectIDForData(CommitType, data), data)
|
| +}
|
| +
|
| +// CommitFromRawWithID returns a Commit parsed from the hash-object compatible
|
| +// commit text format. This assumes that |id| is the correct id for data, and
|
| +// does not hash or verify its correctness. Only use this if you trust the
|
| +// origin of |data|.
|
| +func NewCommitFromRawWithID(id Identifiable, data []byte) (ret *Commit, err error) {
|
| + ret = new(Commit)
|
| + buf := bytes.NewBuffer(data)
|
| + nom := infra_util.Nom(buf)
|
| +
|
| + ret.id = id.ID()
|
| + ret.tree, err = MakeObjectIDErr(strings.Split(nom('\n'), " ")[1])
|
| + if err != nil {
|
| + return
|
| + }
|
| + ret.parents = make([]*ObjectID, 0, 1)
|
| + line := nom('\n')
|
| + for strings.HasPrefix(line, "parent ") {
|
| + id, err = MakeObjectIDErr(strings.Split(line, " ")[1])
|
| + if err != nil {
|
| + return
|
| + }
|
| + ret.parents = append(ret.parents, id.ID())
|
| + line = nom('\n')
|
| + }
|
| + ret.author, err = MakeUserFromCommitLine("author", line)
|
| + if err != nil {
|
| + return
|
| + }
|
| + ret.committer, err = MakeUserFromCommitLine("committer", nom('\n'))
|
| + if err != nil {
|
| + return
|
| + }
|
| + ret.extraHeaders = make([]string, 0)
|
| + line = nom('\n')
|
| + for len(line) != 0 {
|
| + ret.extraHeaders = append(ret.extraHeaders, line)
|
| + line = nom('\n')
|
| + }
|
| + ret.messageRaw = buf.String()
|
| + return
|
| +}
|
| +
|
| +// Member functions ////////////////////////////////////////////////////////////
|
| +
|
| +func (c *Commit) ID() *ObjectID { return c.id }
|
| +func (c *Commit) Type() ObjectType { return CommitType }
|
| +func (c *Commit) Complete() bool { return *c.id != NoID }
|
| +
|
| +func (c *Commit) Tree() *ObjectID { return c.tree }
|
| +func (c *Commit) Parents() (r []*ObjectID) { return append(r, c.parents...) }
|
| +func (c *Commit) Author() *User { return &c.author }
|
| +func (c *Commit) Committer() *User { return &c.committer }
|
| +func (c *Commit) ExtraHeaders() (r []string) { return append(r, c.extraHeaders...) }
|
| +func (c *Commit) MessageRaw() string { return c.messageRaw }
|
| +
|
| +func (c *Commit) String() string { return fmt.Sprintf("Commit(%s, ...)", c.id) }
|
| +
|
| +// Returns a partial Commit with the id and cache data cleared. This is
|
| +// used by the Set* methods. If this Commit is already unidentified, avoid
|
| +// copying it and return c directly.
|
| +func (c *Commit) partial() *Commit {
|
| + if *c.id != NoID {
|
| + return &Commit{
|
| + id: &NoID,
|
| + tree: c.tree,
|
| + parents: c.parents,
|
| + author: c.author,
|
| + committer: c.committer,
|
| + extraHeaders: c.extraHeaders,
|
| + messageRaw: c.messageRaw,
|
| + }
|
| + }
|
| + return c
|
| +}
|
| +
|
| +func (c *Commit) SetTree(t *ObjectID) (ret *Commit) {
|
| + ret = c.partial()
|
| + ret.tree = t
|
| + return
|
| +}
|
| +
|
| +func (c *Commit) SetParents(ps []*ObjectID) (ret *Commit) {
|
| + ret = c.partial()
|
| + ret.parents = append([]*ObjectID{}, ps...)
|
| + return
|
| +}
|
| +
|
| +func (c *Commit) SetRawMessage(msg string) (ret *Commit) {
|
| + ret = c.partial()
|
| + ret.messageRaw = msg
|
| + return
|
| +}
|
| +
|
| +// RawString returns a `git hash-object` compatible string for this Commit
|
| +func (c *Commit) RawString() string {
|
| + buf := &bytes.Buffer{}
|
| + fmt.Fprintln(buf, "tree", c.Tree())
|
| + for _, p := range c.parents {
|
| + fmt.Fprintln(buf, "parent", p)
|
| + }
|
| + fmt.Fprintln(buf, "author", c.author.RawString())
|
| + fmt.Fprintln(buf, "committer", c.committer.RawString())
|
| + for _, l := range c.extraHeaders {
|
| + fmt.Fprintln(buf, l)
|
| + }
|
| + fmt.Fprintln(buf)
|
| + fmt.Fprint(buf, c.MessageRaw())
|
| + if *c.id == NoID {
|
| + c.id = MakeObjectIDForData(CommitType, buf.Bytes())
|
| + }
|
| + return buf.String()
|
| +}
|
| +
|
| +// MessageRawLines returns a cached slice of lines in MessageRaw, which
|
| +// includes all lines in the commit 'message' (body and footers).
|
| +func (c *Commit) MessageRawLines() []string {
|
| + if c.messageRawLines == nil {
|
| + c.messageRawLines = strings.Split(strings.TrimRight(c.messageRaw, "\n"), "\n")
|
| + }
|
| + return c.messageRawLines
|
| +}
|
| +
|
| +// MessageLines returns a cached slice of lines in MessageRaw, excluding
|
| +// footer lines.
|
| +func (c *Commit) MessageLines() []string {
|
| + c.parseMessage()
|
| + return c.messageLines
|
| +}
|
| +
|
| +// FooterPairs returns a cached slice of all Footers found in this Commit.
|
| +//
|
| +// Footers are found in the last paragraph of the Commit message, assuming
|
| +// that each of the lines in the last paragraph looks like:
|
| +// ([^:]*):\s(.*)
|
| +//
|
| +// The first group is the Key, and the second group is the Value.
|
| +func (c *Commit) FooterPairs() []Footer {
|
| + c.parseMessage()
|
| + return c.footerPairs
|
| +}
|
| +
|
| +// Footers returns a cached map of Key -> []Value. This is often more convenient
|
| +// than iterating through FooterPairs().
|
| +func (c *Commit) Footers() map[string][]string {
|
| + c.parseMessage()
|
| + return c.footers
|
| +}
|
| +
|
| +// Private functions
|
| +
|
| +// parseMessage parses the message for footer_pairs, footers, message_lines
|
| +func (c *Commit) parseMessage() {
|
| + if c.messageLines != nil {
|
| + return
|
| + }
|
| + allLines := c.MessageRawLines()
|
| + i := len(allLines) - 1
|
| + for ; i >= 0; i-- {
|
| + line := allLines[i]
|
| + if len(line) == 0 {
|
| + break
|
| + } else if !strings.Contains(line, ": ") {
|
| + // invalid footer
|
| + i = -1
|
| + break
|
| + }
|
| + }
|
| +
|
| + if i != -1 {
|
| + c.messageLines = allLines[:i]
|
| +
|
| + pairs := make([]Footer, 0, len(allLines)-i)
|
| + footers := map[string][]string{}
|
| + for i++; i < len(allLines); i++ {
|
| + line := allLines[i]
|
| + bits := strings.SplitN(line, ": ", 2)
|
| + key, value := bits[0], bits[1]
|
| + pairs = append(pairs, Footer{key, value})
|
| + footers[key] = append(footers[key], value)
|
| + }
|
| + c.footerPairs = pairs
|
| + c.footers = footers
|
| + } else {
|
| + c.messageLines = allLines
|
| + c.footerPairs = []Footer{}
|
| + }
|
| +}
|
|
|