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