feat(third_party): Check in git-appraise
This commit is contained in:
parent
e03f063052
commit
fe642c30f0
38 changed files with 7300 additions and 0 deletions
987
third_party/go/git-appraise/repository/git.go
vendored
Normal file
987
third_party/go/git-appraise/repository/git.go
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
94
third_party/go/git-appraise/repository/git_test.go
vendored
Normal file
94
third_party/go/git-appraise/repository/git_test.go
vendored
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
613
third_party/go/git-appraise/repository/mock_repo.go
vendored
Normal file
613
third_party/go/git-appraise/repository/mock_repo.go
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
221
third_party/go/git-appraise/repository/repo.go
vendored
Normal file
221
third_party/go/git-appraise/repository/repo.go
vendored
Normal file
|
|
@ -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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue