| 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 |
| 5 package git |
| 6 |
| 7 import ( |
| 8 "bytes" |
| 9 "fmt" |
| 10 "strings" |
| 11 |
| 12 "infra/libs/infra_util" |
| 13 ) |
| 14 |
| 15 // Types /////////////////////////////////////////////////////////////////////// |
| 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 // Commit represents an immutable git commit. |
| 24 // |
| 25 // It also will lazily parse the message for footers. |
| 26 type Commit struct { |
| 27 id *ObjectID |
| 28 |
| 29 // TODO(iannucci): Make these real Object's |
| 30 tree *ObjectID |
| 31 parents []*ObjectID |
| 32 |
| 33 author User |
| 34 committer User |
| 35 extraHeaders []string |
| 36 messageRaw string |
| 37 |
| 38 // private cache fields |
| 39 messageRawLines []string |
| 40 messageLines []string |
| 41 footerPairs []Footer |
| 42 footers map[string][]string |
| 43 } |
| 44 |
| 45 // Constructors /////////////////////////////////////////////////////////////// |
| 46 |
| 47 // CommitFromRaw returns a Commit parsed from the hash-object compatible commit |
| 48 // text format. |
| 49 // |
| 50 // This will calculate and fill in the Commit.ID from the actual commit data |
| 51 // provided. |
| 52 func NewCommitFromRaw(data []byte) (*Commit, error) { |
| 53 return NewCommitFromRawWithID(MakeObjectIDForData(CommitType, data), dat
a) |
| 54 } |
| 55 |
| 56 // CommitFromRawWithID returns a Commit parsed from the hash-object compatible |
| 57 // commit text format. This assumes that |id| is the correct id for data, and |
| 58 // does not hash or verify its correctness. Only use this if you trust the |
| 59 // origin of |data|. |
| 60 func NewCommitFromRawWithID(id Identifiable, data []byte) (ret *Commit, err erro
r) { |
| 61 ret = new(Commit) |
| 62 buf := bytes.NewBuffer(data) |
| 63 nom := infra_util.Nom(buf) |
| 64 |
| 65 ret.id = id.ID() |
| 66 ret.tree, err = MakeObjectIDErr(strings.Split(nom('\n'), " ")[1]) |
| 67 if err != nil { |
| 68 return |
| 69 } |
| 70 ret.parents = make([]*ObjectID, 0, 1) |
| 71 line := nom('\n') |
| 72 for strings.HasPrefix(line, "parent ") { |
| 73 id, err = MakeObjectIDErr(strings.Split(line, " ")[1]) |
| 74 if err != nil { |
| 75 return |
| 76 } |
| 77 ret.parents = append(ret.parents, id.ID()) |
| 78 line = nom('\n') |
| 79 } |
| 80 ret.author, err = MakeUserFromCommitLine("author", line) |
| 81 if err != nil { |
| 82 return |
| 83 } |
| 84 ret.committer, err = MakeUserFromCommitLine("committer", nom('\n')) |
| 85 if err != nil { |
| 86 return |
| 87 } |
| 88 ret.extraHeaders = make([]string, 0) |
| 89 line = nom('\n') |
| 90 for len(line) != 0 { |
| 91 ret.extraHeaders = append(ret.extraHeaders, line) |
| 92 line = nom('\n') |
| 93 } |
| 94 ret.messageRaw = buf.String() |
| 95 return |
| 96 } |
| 97 |
| 98 // Member functions //////////////////////////////////////////////////////////// |
| 99 |
| 100 func (c *Commit) ID() *ObjectID { return c.id } |
| 101 func (c *Commit) Type() ObjectType { return CommitType } |
| 102 func (c *Commit) Complete() bool { return *c.id != NoID } |
| 103 |
| 104 func (c *Commit) Tree() *ObjectID { return c.tree } |
| 105 func (c *Commit) Parents() (r []*ObjectID) { return append(r, c.parents...) } |
| 106 func (c *Commit) Author() *User { return &c.author } |
| 107 func (c *Commit) Committer() *User { return &c.committer } |
| 108 func (c *Commit) ExtraHeaders() (r []string) { return append(r, c.extraHeaders..
.) } |
| 109 func (c *Commit) MessageRaw() string { return c.messageRaw } |
| 110 |
| 111 func (c *Commit) String() string { return fmt.Sprintf("Commit(%s, ...)", c.id) } |
| 112 |
| 113 // Returns a partial Commit with the id and cache data cleared. This is |
| 114 // used by the Set* methods. If this Commit is already unidentified, avoid |
| 115 // copying it and return c directly. |
| 116 func (c *Commit) partial() *Commit { |
| 117 if *c.id != NoID { |
| 118 return &Commit{ |
| 119 id: &NoID, |
| 120 tree: c.tree, |
| 121 parents: c.parents, |
| 122 author: c.author, |
| 123 committer: c.committer, |
| 124 extraHeaders: c.extraHeaders, |
| 125 messageRaw: c.messageRaw, |
| 126 } |
| 127 } |
| 128 return c |
| 129 } |
| 130 |
| 131 func (c *Commit) SetTree(t *ObjectID) (ret *Commit) { |
| 132 ret = c.partial() |
| 133 ret.tree = t |
| 134 return |
| 135 } |
| 136 |
| 137 func (c *Commit) SetParents(ps []*ObjectID) (ret *Commit) { |
| 138 ret = c.partial() |
| 139 ret.parents = append([]*ObjectID{}, ps...) |
| 140 return |
| 141 } |
| 142 |
| 143 func (c *Commit) SetRawMessage(msg string) (ret *Commit) { |
| 144 ret = c.partial() |
| 145 ret.messageRaw = msg |
| 146 return |
| 147 } |
| 148 |
| 149 // RawString returns a `git hash-object` compatible string for this Commit |
| 150 func (c *Commit) RawString() string { |
| 151 buf := &bytes.Buffer{} |
| 152 fmt.Fprintln(buf, "tree", c.Tree()) |
| 153 for _, p := range c.parents { |
| 154 fmt.Fprintln(buf, "parent", p) |
| 155 } |
| 156 fmt.Fprintln(buf, "author", c.author.RawString()) |
| 157 fmt.Fprintln(buf, "committer", c.committer.RawString()) |
| 158 for _, l := range c.extraHeaders { |
| 159 fmt.Fprintln(buf, l) |
| 160 } |
| 161 fmt.Fprintln(buf) |
| 162 fmt.Fprint(buf, c.MessageRaw()) |
| 163 if *c.id == NoID { |
| 164 c.id = MakeObjectIDForData(CommitType, buf.Bytes()) |
| 165 } |
| 166 return buf.String() |
| 167 } |
| 168 |
| 169 // MessageRawLines returns a cached slice of lines in MessageRaw, which |
| 170 // includes all lines in the commit 'message' (body and footers). |
| 171 func (c *Commit) MessageRawLines() []string { |
| 172 if c.messageRawLines == nil { |
| 173 c.messageRawLines = strings.Split(strings.TrimRight(c.messageRaw
, "\n"), "\n") |
| 174 } |
| 175 return c.messageRawLines |
| 176 } |
| 177 |
| 178 // MessageLines returns a cached slice of lines in MessageRaw, excluding |
| 179 // footer lines. |
| 180 func (c *Commit) MessageLines() []string { |
| 181 c.parseMessage() |
| 182 return c.messageLines |
| 183 } |
| 184 |
| 185 // FooterPairs returns a cached slice of all Footers found in this Commit. |
| 186 // |
| 187 // Footers are found in the last paragraph of the Commit message, assuming |
| 188 // that each of the lines in the last paragraph looks like: |
| 189 // ([^:]*):\s(.*) |
| 190 // |
| 191 // The first group is the Key, and the second group is the Value. |
| 192 func (c *Commit) FooterPairs() []Footer { |
| 193 c.parseMessage() |
| 194 return c.footerPairs |
| 195 } |
| 196 |
| 197 // Footers returns a cached map of Key -> []Value. This is often more convenient |
| 198 // than iterating through FooterPairs(). |
| 199 func (c *Commit) Footers() map[string][]string { |
| 200 c.parseMessage() |
| 201 return c.footers |
| 202 } |
| 203 |
| 204 // Private functions |
| 205 |
| 206 // parseMessage parses the message for footer_pairs, footers, message_lines |
| 207 func (c *Commit) parseMessage() { |
| 208 if c.messageLines != nil { |
| 209 return |
| 210 } |
| 211 allLines := c.MessageRawLines() |
| 212 i := len(allLines) - 1 |
| 213 for ; i >= 0; i-- { |
| 214 line := allLines[i] |
| 215 if len(line) == 0 { |
| 216 break |
| 217 } else if !strings.Contains(line, ": ") { |
| 218 // invalid footer |
| 219 i = -1 |
| 220 break |
| 221 } |
| 222 } |
| 223 |
| 224 if i != -1 { |
| 225 c.messageLines = allLines[:i] |
| 226 |
| 227 pairs := make([]Footer, 0, len(allLines)-i) |
| 228 footers := map[string][]string{} |
| 229 for i++; i < len(allLines); i++ { |
| 230 line := allLines[i] |
| 231 bits := strings.SplitN(line, ": ", 2) |
| 232 key, value := bits[0], bits[1] |
| 233 pairs = append(pairs, Footer{key, value}) |
| 234 footers[key] = append(footers[key], value) |
| 235 } |
| 236 c.footerPairs = pairs |
| 237 c.footers = footers |
| 238 } else { |
| 239 c.messageLines = allLines |
| 240 c.footerPairs = []Footer{} |
| 241 } |
| 242 } |
| OLD | NEW |