Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(180)

Side by Side Diff: go/src/infra/libs/git/commit.go

Issue 662113003: Drover's back, baby! (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git/+/master
Patch Set: Lots of fixes Created 6 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698