| 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 repo |
| 6 |
| 7 import ( |
| 8 "bufio" |
| 9 "bytes" |
| 10 "fmt" |
| 11 "io" |
| 12 "os/exec" |
| 13 "strconv" |
| 14 "strings" |
| 15 |
| 16 "infra/libs/infra_util" |
| 17 |
| 18 . "infra/libs/git" |
| 19 ) |
| 20 |
| 21 // Types /////////////////////////////////////////////////////////////////////// |
| 22 |
| 23 // Repo represents a local git repository at the path |Path|. |
| 24 type Repo struct { |
| 25 // The path on disk to the location of the git repo. |
| 26 path string |
| 27 |
| 28 catFile catFileService |
| 29 catFileCheck catFileService |
| 30 } |
| 31 |
| 32 // Constructors /////////////////////////////////////////////////////////////// |
| 33 |
| 34 func NewRepo(path string) *Repo { |
| 35 return &Repo{path, |
| 36 newCatFileService(path, false), |
| 37 newCatFileService(path, true), |
| 38 } |
| 39 } |
| 40 |
| 41 // Member functions //////////////////////////////////////////////////////////// |
| 42 |
| 43 func (r *Repo) Path() string { return r.path } |
| 44 |
| 45 // RunInput runs a git command in the Repo, passing input on stdin, and returnin
g |
| 46 // the stdout and success (i.e. did the git command return 0?) |
| 47 func (r *Repo) RunInput(input string, cmd ...string) (string, bool) { |
| 48 ex := exec.Command("git", cmd...) |
| 49 ex.Dir = r.Path() |
| 50 ex.Stdin = bytes.NewBufferString(input) |
| 51 out, err := ex.Output() |
| 52 if err == nil { |
| 53 return string(out), true |
| 54 } else if _, ok := err.(*exec.ExitError); ok { |
| 55 return string(out), false |
| 56 } else { |
| 57 panic(err) |
| 58 } |
| 59 } |
| 60 |
| 61 // Run runs a git command in the Repo, with no input, returning the stdout and |
| 62 // success. |
| 63 func (r *Repo) Run(cmd ...string) (string, bool) { |
| 64 return r.RunInput("", cmd...) |
| 65 } |
| 66 |
| 67 // RunOk runs a git command in the repo with no input, and returns its success |
| 68 // (did it exit 0?) |
| 69 func (r *Repo) RunOk(cmd ...string) bool { |
| 70 _, ok := r.Run(cmd...) |
| 71 return ok |
| 72 } |
| 73 |
| 74 // MustRunOk is the same as RunOutput, but it panic's if the command fails |
| 75 func (r *Repo) MustRunOk(cmd ...string) { |
| 76 _, ok := r.Run(cmd...) |
| 77 if !ok { |
| 78 panic(fmt.Errorf("in (%s) faild to run %v", r.path, append([]str
ing{"git"}, cmd...))) |
| 79 } |
| 80 } |
| 81 |
| 82 // RunOutput runs a git command in the repo with no input, and returns its |
| 83 // stdout |
| 84 func (r *Repo) RunOutput(cmd ...string) string { |
| 85 out, _ := r.Run(cmd...) |
| 86 return out |
| 87 } |
| 88 |
| 89 // RunOutputS is the same as RunOutput, except it strips the trailing newline |
| 90 func (r *Repo) RunOutputS(cmd ...string) string { |
| 91 out, _ := r.Run(cmd...) |
| 92 return strings.TrimRight(out, "\n") |
| 93 } |
| 94 |
| 95 // MustRunOutput is the same as RunOutput, but it panic's if the command fails |
| 96 func (r *Repo) MustRunOutput(cmd ...string) string { |
| 97 out, ok := r.Run(cmd...) |
| 98 if !ok { |
| 99 panic(fmt.Errorf("in (%s) faild to run %v", r.path, append([]str
ing{"git"}, cmd...))) |
| 100 } |
| 101 return out |
| 102 } |
| 103 |
| 104 // GetObject retrieves a real representation of objectish. |
| 105 // |
| 106 // This may return MissingError if the object is not found. |
| 107 func (r *Repo) GetObject(objectish string) (InternableObject, error) { |
| 108 rsp := r.catFile.request(objectish) |
| 109 if rsp == nil { |
| 110 return nil, nil |
| 111 } |
| 112 return ObjectFromRawWithID(rsp.id, rsp.typ, rsp.data) |
| 113 } |
| 114 |
| 115 func (r *Repo) GetObjectID(id Identifiable) (InternableObject, error) { |
| 116 return r.GetObject(id.ID().String()) |
| 117 } |
| 118 |
| 119 func (r *Repo) ObjectInfo(objectish string) *ObjectInfo { |
| 120 rslt := r.catFileCheck.request(objectish) |
| 121 if rslt != nil { |
| 122 return &ObjectInfo{rslt.typ, rslt.size, rslt.id} |
| 123 } else { |
| 124 return nil |
| 125 } |
| 126 } |
| 127 |
| 128 func (r *Repo) ObjectInfoID(id Identifiable) *ObjectInfo { |
| 129 return r.ObjectInfo(id.ID().String()) |
| 130 } |
| 131 |
| 132 func (r *Repo) HasObject(objectish string) bool { |
| 133 return r.ObjectInfo(objectish) != nil |
| 134 } |
| 135 |
| 136 func (r *Repo) HasObjectID(id Identifiable) bool { |
| 137 return r.ObjectInfoID(id) != nil |
| 138 } |
| 139 |
| 140 func (r *Repo) GetTextDiff(left, right string) (string, error) { |
| 141 rslt, ok := r.Run("diff", left, right) |
| 142 if !ok { |
| 143 return "", fmt.Errorf("cannot diff(%s, %s): %s", left, right, rs
lt) |
| 144 } |
| 145 return rslt, nil |
| 146 } |
| 147 |
| 148 // DiffTree computes the 2-tree diff (with copy/rename detection) and returns |
| 149 // a parsed TreeDiff of what it found. |
| 150 // |
| 151 // This diff-tree invocation is done with -t, which implies that it is recursive
, |
| 152 // and that the actual intermediate tree objects will also be contianed in the |
| 153 // return value. |
| 154 func (r *Repo) DiffTree(left, right string) (ret TreeDiff, err error) { |
| 155 atoi := func(s string, base int) int { |
| 156 ret, err := strconv.ParseInt(s, base, 0) |
| 157 if err != nil { |
| 158 panic(err) |
| 159 } |
| 160 return int(ret) |
| 161 } |
| 162 |
| 163 lines := strings.Split(strings.TrimRight( |
| 164 r.RunOutput("diff-tree", "-t", "-z", "-M", "-M", "-C", left, rig
ht), "\000"), |
| 165 "\000") |
| 166 |
| 167 infoStream := make(chan string, len(lines)) |
| 168 for _, line := range lines { |
| 169 infoStream <- line |
| 170 } |
| 171 close(infoStream) |
| 172 for header := range infoStream { |
| 173 if len(header) == 0 { |
| 174 break |
| 175 } |
| 176 if header[0] != ':' { |
| 177 return nil, fmt.Errorf("git.DiffTree: desynchronized par
sing error") |
| 178 } |
| 179 info := strings.Fields(strings.TrimLeft(header, ":")) |
| 180 // old_mode new_mode old_id new_id action |
| 181 // oldPath (if action[0] in "RC") |
| 182 // newPath |
| 183 action := info[4] |
| 184 similarity := 0 |
| 185 oldPath := <-infoStream |
| 186 newPath := oldPath |
| 187 if action[0] == 'R' || action[0] == 'C' { |
| 188 newPath = <-infoStream |
| 189 similarity = atoi(action[1:], 10) |
| 190 } |
| 191 |
| 192 old_id, err := MakeObjectIDErr(info[2]) |
| 193 if err != nil { |
| 194 return nil, err |
| 195 } |
| 196 old_c, err := NewEmptyChild(Mode(atoi(info[0], 8)), old_id) |
| 197 if err != nil { |
| 198 return nil, err |
| 199 } |
| 200 |
| 201 new_id, err := MakeObjectIDErr(info[3]) |
| 202 if err != nil { |
| 203 return nil, err |
| 204 } |
| 205 new_c, err := NewEmptyChild(Mode(atoi(info[1], 8)), new_id) |
| 206 if err != nil { |
| 207 return nil, err |
| 208 } |
| 209 |
| 210 ret = append(ret, TreeDiffEntry{ |
| 211 Action: action, |
| 212 Similarity: similarity, |
| 213 Old: TreeDiffEntryHalf{ |
| 214 *old_c, |
| 215 oldPath, |
| 216 }, |
| 217 New: TreeDiffEntryHalf{ |
| 218 *new_c, |
| 219 newPath, |
| 220 }, |
| 221 }) |
| 222 } |
| 223 |
| 224 return |
| 225 } |
| 226 |
| 227 // Intern takes an InternableObject (Blob, Tree, Commit), and writes it into |
| 228 // the on-disk Repo. |
| 229 func (r *Repo) Intern(obj InternableObject) (*ObjectID, error) { |
| 230 return InternImpl(r, obj, func(data string) (string, error) { |
| 231 cmd := []string{"hash-object", "-t", obj.Type().String(), "-w",
"--stdin"} |
| 232 out, ok := r.RunInput(data, cmd...) |
| 233 if !ok { |
| 234 return "", fmt.Errorf("error running %s <- %s: not ok",
cmd, data) |
| 235 } |
| 236 return string(out), nil |
| 237 }) |
| 238 } |
| 239 |
| 240 /// Private |
| 241 |
| 242 type catFileService struct { |
| 243 path string |
| 244 ch chan<- catFileRequest |
| 245 } |
| 246 |
| 247 type catFileRequest struct { |
| 248 objectish string |
| 249 reply chan<- *catFileReply |
| 250 } |
| 251 |
| 252 type catFileReply struct { |
| 253 id *ObjectID |
| 254 typ ObjectType |
| 255 size int |
| 256 data []byte |
| 257 } |
| 258 |
| 259 func (c *catFileService) request(objectish string) *catFileReply { |
| 260 if strings.ContainsAny(objectish, "\n") { |
| 261 // These requests are generated internally from the library. The
caller |
| 262 // should check or recover if they think that objectish can cont
ain |
| 263 // a newline. |
| 264 panic(fmt.Errorf("catFile request may not contain a newline")) |
| 265 } |
| 266 |
| 267 rchan := make(chan *catFileReply, 1) |
| 268 c.ch <- catFileRequest{objectish, rchan} |
| 269 return <-rchan |
| 270 } |
| 271 |
| 272 func newCatFileService(path string, checkOnly bool) catFileService { |
| 273 ch := make(chan catFileRequest) |
| 274 batchMode := "--batch" |
| 275 if checkOnly { |
| 276 batchMode = "--batch-check" |
| 277 } |
| 278 go func() { |
| 279 var err error |
| 280 var in io.WriteCloser |
| 281 var nom func(byte) string |
| 282 var yoink func(int) []byte |
| 283 var out *bufio.Reader |
| 284 |
| 285 started := false |
| 286 for req := range ch { |
| 287 if !started { |
| 288 started = true |
| 289 catFile := exec.Command("git", "cat-file", batch
Mode) |
| 290 catFile.Dir = path |
| 291 |
| 292 in, err = catFile.StdinPipe() |
| 293 if err != nil { |
| 294 panic(err) |
| 295 } |
| 296 defer in.Close() |
| 297 |
| 298 outRaw, err := catFile.StdoutPipe() |
| 299 if err != nil { |
| 300 panic(err) |
| 301 } |
| 302 defer outRaw.Close() |
| 303 |
| 304 if err = catFile.Start(); err != nil { |
| 305 panic(err) |
| 306 } |
| 307 |
| 308 out = bufio.NewReader(outRaw) |
| 309 nom = infra_util.Nom(out) |
| 310 yoink = infra_util.Yoink(out) |
| 311 } |
| 312 |
| 313 func() { |
| 314 defer close(req.reply) |
| 315 in.Write([]byte(req.objectish + "\n")) |
| 316 rsp := nom('\n') |
| 317 if strings.HasSuffix(rsp, " missing") { |
| 318 req.reply <- nil |
| 319 return |
| 320 } |
| 321 |
| 322 parts := strings.Split(rsp, " ") |
| 323 objID, typ, sizeStr := parts[0], parts[1], parts
[2] |
| 324 size, err := strconv.ParseUint(sizeStr, 10, 64) |
| 325 if err != nil { |
| 326 panic(err) // git itself returned a non-
integer size field!? |
| 327 } |
| 328 |
| 329 data := []byte{} |
| 330 if !checkOnly { |
| 331 data = yoink(int(size)) |
| 332 out.ReadByte() // drop extra newline |
| 333 } |
| 334 req.reply <- &catFileReply{ |
| 335 id: MakeObjectID(objID), |
| 336 typ: MakeObjectType(typ), |
| 337 data: data, |
| 338 size: int(size), |
| 339 } |
| 340 }() |
| 341 } |
| 342 }() |
| 343 |
| 344 return catFileService{path, ch} |
| 345 } |
| OLD | NEW |