diff options
Diffstat (limited to 'third_party/go/git-appraise/repository')
-rw-r--r-- | third_party/go/git-appraise/repository/git.go | 987 | ||||
-rw-r--r-- | third_party/go/git-appraise/repository/git_test.go | 94 | ||||
-rw-r--r-- | third_party/go/git-appraise/repository/mock_repo.go | 613 | ||||
-rw-r--r-- | third_party/go/git-appraise/repository/repo.go | 221 |
4 files changed, 1915 insertions, 0 deletions
diff --git a/third_party/go/git-appraise/repository/git.go b/third_party/go/git-appraise/repository/git.go new file mode 100644 index 000000000000..31d27ea6d2ec --- /dev/null +++ b/third_party/go/git-appraise/repository/git.go @@ -0,0 +1,987 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package repository contains helper methods for working with the Git repo. +package repository + +import ( + "bufio" + "bytes" + "crypto/sha1" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "strconv" + "strings" +) + +const branchRefPrefix = "refs/heads/" + +// GitRepo represents an instance of a (local) git repository. +type GitRepo struct { + Path string +} + +// Run the given git command with the given I/O reader/writers, returning an error if it fails. +func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error { + cmd := exec.Command("git", args...) + cmd.Dir = repo.Path + cmd.Stdin = stdin + cmd.Stdout = stdout + cmd.Stderr = stderr + return cmd.Run() +} + +// Run the given git command and return its stdout, or an error if the command fails. +func (repo *GitRepo) runGitCommandRaw(args ...string) (string, string, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + err := repo.runGitCommandWithIO(nil, &stdout, &stderr, args...) + return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err +} + +// Run the given git command and return its stdout, or an error if the command fails. +func (repo *GitRepo) runGitCommand(args ...string) (string, error) { + stdout, stderr, err := repo.runGitCommandRaw(args...) + if err != nil { + if stderr == "" { + stderr = "Error running git command: " + strings.Join(args, " ") + } + err = fmt.Errorf(stderr) + } + return stdout, err +} + +// Run the given git command using the same stdin, stdout, and stderr as the review tool. +func (repo *GitRepo) runGitCommandInline(args ...string) error { + return repo.runGitCommandWithIO(os.Stdin, os.Stdout, os.Stderr, args...) +} + +// NewGitRepo determines if the given working directory is inside of a git repository, +// and returns the corresponding GitRepo instance if it is. +func NewGitRepo(path string) (*GitRepo, error) { + repo := &GitRepo{Path: path} + _, _, err := repo.runGitCommandRaw("rev-parse") + if err == nil { + return repo, nil + } + if _, ok := err.(*exec.ExitError); ok { + return nil, err + } + return nil, err +} + +// GetPath returns the path to the repo. +func (repo *GitRepo) GetPath() string { + return repo.Path +} + +// GetRepoStateHash returns a hash which embodies the entire current state of a repository. +func (repo *GitRepo) GetRepoStateHash() (string, error) { + stateSummary, error := repo.runGitCommand("show-ref") + return fmt.Sprintf("%x", sha1.Sum([]byte(stateSummary))), error +} + +// GetUserEmail returns the email address that the user has used to configure git. +func (repo *GitRepo) GetUserEmail() (string, error) { + return repo.runGitCommand("config", "user.email") +} + +// GetUserSigningKey returns the key id the user has configured for +// sigining git artifacts. +func (repo *GitRepo) GetUserSigningKey() (string, error) { + return repo.runGitCommand("config", "user.signingKey") +} + +// GetCoreEditor returns the name of the editor that the user has used to configure git. +func (repo *GitRepo) GetCoreEditor() (string, error) { + return repo.runGitCommand("var", "GIT_EDITOR") +} + +// GetSubmitStrategy returns the way in which a review is submitted +func (repo *GitRepo) GetSubmitStrategy() (string, error) { + submitStrategy, _ := repo.runGitCommand("config", "appraise.submit") + return submitStrategy, nil +} + +// HasUncommittedChanges returns true if there are local, uncommitted changes. +func (repo *GitRepo) HasUncommittedChanges() (bool, error) { + out, err := repo.runGitCommand("status", "--porcelain") + if err != nil { + return false, err + } + if len(out) > 0 { + return true, nil + } + return false, nil +} + +// VerifyCommit verifies that the supplied hash points to a known commit. +func (repo *GitRepo) VerifyCommit(hash string) error { + out, err := repo.runGitCommand("cat-file", "-t", hash) + if err != nil { + return err + } + objectType := strings.TrimSpace(string(out)) + if objectType != "commit" { + return fmt.Errorf("Hash %q points to a non-commit object of type %q", hash, objectType) + } + return nil +} + +// VerifyGitRef verifies that the supplied ref points to a known commit. +func (repo *GitRepo) VerifyGitRef(ref string) error { + _, err := repo.runGitCommand("show-ref", "--verify", ref) + return err +} + +// GetHeadRef returns the ref that is the current HEAD. +func (repo *GitRepo) GetHeadRef() (string, error) { + return repo.runGitCommand("symbolic-ref", "HEAD") +} + +// GetCommitHash returns the hash of the commit pointed to by the given ref. +func (repo *GitRepo) GetCommitHash(ref string) (string, error) { + return repo.runGitCommand("show", "-s", "--format=%H", ref) +} + +// ResolveRefCommit returns the commit pointed to by the given ref, which may be a remote ref. +// +// This differs from GetCommitHash which only works on exact matches, in that it will try to +// intelligently handle the scenario of a ref not existing locally, but being known to exist +// in a remote repo. +// +// This method should be used when a command may be performed by either the reviewer or the +// reviewee, while GetCommitHash should be used when the encompassing command should only be +// performed by the reviewee. +func (repo *GitRepo) ResolveRefCommit(ref string) (string, error) { + if err := repo.VerifyGitRef(ref); err == nil { + return repo.GetCommitHash(ref) + } + if strings.HasPrefix(ref, "refs/heads/") { + // The ref is a branch. Check if it exists in exactly one remote + pattern := strings.Replace(ref, "refs/heads", "**", 1) + matchingOutput, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", pattern) + if err != nil { + return "", err + } + matchingRefs := strings.Split(matchingOutput, "\n") + if len(matchingRefs) == 1 && matchingRefs[0] != "" { + // There is exactly one match + return repo.GetCommitHash(matchingRefs[0]) + } + return "", fmt.Errorf("Unable to find a git ref matching the pattern %q", pattern) + } + return "", fmt.Errorf("Unknown git ref %q", ref) +} + +// GetCommitMessage returns the message stored in the commit pointed to by the given ref. +func (repo *GitRepo) GetCommitMessage(ref string) (string, error) { + return repo.runGitCommand("show", "-s", "--format=%B", ref) +} + +// GetCommitTime returns the commit time of the commit pointed to by the given ref. +func (repo *GitRepo) GetCommitTime(ref string) (string, error) { + return repo.runGitCommand("show", "-s", "--format=%ct", ref) +} + +// GetLastParent returns the last parent of the given commit (as ordered by git). +func (repo *GitRepo) GetLastParent(ref string) (string, error) { + return repo.runGitCommand("rev-list", "--skip", "1", "-n", "1", ref) +} + +// GetCommitDetails returns the details of a commit's metadata. +func (repo GitRepo) GetCommitDetails(ref string) (*CommitDetails, error) { + var err error + show := func(formatString string) (result string) { + if err != nil { + return "" + } + result, err = repo.runGitCommand("show", "-s", ref, fmt.Sprintf("--format=tformat:%s", formatString)) + return result + } + + jsonFormatString := "{\"tree\":\"%T\", \"time\": \"%at\"}" + detailsJSON := show(jsonFormatString) + if err != nil { + return nil, err + } + var details CommitDetails + err = json.Unmarshal([]byte(detailsJSON), &details) + if err != nil { + return nil, err + } + details.Author = show("%an") + details.AuthorEmail = show("%ae") + details.Summary = show("%s") + parentsString := show("%P") + details.Parents = strings.Split(parentsString, " ") + if err != nil { + return nil, err + } + return &details, nil +} + +// MergeBase determines if the first commit that is an ancestor of the two arguments. +func (repo *GitRepo) MergeBase(a, b string) (string, error) { + return repo.runGitCommand("merge-base", a, b) +} + +// IsAncestor determines if the first argument points to a commit that is an ancestor of the second. +func (repo *GitRepo) IsAncestor(ancestor, descendant string) (bool, error) { + _, _, err := repo.runGitCommandRaw("merge-base", "--is-ancestor", ancestor, descendant) + if err == nil { + return true, nil + } + if _, ok := err.(*exec.ExitError); ok { + return false, nil + } + return false, fmt.Errorf("Error while trying to determine commit ancestry: %v", err) +} + +// Diff computes the diff between two given commits. +func (repo *GitRepo) Diff(left, right string, diffArgs ...string) (string, error) { + args := []string{"diff"} + args = append(args, diffArgs...) + args = append(args, fmt.Sprintf("%s..%s", left, right)) + return repo.runGitCommand(args...) +} + +// Show returns the contents of the given file at the given commit. +func (repo *GitRepo) Show(commit, path string) (string, error) { + return repo.runGitCommand("show", fmt.Sprintf("%s:%s", commit, path)) +} + +// SwitchToRef changes the currently-checked-out ref. +func (repo *GitRepo) SwitchToRef(ref string) error { + // If the ref starts with "refs/heads/", then we have to trim that prefix, + // or else we will wind up in a detached HEAD state. + if strings.HasPrefix(ref, branchRefPrefix) { + ref = ref[len(branchRefPrefix):] + } + _, err := repo.runGitCommand("checkout", ref) + return err +} + +// mergeArchives merges two archive refs. +func (repo *GitRepo) mergeArchives(archive, remoteArchive string) error { + remoteHash, err := repo.GetCommitHash(remoteArchive) + if err != nil { + return err + } + if remoteHash == "" { + // The remote archive does not exist, so we have nothing to do + return nil + } + + archiveHash, err := repo.GetCommitHash(archive) + if err != nil { + return err + } + if archiveHash == "" { + // The local archive does not exist, so we merely need to set it + _, err := repo.runGitCommand("update-ref", archive, remoteHash) + return err + } + + isAncestor, err := repo.IsAncestor(archiveHash, remoteHash) + if err != nil { + return err + } + if isAncestor { + // The archive can simply be fast-forwarded + _, err := repo.runGitCommand("update-ref", archive, remoteHash, archiveHash) + return err + } + + // Create a merge commit of the two archives + refDetails, err := repo.GetCommitDetails(remoteArchive) + if err != nil { + return err + } + newArchiveHash, err := repo.runGitCommand("commit-tree", "-p", remoteHash, "-p", archiveHash, "-m", "Merge local and remote archives", refDetails.Tree) + if err != nil { + return err + } + newArchiveHash = strings.TrimSpace(newArchiveHash) + _, err = repo.runGitCommand("update-ref", archive, newArchiveHash, archiveHash) + return err +} + +// ArchiveRef adds the current commit pointed to by the 'ref' argument +// under the ref specified in the 'archive' argument. +// +// Both the 'ref' and 'archive' arguments are expected to be the fully +// qualified names of git refs (e.g. 'refs/heads/my-change' or +// 'refs/devtools/archives/reviews'). +// +// If the ref pointed to by the 'archive' argument does not exist +// yet, then it will be created. +func (repo *GitRepo) ArchiveRef(ref, archive string) error { + refHash, err := repo.GetCommitHash(ref) + if err != nil { + return err + } + refDetails, err := repo.GetCommitDetails(ref) + if err != nil { + return err + } + + commitTreeArgs := []string{"commit-tree"} + archiveHash, err := repo.GetCommitHash(archive) + if err != nil { + archiveHash = "" + } else { + commitTreeArgs = append(commitTreeArgs, "-p", archiveHash) + } + commitTreeArgs = append(commitTreeArgs, "-p", refHash, "-m", fmt.Sprintf("Archive %s", refHash), refDetails.Tree) + newArchiveHash, err := repo.runGitCommand(commitTreeArgs...) + if err != nil { + return err + } + newArchiveHash = strings.TrimSpace(newArchiveHash) + updateRefArgs := []string{"update-ref", archive, newArchiveHash} + if archiveHash != "" { + updateRefArgs = append(updateRefArgs, archiveHash) + } + _, err = repo.runGitCommand(updateRefArgs...) + return err +} + +// MergeRef merges the given ref into the current one. +// +// The ref argument is the ref to merge, and fastForward indicates that the +// current ref should only move forward, as opposed to creating a bubble merge. +// The messages argument(s) provide text that should be included in the default +// merge commit message (separated by blank lines). +func (repo *GitRepo) MergeRef(ref string, fastForward bool, messages ...string) error { + args := []string{"merge"} + if fastForward { + args = append(args, "--ff", "--ff-only") + } else { + args = append(args, "--no-ff") + } + if len(messages) > 0 { + commitMessage := strings.Join(messages, "\n\n") + args = append(args, "-e", "-m", commitMessage) + } + args = append(args, ref) + return repo.runGitCommandInline(args...) +} + +// MergeAndSignRef merges the given ref into the current one and signs the +// merge. +// +// The ref argument is the ref to merge, and fastForward indicates that the +// current ref should only move forward, as opposed to creating a bubble merge. +// The messages argument(s) provide text that should be included in the default +// merge commit message (separated by blank lines). +func (repo *GitRepo) MergeAndSignRef(ref string, fastForward bool, + messages ...string) error { + + args := []string{"merge"} + if fastForward { + args = append(args, "--ff", "--ff-only", "-S") + } else { + args = append(args, "--no-ff", "-S") + } + if len(messages) > 0 { + commitMessage := strings.Join(messages, "\n\n") + args = append(args, "-e", "-m", commitMessage) + } + args = append(args, ref) + return repo.runGitCommandInline(args...) +} + +// RebaseRef rebases the current ref onto the given one. +func (repo *GitRepo) RebaseRef(ref string) error { + return repo.runGitCommandInline("rebase", "-i", ref) +} + +// RebaseAndSignRef rebases the current ref onto the given one and signs the +// result. +func (repo *GitRepo) RebaseAndSignRef(ref string) error { + return repo.runGitCommandInline("rebase", "-S", "-i", ref) +} + +// ListCommits returns the list of commits reachable from the given ref. +// +// The generated list is in chronological order (with the oldest commit first). +// +// If the specified ref does not exist, then this method returns an empty result. +func (repo *GitRepo) ListCommits(ref string) []string { + var stdout bytes.Buffer + var stderr bytes.Buffer + if err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "rev-list", "--reverse", ref); err != nil { + return nil + } + + byteLines := bytes.Split(stdout.Bytes(), []byte("\n")) + var commits []string + for _, byteLine := range byteLines { + commits = append(commits, string(byteLine)) + } + return commits +} + +// ListCommitsBetween returns the list of commits between the two given revisions. +// +// The "from" parameter is the starting point (exclusive), and the "to" +// parameter is the ending point (inclusive). +// +// The "from" commit does not need to be an ancestor of the "to" commit. If it +// is not, then the merge base of the two is used as the starting point. +// Admittedly, this makes calling these the "between" commits is a bit of a +// misnomer, but it also makes the method easier to use when you want to +// generate the list of changes in a feature branch, as it eliminates the need +// to explicitly calculate the merge base. This also makes the semantics of the +// method compatible with git's built-in "rev-list" command. +// +// The generated list is in chronological order (with the oldest commit first). +func (repo *GitRepo) ListCommitsBetween(from, to string) ([]string, error) { + out, err := repo.runGitCommand("rev-list", "--reverse", from+".."+to) + if err != nil { + return nil, err + } + if out == "" { + return nil, nil + } + return strings.Split(out, "\n"), nil +} + +// GetNotes uses the "git" command-line tool to read the notes from the given ref for a given revision. +func (repo *GitRepo) GetNotes(notesRef, revision string) []Note { + var notes []Note + rawNotes, err := repo.runGitCommand("notes", "--ref", notesRef, "show", revision) + if err != nil { + // We just assume that this means there are no notes + return nil + } + for _, line := range strings.Split(rawNotes, "\n") { + notes = append(notes, Note([]byte(line))) + } + return notes +} + +func stringsReader(s []*string) io.Reader { + var subReaders []io.Reader + for _, strPtr := range s { + subReader := strings.NewReader(*strPtr) + subReaders = append(subReaders, subReader, strings.NewReader("\n")) + } + return io.MultiReader(subReaders...) +} + +// splitBatchCheckOutput parses the output of a 'git cat-file --batch-check=...' command. +// +// The output is expected to be formatted as a series of entries, with each +// entry consisting of: +// 1. The SHA1 hash of the git object being output, followed by a space. +// 2. The git "type" of the object (commit, blob, tree, missing, etc), followed by a newline. +// +// To generate this format, make sure that the 'git cat-file' command includes +// the argument '--batch-check=%(objectname) %(objecttype)'. +// +// The return value is a map from object hash to a boolean indicating if that object is a commit. +func splitBatchCheckOutput(out *bytes.Buffer) (map[string]bool, error) { + isCommit := make(map[string]bool) + reader := bufio.NewReader(out) + for { + nameLine, err := reader.ReadString(byte(' ')) + if err == io.EOF { + return isCommit, nil + } + if err != nil { + return nil, fmt.Errorf("Failure while reading the next object name: %v", err) + } + nameLine = strings.TrimSuffix(nameLine, " ") + typeLine, err := reader.ReadString(byte('\n')) + if err != nil && err != io.EOF { + return nil, fmt.Errorf("Failure while reading the next object type: %q - %v", nameLine, err) + } + typeLine = strings.TrimSuffix(typeLine, "\n") + if typeLine == "commit" { + isCommit[nameLine] = true + } + } +} + +// splitBatchCatFileOutput parses the output of a 'git cat-file --batch=...' command. +// +// The output is expected to be formatted as a series of entries, with each +// entry consisting of: +// 1. The SHA1 hash of the git object being output, followed by a newline. +// 2. The size of the object's contents in bytes, followed by a newline. +// 3. The objects contents. +// +// To generate this format, make sure that the 'git cat-file' command includes +// the argument '--batch=%(objectname)\n%(objectsize)'. +func splitBatchCatFileOutput(out *bytes.Buffer) (map[string][]byte, error) { + contentsMap := make(map[string][]byte) + reader := bufio.NewReader(out) + for { + nameLine, err := reader.ReadString(byte('\n')) + if strings.HasSuffix(nameLine, "\n") { + nameLine = strings.TrimSuffix(nameLine, "\n") + } + if err == io.EOF { + return contentsMap, nil + } + if err != nil { + return nil, fmt.Errorf("Failure while reading the next object name: %v", err) + } + sizeLine, err := reader.ReadString(byte('\n')) + if strings.HasSuffix(sizeLine, "\n") { + sizeLine = strings.TrimSuffix(sizeLine, "\n") + } + if err != nil { + return nil, fmt.Errorf("Failure while reading the next object size: %q - %v", nameLine, err) + } + size, err := strconv.Atoi(sizeLine) + if err != nil { + return nil, fmt.Errorf("Failure while parsing the next object size: %q - %v", nameLine, err) + } + contentBytes := make([]byte, size, size) + readDest := contentBytes + len := 0 + err = nil + for err == nil && len < size { + nextLen := 0 + nextLen, err = reader.Read(readDest) + len += nextLen + readDest = contentBytes[len:] + } + contentsMap[nameLine] = contentBytes + if err == io.EOF { + return contentsMap, nil + } + if err != nil { + return nil, err + } + for bs, err := reader.Peek(1); err == nil && bs[0] == byte('\n'); bs, err = reader.Peek(1) { + reader.ReadByte() + } + } +} + +// notesMapping represents the association between a git object and the notes for that object. +type notesMapping struct { + ObjectHash *string + NotesHash *string +} + +// notesOverview represents a high-level overview of all the notes under a single notes ref. +type notesOverview struct { + NotesMappings []*notesMapping + ObjectHashesReader io.Reader + NotesHashesReader io.Reader +} + +// notesOverview returns an overview of the git notes stored under the given ref. +func (repo *GitRepo) notesOverview(notesRef string) (*notesOverview, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + if err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "notes", "--ref", notesRef, "list"); err != nil { + return nil, err + } + + var notesMappings []*notesMapping + var objHashes []*string + var notesHashes []*string + outScanner := bufio.NewScanner(&stdout) + for outScanner.Scan() { + line := outScanner.Text() + lineParts := strings.Split(line, " ") + if len(lineParts) != 2 { + return nil, fmt.Errorf("Malformed output line from 'git-notes list': %q", line) + } + objHash := &lineParts[1] + notesHash := &lineParts[0] + notesMappings = append(notesMappings, ¬esMapping{ + ObjectHash: objHash, + NotesHash: notesHash, + }) + objHashes = append(objHashes, objHash) + notesHashes = append(notesHashes, notesHash) + } + err := outScanner.Err() + if err != nil && err != io.EOF { + return nil, fmt.Errorf("Failure parsing the output of 'git-notes list': %v", err) + } + return ¬esOverview{ + NotesMappings: notesMappings, + ObjectHashesReader: stringsReader(objHashes), + NotesHashesReader: stringsReader(notesHashes), + }, nil +} + +// getIsCommitMap returns a mapping of all the annotated objects that are commits. +func (overview *notesOverview) getIsCommitMap(repo *GitRepo) (map[string]bool, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + if err := repo.runGitCommandWithIO(overview.ObjectHashesReader, &stdout, &stderr, "cat-file", "--batch-check=%(objectname) %(objecttype)"); err != nil { + return nil, fmt.Errorf("Failure performing a batch file check: %v", err) + } + isCommit, err := splitBatchCheckOutput(&stdout) + if err != nil { + return nil, fmt.Errorf("Failure parsing the output of a batch file check: %v", err) + } + return isCommit, nil +} + +// getNoteContentsMap returns a mapping from all the notes hashes to their contents. +func (overview *notesOverview) getNoteContentsMap(repo *GitRepo) (map[string][]byte, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + if err := repo.runGitCommandWithIO(overview.NotesHashesReader, &stdout, &stderr, "cat-file", "--batch=%(objectname)\n%(objectsize)"); err != nil { + return nil, fmt.Errorf("Failure performing a batch file read: %v", err) + } + noteContentsMap, err := splitBatchCatFileOutput(&stdout) + if err != nil { + return nil, fmt.Errorf("Failure parsing the output of a batch file read: %v", err) + } + return noteContentsMap, nil +} + +// GetAllNotes reads the contents of the notes under the given ref for every commit. +// +// The returned value is a mapping from commit hash to the list of notes for that commit. +// +// This is the batch version of the corresponding GetNotes(...) method. +func (repo *GitRepo) GetAllNotes(notesRef string) (map[string][]Note, error) { + // This code is unfortunately quite complicated, but it needs to be so. + // + // Conceptually, this is equivalent to: + // result := make(map[string][]Note) + // for _, commit := range repo.ListNotedRevisions(notesRef) { + // result[commit] = repo.GetNotes(notesRef, commit) + // } + // return result, nil + // + // However, that logic would require separate executions of the 'git' + // command for every annotated commit. For a repo with 10s of thousands + // of reviews, that would mean calling Cmd.Run(...) 10s of thousands of + // times. That, in turn, would take so long that the tool would be unusable. + // + // This method avoids that by taking advantage of the 'git cat-file --batch="..."' + // command. That allows us to use a single invocation of Cmd.Run(...) to + // inspect multiple git objects at once. + // + // As such, regardless of the number of reviews in a repo, we can get all + // of the notes using a total of three invocations of Cmd.Run(...): + // 1. One to list all the annotated objects (and their notes hash) + // 2. A second one to filter out all of the annotated objects that are not commits. + // 3. A final one to get the contents of all of the notes blobs. + overview, err := repo.notesOverview(notesRef) + if err != nil { + return nil, err + } + isCommit, err := overview.getIsCommitMap(repo) + if err != nil { + return nil, fmt.Errorf("Failure building the set of commit objects: %v", err) + } + noteContentsMap, err := overview.getNoteContentsMap(repo) + if err != nil { + return nil, fmt.Errorf("Failure building the mapping from notes hash to contents: %v", err) + } + commitNotesMap := make(map[string][]Note) + for _, notesMapping := range overview.NotesMappings { + if !isCommit[*notesMapping.ObjectHash] { + continue + } + noteBytes := noteContentsMap[*notesMapping.NotesHash] + byteSlices := bytes.Split(noteBytes, []byte("\n")) + var notes []Note + for _, slice := range byteSlices { + notes = append(notes, Note(slice)) + } + commitNotesMap[*notesMapping.ObjectHash] = notes + } + + return commitNotesMap, nil +} + +// AppendNote appends a note to a revision under the given ref. +func (repo *GitRepo) AppendNote(notesRef, revision string, note Note) error { + _, err := repo.runGitCommand("notes", "--ref", notesRef, "append", "-m", string(note), revision) + return err +} + +// ListNotedRevisions returns the collection of revisions that are annotated by notes in the given ref. +func (repo *GitRepo) ListNotedRevisions(notesRef string) []string { + var revisions []string + notesListOut, err := repo.runGitCommand("notes", "--ref", notesRef, "list") + if err != nil { + return nil + } + notesList := strings.Split(notesListOut, "\n") + for _, notePair := range notesList { + noteParts := strings.SplitN(notePair, " ", 2) + if len(noteParts) == 2 { + objHash := noteParts[1] + objType, err := repo.runGitCommand("cat-file", "-t", objHash) + // If a note points to an object that we do not know about (yet), then err will not + // be nil. We can safely just ignore those notes. + if err == nil && objType == "commit" { + revisions = append(revisions, objHash) + } + } + } + return revisions +} + +// PushNotes pushes git notes to a remote repo. +func (repo *GitRepo) PushNotes(remote, notesRefPattern string) error { + refspec := fmt.Sprintf("%s:%s", notesRefPattern, notesRefPattern) + + // The push is liable to fail if the user forgot to do a pull first, so + // we treat errors as user errors rather than fatal errors. + err := repo.runGitCommandInline("push", remote, refspec) + if err != nil { + return fmt.Errorf("Failed to push to the remote '%s': %v", remote, err) + } + return nil +} + +// PushNotesAndArchive pushes the given notes and archive refs to a remote repo. +func (repo *GitRepo) PushNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error { + notesRefspec := fmt.Sprintf("%s:%s", notesRefPattern, notesRefPattern) + archiveRefspec := fmt.Sprintf("%s:%s", archiveRefPattern, archiveRefPattern) + err := repo.runGitCommandInline("push", remote, notesRefspec, archiveRefspec) + if err != nil { + return fmt.Errorf("Failed to push the local archive to the remote '%s': %v", remote, err) + } + return nil +} + +func getRemoteNotesRef(remote, localNotesRef string) string { + relativeNotesRef := strings.TrimPrefix(localNotesRef, "refs/notes/") + return "refs/notes/" + remote + "/" + relativeNotesRef +} + +// MergeNotes merges in the remote's state of the notes reference into the +// local repository's. +func (repo *GitRepo) MergeNotes(remote, notesRefPattern string) error { + remoteRefs, err := repo.runGitCommand("ls-remote", remote, notesRefPattern) + if err != nil { + return err + } + for _, line := range strings.Split(remoteRefs, "\n") { + lineParts := strings.Split(line, "\t") + if len(lineParts) == 2 { + ref := lineParts[1] + remoteRef := getRemoteNotesRef(remote, ref) + _, err := repo.runGitCommand("notes", "--ref", ref, "merge", remoteRef, "-s", "cat_sort_uniq") + if err != nil { + return err + } + } + } + return nil +} + +// PullNotes fetches the contents of the given notes ref from a remote repo, +// and then merges them with the corresponding local notes using the +// "cat_sort_uniq" strategy. +func (repo *GitRepo) PullNotes(remote, notesRefPattern string) error { + remoteNotesRefPattern := getRemoteNotesRef(remote, notesRefPattern) + fetchRefSpec := fmt.Sprintf("+%s:%s", notesRefPattern, remoteNotesRefPattern) + err := repo.runGitCommandInline("fetch", remote, fetchRefSpec) + if err != nil { + return err + } + + return repo.MergeNotes(remote, notesRefPattern) +} + +func getRemoteArchiveRef(remote, archiveRefPattern string) string { + relativeArchiveRef := strings.TrimPrefix(archiveRefPattern, "refs/devtools/archives/") + return "refs/devtools/remoteArchives/" + remote + "/" + relativeArchiveRef +} + +// MergeArchives merges in the remote's state of the archives reference into +// the local repository's. +func (repo *GitRepo) MergeArchives(remote, archiveRefPattern string) error { + remoteRefs, err := repo.runGitCommand("ls-remote", remote, archiveRefPattern) + if err != nil { + return err + } + for _, line := range strings.Split(remoteRefs, "\n") { + lineParts := strings.Split(line, "\t") + if len(lineParts) == 2 { + ref := lineParts[1] + remoteRef := getRemoteArchiveRef(remote, ref) + if err := repo.mergeArchives(ref, remoteRef); err != nil { + return err + } + } + } + return nil +} + +func (repo *GitRepo) fetchNotes(remote, notesRefPattern, + archiveRefPattern string) error { + + remoteArchiveRef := getRemoteArchiveRef(remote, archiveRefPattern) + archiveFetchRefSpec := fmt.Sprintf("+%s:%s", archiveRefPattern, remoteArchiveRef) + + remoteNotesRefPattern := getRemoteNotesRef(remote, notesRefPattern) + notesFetchRefSpec := fmt.Sprintf("+%s:%s", notesRefPattern, remoteNotesRefPattern) + + return repo.runGitCommandInline("fetch", remote, notesFetchRefSpec, archiveFetchRefSpec) +} + +// PullNotesAndArchive fetches the contents of the notes and archives refs from +// a remote repo, and merges them with the corresponding local refs. +// +// For notes refs, we assume that every note can be automatically merged using +// the 'cat_sort_uniq' strategy (the git-appraise schemas fit that requirement), +// so we automatically merge the remote notes into the local notes. +// +// For "archive" refs, they are expected to be used solely for maintaining +// reachability of commits that are part of the history of any reviews, +// so we do not maintain any consistency with their tree objects. Instead, +// we merely ensure that their history graph includes every commit that we +// intend to keep. +func (repo *GitRepo) PullNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error { + err := repo.fetchNotes(remote, notesRefPattern, archiveRefPattern) + if err != nil { + return err + } + + err = repo.MergeNotes(remote, notesRefPattern) + if err != nil { + return err + } + return repo.MergeArchives(remote, archiveRefPattern) +} + +// FetchAndReturnNewReviewHashes fetches the notes "branches" and then susses +// out the IDs (the revision the review points to) of any new reviews, then +// returns that list of IDs. +// +// This is accomplished by determining which files in the notes tree have +// changed because the _names_ of these files correspond to the revisions they +// point to. +func (repo *GitRepo) FetchAndReturnNewReviewHashes(remote, notesRefPattern, + archiveRefPattern string) ([]string, error) { + + // Record the current state of the reviews and comments refs. + var ( + getAllRevs, getAllComs bool + reviewsList, commentsList []string + ) + reviewBeforeHash, err := repo.GetCommitHash( + "notes/" + remote + "/devtools/reviews") + getAllRevs = err != nil + + commentBeforeHash, err := repo.GetCommitHash( + "notes/" + remote + "/devtools/discuss") + getAllComs = err != nil + + // Update them from the remote. + err = repo.fetchNotes(remote, notesRefPattern, archiveRefPattern) + if err != nil { + return nil, err + } + + // Now, if either of these are new refs, we just use the whole tree at that + // new ref. Otherwise we see which reviews or comments changed and collect + // them into a list. + if getAllRevs { + hash, err := repo.GetCommitHash( + "notes/" + remote + "/devtools/reviews") + // It is possible that even after we've pulled that this ref still + // isn't present (because there are no reviews yet). + if err == nil { + rvws, err := repo.runGitCommand("ls-tree", "-r", "--name-only", + hash) + if err != nil { + return nil, err + } + reviewsList = strings.Split(strings.Replace(rvws, "/", "", -1), + "\n") + } + } else { + reviewAfterHash, err := repo.GetCommitHash( + "notes/" + remote + "/devtools/reviews") + if err != nil { + return nil, err + } + + // Only run through this if the fetch fetched new revisions. + // Otherwise leave reviewsList as its default value, an empty slice + // of strings. + if reviewBeforeHash != reviewAfterHash { + newReviewsRaw, err := repo.runGitCommand("diff", "--name-only", + reviewBeforeHash, reviewAfterHash) + if err != nil { + return nil, err + } + reviewsList = strings.Split(strings.Replace(newReviewsRaw, + "/", "", -1), "\n") + } + } + + if getAllComs { + hash, err := repo.GetCommitHash( + "notes/" + remote + "/devtools/discuss") + // It is possible that even after we've pulled that this ref still + // isn't present (because there are no comments yet). + if err == nil { + rvws, err := repo.runGitCommand("ls-tree", "-r", "--name-only", + hash) + if err != nil { + return nil, err + } + commentsList = strings.Split(strings.Replace(rvws, "/", "", -1), + "\n") + } + } else { + commentAfterHash, err := repo.GetCommitHash( + "notes/" + remote + "/devtools/discuss") + if err != nil { + return nil, err + } + + // Only run through this if the fetch fetched new revisions. + // Otherwise leave commentsList as its default value, an empty slice + // of strings. + if commentBeforeHash != commentAfterHash { + newCommentsRaw, err := repo.runGitCommand("diff", "--name-only", + commentBeforeHash, commentAfterHash) + if err != nil { + return nil, err + } + commentsList = strings.Split(strings.Replace(newCommentsRaw, + "/", "", -1), "\n") + } + } + + // Now that we have our two lists, we need to merge them. + updatedReviewSet := make(map[string]struct{}) + for _, hash := range append(reviewsList, commentsList...) { + updatedReviewSet[hash] = struct{}{} + } + + updatedReviews := make([]string, 0, len(updatedReviewSet)) + for key, _ := range updatedReviewSet { + updatedReviews = append(updatedReviews, key) + } + return updatedReviews, nil +} diff --git a/third_party/go/git-appraise/repository/git_test.go b/third_party/go/git-appraise/repository/git_test.go new file mode 100644 index 000000000000..e1a9e2b2eace --- /dev/null +++ b/third_party/go/git-appraise/repository/git_test.go @@ -0,0 +1,94 @@ +/* +Copyright 2016 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repository + +import ( + "bytes" + "testing" +) + +const ( + simpleBatchCheckOutput = `ddbdcb9d5aa71d35de481789bacece9a2f8138d0 commit +de9ebcdf2a1e93365eefc2739f73f2c68a280c11 commit +def9abf52f9a17d4f168e05bc420557a87a55961 commit +df324616ea2bc9bf6fc7025fc80a373ecec687b6 missing +dfdd159c9c11c08d84c8c050d2a1a4db29147916 commit +e4e48e2b4d76ac305cf76fee1d1c8c0283127d71 commit +e6ae4ed08704fe3c258ab486b07a36e28c3c238a commit +e807a993d1807b154294b9875b9d926b6f246d0c commit +e90f75882526e9bc5a71af64d60ea50092ed0b1d commit` + simpleBatchCatFileOutput = `c1f5a5f135b171cc963b822d338000d185f1ae4f +342 +{"timestamp":"1450315153","v":0,"agent":"Jenkins(1.627) GitNotesJobLogger","url":"https://jenkins-dot-developer-tools-bundle.appspot.com/job/git-appraise/105/"} + +{"timestamp":"1450315161","v":0,"agent":"Jenkins(1.627) GitNotesJobLogger","url":"https://jenkins-dot-developer-tools-bundle.appspot.com/job/git-appraise/105/","status":"success"} + +31ea4952450bbe5db0d6a7a7903e451925106c0f +141 +{"timestamp":"1440202534","url":"https://travis-ci.org/google/git-appraise/builds/76722074","agent":"continuous-integration/travis-ci/push"} + +bde25250a9f6dc9c56f16befa5a2d73c8558b472 +342 +{"timestamp":"1450434854","v":0,"agent":"Jenkins(1.627) GitNotesJobLogger","url":"https://jenkins-dot-developer-tools-bundle.appspot.com/job/git-appraise/112/"} + +{"timestamp":"1450434860","v":0,"agent":"Jenkins(1.627) GitNotesJobLogger","url":"https://jenkins-dot-developer-tools-bundle.appspot.com/job/git-appraise/112/","status":"success"} + +3128dc6881bf7647aea90fef1f4fbf883df6a8fe +342 +{"timestamp":"1457445850","v":0,"agent":"Jenkins(1.627) GitNotesJobLogger","url":"https://jenkins-dot-developer-tools-bundle.appspot.com/job/git-appraise/191/"} + +{"timestamp":"1457445856","v":0,"agent":"Jenkins(1.627) GitNotesJobLogger","url":"https://jenkins-dot-developer-tools-bundle.appspot.com/job/git-appraise/191/","status":"success"} + +` +) + +func TestSplitBatchCheckOutput(t *testing.T) { + buf := bytes.NewBuffer([]byte(simpleBatchCheckOutput)) + commitsMap, err := splitBatchCheckOutput(buf) + if err != nil { + t.Fatal(err) + } + if !commitsMap["ddbdcb9d5aa71d35de481789bacece9a2f8138d0"] { + t.Fatal("Failed to recognize the first commit as valid") + } + if !commitsMap["de9ebcdf2a1e93365eefc2739f73f2c68a280c11"] { + t.Fatal("Failed to recognize the second commit as valid") + } + if !commitsMap["e90f75882526e9bc5a71af64d60ea50092ed0b1d"] { + t.Fatal("Failed to recognize the last commit as valid") + } + if commitsMap["df324616ea2bc9bf6fc7025fc80a373ecec687b6"] { + t.Fatal("Failed to filter out a missing object") + } +} + +func TestSplitBatchCatFileOutput(t *testing.T) { + buf := bytes.NewBuffer([]byte(simpleBatchCatFileOutput)) + notesMap, err := splitBatchCatFileOutput(buf) + if err != nil { + t.Fatal(err) + } + if len(notesMap["c1f5a5f135b171cc963b822d338000d185f1ae4f"]) != 342 { + t.Fatal("Failed to parse the contents of the first cat'ed file") + } + if len(notesMap["31ea4952450bbe5db0d6a7a7903e451925106c0f"]) != 141 { + t.Fatal("Failed to parse the contents of the second cat'ed file") + } + if len(notesMap["3128dc6881bf7647aea90fef1f4fbf883df6a8fe"]) != 342 { + t.Fatal("Failed to parse the contents of the last cat'ed file") + } +} diff --git a/third_party/go/git-appraise/repository/mock_repo.go b/third_party/go/git-appraise/repository/mock_repo.go new file mode 100644 index 000000000000..2d8debe48387 --- /dev/null +++ b/third_party/go/git-appraise/repository/mock_repo.go @@ -0,0 +1,613 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repository + +import ( + "crypto/sha1" + "encoding/json" + "fmt" + "strings" +) + +// Constants used for testing. +// We initialize our mock repo with two branches (one of which holds a pending review), +// and commit history that looks like this: +// +// Master Branch: A--B--D--E--F--J +// \ / \ \ +// C \ \ +// \ \ +// Review Branch: G--H--I +// +// Where commits "B" and "D" represent reviews that have been submitted, and "G" +// is a pending review. +const ( + TestTargetRef = "refs/heads/master" + TestReviewRef = "refs/heads/ojarjur/mychange" + TestAlternateReviewRef = "refs/review/mychange" + TestRequestsRef = "refs/notes/devtools/reviews" + TestCommentsRef = "refs/notes/devtools/discuss" + + TestCommitA = "A" + TestCommitB = "B" + TestCommitC = "C" + TestCommitD = "D" + TestCommitE = "E" + TestCommitF = "F" + TestCommitG = "G" + TestCommitH = "H" + TestCommitI = "I" + TestCommitJ = "J" + + TestRequestB = `{"timestamp": "0000000001", "reviewRef": "refs/heads/ojarjur/mychange", "targetRef": "refs/heads/master", "requester": "ojarjur", "reviewers": ["ojarjur"], "description": "B"}` + TestRequestD = `{"timestamp": "0000000002", "reviewRef": "refs/heads/ojarjur/mychange", "targetRef": "refs/heads/master", "requester": "ojarjur", "reviewers": ["ojarjur"], "description": "D"}` + TestRequestG = `{"timestamp": "0000000004", "reviewRef": "refs/heads/ojarjur/mychange", "targetRef": "refs/heads/master", "requester": "ojarjur", "reviewers": ["ojarjur"], "description": "G"} + +{"timestamp": "0000000005", "reviewRef": "refs/heads/ojarjur/mychange", "targetRef": "refs/heads/master", "requester": "ojarjur", "reviewers": ["ojarjur"], "description": "Updated description of G"} + +{"timestamp": "0000000005", "reviewRef": "refs/heads/ojarjur/mychange", "targetRef": "refs/heads/master", "requester": "ojarjur", "reviewers": ["ojarjur"], "description": "Final description of G"}` + + TestDiscussB = `{"timestamp": "0000000001", "author": "ojarjur", "location": {"commit": "B"}, "resolved": true}` + TestDiscussD = `{"timestamp": "0000000003", "author": "ojarjur", "location": {"commit": "E"}, "resolved": true}` +) + +type mockCommit struct { + Message string `json:"message,omitempty"` + Time string `json:"time,omitempty"` + Parents []string `json:"parents,omitempty"` +} + +// mockRepoForTest defines an instance of Repo that can be used for testing. +type mockRepoForTest struct { + Head string + Refs map[string]string `json:"refs,omitempty"` + Commits map[string]mockCommit `json:"commits,omitempty"` + Notes map[string]map[string]string `json:"notes,omitempty"` +} + +func (r *mockRepoForTest) createCommit(message string, time string, parents []string) (string, error) { + newCommit := mockCommit{ + Message: message, + Time: time, + Parents: parents, + } + newCommitJSON, err := json.Marshal(newCommit) + if err != nil { + return "", err + } + newCommitHash := fmt.Sprintf("%x", sha1.Sum([]byte(newCommitJSON))) + r.Commits[newCommitHash] = newCommit + return newCommitHash, nil +} + +// NewMockRepoForTest returns a mocked-out instance of the Repo interface that has been pre-populated with test data. +func NewMockRepoForTest() Repo { + commitA := mockCommit{ + Message: "First commit", + Time: "0", + Parents: nil, + } + commitB := mockCommit{ + Message: "Second commit", + Time: "1", + Parents: []string{TestCommitA}, + } + commitC := mockCommit{ + Message: "No, I'm the second commit", + Time: "1", + Parents: []string{TestCommitA}, + } + commitD := mockCommit{ + Message: "Fourth commit", + Time: "2", + Parents: []string{TestCommitB, TestCommitC}, + } + commitE := mockCommit{ + Message: "Fifth commit", + Time: "3", + Parents: []string{TestCommitD}, + } + commitF := mockCommit{ + Message: "Sixth commit", + Time: "4", + Parents: []string{TestCommitE}, + } + commitG := mockCommit{ + Message: "No, I'm the sixth commit", + Time: "4", + Parents: []string{TestCommitE}, + } + commitH := mockCommit{ + Message: "Seventh commit", + Time: "5", + Parents: []string{TestCommitG, TestCommitF}, + } + commitI := mockCommit{ + Message: "Eighth commit", + Time: "6", + Parents: []string{TestCommitH}, + } + commitJ := mockCommit{ + Message: "No, I'm the eighth commit", + Time: "6", + Parents: []string{TestCommitF}, + } + return &mockRepoForTest{ + Head: TestTargetRef, + Refs: map[string]string{ + TestTargetRef: TestCommitJ, + TestReviewRef: TestCommitI, + TestAlternateReviewRef: TestCommitI, + }, + Commits: map[string]mockCommit{ + TestCommitA: commitA, + TestCommitB: commitB, + TestCommitC: commitC, + TestCommitD: commitD, + TestCommitE: commitE, + TestCommitF: commitF, + TestCommitG: commitG, + TestCommitH: commitH, + TestCommitI: commitI, + TestCommitJ: commitJ, + }, + Notes: map[string]map[string]string{ + TestRequestsRef: map[string]string{ + TestCommitB: TestRequestB, + TestCommitD: TestRequestD, + TestCommitG: TestRequestG, + }, + TestCommentsRef: map[string]string{ + TestCommitB: TestDiscussB, + TestCommitD: TestDiscussD, + }, + }, + } +} + +// GetPath returns the path to the repo. +func (r *mockRepoForTest) GetPath() string { return "~/mockRepo/" } + +// GetRepoStateHash returns a hash which embodies the entire current state of a repository. +func (r *mockRepoForTest) GetRepoStateHash() (string, error) { + repoJSON, err := json.Marshal(r) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", sha1.Sum([]byte(repoJSON))), nil +} + +// GetUserEmail returns the email address that the user has used to configure git. +func (r *mockRepoForTest) GetUserEmail() (string, error) { return "user@example.com", nil } + +// GetUserSigningKey returns the key id the user has configured for +// sigining git artifacts. +func (r *mockRepoForTest) GetUserSigningKey() (string, error) { + return "gpgsig", nil +} + +// GetCoreEditor returns the name of the editor that the user has used to configure git. +func (r *mockRepoForTest) GetCoreEditor() (string, error) { return "vi", nil } + +// GetSubmitStrategy returns the way in which a review is submitted +func (r *mockRepoForTest) GetSubmitStrategy() (string, error) { return "merge", nil } + +// HasUncommittedChanges returns true if there are local, uncommitted changes. +func (r *mockRepoForTest) HasUncommittedChanges() (bool, error) { return false, nil } + +func (r *mockRepoForTest) resolveLocalRef(ref string) (string, error) { + if ref == "HEAD" { + ref = r.Head + } + if commit, ok := r.Refs[ref]; ok { + return commit, nil + } + if _, ok := r.Commits[ref]; ok { + return ref, nil + } + return "", fmt.Errorf("The ref %q does not exist", ref) +} + +// VerifyCommit verifies that the supplied hash points to a known commit. +func (r *mockRepoForTest) VerifyCommit(hash string) error { + if _, ok := r.Commits[hash]; !ok { + return fmt.Errorf("The given hash %q is not a known commit", hash) + } + return nil +} + +// VerifyGitRef verifies that the supplied ref points to a known commit. +func (r *mockRepoForTest) VerifyGitRef(ref string) error { + _, err := r.resolveLocalRef(ref) + return err +} + +// GetHeadRef returns the ref that is the current HEAD. +func (r *mockRepoForTest) GetHeadRef() (string, error) { return r.Head, nil } + +// GetCommitHash returns the hash of the commit pointed to by the given ref. +func (r *mockRepoForTest) GetCommitHash(ref string) (string, error) { + err := r.VerifyGitRef(ref) + if err != nil { + return "", err + } + return r.resolveLocalRef(ref) +} + +// ResolveRefCommit returns the commit pointed to by the given ref, which may be a remote ref. +// +// This differs from GetCommitHash which only works on exact matches, in that it will try to +// intelligently handle the scenario of a ref not existing locally, but being known to exist +// in a remote repo. +// +// This method should be used when a command may be performed by either the reviewer or the +// reviewee, while GetCommitHash should be used when the encompassing command should only be +// performed by the reviewee. +func (r *mockRepoForTest) ResolveRefCommit(ref string) (string, error) { + if commit, err := r.resolveLocalRef(ref); err == nil { + return commit, err + } + return r.resolveLocalRef(strings.Replace(ref, "refs/heads/", "refs/remotes/origin/", 1)) +} + +func (r *mockRepoForTest) getCommit(ref string) (mockCommit, error) { + commit, err := r.resolveLocalRef(ref) + return r.Commits[commit], err +} + +// GetCommitMessage returns the message stored in the commit pointed to by the given ref. +func (r *mockRepoForTest) GetCommitMessage(ref string) (string, error) { + commit, err := r.getCommit(ref) + if err != nil { + return "", err + } + return commit.Message, nil +} + +// GetCommitTime returns the commit time of the commit pointed to by the given ref. +func (r *mockRepoForTest) GetCommitTime(ref string) (string, error) { + commit, err := r.getCommit(ref) + if err != nil { + return "", err + } + return commit.Time, nil +} + +// GetLastParent returns the last parent of the given commit (as ordered by git). +func (r *mockRepoForTest) GetLastParent(ref string) (string, error) { + commit, err := r.getCommit(ref) + if len(commit.Parents) > 0 { + return commit.Parents[len(commit.Parents)-1], err + } + return "", err +} + +// GetCommitDetails returns the details of a commit's metadata. +func (r *mockRepoForTest) GetCommitDetails(ref string) (*CommitDetails, error) { + commit, err := r.getCommit(ref) + if err != nil { + return nil, err + } + var details CommitDetails + details.Author = "Test Author" + details.AuthorEmail = "author@example.com" + details.Summary = commit.Message + details.Time = commit.Time + details.Parents = commit.Parents + return &details, nil +} + +// ancestors returns the breadth-first traversal of a commit's ancestors +func (r *mockRepoForTest) ancestors(commit string) ([]string, error) { + queue := []string{commit} + var ancestors []string + for queue != nil { + var nextQueue []string + for _, c := range queue { + commit, err := r.getCommit(c) + if err != nil { + return nil, err + } + parents := commit.Parents + nextQueue = append(nextQueue, parents...) + ancestors = append(ancestors, parents...) + } + queue = nextQueue + } + return ancestors, nil +} + +// IsAncestor determines if the first argument points to a commit that is an ancestor of the second. +func (r *mockRepoForTest) IsAncestor(ancestor, descendant string) (bool, error) { + var err error + ancestor, err = r.resolveLocalRef(ancestor) + if err != nil { + return false, err + } + descendant, err = r.resolveLocalRef(descendant) + if err != nil { + return false, err + } + if ancestor == descendant { + return true, nil + } + descendantCommit, err := r.getCommit(descendant) + if err != nil { + return false, err + } + for _, parent := range descendantCommit.Parents { + if t, e := r.IsAncestor(ancestor, parent); e == nil && t { + return true, nil + } + } + return false, nil +} + +// MergeBase determines if the first commit that is an ancestor of the two arguments. +func (r *mockRepoForTest) MergeBase(a, b string) (string, error) { + ancestors, err := r.ancestors(a) + if err != nil { + return "", err + } + for _, ancestor := range ancestors { + if t, e := r.IsAncestor(ancestor, b); e == nil && t { + return ancestor, nil + } + } + return "", nil +} + +// Diff computes the diff between two given commits. +func (r *mockRepoForTest) Diff(left, right string, diffArgs ...string) (string, error) { + return fmt.Sprintf("Diff between %q and %q", left, right), nil +} + +// Show returns the contents of the given file at the given commit. +func (r *mockRepoForTest) Show(commit, path string) (string, error) { + return fmt.Sprintf("%s:%s", commit, path), nil +} + +// SwitchToRef changes the currently-checked-out ref. +func (r *mockRepoForTest) SwitchToRef(ref string) error { + r.Head = ref + return nil +} + +// ArchiveRef adds the current commit pointed to by the 'ref' argument +// under the ref specified in the 'archive' argument. +// +// Both the 'ref' and 'archive' arguments are expected to be the fully +// qualified names of git refs (e.g. 'refs/heads/my-change' or +// 'refs/archive/devtools'). +// +// If the ref pointed to by the 'archive' argument does not exist +// yet, then it will be created. +func (r *mockRepoForTest) ArchiveRef(ref, archive string) error { + commitToArchive, err := r.resolveLocalRef(ref) + if err != nil { + return err + } + var archiveParents []string + if archiveCommit, err := r.resolveLocalRef(archive); err == nil { + archiveParents = []string{archiveCommit, commitToArchive} + } else { + archiveParents = []string{commitToArchive} + } + archiveCommit, err := r.createCommit("Archiving", "Nowish", archiveParents) + if err != nil { + return err + } + r.Refs[archive] = archiveCommit + return nil +} + +// MergeRef merges the given ref into the current one. +// +// The ref argument is the ref to merge, and fastForward indicates that the +// current ref should only move forward, as opposed to creating a bubble merge. +func (r *mockRepoForTest) MergeRef(ref string, fastForward bool, messages ...string) error { + newCommitHash, err := r.resolveLocalRef(ref) + if err != nil { + return err + } + if !fastForward { + origCommit, err := r.resolveLocalRef(r.Head) + if err != nil { + return err + } + newCommit, err := r.getCommit(ref) + if err != nil { + return err + } + message := strings.Join(messages, "\n\n") + time := newCommit.Time + parents := []string{origCommit, newCommitHash} + newCommitHash, err = r.createCommit(message, time, parents) + if err != nil { + return err + } + } + r.Refs[r.Head] = newCommitHash + return nil +} + +// MergeAndSignRef merges the given ref into the current one and signs the +// merge. +// +// The ref argument is the ref to merge, and fastForward indicates that the +// current ref should only move forward, as opposed to creating a bubble merge. +func (r *mockRepoForTest) MergeAndSignRef(ref string, fastForward bool, + messages ...string) error { + return nil +} + +// RebaseRef rebases the current ref onto the given one. +func (r *mockRepoForTest) RebaseRef(ref string) error { + parentHash := r.Refs[ref] + origCommit, err := r.getCommit(r.Head) + if err != nil { + return err + } + newCommitHash, err := r.createCommit(origCommit.Message, origCommit.Time, []string{parentHash}) + if err != nil { + return err + } + if strings.HasPrefix(r.Head, "refs/heads/") { + r.Refs[r.Head] = newCommitHash + } else { + // The current head is not a branch, so updating + // it should leave us in a detached-head state. + r.Head = newCommitHash + } + return nil +} + +// RebaseAndSignRef rebases the current ref onto the given one and signs the +// result. +func (r *mockRepoForTest) RebaseAndSignRef(ref string) error { return nil } + +// ListCommits returns the list of commits reachable from the given ref. +// +// The generated list is in chronological order (with the oldest commit first). +// +// If the specified ref does not exist, then this method returns an empty result. +func (r *mockRepoForTest) ListCommits(ref string) []string { return nil } + +// ListCommitsBetween returns the list of commits between the two given revisions. +// +// The "from" parameter is the starting point (exclusive), and the "to" +// parameter is the ending point (inclusive). +// +// The "from" commit does not need to be an ancestor of the "to" commit. If it +// is not, then the merge base of the two is used as the starting point. +// Admittedly, this makes calling these the "between" commits is a bit of a +// misnomer, but it also makes the method easier to use when you want to +// generate the list of changes in a feature branch, as it eliminates the need +// to explicitly calculate the merge base. This also makes the semantics of the +// method compatible with git's built-in "rev-list" command. +// +// The generated list is in chronological order (with the oldest commit first). +func (r *mockRepoForTest) ListCommitsBetween(from, to string) ([]string, error) { + commits := []string{to} + potentialCommits, _ := r.ancestors(to) + for _, commit := range potentialCommits { + blocked, err := r.IsAncestor(commit, from) + if err != nil { + return nil, err + } + if !blocked { + commits = append(commits, commit) + } + } + return commits, nil +} + +// GetNotes reads the notes from the given ref that annotate the given revision. +func (r *mockRepoForTest) GetNotes(notesRef, revision string) []Note { + notesText := r.Notes[notesRef][revision] + var notes []Note + for _, line := range strings.Split(notesText, "\n") { + notes = append(notes, Note(line)) + } + return notes +} + +// GetAllNotes reads the contents of the notes under the given ref for every commit. +// +// The returned value is a mapping from commit hash to the list of notes for that commit. +// +// This is the batch version of the corresponding GetNotes(...) method. +func (r *mockRepoForTest) GetAllNotes(notesRef string) (map[string][]Note, error) { + notesMap := make(map[string][]Note) + for _, commit := range r.ListNotedRevisions(notesRef) { + notesMap[commit] = r.GetNotes(notesRef, commit) + } + return notesMap, nil +} + +// AppendNote appends a note to a revision under the given ref. +func (r *mockRepoForTest) AppendNote(ref, revision string, note Note) error { + existingNotes := r.Notes[ref][revision] + newNotes := existingNotes + "\n" + string(note) + r.Notes[ref][revision] = newNotes + return nil +} + +// ListNotedRevisions returns the collection of revisions that are annotated by notes in the given ref. +func (r *mockRepoForTest) ListNotedRevisions(notesRef string) []string { + var revisions []string + for revision := range r.Notes[notesRef] { + if _, ok := r.Commits[revision]; ok { + revisions = append(revisions, revision) + } + } + return revisions +} + +// PushNotes pushes git notes to a remote repo. +func (r *mockRepoForTest) PushNotes(remote, notesRefPattern string) error { return nil } + +// PullNotes fetches the contents of the given notes ref from a remote repo, +// and then merges them with the corresponding local notes using the +// "cat_sort_uniq" strategy. +func (r *mockRepoForTest) PullNotes(remote, notesRefPattern string) error { return nil } + +// PushNotesAndArchive pushes the given notes and archive refs to a remote repo. +func (r *mockRepoForTest) PushNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error { + return nil +} + +// PullNotesAndArchive fetches the contents of the notes and archives refs from +// a remote repo, and merges them with the corresponding local refs. +// +// For notes refs, we assume that every note can be automatically merged using +// the 'cat_sort_uniq' strategy (the git-appraise schemas fit that requirement), +// so we automatically merge the remote notes into the local notes. +// +// For "archive" refs, they are expected to be used solely for maintaining +// reachability of commits that are part of the history of any reviews, +// so we do not maintain any consistency with their tree objects. Instead, +// we merely ensure that their history graph includes every commit that we +// intend to keep. +func (r *mockRepoForTest) PullNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error { + return nil +} + +// MergeNotes merges in the remote's state of the archives reference into +// the local repository's. +func (repo *mockRepoForTest) MergeNotes(remote, notesRefPattern string) error { + return nil +} + +// MergeArchives merges in the remote's state of the archives reference into +// the local repository's. +func (repo *mockRepoForTest) MergeArchives(remote, + archiveRefPattern string) error { + return nil +} + +// FetchAndReturnNewReviewHashes fetches the notes "branches" and then susses +// out the IDs (the revision the review points to) of any new reviews, then +// returns that list of IDs. +// +// This is accomplished by determining which files in the notes tree have +// changed because the _names_ of these files correspond to the revisions they +// point to. +func (repo *mockRepoForTest) FetchAndReturnNewReviewHashes(remote, notesRefPattern, + archiveRefPattern string) ([]string, error) { + return nil, nil +} diff --git a/third_party/go/git-appraise/repository/repo.go b/third_party/go/git-appraise/repository/repo.go new file mode 100644 index 000000000000..91acd177edf0 --- /dev/null +++ b/third_party/go/git-appraise/repository/repo.go @@ -0,0 +1,221 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package repository contains helper methods for working with a Git repo. +package repository + +// Note represents the contents of a git-note +type Note []byte + +// CommitDetails represents the contents of a commit. +type CommitDetails struct { + Author string `json:"author,omitempty"` + AuthorEmail string `json:"authorEmail,omitempty"` + Tree string `json:"tree,omitempty"` + Time string `json:"time,omitempty"` + Parents []string `json:"parents,omitempty"` + Summary string `json:"summary,omitempty"` +} + +// Repo represents a source code repository. +type Repo interface { + // GetPath returns the path to the repo. + GetPath() string + + // GetRepoStateHash returns a hash which embodies the entire current state of a repository. + GetRepoStateHash() (string, error) + + // GetUserEmail returns the email address that the user has used to configure git. + GetUserEmail() (string, error) + + // GetUserSigningKey returns the key id the user has configured for + // sigining git artifacts. + GetUserSigningKey() (string, error) + + // GetCoreEditor returns the name of the editor that the user has used to configure git. + GetCoreEditor() (string, error) + + // GetSubmitStrategy returns the way in which a review is submitted + GetSubmitStrategy() (string, error) + + // HasUncommittedChanges returns true if there are local, uncommitted changes. + HasUncommittedChanges() (bool, error) + + // VerifyCommit verifies that the supplied hash points to a known commit. + VerifyCommit(hash string) error + + // VerifyGitRef verifies that the supplied ref points to a known commit. + VerifyGitRef(ref string) error + + // GetHeadRef returns the ref that is the current HEAD. + GetHeadRef() (string, error) + + // GetCommitHash returns the hash of the commit pointed to by the given ref. + GetCommitHash(ref string) (string, error) + + // ResolveRefCommit returns the commit pointed to by the given ref, which may be a remote ref. + // + // This differs from GetCommitHash which only works on exact matches, in that it will try to + // intelligently handle the scenario of a ref not existing locally, but being known to exist + // in a remote repo. + // + // This method should be used when a command may be performed by either the reviewer or the + // reviewee, while GetCommitHash should be used when the encompassing command should only be + // performed by the reviewee. + ResolveRefCommit(ref string) (string, error) + + // GetCommitMessage returns the message stored in the commit pointed to by the given ref. + GetCommitMessage(ref string) (string, error) + + // GetCommitTime returns the commit time of the commit pointed to by the given ref. + GetCommitTime(ref string) (string, error) + + // GetLastParent returns the last parent of the given commit (as ordered by git). + GetLastParent(ref string) (string, error) + + // GetCommitDetails returns the details of a commit's metadata. + GetCommitDetails(ref string) (*CommitDetails, error) + + // MergeBase determines if the first commit that is an ancestor of the two arguments. + MergeBase(a, b string) (string, error) + + // IsAncestor determines if the first argument points to a commit that is an ancestor of the second. + IsAncestor(ancestor, descendant string) (bool, error) + + // Diff computes the diff between two given commits. + Diff(left, right string, diffArgs ...string) (string, error) + + // Show returns the contents of the given file at the given commit. + Show(commit, path string) (string, error) + + // SwitchToRef changes the currently-checked-out ref. + SwitchToRef(ref string) error + + // ArchiveRef adds the current commit pointed to by the 'ref' argument + // under the ref specified in the 'archive' argument. + // + // Both the 'ref' and 'archive' arguments are expected to be the fully + // qualified names of git refs (e.g. 'refs/heads/my-change' or + // 'refs/archive/devtools'). + // + // If the ref pointed to by the 'archive' argument does not exist + // yet, then it will be created. + ArchiveRef(ref, archive string) error + + // MergeRef merges the given ref into the current one. + // + // The ref argument is the ref to merge, and fastForward indicates that the + // current ref should only move forward, as opposed to creating a bubble merge. + // The messages argument(s) provide text that should be included in the default + // merge commit message (separated by blank lines). + MergeRef(ref string, fastForward bool, messages ...string) error + + // MergeAndSignRef merges the given ref into the current one and signs the + // merge. + // + // The ref argument is the ref to merge, and fastForward indicates that the + // current ref should only move forward, as opposed to creating a bubble merge. + // The messages argument(s) provide text that should be included in the default + // merge commit message (separated by blank lines). + MergeAndSignRef(ref string, fastForward bool, messages ...string) error + + // RebaseRef rebases the current ref onto the given one. + RebaseRef(ref string) error + + // RebaseAndSignRef rebases the current ref onto the given one and signs + // the result. + RebaseAndSignRef(ref string) error + + // ListCommits returns the list of commits reachable from the given ref. + // + // The generated list is in chronological order (with the oldest commit first). + // + // If the specified ref does not exist, then this method returns an empty result. + ListCommits(ref string) []string + + // ListCommitsBetween returns the list of commits between the two given revisions. + // + // The "from" parameter is the starting point (exclusive), and the "to" + // parameter is the ending point (inclusive). + // + // The "from" commit does not need to be an ancestor of the "to" commit. If it + // is not, then the merge base of the two is used as the starting point. + // Admittedly, this makes calling these the "between" commits is a bit of a + // misnomer, but it also makes the method easier to use when you want to + // generate the list of changes in a feature branch, as it eliminates the need + // to explicitly calculate the merge base. This also makes the semantics of the + // method compatible with git's built-in "rev-list" command. + // + // The generated list is in chronological order (with the oldest commit first). + ListCommitsBetween(from, to string) ([]string, error) + + // GetNotes reads the notes from the given ref that annotate the given revision. + GetNotes(notesRef, revision string) []Note + + // GetAllNotes reads the contents of the notes under the given ref for every commit. + // + // The returned value is a mapping from commit hash to the list of notes for that commit. + // + // This is the batch version of the corresponding GetNotes(...) method. + GetAllNotes(notesRef string) (map[string][]Note, error) + + // AppendNote appends a note to a revision under the given ref. + AppendNote(ref, revision string, note Note) error + + // ListNotedRevisions returns the collection of revisions that are annotated by notes in the given ref. + ListNotedRevisions(notesRef string) []string + + // PushNotes pushes git notes to a remote repo. + PushNotes(remote, notesRefPattern string) error + + // PullNotes fetches the contents of the given notes ref from a remote repo, + // and then merges them with the corresponding local notes using the + // "cat_sort_uniq" strategy. + PullNotes(remote, notesRefPattern string) error + + // PushNotesAndArchive pushes the given notes and archive refs to a remote repo. + PushNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error + + // PullNotesAndArchive fetches the contents of the notes and archives refs from + // a remote repo, and merges them with the corresponding local refs. + // + // For notes refs, we assume that every note can be automatically merged using + // the 'cat_sort_uniq' strategy (the git-appraise schemas fit that requirement), + // so we automatically merge the remote notes into the local notes. + // + // For "archive" refs, they are expected to be used solely for maintaining + // reachability of commits that are part of the history of any reviews, + // so we do not maintain any consistency with their tree objects. Instead, + // we merely ensure that their history graph includes every commit that we + // intend to keep. + PullNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error + + // MergeNotes merges in the remote's state of the archives reference into + // the local repository's. + MergeNotes(remote, notesRefPattern string) error + // MergeArchives merges in the remote's state of the archives reference + // into the local repository's. + MergeArchives(remote, archiveRefPattern string) error + + // FetchAndReturnNewReviewHashes fetches the notes "branches" and then + // susses out the IDs (the revision the review points to) of any new + // reviews, then returns that list of IDs. + // + // This is accomplished by determining which files in the notes tree have + // changed because the _names_ of these files correspond to the revisions + // they point to. + FetchAndReturnNewReviewHashes(remote, notesRefPattern, archiveRefPattern string) ([]string, error) +} |