Chromium Code Reviews| 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..2b04dae84ec0b1724ce8ee3d764b70368126cc7f |
| --- /dev/null |
| +++ b/go/src/infra/libs/git/commit.go |
| @@ -0,0 +1,279 @@ |
| +package git |
| + |
| +import "bytes" |
|
M-A Ruel
2014/10/18 00:47:05
import (
"bytes"
"fmt"
..
)
iannucci
2014/10/20 21:11:56
Done.
|
| +import "fmt" |
| +import "regexp" |
| +import "strconv" |
| +import "strings" |
| +import "time" |
| + |
| +import "infra/libs/infra_util" |
| + |
| +// Footer represents the Key/Value pair of a single git commit footer. |
| +type Footer struct { |
| + Key string |
| + Value string |
| +} |
| + |
| +// User represents an author/committer line in a Commit |
| +type User struct { |
| + Name string |
| + Email string |
| + Time time.Time |
| +} |
| + |
| +// RawString returns a `git hash-object` compatible string for this User |
| +func (u *User) RawString() string { |
| + return fmt.Sprintf("%s <%s> %d %s", u.Name, u.Email, u.Time.Unix(), |
|
M-A Ruel
2014/10/18 00:47:04
No word wrap, use "gomt -w -s *.go" and "goimports
iannucci
2014/10/20 21:11:56
I did... it doesn't care about wrap. Are we abando
M-A Ruel
2014/10/21 00:55:53
Personally, I wrap comments at 80 cols but not fun
|
| + u.Time.Format("-0700")) |
|
M-A Ruel
2014/10/18 00:47:04
PDT, really?
iannucci
2014/10/20 21:11:57
It's the example format, not a hard-coded string..
|
| +} |
| + |
| +// Commit represents an immutable git commit. |
| +// |
| +// It also will lazily parse the message for footers. |
| +type Commit struct { |
|
M-A Ruel
2014/10/18 00:47:05
Is it implementing an interface? Otherwise why all
iannucci
2014/10/20 21:11:57
Discussed offline. This is to make the object immu
|
| + id ObjectID |
| + |
| + // TODO(riannucci): Make these real Object's |
|
M-A Ruel
2014/10/18 00:47:04
iannucci
iannucci
2014/10/20 21:11:57
Oops. Bad text macro. Done.
|
| + tree ObjectID |
| + parents []ObjectID |
| + |
| + author User |
| + committer User |
| + extraHeaders []string |
| + messageRaw string |
| + |
| + // private cache fields |
| + messageRawLines *[]string |
|
M-A Ruel
2014/10/18 00:47:05
Why *[]string and not []string?
iannucci
2014/10/20 21:11:56
because I want to distinguish between set-and-empt
|
| + messageLines *[]string |
| + footerPairs *[]Footer |
| + footers *map[string][]string |
|
M-A Ruel
2014/10/18 00:47:05
why pointer?
iannucci
2014/10/20 21:11:56
Same as above.
|
| +} |
| + |
| +func (c *Commit) ID() ObjectID { return c.id } |
| +func (c *Commit) Type() string { return "commit" } |
|
M-A Ruel
2014/10/18 00:47:04
You should:
type ObjectType string
const (
Comm
iannucci
2014/10/20 21:11:57
Better, made it a uint8 implementing String().
|
| +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...) } |
|
M-A Ruel
2014/10/18 00:47:05
You meant:
return c.extraHeaders[:]
iannucci
2014/10/20 21:11:56
This is to actually copy the headers, since this o
|
| +func (c *Commit) MessageRaw() string { return c.messageRaw } |
|
M-A Ruel
2014/10/18 00:47:05
Note that golang string is unicode, but it's possi
iannucci
2014/10/20 21:11:56
Hm... I thought that go strings were actually just
M-A Ruel
2014/10/21 00:55:53
The very last sentence ends with:
".. although the
|
| + |
| +func (c Commit) Partial() Commit { |
| + return Commit{ |
| + tree: c.tree, |
| + parents: c.parents, |
| + author: c.author, |
| + committer: c.committer, |
| + extraHeaders: c.extraHeaders, |
| + messageRaw: c.messageRaw, |
| + } |
| +} |
| + |
| +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("commit", 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 { |
| + split := strings.Split(strings.TrimRight(c.messageRaw, "\n"), "\n") |
| + c.messageRawLines = &split |
| + } |
| + return *c.messageRawLines |
| +} |
| + |
| +// MessageLines returns a cached slice of lines in MessageRaw, excluding |
| +// footer lines. |
| +func (c *Commit) MessageLines() []string { |
| + if c.messageLines == nil { |
| + 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 { |
| + if c.footerPairs == nil { |
| + 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 { |
| + if c.footers == nil { |
| + c.parseMessage() |
| + } |
| + return *c.footers |
| +} |
| + |
| +// 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 CommitFromRaw(data []byte) (*Commit, error) { |
| + return CommitFromRawWithID(MakeObjectIDForData("commit", 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 CommitFromRawWithID(id ObjectID, data []byte) (ret *Commit, err error) { |
| + ret = new(Commit) |
| + buf := bytes.NewBuffer(data) |
| + nom := infra_util.Nom(buf) |
| + |
| + ret.id = id |
| + ret.tree = MakeObjectID(strings.Split(nom('\n'), " ")[1]) |
| + ret.parents = make([]ObjectID, 0, 1) |
| + line := nom('\n') |
| + for strings.HasPrefix(line, "parent ") { |
| + ret.parents = append(ret.parents, MakeObjectID(strings.Split(line, " ")[1])) |
| + line = nom('\n') |
| + } |
| + ret.author, err = decodeUser("author", line) |
| + if err != nil { |
| + return |
| + } |
| + ret.committer, err = decodeUser("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 |
| +} |
| + |
| +// Private functions |
| + |
| +// Parses the message for footer_pairs, footers, message_lines |
| +func (c *Commit) parseMessage() { |
| + allLines := c.MessageRawLines() |
| + i := -1 |
| + for i = len(allLines) - 1; i >= 0; i-- { |
| + line := allLines[i] |
| + if len(line) == 0 { |
| + break |
| + } else if !strings.Contains(line, ": ") { |
| + // invalid footer |
| + i = -1 |
| + break |
| + } |
| + } |
| + |
| + if i != -1 { |
| + rest := allLines[:i] |
| + c.messageLines = &rest |
| + |
| + 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{} |
| + } |
| +} |
| + |
| +// author|committer SP <user_name> SP '<' <user_email> '>' SP timecode +0000 |
| +// I would use named groups, but the interface to access them is terrible |
| +// in the regexp package... |
| +var UserRegex = regexp.MustCompile( |
|
M-A Ruel
2014/10/18 00:47:05
it's not private if it is Upper case
iannucci
2014/10/20 21:11:57
Oops. Done.
|
| + `^(author|committer) ` + |
| + `([^<]*) ` + // user_name |
| + `<([^>]*)> ` + // user_email |
| + `(\d*) ` + // timecode |
| + `([+-]\d{4})$`) //timezone |
| + |
| +func decodeUser(lineType, line string) (ret User, err error) { |
| + defer func() { |
| + if r := recover(); r != nil { |
|
M-A Ruel
2014/10/18 00:47:05
what would panic that you'd want to catch this way
iannucci
2014/10/20 21:11:56
Leftover code :). Removed.
|
| + err = r.(error) |
| + } |
| + }() |
| + |
| + fields := UserRegex.FindStringSubmatch(line) |
| + if len(fields) == 0 { |
| + err = fmt.Errorf("Incompatible user line: %#v", line) |
| + return |
| + } |
| + if fields[1] != lineType { |
| + err = fmt.Errorf("Expected to parse %s but got %s instead", lineType, fields[1]) |
| + return |
| + } |
| + t, err := time.Parse("-0700", fields[5]) |
| + if err != nil { |
| + return |
| + } |
| + |
| + timecode, err := strconv.ParseInt(fields[4], 10, 64) |
| + if err != nil { |
| + return |
| + } |
| + |
| + ret = User{ |
| + Name: fields[2], |
| + Email: fields[3], |
| + Time: time.Unix(timecode, 0).In(t.Location()), |
| + } |
| + return |
| +} |