git.go

  1// Package repository contains helper methods for working with the Git repo.
  2package repository
  3
  4import (
  5	"bytes"
  6	"crypto/sha1"
  7	"fmt"
  8	"io"
  9	"os"
 10	"os/exec"
 11	"strings"
 12)
 13
 14const branchRefPrefix = "refs/heads/"
 15
 16// GitRepo represents an instance of a (local) git repository.
 17type GitRepo struct {
 18	Path string
 19}
 20
 21// Run the given git command with the given I/O reader/writers, returning an error if it fails.
 22func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
 23	cmd := exec.Command("git", args...)
 24	cmd.Dir = repo.Path
 25	cmd.Stdin = stdin
 26	cmd.Stdout = stdout
 27	cmd.Stderr = stderr
 28	return cmd.Run()
 29}
 30
 31// Run the given git command and return its stdout, or an error if the command fails.
 32func (repo *GitRepo) runGitCommandRaw(args ...string) (string, string, error) {
 33	var stdout bytes.Buffer
 34	var stderr bytes.Buffer
 35	err := repo.runGitCommandWithIO(nil, &stdout, &stderr, args...)
 36	return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
 37}
 38
 39// Run the given git command and return its stdout, or an error if the command fails.
 40func (repo *GitRepo) runGitCommand(args ...string) (string, error) {
 41	stdout, stderr, err := repo.runGitCommandRaw(args...)
 42	if err != nil {
 43		if stderr == "" {
 44			stderr = "Error running git command: " + strings.Join(args, " ")
 45		}
 46		err = fmt.Errorf(stderr)
 47	}
 48	return stdout, err
 49}
 50
 51// Run the given git command using the same stdin, stdout, and stderr as the review tool.
 52func (repo *GitRepo) runGitCommandInline(args ...string) error {
 53	return repo.runGitCommandWithIO(os.Stdin, os.Stdout, os.Stderr, args...)
 54}
 55
 56// NewGitRepo determines if the given working directory is inside of a git repository,
 57// and returns the corresponding GitRepo instance if it is.
 58func NewGitRepo(path string) (*GitRepo, error) {
 59	repo := &GitRepo{Path: path}
 60	_, _, err := repo.runGitCommandRaw("rev-parse")
 61	if err == nil {
 62		return repo, nil
 63	}
 64	if _, ok := err.(*exec.ExitError); ok {
 65		return nil, err
 66	}
 67	return nil, err
 68}
 69
 70// GetPath returns the path to the repo.
 71func (repo *GitRepo) GetPath() string {
 72	return repo.Path
 73}
 74
 75// GetRepoStateHash returns a hash which embodies the entire current state of a repository.
 76func (repo *GitRepo) GetRepoStateHash() (string, error) {
 77	stateSummary, err := repo.runGitCommand("show-ref")
 78	return fmt.Sprintf("%x", sha1.Sum([]byte(stateSummary))), err
 79}
 80
 81// GetUserEmail returns the email address that the user has used to configure git.
 82func (repo *GitRepo) GetUserEmail() (string, error) {
 83	return repo.runGitCommand("config", "user.email")
 84}
 85
 86// GetCoreEditor returns the name of the editor that the user has used to configure git.
 87func (repo *GitRepo) GetCoreEditor() (string, error) {
 88	return repo.runGitCommand("var", "GIT_EDITOR")
 89}
 90
 91
 92// PullRefs pull git refs from a remote
 93func (repo *GitRepo) PullRefs(remote string, refPattern string) error {
 94	refspec := fmt.Sprintf("%s:%s", refPattern, refPattern)
 95
 96	// The push is liable to fail if the user forgot to do a pull first, so
 97	// we treat errors as user errors rather than fatal errors.
 98	err := repo.runGitCommandInline("push", remote, refspec)
 99	if err != nil {
100		return fmt.Errorf("failed to push to the remote '%s': %v", remote, err)
101	}
102	return nil
103}
104
105// PushRefs push git refs to a remote
106func (repo *GitRepo) PushRefs(remote string, refPattern string) error {
107	remoteNotesRefPattern := getRemoteNotesRef(remote, refPattern)
108	fetchRefSpec := fmt.Sprintf("+%s:%s", refPattern, remoteNotesRefPattern)
109	err := repo.runGitCommandInline("fetch", remote, fetchRefSpec)
110	if err != nil {
111		return err
112	}
113
114	return repo.mergeRemoteNotes(remote, notesRefPattern)
115}
116
117/*
118//
119//// GetSubmitStrategy returns the way in which a review is submitted
120//func (repo *GitRepo) GetSubmitStrategy() (string, error) {
121//	submitStrategy, _ := repo.runGitCommand("config", "appraise.submit")
122//	return submitStrategy, nil
123//}
124//
125//// HasUncommittedChanges returns true if there are local, uncommitted changes.
126//func (repo *GitRepo) HasUncommittedChanges() (bool, error) {
127//	out, err := repo.runGitCommand("status", "--porcelain")
128//	if err != nil {
129//		return false, err
130//	}
131//	if len(out) > 0 {
132//		return true, nil
133//	}
134//	return false, nil
135//}
136//
137//// VerifyCommit verifies that the supplied hash points to a known commit.
138//func (repo *GitRepo) VerifyCommit(hash string) error {
139//	out, err := repo.runGitCommand("cat-file", "-t", hash)
140//	if err != nil {
141//		return err
142//	}
143//	objectType := strings.TrimSpace(string(out))
144//	if objectType != "commit" {
145//		return fmt.Errorf("Hash %q points to a non-commit object of type %q", hash, objectType)
146//	}
147//	return nil
148//}
149//
150//// VerifyGitRef verifies that the supplied ref points to a known commit.
151//func (repo *GitRepo) VerifyGitRef(ref string) error {
152//	_, err := repo.runGitCommand("show-ref", "--verify", ref)
153//	return err
154//}
155//
156//// GetHeadRef returns the ref that is the current HEAD.
157//func (repo *GitRepo) GetHeadRef() (string, error) {
158//	return repo.runGitCommand("symbolic-ref", "HEAD")
159//}
160//
161//// GetCommitHash returns the hash of the commit pointed to by the given ref.
162//func (repo *GitRepo) GetCommitHash(ref string) (string, error) {
163//	return repo.runGitCommand("show", "-s", "--format=%H", ref)
164//}
165//
166//// ResolveRefCommit returns the commit pointed to by the given ref, which may be a remote ref.
167////
168//// This differs from GetCommitHash which only works on exact matches, in that it will try to
169//// intelligently handle the scenario of a ref not existing locally, but being known to exist
170//// in a remote repo.
171////
172//// This method should be used when a command may be performed by either the reviewer or the
173//// reviewee, while GetCommitHash should be used when the encompassing command should only be
174//// performed by the reviewee.
175//func (repo *GitRepo) ResolveRefCommit(ref string) (string, error) {
176//	if err := repo.VerifyGitRef(ref); err == nil {
177//		return repo.GetCommitHash(ref)
178//	}
179//	if strings.HasPrefix(ref, "refs/heads/") {
180//		// The ref is a branch. Check if it exists in exactly one remote
181//		pattern := strings.Replace(ref, "refs/heads", "**", 1)
182//		matchingOutput, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", pattern)
183//		if err != nil {
184//			return "", err
185//		}
186//		matchingRefs := strings.Split(matchingOutput, "\n")
187//		if len(matchingRefs) == 1 && matchingRefs[0] != "" {
188//			// There is exactly one match
189//			return repo.GetCommitHash(matchingRefs[0])
190//		}
191//		return "", fmt.Errorf("Unable to find a git ref matching the pattern %q", pattern)
192//	}
193//	return "", fmt.Errorf("Unknown git ref %q", ref)
194//}
195//
196//// GetCommitMessage returns the message stored in the commit pointed to by the given ref.
197//func (repo *GitRepo) GetCommitMessage(ref string) (string, error) {
198//	return repo.runGitCommand("show", "-s", "--format=%B", ref)
199//}
200//
201//// GetCommitTime returns the commit time of the commit pointed to by the given ref.
202//func (repo *GitRepo) GetCommitTime(ref string) (string, error) {
203//	return repo.runGitCommand("show", "-s", "--format=%ct", ref)
204//}
205//
206//// GetLastParent returns the last parent of the given commit (as ordered by git).
207//func (repo *GitRepo) GetLastParent(ref string) (string, error) {
208//	return repo.runGitCommand("rev-list", "--skip", "1", "-n", "1", ref)
209//}
210//
211//// GetCommitDetails returns the details of a commit's metadata.
212//func (repo GitRepo) GetCommitDetails(ref string) (*CommitDetails, error) {
213//	var err error
214//	show := func(formatString string) (result string) {
215//		if err != nil {
216//			return ""
217//		}
218//		result, err = repo.runGitCommand("show", "-s", ref, fmt.Sprintf("--format=tformat:%s", formatString))
219//		return result
220//	}
221//
222//	jsonFormatString := "{\"tree\":\"%T\", \"time\": \"%at\"}"
223//	detailsJSON := show(jsonFormatString)
224//	if err != nil {
225//		return nil, err
226//	}
227//	var details CommitDetails
228//	err = json.Unmarshal([]byte(detailsJSON), &details)
229//	if err != nil {
230//		return nil, err
231//	}
232//	details.Author = show("%an")
233//	details.AuthorEmail = show("%ae")
234//	details.Summary = show("%s")
235//	parentsString := show("%P")
236//	details.Parents = strings.Split(parentsString, " ")
237//	if err != nil {
238//		return nil, err
239//	}
240//	return &details, nil
241//}
242//
243//// MergeBase determines if the first commit that is an ancestor of the two arguments.
244//func (repo *GitRepo) MergeBase(a, b string) (string, error) {
245//	return repo.runGitCommand("merge-base", a, b)
246//}
247//
248//// IsAncestor determines if the first argument points to a commit that is an ancestor of the second.
249//func (repo *GitRepo) IsAncestor(ancestor, descendant string) (bool, error) {
250//	_, _, err := repo.runGitCommandRaw("merge-base", "--is-ancestor", ancestor, descendant)
251//	if err == nil {
252//		return true, nil
253//	}
254//	if _, ok := err.(*exec.ExitError); ok {
255//		return false, nil
256//	}
257//	return false, fmt.Errorf("Error while trying to determine commit ancestry: %v", err)
258//}
259//
260//// Diff computes the diff between two given commits.
261//func (repo *GitRepo) Diff(left, right string, diffArgs ...string) (string, error) {
262//	args := []string{"diff"}
263//	args = append(args, diffArgs...)
264//	args = append(args, fmt.Sprintf("%s..%s", left, right))
265//	return repo.runGitCommand(args...)
266//}
267//
268//// Show returns the contents of the given file at the given commit.
269//func (repo *GitRepo) Show(commit, path string) (string, error) {
270//	return repo.runGitCommand("show", fmt.Sprintf("%s:%s", commit, path))
271//}
272//
273//// SwitchToRef changes the currently-checked-out ref.
274//func (repo *GitRepo) SwitchToRef(ref string) error {
275//	// If the ref starts with "refs/heads/", then we have to trim that prefix,
276//	// or else we will wind up in a detached HEAD state.
277//	if strings.HasPrefix(ref, branchRefPrefix) {
278//		ref = ref[len(branchRefPrefix):]
279//	}
280//	_, err := repo.runGitCommand("checkout", ref)
281//	return err
282//}
283//
284//// mergeArchives merges two archive refs.
285//func (repo *GitRepo) mergeArchives(archive, remoteArchive string) error {
286//	remoteHash, err := repo.GetCommitHash(remoteArchive)
287//	if err != nil {
288//		return err
289//	}
290//	if remoteHash == "" {
291//		// The remote archive does not exist, so we have nothing to do
292//		return nil
293//	}
294//
295//	archiveHash, err := repo.GetCommitHash(archive)
296//	if err != nil {
297//		return err
298//	}
299//	if archiveHash == "" {
300//		// The local archive does not exist, so we merely need to set it
301//		_, err := repo.runGitCommand("update-ref", archive, remoteHash)
302//		return err
303//	}
304//
305//	isAncestor, err := repo.IsAncestor(archiveHash, remoteHash)
306//	if err != nil {
307//		return err
308//	}
309//	if isAncestor {
310//		// The archive can simply be fast-forwarded
311//		_, err := repo.runGitCommand("update-ref", archive, remoteHash, archiveHash)
312//		return err
313//	}
314//
315//	// Create a merge commit of the two archives
316//	refDetails, err := repo.GetCommitDetails(remoteArchive)
317//	if err != nil {
318//		return err
319//	}
320//	newArchiveHash, err := repo.runGitCommand("commit-tree", "-p", remoteHash, "-p", archiveHash, "-m", "Merge local and remote archives", refDetails.Tree)
321//	if err != nil {
322//		return err
323//	}
324//	newArchiveHash = strings.TrimSpace(newArchiveHash)
325//	_, err = repo.runGitCommand("update-ref", archive, newArchiveHash, archiveHash)
326//	return err
327//}
328//
329//// ArchiveRef adds the current commit pointed to by the 'ref' argument
330//// under the ref specified in the 'archive' argument.
331////
332//// Both the 'ref' and 'archive' arguments are expected to be the fully
333//// qualified names of git refs (e.g. 'refs/heads/my-change' or
334//// 'refs/devtools/archives/reviews').
335////
336//// If the ref pointed to by the 'archive' argument does not exist
337//// yet, then it will be created.
338//func (repo *GitRepo) ArchiveRef(ref, archive string) error {
339//	refHash, err := repo.GetCommitHash(ref)
340//	if err != nil {
341//		return err
342//	}
343//	refDetails, err := repo.GetCommitDetails(ref)
344//	if err != nil {
345//		return err
346//	}
347//
348//	commitTreeArgs := []string{"commit-tree"}
349//	archiveHash, err := repo.GetCommitHash(archive)
350//	if err != nil {
351//		archiveHash = ""
352//	} else {
353//		commitTreeArgs = append(commitTreeArgs, "-p", archiveHash)
354//	}
355//	commitTreeArgs = append(commitTreeArgs, "-p", refHash, "-m", fmt.Sprintf("Archive %s", refHash), refDetails.Tree)
356//	newArchiveHash, err := repo.runGitCommand(commitTreeArgs...)
357//	if err != nil {
358//		return err
359//	}
360//	newArchiveHash = strings.TrimSpace(newArchiveHash)
361//	updateRefArgs := []string{"update-ref", archive, newArchiveHash}
362//	if archiveHash != "" {
363//		updateRefArgs = append(updateRefArgs, archiveHash)
364//	}
365//	_, err = repo.runGitCommand(updateRefArgs...)
366//	return err
367//}
368//
369//// MergeRef merges the given ref into the current one.
370////
371//// The ref argument is the ref to merge, and fastForward indicates that the
372//// current ref should only move forward, as opposed to creating a bubble merge.
373//// The messages argument(s) provide text that should be included in the default
374//// merge commit message (separated by blank lines).
375//func (repo *GitRepo) MergeRef(ref string, fastForward bool, messages ...string) error {
376//	args := []string{"merge"}
377//	if fastForward {
378//		args = append(args, "--ff", "--ff-only")
379//	} else {
380//		args = append(args, "--no-ff")
381//	}
382//	if len(messages) > 0 {
383//		commitMessage := strings.Join(messages, "\n\n")
384//		args = append(args, "-e", "-m", commitMessage)
385//	}
386//	args = append(args, ref)
387//	return repo.runGitCommandInline(args...)
388//}
389//
390//// RebaseRef rebases the current ref onto the given one.
391//func (repo *GitRepo) RebaseRef(ref string) error {
392//	return repo.runGitCommandInline("rebase", "-i", ref)
393//}
394//
395//// ListCommits returns the list of commits reachable from the given ref.
396////
397//// The generated list is in chronological order (with the oldest commit first).
398////
399//// If the specified ref does not exist, then this method returns an empty result.
400//func (repo *GitRepo) ListCommits(ref string) []string {
401//	var stdout bytes.Buffer
402//	var stderr bytes.Buffer
403//	if err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "rev-list", "--reverse", ref); err != nil {
404//		return nil
405//	}
406//
407//	byteLines := bytes.Split(stdout.Bytes(), []byte("\n"))
408//	var commits []string
409//	for _, byteLine := range byteLines {
410//		commits = append(commits, string(byteLine))
411//	}
412//	return commits
413//}
414//
415//// ListCommitsBetween returns the list of commits between the two given revisions.
416////
417//// The "from" parameter is the starting point (exclusive), and the "to"
418//// parameter is the ending point (inclusive).
419////
420//// The "from" commit does not need to be an ancestor of the "to" commit. If it
421//// is not, then the merge base of the two is used as the starting point.
422//// Admittedly, this makes calling these the "between" commits is a bit of a
423//// misnomer, but it also makes the method easier to use when you want to
424//// generate the list of changes in a feature branch, as it eliminates the need
425//// to explicitly calculate the merge base. This also makes the semantics of the
426//// method compatible with git's built-in "rev-list" command.
427////
428//// The generated list is in chronological order (with the oldest commit first).
429//func (repo *GitRepo) ListCommitsBetween(from, to string) ([]string, error) {
430//	out, err := repo.runGitCommand("rev-list", "--reverse", from+".."+to)
431//	if err != nil {
432//		return nil, err
433//	}
434//	if out == "" {
435//		return nil, nil
436//	}
437//	return strings.Split(out, "\n"), nil
438//}
439//
440//// GetNotes uses the "git" command-line tool to read the notes from the given ref for a given revision.
441//func (repo *GitRepo) GetNotes(notesRef, revision string) []Note {
442//	var notes []Note
443//	rawNotes, err := repo.runGitCommand("notes", "--ref", notesRef, "show", revision)
444//	if err != nil {
445//		// We just assume that this means there are no notes
446//		return nil
447//	}
448//	for _, line := range strings.Split(rawNotes, "\n") {
449//		notes = append(notes, Note([]byte(line)))
450//	}
451//	return notes
452//}
453//
454//func stringsReader(s []*string) io.Reader {
455//	var subReaders []io.Reader
456//	for _, strPtr := range s {
457//		subReader := strings.NewReader(*strPtr)
458//		subReaders = append(subReaders, subReader, strings.NewReader("\n"))
459//	}
460//	return io.MultiReader(subReaders...)
461//}
462//
463//// splitBatchCheckOutput parses the output of a 'git cat-file --batch-check=...' command.
464////
465//// The output is expected to be formatted as a series of entries, with each
466//// entry consisting of:
467//// 1. The SHA1 hash of the git object being output, followed by a space.
468//// 2. The git "type" of the object (commit, blob, tree, missing, etc), followed by a newline.
469////
470//// To generate this format, make sure that the 'git cat-file' command includes
471//// the argument '--batch-check=%(objectname) %(objecttype)'.
472////
473//// The return value is a map from object hash to a boolean indicating if that object is a commit.
474//func splitBatchCheckOutput(out *bytes.Buffer) (map[string]bool, error) {
475//	isCommit := make(map[string]bool)
476//	reader := bufio.NewReader(out)
477//	for {
478//		nameLine, err := reader.ReadString(byte(' '))
479//		if err == io.EOF {
480//			return isCommit, nil
481//		}
482//		if err != nil {
483//			return nil, fmt.Errorf("Failure while reading the next object name: %v", err)
484//		}
485//		nameLine = strings.TrimSuffix(nameLine, " ")
486//		typeLine, err := reader.ReadString(byte('\n'))
487//		if err != nil && err != io.EOF {
488//			return nil, fmt.Errorf("Failure while reading the next object type: %q - %v", nameLine, err)
489//		}
490//		typeLine = strings.TrimSuffix(typeLine, "\n")
491//		if typeLine == "commit" {
492//			isCommit[nameLine] = true
493//		}
494//	}
495//}
496//
497//// splitBatchCatFileOutput parses the output of a 'git cat-file --batch=...' command.
498////
499//// The output is expected to be formatted as a series of entries, with each
500//// entry consisting of:
501//// 1. The SHA1 hash of the git object being output, followed by a newline.
502//// 2. The size of the object's contents in bytes, followed by a newline.
503//// 3. The objects contents.
504////
505//// To generate this format, make sure that the 'git cat-file' command includes
506//// the argument '--batch=%(objectname)\n%(objectsize)'.
507//func splitBatchCatFileOutput(out *bytes.Buffer) (map[string][]byte, error) {
508//	contentsMap := make(map[string][]byte)
509//	reader := bufio.NewReader(out)
510//	for {
511//		nameLine, err := reader.ReadString(byte('\n'))
512//		if strings.HasSuffix(nameLine, "\n") {
513//			nameLine = strings.TrimSuffix(nameLine, "\n")
514//		}
515//		if err == io.EOF {
516//			return contentsMap, nil
517//		}
518//		if err != nil {
519//			return nil, fmt.Errorf("Failure while reading the next object name: %v", err)
520//		}
521//		sizeLine, err := reader.ReadString(byte('\n'))
522//		if strings.HasSuffix(sizeLine, "\n") {
523//			sizeLine = strings.TrimSuffix(sizeLine, "\n")
524//		}
525//		if err != nil {
526//			return nil, fmt.Errorf("Failure while reading the next object size: %q - %v", nameLine, err)
527//		}
528//		size, err := strconv.Atoi(sizeLine)
529//		if err != nil {
530//			return nil, fmt.Errorf("Failure while parsing the next object size: %q - %v", nameLine, err)
531//		}
532//		contentBytes := make([]byte, size, size)
533//		readDest := contentBytes
534//		len := 0
535//		err = nil
536//		for err == nil && len < size {
537//			nextLen := 0
538//			nextLen, err = reader.Read(readDest)
539//			len += nextLen
540//			readDest = contentBytes[len:]
541//		}
542//		contentsMap[nameLine] = contentBytes
543//		if err == io.EOF {
544//			return contentsMap, nil
545//		}
546//		if err != nil {
547//			return nil, err
548//		}
549//		for bs, err := reader.Peek(1); err == nil && bs[0] == byte('\n'); bs, err = reader.Peek(1) {
550//			reader.ReadByte()
551//		}
552//	}
553//}
554//
555//// notesMapping represents the association between a git object and the notes for that object.
556//type notesMapping struct {
557//	ObjectHash *string
558//	NotesHash  *string
559//}
560//
561//// notesOverview represents a high-level overview of all the notes under a single notes ref.
562//type notesOverview struct {
563//	NotesMappings      []*notesMapping
564//	ObjectHashesReader io.Reader
565//	NotesHashesReader  io.Reader
566//}
567//
568//// notesOverview returns an overview of the git notes stored under the given ref.
569//func (repo *GitRepo) notesOverview(notesRef string) (*notesOverview, error) {
570//	var stdout bytes.Buffer
571//	var stderr bytes.Buffer
572//	if err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "notes", "--ref", notesRef, "list"); err != nil {
573//		return nil, err
574//	}
575//
576//	var notesMappings []*notesMapping
577//	var objHashes []*string
578//	var notesHashes []*string
579//	outScanner := bufio.NewScanner(&stdout)
580//	for outScanner.Scan() {
581//		line := outScanner.Text()
582//		lineParts := strings.Split(line, " ")
583//		if len(lineParts) != 2 {
584//			return nil, fmt.Errorf("Malformed output line from 'git-notes list': %q", line)
585//		}
586//		objHash := &lineParts[1]
587//		notesHash := &lineParts[0]
588//		notesMappings = append(notesMappings, &notesMapping{
589//			ObjectHash: objHash,
590//			NotesHash:  notesHash,
591//		})
592//		objHashes = append(objHashes, objHash)
593//		notesHashes = append(notesHashes, notesHash)
594//	}
595//	err := outScanner.Err()
596//	if err != nil && err != io.EOF {
597//		return nil, fmt.Errorf("Failure parsing the output of 'git-notes list': %v", err)
598//	}
599//	return &notesOverview{
600//		NotesMappings:      notesMappings,
601//		ObjectHashesReader: stringsReader(objHashes),
602//		NotesHashesReader:  stringsReader(notesHashes),
603//	}, nil
604//}
605//
606//// getIsCommitMap returns a mapping of all the annotated objects that are commits.
607//func (overview *notesOverview) getIsCommitMap(repo *GitRepo) (map[string]bool, error) {
608//	var stdout bytes.Buffer
609//	var stderr bytes.Buffer
610//	if err := repo.runGitCommandWithIO(overview.ObjectHashesReader, &stdout, &stderr, "cat-file", "--batch-check=%(objectname) %(objecttype)"); err != nil {
611//		return nil, fmt.Errorf("Failure performing a batch file check: %v", err)
612//	}
613//	isCommit, err := splitBatchCheckOutput(&stdout)
614//	if err != nil {
615//		return nil, fmt.Errorf("Failure parsing the output of a batch file check: %v", err)
616//	}
617//	return isCommit, nil
618//}
619//
620//// getNoteContentsMap returns a mapping from all the notes hashes to their contents.
621//func (overview *notesOverview) getNoteContentsMap(repo *GitRepo) (map[string][]byte, error) {
622//	var stdout bytes.Buffer
623//	var stderr bytes.Buffer
624//	if err := repo.runGitCommandWithIO(overview.NotesHashesReader, &stdout, &stderr, "cat-file", "--batch=%(objectname)\n%(objectsize)"); err != nil {
625//		return nil, fmt.Errorf("Failure performing a batch file read: %v", err)
626//	}
627//	noteContentsMap, err := splitBatchCatFileOutput(&stdout)
628//	if err != nil {
629//		return nil, fmt.Errorf("Failure parsing the output of a batch file read: %v", err)
630//	}
631//	return noteContentsMap, nil
632//}
633//
634//// GetAllNotes reads the contents of the notes under the given ref for every commit.
635////
636//// The returned value is a mapping from commit hash to the list of notes for that commit.
637////
638//// This is the batch version of the corresponding GetNotes(...) method.
639//func (repo *GitRepo) GetAllNotes(notesRef string) (map[string][]Note, error) {
640//	// This code is unfortunately quite complicated, but it needs to be so.
641//	//
642//	// Conceptually, this is equivalent to:
643//	//   result := make(map[string][]Note)
644//	//   for _, commit := range repo.ListNotedRevisions(notesRef) {
645//	//     result[commit] = repo.GetNotes(notesRef, commit)
646//	//   }
647//	//   return result, nil
648//	//
649//	// However, that logic would require separate executions of the 'git'
650//	// command for every annotated commit. For a repo with 10s of thousands
651//	// of reviews, that would mean calling Cmd.Run(...) 10s of thousands of
652//	// times. That, in turn, would take so long that the tool would be unusable.
653//	//
654//	// This method avoids that by taking advantage of the 'git cat-file --batch="..."'
655//	// command. That allows us to use a single invocation of Cmd.Run(...) to
656//	// inspect multiple git objects at once.
657//	//
658//	// As such, regardless of the number of reviews in a repo, we can get all
659//	// of the notes using a total of three invocations of Cmd.Run(...):
660//	//  1. One to list all the annotated objects (and their notes hash)
661//	//  2. A second one to filter out all of the annotated objects that are not commits.
662//	//  3. A final one to get the contents of all of the notes blobs.
663//	overview, err := repo.notesOverview(notesRef)
664//	if err != nil {
665//		return nil, err
666//	}
667//	isCommit, err := overview.getIsCommitMap(repo)
668//	if err != nil {
669//		return nil, fmt.Errorf("Failure building the set of commit objects: %v", err)
670//	}
671//	noteContentsMap, err := overview.getNoteContentsMap(repo)
672//	if err != nil {
673//		return nil, fmt.Errorf("Failure building the mapping from notes hash to contents: %v", err)
674//	}
675//	commitNotesMap := make(map[string][]Note)
676//	for _, notesMapping := range overview.NotesMappings {
677//		if !isCommit[*notesMapping.ObjectHash] {
678//			continue
679//		}
680//		noteBytes := noteContentsMap[*notesMapping.NotesHash]
681//		byteSlices := bytes.Split(noteBytes, []byte("\n"))
682//		var notes []Note
683//		for _, slice := range byteSlices {
684//			notes = append(notes, Note(slice))
685//		}
686//		commitNotesMap[*notesMapping.ObjectHash] = notes
687//	}
688//
689//	return commitNotesMap, nil
690//}
691//
692//// AppendNote appends a note to a revision under the given ref.
693//func (repo *GitRepo) AppendNote(notesRef, revision string, note Note) error {
694//	_, err := repo.runGitCommand("notes", "--ref", notesRef, "append", "-m", string(note), revision)
695//	return err
696//}
697//
698//// ListNotedRevisions returns the collection of revisions that are annotated by notes in the given ref.
699//func (repo *GitRepo) ListNotedRevisions(notesRef string) []string {
700//	var revisions []string
701//	notesListOut, err := repo.runGitCommand("notes", "--ref", notesRef, "list")
702//	if err != nil {
703//		return nil
704//	}
705//	notesList := strings.Split(notesListOut, "\n")
706//	for _, notePair := range notesList {
707//		noteParts := strings.SplitN(notePair, " ", 2)
708//		if len(noteParts) == 2 {
709//			objHash := noteParts[1]
710//			objType, err := repo.runGitCommand("cat-file", "-t", objHash)
711//			// If a note points to an object that we do not know about (yet), then err will not
712//			// be nil. We can safely just ignore those notes.
713//			if err == nil && objType == "commit" {
714//				revisions = append(revisions, objHash)
715//			}
716//		}
717//	}
718//	return revisions
719//}
720//
721//// PushNotes pushes git notes to a remote repo.
722//func (repo *GitRepo) PushNotes(remote, notesRefPattern string) error {
723//	refspec := fmt.Sprintf("%s:%s", notesRefPattern, notesRefPattern)
724//
725//	// The push is liable to fail if the user forgot to do a pull first, so
726//	// we treat errors as user errors rather than fatal errors.
727//	err := repo.runGitCommandInline("push", remote, refspec)
728//	if err != nil {
729//		return fmt.Errorf("Failed to push to the remote '%s': %v", remote, err)
730//	}
731//	return nil
732//}
733//
734//// PushNotesAndArchive pushes the given notes and archive refs to a remote repo.
735//func (repo *GitRepo) PushNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error {
736//	notesRefspec := fmt.Sprintf("%s:%s", notesRefPattern, notesRefPattern)
737//	archiveRefspec := fmt.Sprintf("%s:%s", archiveRefPattern, archiveRefPattern)
738//	err := repo.runGitCommandInline("push", remote, notesRefspec, archiveRefspec)
739//	if err != nil {
740//		return fmt.Errorf("Failed to push the local archive to the remote '%s': %v", remote, err)
741//	}
742//	return nil
743//}
744//
745//func getRemoteNotesRef(remote, localNotesRef string) string {
746//	relativeNotesRef := strings.TrimPrefix(localNotesRef, "refs/notes/")
747//	return "refs/notes/" + remote + "/" + relativeNotesRef
748//}
749//
750//func (repo *GitRepo) mergeRemoteNotes(remote, notesRefPattern string) error {
751//	remoteRefs, err := repo.runGitCommand("ls-remote", remote, notesRefPattern)
752//	if err != nil {
753//		return err
754//	}
755//	for _, line := range strings.Split(remoteRefs, "\n") {
756//		lineParts := strings.Split(line, "\t")
757//		if len(lineParts) == 2 {
758//			ref := lineParts[1]
759//			remoteRef := getRemoteNotesRef(remote, ref)
760//			_, err := repo.runGitCommand("notes", "--ref", ref, "merge", remoteRef, "-s", "cat_sort_uniq")
761//			if err != nil {
762//				return err
763//			}
764//		}
765//	}
766//	return nil
767//}
768//
769//// PullNotes fetches the contents of the given notes ref from a remote repo,
770//// and then merges them with the corresponding local notes using the
771//// "cat_sort_uniq" strategy.
772//func (repo *GitRepo) PullNotes(remote, notesRefPattern string) error {
773//	remoteNotesRefPattern := getRemoteNotesRef(remote, notesRefPattern)
774//	fetchRefSpec := fmt.Sprintf("+%s:%s", notesRefPattern, remoteNotesRefPattern)
775//	err := repo.runGitCommandInline("fetch", remote, fetchRefSpec)
776//	if err != nil {
777//		return err
778//	}
779//
780//	return repo.mergeRemoteNotes(remote, notesRefPattern)
781//}
782//
783//func getRemoteArchiveRef(remote, archiveRefPattern string) string {
784//	relativeArchiveRef := strings.TrimPrefix(archiveRefPattern, "refs/devtools/archives/")
785//	return "refs/devtools/remoteArchives/" + remote + "/" + relativeArchiveRef
786//}
787//
788//func (repo *GitRepo) mergeRemoteArchives(remote, archiveRefPattern string) error {
789//	remoteRefs, err := repo.runGitCommand("ls-remote", remote, archiveRefPattern)
790//	if err != nil {
791//		return err
792//	}
793//	for _, line := range strings.Split(remoteRefs, "\n") {
794//		lineParts := strings.Split(line, "\t")
795//		if len(lineParts) == 2 {
796//			ref := lineParts[1]
797//			remoteRef := getRemoteArchiveRef(remote, ref)
798//			if err := repo.mergeArchives(ref, remoteRef); err != nil {
799//				return err
800//			}
801//		}
802//	}
803//	return nil
804//}
805//
806//// PullNotesAndArchive fetches the contents of the notes and archives refs from
807//// a remote repo, and merges them with the corresponding local refs.
808////
809//// For notes refs, we assume that every note can be automatically merged using
810//// the 'cat_sort_uniq' strategy (the git-appraise schemas fit that requirement),
811//// so we automatically merge the remote notes into the local notes.
812////
813//// For "archive" refs, they are expected to be used solely for maintaining
814//// reachability of commits that are part of the history of any reviews,
815//// so we do not maintain any consistency with their tree objects. Instead,
816//// we merely ensure that their history graph includes every commit that we
817//// intend to keep.
818//func (repo *GitRepo) PullNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error {
819//	remoteArchiveRef := getRemoteArchiveRef(remote, archiveRefPattern)
820//	archiveFetchRefSpec := fmt.Sprintf("+%s:%s", archiveRefPattern, remoteArchiveRef)
821//
822//	remoteNotesRefPattern := getRemoteNotesRef(remote, notesRefPattern)
823//	notesFetchRefSpec := fmt.Sprintf("+%s:%s", notesRefPattern, remoteNotesRefPattern)
824//
825//	err := repo.runGitCommandInline("fetch", remote, notesFetchRefSpec, archiveFetchRefSpec)
826//	if err != nil {
827//		return err
828//	}
829//
830//	if err := repo.mergeRemoteNotes(remote, notesRefPattern); err != nil {
831//		return err
832//	}
833//	if err := repo.mergeRemoteArchives(remote, archiveRefPattern); err != nil {
834//		return err
835//	}
836//	return nil
837//}
838*/