Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 package git | |
| 2 | |
| 3 import "bytes" | |
|
M-A Ruel
2014/10/18 00:47:05
import (
"bytes"
"fmt"
..
)
iannucci
2014/10/20 21:11:56
Done.
| |
| 4 import "fmt" | |
| 5 import "regexp" | |
| 6 import "strconv" | |
| 7 import "strings" | |
| 8 import "time" | |
| 9 | |
| 10 import "infra/libs/infra_util" | |
| 11 | |
| 12 // Footer represents the Key/Value pair of a single git commit footer. | |
| 13 type Footer struct { | |
| 14 Key string | |
| 15 Value string | |
| 16 } | |
| 17 | |
| 18 // User represents an author/committer line in a Commit | |
| 19 type User struct { | |
| 20 Name string | |
| 21 Email string | |
| 22 Time time.Time | |
| 23 } | |
| 24 | |
| 25 // RawString returns a `git hash-object` compatible string for this User | |
| 26 func (u *User) RawString() string { | |
| 27 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
| |
| 28 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..
| |
| 29 } | |
| 30 | |
| 31 // Commit represents an immutable git commit. | |
| 32 // | |
| 33 // It also will lazily parse the message for footers. | |
| 34 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
| |
| 35 id ObjectID | |
| 36 | |
| 37 // 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.
| |
| 38 tree ObjectID | |
| 39 parents []ObjectID | |
| 40 | |
| 41 author User | |
| 42 committer User | |
| 43 extraHeaders []string | |
| 44 messageRaw string | |
| 45 | |
| 46 // private cache fields | |
| 47 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
| |
| 48 messageLines *[]string | |
| 49 footerPairs *[]Footer | |
| 50 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.
| |
| 51 } | |
| 52 | |
| 53 func (c *Commit) ID() ObjectID { return c.id } | |
| 54 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().
| |
| 55 func (c *Commit) Complete() bool { return c.id != NoID } | |
| 56 | |
| 57 func (c *Commit) Tree() ObjectID { return c.tree } | |
| 58 func (c *Commit) Parents() (r []ObjectID) { return append(r, c.parents...) } | |
| 59 func (c *Commit) Author() User { return c.author } | |
| 60 func (c *Commit) Committer() User { return c.committer } | |
| 61 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
| |
| 62 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
| |
| 63 | |
| 64 func (c Commit) Partial() Commit { | |
| 65 return Commit{ | |
| 66 tree: c.tree, | |
| 67 parents: c.parents, | |
| 68 author: c.author, | |
| 69 committer: c.committer, | |
| 70 extraHeaders: c.extraHeaders, | |
| 71 messageRaw: c.messageRaw, | |
| 72 } | |
| 73 } | |
| 74 | |
| 75 func (c Commit) SetTree(t ObjectID) (ret Commit) { | |
| 76 ret = c.Partial() | |
| 77 ret.tree = t | |
| 78 return | |
| 79 } | |
| 80 | |
| 81 func (c Commit) SetParents(ps []ObjectID) (ret Commit) { | |
| 82 ret = c.Partial() | |
| 83 ret.parents = append([]ObjectID{}, ps...) | |
| 84 return | |
| 85 } | |
| 86 | |
| 87 func (c Commit) SetRawMessage(msg string) (ret Commit) { | |
| 88 ret = c.Partial() | |
| 89 ret.messageRaw = msg | |
| 90 return | |
| 91 } | |
| 92 | |
| 93 // RawString returns a `git hash-object` compatible string for this Commit | |
| 94 func (c *Commit) RawString() string { | |
| 95 buf := &bytes.Buffer{} | |
| 96 fmt.Fprintln(buf, "tree", c.Tree()) | |
| 97 for _, p := range c.parents { | |
| 98 fmt.Fprintln(buf, "parent", p) | |
| 99 } | |
| 100 fmt.Fprintln(buf, "author", c.author.RawString()) | |
| 101 fmt.Fprintln(buf, "committer", c.committer.RawString()) | |
| 102 for _, l := range c.extraHeaders { | |
| 103 fmt.Fprintln(buf, l) | |
| 104 } | |
| 105 fmt.Fprintln(buf) | |
| 106 fmt.Fprint(buf, c.MessageRaw()) | |
| 107 if c.id == NoID { | |
| 108 c.id = MakeObjectIDForData("commit", buf.Bytes()) | |
| 109 } | |
| 110 return buf.String() | |
| 111 } | |
| 112 | |
| 113 // MessageRawLines returns a cached slice of lines in MessageRaw, which | |
| 114 // includes all lines in the commit 'message' (body and footers). | |
| 115 func (c *Commit) MessageRawLines() []string { | |
| 116 if c.messageRawLines == nil { | |
| 117 split := strings.Split(strings.TrimRight(c.messageRaw, "\n"), "\ n") | |
| 118 c.messageRawLines = &split | |
| 119 } | |
| 120 return *c.messageRawLines | |
| 121 } | |
| 122 | |
| 123 // MessageLines returns a cached slice of lines in MessageRaw, excluding | |
| 124 // footer lines. | |
| 125 func (c *Commit) MessageLines() []string { | |
| 126 if c.messageLines == nil { | |
| 127 c.parseMessage() | |
| 128 } | |
| 129 return *c.messageLines | |
| 130 } | |
| 131 | |
| 132 // FooterPairs returns a cached slice of all Footers found in this Commit. | |
| 133 // | |
| 134 // Footers are found in the last paragraph of the Commit message, assuming | |
| 135 // that each of the lines in the last paragraph looks like: | |
| 136 // ([^:]*):\s(.*) | |
| 137 // | |
| 138 // The first group is the Key, and the second group is the Value. | |
| 139 func (c *Commit) FooterPairs() []Footer { | |
| 140 if c.footerPairs == nil { | |
| 141 c.parseMessage() | |
| 142 } | |
| 143 return *c.footerPairs | |
| 144 } | |
| 145 | |
| 146 // Footers returns a cached map of Key -> []Value. This is often more convenient | |
| 147 // than iterating through FooterPairs(). | |
| 148 func (c *Commit) Footers() map[string][]string { | |
| 149 if c.footers == nil { | |
| 150 c.parseMessage() | |
| 151 } | |
| 152 return *c.footers | |
| 153 } | |
| 154 | |
| 155 // CommitFromRaw returns a Commit parsed from the hash-object compatible commit | |
| 156 // text format. | |
| 157 // | |
| 158 // This will calculate and fill in the Commit.ID from the actual commit data | |
| 159 // provided. | |
| 160 func CommitFromRaw(data []byte) (*Commit, error) { | |
| 161 return CommitFromRawWithID(MakeObjectIDForData("commit", data), data) | |
| 162 } | |
| 163 | |
| 164 // CommitFromRawWithID returns a Commit parsed from the hash-object compatible | |
| 165 // commit text format. This assumes that |id| is the correct id for data, and | |
| 166 // does not hash or verify its correctness. Only use this if you trust the | |
| 167 // origin of |data|. | |
| 168 func CommitFromRawWithID(id ObjectID, data []byte) (ret *Commit, err error) { | |
| 169 ret = new(Commit) | |
| 170 buf := bytes.NewBuffer(data) | |
| 171 nom := infra_util.Nom(buf) | |
| 172 | |
| 173 ret.id = id | |
| 174 ret.tree = MakeObjectID(strings.Split(nom('\n'), " ")[1]) | |
| 175 ret.parents = make([]ObjectID, 0, 1) | |
| 176 line := nom('\n') | |
| 177 for strings.HasPrefix(line, "parent ") { | |
| 178 ret.parents = append(ret.parents, MakeObjectID(strings.Split(lin e, " ")[1])) | |
| 179 line = nom('\n') | |
| 180 } | |
| 181 ret.author, err = decodeUser("author", line) | |
| 182 if err != nil { | |
| 183 return | |
| 184 } | |
| 185 ret.committer, err = decodeUser("committer", nom('\n')) | |
| 186 if err != nil { | |
| 187 return | |
| 188 } | |
| 189 ret.extraHeaders = make([]string, 0) | |
| 190 line = nom('\n') | |
| 191 for len(line) != 0 { | |
| 192 ret.extraHeaders = append(ret.extraHeaders, line) | |
| 193 line = nom('\n') | |
| 194 } | |
| 195 ret.messageRaw = buf.String() | |
| 196 return | |
| 197 } | |
| 198 | |
| 199 // Private functions | |
| 200 | |
| 201 // Parses the message for footer_pairs, footers, message_lines | |
| 202 func (c *Commit) parseMessage() { | |
| 203 allLines := c.MessageRawLines() | |
| 204 i := -1 | |
| 205 for i = len(allLines) - 1; i >= 0; i-- { | |
| 206 line := allLines[i] | |
| 207 if len(line) == 0 { | |
| 208 break | |
| 209 } else if !strings.Contains(line, ": ") { | |
| 210 // invalid footer | |
| 211 i = -1 | |
| 212 break | |
| 213 } | |
| 214 } | |
| 215 | |
| 216 if i != -1 { | |
| 217 rest := allLines[:i] | |
| 218 c.messageLines = &rest | |
| 219 | |
| 220 pairs := make([]Footer, 0, len(allLines)-i) | |
| 221 footers := map[string][]string{} | |
| 222 for i++; i < len(allLines); i++ { | |
| 223 line := allLines[i] | |
| 224 bits := strings.SplitN(line, ": ", 2) | |
| 225 key, value := bits[0], bits[1] | |
| 226 pairs = append(pairs, Footer{key, value}) | |
| 227 footers[key] = append(footers[key], value) | |
| 228 } | |
| 229 c.footerPairs = &pairs | |
| 230 c.footers = &footers | |
| 231 } else { | |
| 232 c.messageLines = &allLines | |
| 233 c.footerPairs = &[]Footer{} | |
| 234 } | |
| 235 } | |
| 236 | |
| 237 // author|committer SP <user_name> SP '<' <user_email> '>' SP timecode +0000 | |
| 238 // I would use named groups, but the interface to access them is terrible | |
| 239 // in the regexp package... | |
| 240 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.
| |
| 241 `^(author|committer) ` + | |
| 242 `([^<]*) ` + // user_name | |
| 243 `<([^>]*)> ` + // user_email | |
| 244 `(\d*) ` + // timecode | |
| 245 `([+-]\d{4})$`) //timezone | |
| 246 | |
| 247 func decodeUser(lineType, line string) (ret User, err error) { | |
| 248 defer func() { | |
| 249 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.
| |
| 250 err = r.(error) | |
| 251 } | |
| 252 }() | |
| 253 | |
| 254 fields := UserRegex.FindStringSubmatch(line) | |
| 255 if len(fields) == 0 { | |
| 256 err = fmt.Errorf("Incompatible user line: %#v", line) | |
| 257 return | |
| 258 } | |
| 259 if fields[1] != lineType { | |
| 260 err = fmt.Errorf("Expected to parse %s but got %s instead", line Type, fields[1]) | |
| 261 return | |
| 262 } | |
| 263 t, err := time.Parse("-0700", fields[5]) | |
| 264 if err != nil { | |
| 265 return | |
| 266 } | |
| 267 | |
| 268 timecode, err := strconv.ParseInt(fields[4], 10, 64) | |
| 269 if err != nil { | |
| 270 return | |
| 271 } | |
| 272 | |
| 273 ret = User{ | |
| 274 Name: fields[2], | |
| 275 Email: fields[3], | |
| 276 Time: time.Unix(timecode, 0).In(t.Location()), | |
| 277 } | |
| 278 return | |
| 279 } | |
| OLD | NEW |