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 "bufio" | |
| 8 "bytes" | |
| 9 "fmt" | |
| 10 "os/exec" | |
| 11 "strconv" | |
| 12 "strings" | |
| 13 "sync" | |
| 14 "sync/atomic" | |
| 15 "unsafe" | |
| 16 | |
| 17 "infra/libs/infra_util" | |
| 18 ) | |
| 19 | |
| 20 type blobOpt bool | |
| 21 | |
| 22 const ( | |
| 23 WithBlobs blobOpt = true | |
| 24 NoBlobs = false | |
|
M-A Ruel
2014/10/21 00:55:54
If the type is not exported, the consts should not
iannucci
2016/05/23 21:53:42
Why not? I don't want them constructing their own
| |
| 25 ) | |
| 26 | |
| 27 type fullTree bool | |
| 28 | |
| 29 const ( | |
| 30 FullTree fullTree = true | |
| 31 MissingOK = false | |
| 32 ) | |
| 33 | |
| 34 // Repo represents a local git repository at the path |Path|. | |
| 35 type Repo struct { | |
| 36 // The path on disk to the location of the git repo. | |
| 37 Path string | |
| 38 | |
| 39 catFile *chan<- catFileRequest | |
|
M-A Ruel
2014/10/21 00:55:54
I still don't understand why it's a pointer. Creat
| |
| 40 catFileCheck *chan<- catFileRequest | |
| 41 } | |
| 42 | |
| 43 // TreeDiff represents the difference between two Treeish objects | |
| 44 type TreeDiff []TreeDiffEntry | |
| 45 | |
| 46 // TreeDiffEntry represents the before and after of one path in the repo. | |
| 47 // Note that the Old.Name and New.Name may be different if this item was | |
| 48 // Moved or Copied. | |
| 49 type TreeDiffEntry struct { | |
| 50 // Actino is one of "ACDMRTUX" | |
|
M-A Ruel
2014/10/21 00:55:54
Action
iannucci
2016/05/23 21:53:42
oops. done.
| |
| 51 // U is for unmerged... if you're just comparing trees you should never see this | |
| 52 // X is probably a bug in git... you should also never see this. | |
| 53 // T is a type change, so if a tree turned into a blob, for example | |
| 54 Action string | |
| 55 | |
| 56 // For Action types 'R' or 'C', what percentage (from 0-100) are the old and | |
| 57 // new blobs similar. | |
| 58 Similarity int | |
| 59 | |
| 60 Old TreeDiffEntryHalf | |
| 61 New TreeDiffEntryHalf | |
| 62 } | |
| 63 | |
| 64 // TreeDiffEntryHalf is one entry in a TreeDiffEntry, either the Old or New half . | |
| 65 type TreeDiffEntryHalf struct { | |
| 66 Child | |
| 67 Name string | |
| 68 } | |
| 69 | |
| 70 func (t *TreeDiffEntryHalf) String() string { | |
| 71 return fmt.Sprintf("%s: %s", t.Name, t.Child) | |
| 72 } | |
| 73 | |
| 74 // InternableObject is an Object which may be interned into a git repo | |
| 75 // (e.g. hash-object'd). EmptyObjects are NOT InternableObjects. | |
| 76 type InternableObject interface { | |
| 77 Object | |
| 78 | |
| 79 // A hash-object compatible string. May panic if this is an Object which is | |
| 80 // !Complete(). Note that for Trees this does NOT imply that all of the | |
| 81 // Tree's children are Complete(). | |
| 82 RawString() string | |
|
M-A Ruel
2014/10/21 00:55:54
As with the other part, I think []byte is a bit mo
iannucci
2016/05/23 21:53:42
but then I have to copy all that data or ppl could
| |
| 83 } | |
| 84 | |
| 85 // RunInput runs a git command in the Repo, passing input on stdin, and returnin g | |
| 86 // the stdout and success (i.e. did the git command return 0?) | |
| 87 func (r *Repo) RunInput(input string, cmd ...string) (string, bool) { | |
|
M-A Ruel
2014/10/21 00:55:54
That's an example of a function that could accept
iannucci
2016/05/23 21:53:42
I'm not sure why that would be helpful though, sin
| |
| 88 ex := exec.Command("git", cmd...) | |
| 89 ex.Dir = r.Path | |
| 90 ex.Stdin = bytes.NewBufferString(input) | |
| 91 out, err := ex.Output() | |
| 92 if err == nil { | |
| 93 return string(out), true | |
| 94 } else if _, ok := err.(*exec.ExitError); ok { | |
| 95 return string(out), false | |
| 96 } else { | |
| 97 panic(err) | |
|
Vadim Sh.
2014/10/21 15:26:59
when this can happen?
| |
| 98 } | |
| 99 } | |
| 100 | |
| 101 // Run runs a git command in the Repo, with no input, returning the stdout and | |
| 102 // success. | |
| 103 func (r *Repo) Run(cmd ...string) (string, bool) { | |
| 104 return r.RunInput("", cmd...) | |
| 105 } | |
| 106 | |
| 107 // RunOk runs a git command in the repo with no input, and returns its success | |
| 108 // (did it exit 0?) | |
| 109 func (r *Repo) RunOk(cmd ...string) bool { | |
| 110 _, ok := r.Run(cmd...) | |
| 111 return ok | |
| 112 } | |
| 113 | |
| 114 // RunOutput runs a git command in the repo with no input, and returns its | |
| 115 // stdout | |
| 116 func (r *Repo) RunOutput(cmd ...string) string { | |
| 117 out, _ := r.Run(cmd...) | |
| 118 return out | |
| 119 } | |
| 120 | |
| 121 // GetObject will asynchronously fetch |objectish| from the Repo, and | |
| 122 // return a channel for the result. If the object is missing, GetObject will | |
|
Vadim Sh.
2014/10/21 15:27:00
I don't see it returning a channel... Also it's bl
| |
| 123 // push nil to the channel. | |
| 124 func (r *Repo) GetObject(objectish string) InternableObject { | |
| 125 r.ensureCatFileServer(&r.catFile, false) | |
| 126 | |
| 127 rchan := make(chan *catFileReply, 1) | |
|
Vadim Sh.
2014/10/21 15:26:59
Does go have thread pool implementation?
IMHO, it
| |
| 128 (*r.catFile) <- catFileRequest{ | |
| 129 objectish: objectish, | |
| 130 reply: rchan, | |
| 131 } | |
| 132 | |
| 133 if rsp := <-rchan; rsp == nil { | |
| 134 return nil | |
| 135 } else { | |
| 136 switch rsp.typ { | |
| 137 case BlobType: | |
| 138 return BlobFromRawWithID(rsp.id, rsp.data) | |
| 139 case TreeType: | |
| 140 if t, err := TreeFromRawWithID(rsp.id, rsp.data); err != nil { | |
| 141 panic(err) | |
| 142 } else { | |
| 143 return t | |
| 144 } | |
| 145 case CommitType: | |
| 146 if c, err := CommitFromRawWithID(rsp.id, rsp.data); err != nil { | |
| 147 panic(err) | |
| 148 } else { | |
| 149 return c | |
| 150 } | |
| 151 default: | |
| 152 panic(fmt.Errorf("unsupported object type: %s", rsp.typ) ) | |
| 153 } | |
| 154 } | |
| 155 } | |
| 156 | |
| 157 func (r *Repo) GetObjectID(id ObjectID) InternableObject { | |
| 158 return r.GetObject(id.String()) | |
| 159 } | |
| 160 | |
| 161 // HasObject will return true iff the Repo contains the objectish | |
|
M-A Ruel
2014/10/21 00:55:54
objectish or ref?
iannucci
2016/05/23 21:53:42
objectish. could be a hash, ref, hash:path/to/file
| |
| 162 func (r *Repo) HasObject(objectish string) bool { | |
| 163 r.ensureCatFileServer(&r.catFileCheck, true) | |
| 164 rchan := make(chan *catFileReply, 1) | |
| 165 (*r.catFileCheck) <- catFileRequest{ | |
| 166 objectish: objectish, | |
| 167 reply: rchan, | |
| 168 } | |
| 169 return (<-rchan) != nil | |
| 170 } | |
| 171 | |
| 172 func (r *Repo) HasObjectID(id ObjectID) bool { | |
|
M-A Ruel
2014/10/21 00:55:54
What about just this function and not have HasObje
iannucci
2016/05/23 21:53:42
It is, but it's less powerful.
| |
| 173 return r.HasObject(id.String()) | |
| 174 } | |
| 175 | |
| 176 // GetFullTree gets a recursively-enumerated Tree. | |
| 177 // | |
| 178 // blobOpt: | |
| 179 // WithBlobs - Load blobs from Repo | |
| 180 // NoBlobs - Blobs will be EmptyObject | |
| 181 // | |
| 182 // fullTree: | |
| 183 // FullTree - All entries in tree must load | |
| 184 // MissingOK - Missing entries will remain EmptyObject (or an !Complete() Tree ) | |
| 185 func (r *Repo) GetFullTree(treeish string, b blobOpt, f fullTree) *Tree { | |
|
M-A Ruel
2014/10/21 00:55:54
This function likely doesn't need to be a method.
| |
| 186 base, ok := r.GetObject(treeish).(*Tree) | |
| 187 if !ok { | |
| 188 if f == FullTree { | |
| 189 panic(fmt.Errorf("could not load object %s", treeish)) | |
| 190 } else { | |
| 191 return nil | |
| 192 } | |
| 193 } | |
| 194 grp := sync.WaitGroup{} | |
| 195 for p, c := range base.children { | |
| 196 p := p | |
| 197 c := c | |
| 198 grp.Add(1) | |
| 199 go func() { | |
| 200 defer grp.Done() | |
| 201 switch c.Mode.Type() { | |
| 202 case TreeType: | |
| 203 subtree := r.GetFullTreeID(c.Object.ID(), b, f) | |
| 204 if subtree == nil { | |
| 205 if f == FullTree { | |
| 206 panic(fmt.Errorf("could not load tree %s", c.Object.ID())) | |
| 207 } | |
| 208 } else { | |
| 209 base.children[p] = &Child{subtree, c.Mod e} | |
| 210 } | |
| 211 case BlobType: | |
| 212 if b == WithBlobs { | |
| 213 rslt := r.GetObjectID(c.Object.ID()) | |
| 214 if rslt == nil && f == FullTree { | |
| 215 panic(fmt.Errorf("could not load object %s", c.Object.ID())) | |
| 216 } | |
| 217 base.children[p] = &Child{rslt, c.Mode} | |
| 218 } | |
| 219 } | |
| 220 }() | |
| 221 } | |
| 222 grp.Wait() | |
| 223 return base | |
| 224 } | |
| 225 | |
| 226 func (r *Repo) GetFullTreeID(tree ObjectID, b blobOpt, f fullTree) *Tree { | |
|
M-A Ruel
2014/10/21 00:55:54
Keep this one, remove GetFullTree()
Vadim Sh.
2014/10/21 15:26:59
At this point you gave up writing comments? :)
| |
| 227 return r.GetFullTree(tree.String(), b, f) | |
| 228 } | |
| 229 | |
| 230 func (r *Repo) GetTextDiff(left, right string) (string, error) { | |
| 231 rslt, ok := r.Run("diff", left, right) | |
|
M-A Ruel
2014/10/21 00:55:54
At the very least, you want --no-ext-diff
| |
| 232 if !ok { | |
| 233 return "", fmt.Errorf("cannot diff(%s, %s): %s", left, right, rs lt) | |
| 234 } | |
| 235 return rslt, nil | |
| 236 } | |
| 237 | |
| 238 // DiffTree computes the 2-tree diff (with copy/rename detection) and returns | |
| 239 // a parsed TreeDiff of what it found. | |
| 240 // | |
| 241 // This diff-tree invocation is done with -t, which implies that it is recursive , | |
| 242 // and that the actual intermediate tree objects will also be contianed in the | |
| 243 // return value. | |
| 244 func (r *Repo) DiffTree(left, right string) (ret TreeDiff, err error) { | |
| 245 atoi := func(s string, base int) int { | |
|
M-A Ruel
2014/10/21 00:55:54
Another external function.
I think it should acce
| |
| 246 ret, err := strconv.ParseInt(s, base, 0) | |
| 247 if err != nil { | |
| 248 panic(err) | |
| 249 } | |
| 250 return int(ret) | |
| 251 } | |
| 252 | |
| 253 lines := strings.Split(strings.TrimRight( | |
| 254 r.RunOutput("diff-tree", "-t", "-z", "-M", "-M", "-C", left, rig ht), "\000"), | |
| 255 "\000") | |
| 256 | |
| 257 infoStream := make(chan string, len(lines)) | |
| 258 for _, line := range lines { | |
| 259 infoStream <- line | |
| 260 } | |
| 261 close(infoStream) | |
| 262 for header := range infoStream { | |
| 263 if len(header) == 0 { | |
| 264 break | |
| 265 } | |
| 266 if header[0] != ':' { | |
| 267 return nil, fmt.Errorf("git.DiffTree: desynchronized par sing error") | |
| 268 } | |
| 269 info := strings.Fields(strings.TrimLeft(header, ":")) | |
| 270 // old_mode new_mode old_id new_id action | |
| 271 // oldPath (if action[0] in "RC") | |
| 272 // newPath | |
| 273 action := info[4] | |
| 274 similarity := 0 | |
| 275 oldPath := <-infoStream | |
| 276 newPath := oldPath | |
| 277 if action[0] == 'R' || action[0] == 'C' { | |
| 278 newPath = <-infoStream | |
| 279 similarity = atoi(action[1:], 10) | |
| 280 } | |
| 281 | |
| 282 ret = append(ret, TreeDiffEntry{ | |
| 283 Action: action, | |
| 284 Similarity: similarity, | |
| 285 Old: TreeDiffEntryHalf{ | |
| 286 *NewEmptyChild(Mode(atoi(info[0], 8)), MakeObjec tID(info[2])), | |
| 287 oldPath, | |
| 288 }, | |
| 289 New: TreeDiffEntryHalf{ | |
| 290 *NewEmptyChild(Mode(atoi(info[1], 8)), MakeObjec tID(info[3])), | |
| 291 newPath, | |
| 292 }, | |
| 293 }) | |
| 294 } | |
| 295 | |
| 296 return | |
| 297 } | |
| 298 | |
| 299 // Intern takes an InternableObject (Blob, Tree, Commit), and writes it into | |
| 300 // the on-disk Repo. | |
| 301 func (r *Repo) Intern(obj InternableObject) (ObjectID, error) { | |
|
M-A Ruel
2014/10/21 00:55:54
I find the name surprising.
I would have thought
| |
| 302 gotData := false | |
| 303 var data string | |
| 304 if !obj.Complete() { | |
| 305 gotData = true | |
| 306 data = obj.RawString() | |
| 307 } | |
| 308 | |
| 309 if obj.ID() != NoID && r.HasObjectID(obj.ID()) { | |
| 310 return obj.ID(), nil | |
| 311 } | |
| 312 | |
| 313 switch obj.Type() { | |
| 314 case CommitType, BlobType, TreeType: | |
| 315 default: | |
| 316 return NoID, fmt.Errorf("git.Intern: Unrecognized type %s", obj. Type()) | |
| 317 } | |
| 318 if !gotData { | |
| 319 data = obj.RawString() | |
| 320 } | |
| 321 cmd := []string{"hash-object", "-t", string(obj.Type()), "-w", "--stdin" } | |
| 322 out, ok := r.RunInput(data, cmd...) | |
| 323 if !ok { | |
| 324 return NoID, fmt.Errorf("error running %s <- %s: not ok", cmd, d ata) | |
| 325 } | |
| 326 return MakeObjectID(strings.TrimSpace(string(out))), nil | |
| 327 } | |
| 328 | |
| 329 /// Private | |
| 330 | |
| 331 type catFileReply struct { | |
| 332 id ObjectID | |
| 333 typ ObjectType | |
| 334 size int | |
| 335 data []byte | |
| 336 } | |
| 337 | |
| 338 type catFileRequest struct { | |
| 339 objectish string | |
| 340 reply chan<- *catFileReply | |
|
M-A Ruel
2014/10/21 00:55:54
I think it's fine to reply instances, the object i
| |
| 341 } | |
| 342 | |
| 343 func (r *Repo) ensureCatFileServer(ch **chan<- catFileRequest, checkOnly bool) { | |
| 344 if *ch == nil { | |
| 345 c := make(chan catFileRequest, 16) | |
| 346 swapped := atomic.CompareAndSwapPointer( | |
|
Vadim Sh.
2014/10/21 15:27:00
fancy
| |
| 347 (*unsafe.Pointer)(unsafe.Pointer(ch)), | |
| 348 nil, | |
| 349 unsafe.Pointer(&c), | |
| 350 ) | |
| 351 if swapped { | |
| 352 go r.catFileServer(c, checkOnly) | |
| 353 } | |
| 354 } | |
| 355 } | |
| 356 | |
| 357 func (r *Repo) catFileServer(rchan chan catFileRequest, checkOnly bool) { | |
| 358 defer func() { | |
| 359 atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&r.catFile) ), nil) | |
| 360 close(rchan) | |
| 361 | |
| 362 if err := recover(); err != nil { | |
| 363 fmt.Println("recovering panick'd catFileServer", err) | |
|
M-A Ruel
2014/10/21 00:55:54
Why recover()?
| |
| 364 } | |
| 365 }() | |
| 366 | |
| 367 arg := "--batch" | |
| 368 if checkOnly { | |
| 369 arg = "--batch-check" | |
| 370 } | |
| 371 catFile := exec.Command("git", "cat-file", arg) | |
| 372 catFile.Dir = r.Path | |
| 373 in, err := catFile.StdinPipe() | |
| 374 if err != nil { | |
| 375 panic(err) | |
| 376 } | |
| 377 defer in.Close() | |
| 378 outRaw, err := catFile.StdoutPipe() | |
| 379 if err != nil { | |
| 380 panic(err) | |
| 381 } | |
| 382 defer outRaw.Close() | |
| 383 out := bufio.NewReader(outRaw) | |
| 384 | |
| 385 if err = catFile.Start(); err != nil { | |
| 386 panic(err) | |
| 387 } | |
| 388 | |
| 389 nom := infra_util.Nom(out) | |
| 390 yoink := infra_util.Yoink(out) | |
| 391 | |
| 392 for req := range rchan { | |
|
Vadim Sh.
2014/10/21 15:26:59
who closes rchan? When the goroutine die?
| |
| 393 if strings.ContainsAny(req.objectish, "\n") { | |
| 394 panic("catFile request may not contain a newline") | |
| 395 } | |
| 396 | |
| 397 in.Write([]byte(req.objectish + "\n")) | |
| 398 rsp := nom('\n') | |
| 399 if strings.HasSuffix(rsp, " missing") { | |
| 400 req.reply <- nil | |
| 401 continue | |
| 402 } | |
| 403 | |
| 404 parts := strings.Split(rsp, " ") | |
| 405 objID, typ, sizeStr := parts[0], parts[1], parts[2] | |
| 406 size, err := strconv.ParseUint(sizeStr, 10, 64) | |
| 407 if err != nil { | |
| 408 panic(err) | |
| 409 } | |
| 410 | |
| 411 data := []byte{} | |
| 412 if !checkOnly { | |
| 413 data = yoink(int(size)) | |
| 414 out.ReadByte() // drop extra newline | |
| 415 } | |
| 416 req.reply <- &catFileReply{ | |
| 417 id: MakeObjectID(objID), | |
| 418 typ: MakeObjectType(typ), | |
| 419 data: data, | |
| 420 size: int(size), | |
| 421 } | |
| 422 } | |
| 423 } | |
| OLD | NEW |