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