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
160
third_party/go/git-appraise/review/analyses/analyses.go
vendored
Normal file
160
third_party/go/git-appraise/review/analyses/analyses.go
vendored
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
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 analyses defines the internal representation of static analysis reports.
|
||||
package analyses
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/git-appraise/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
// Ref defines the git-notes ref that we expect to contain analysis reports.
|
||||
Ref = "refs/notes/devtools/analyses"
|
||||
|
||||
// StatusLooksGoodToMe is the status string representing that analyses reported no messages.
|
||||
StatusLooksGoodToMe = "lgtm"
|
||||
// StatusForYourInformation is the status string representing that analyses reported informational messages.
|
||||
StatusForYourInformation = "fyi"
|
||||
// StatusNeedsMoreWork is the status string representing that analyses reported error messages.
|
||||
StatusNeedsMoreWork = "nmw"
|
||||
|
||||
// FormatVersion defines the latest version of the request format supported by the tool.
|
||||
FormatVersion = 0
|
||||
)
|
||||
|
||||
// Report represents a build/test status report generated by analyses tool.
|
||||
// Every field is optional.
|
||||
type Report struct {
|
||||
Timestamp string `json:"timestamp,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
// Version represents the version of the metadata format.
|
||||
Version int `json:"v,omitempty"`
|
||||
}
|
||||
|
||||
// LocationRange represents the location within a source file that an analysis message covers.
|
||||
type LocationRange struct {
|
||||
StartLine uint32 `json:"start_line,omitempty"`
|
||||
StartColumn uint32 `json:"start_column,omitempty"`
|
||||
EndLine uint32 `json:"end_line,omitempty"`
|
||||
EndColumn uint32 `json:"end_column,omitempty"`
|
||||
}
|
||||
|
||||
// Location represents the location within a source tree that an analysis message covers.
|
||||
type Location struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
Range *LocationRange `json:"range,omitempty"`
|
||||
}
|
||||
|
||||
// Note represents a single analysis message.
|
||||
type Note struct {
|
||||
Location *Location `json:"location,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// AnalyzeResponse represents the response from a static-analysis tool.
|
||||
type AnalyzeResponse struct {
|
||||
Notes []Note `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
// ReportDetails represents an entire static analysis run (which might include multiple analysis tools).
|
||||
type ReportDetails struct {
|
||||
AnalyzeResponse []AnalyzeResponse `json:"analyze_response,omitempty"`
|
||||
}
|
||||
|
||||
// GetLintReportResult downloads the details of a lint report and returns the responses embedded in it.
|
||||
func (analysesReport Report) GetLintReportResult() ([]AnalyzeResponse, error) {
|
||||
if analysesReport.URL == "" {
|
||||
return nil, nil
|
||||
}
|
||||
res, err := http.Get(analysesReport.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
analysesResults, err := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var details ReportDetails
|
||||
err = json.Unmarshal([]byte(analysesResults), &details)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return details.AnalyzeResponse, nil
|
||||
}
|
||||
|
||||
// GetNotes downloads the details of an analyses report and returns the notes embedded in it.
|
||||
func (analysesReport Report) GetNotes() ([]Note, error) {
|
||||
reportResults, err := analysesReport.GetLintReportResult()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var reportNotes []Note
|
||||
for _, reportResult := range reportResults {
|
||||
reportNotes = append(reportNotes, reportResult.Notes...)
|
||||
}
|
||||
return reportNotes, nil
|
||||
}
|
||||
|
||||
// Parse parses an analysis report from a git note.
|
||||
func Parse(note repository.Note) (Report, error) {
|
||||
bytes := []byte(note)
|
||||
var report Report
|
||||
err := json.Unmarshal(bytes, &report)
|
||||
return report, err
|
||||
}
|
||||
|
||||
// GetLatestAnalysesReport takes a collection of analysis reports, and returns the one with the most recent timestamp.
|
||||
func GetLatestAnalysesReport(reports []Report) (*Report, error) {
|
||||
timestampReportMap := make(map[int]*Report)
|
||||
var timestamps []int
|
||||
|
||||
for _, report := range reports {
|
||||
timestamp, err := strconv.Atoi(report.Timestamp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timestamps = append(timestamps, timestamp)
|
||||
timestampReportMap[timestamp] = &report
|
||||
}
|
||||
if len(timestamps) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
sort.Sort(sort.Reverse(sort.IntSlice(timestamps)))
|
||||
return timestampReportMap[timestamps[0]], nil
|
||||
}
|
||||
|
||||
// ParseAllValid takes collection of git notes and tries to parse a analyses report
|
||||
// from each one. Any notes that are not valid analyses reports get ignored.
|
||||
func ParseAllValid(notes []repository.Note) []Report {
|
||||
var reports []Report
|
||||
for _, note := range notes {
|
||||
report, err := Parse(note)
|
||||
if err == nil && report.Version == FormatVersion {
|
||||
reports = append(reports, report)
|
||||
}
|
||||
}
|
||||
return reports
|
||||
}
|
||||
77
third_party/go/git-appraise/review/analyses/analyses_test.go
vendored
Normal file
77
third_party/go/git-appraise/review/analyses/analyses_test.go
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
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 analyses
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/git-appraise/repository"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
mockOldReport = `{"timestamp": "0", "url": "https://this-url-does-not-exist.test/analysis.json"}`
|
||||
mockNewReport = `{"timestamp": "1", "url": "%s"}`
|
||||
mockResults = `{
|
||||
"analyze_response": [{
|
||||
"note": [{
|
||||
"location": {
|
||||
"path": "file.txt",
|
||||
"range": {
|
||||
"start_line": 5
|
||||
}
|
||||
},
|
||||
"category": "test",
|
||||
"description": "This is a test"
|
||||
}]
|
||||
}]
|
||||
}`
|
||||
)
|
||||
|
||||
func mockHandler(t *testing.T) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Log(r)
|
||||
fmt.Fprintln(w, mockResults)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLatestResult(t *testing.T) {
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(mockHandler(t)))
|
||||
defer mockServer.Close()
|
||||
|
||||
reports := ParseAllValid([]repository.Note{
|
||||
repository.Note([]byte(mockOldReport)),
|
||||
repository.Note([]byte(fmt.Sprintf(mockNewReport, mockServer.URL))),
|
||||
})
|
||||
|
||||
report, err := GetLatestAnalysesReport(reports)
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error while parsing analysis reports", err)
|
||||
}
|
||||
if report == nil {
|
||||
t.Fatal("Unexpected nil report")
|
||||
}
|
||||
reportResult, err := report.GetLintReportResult()
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error while reading the latest report's results", err)
|
||||
}
|
||||
if len(reportResult) != 1 {
|
||||
t.Fatal("Unexpected report result", reportResult)
|
||||
}
|
||||
}
|
||||
95
third_party/go/git-appraise/review/ci/ci.go
vendored
Normal file
95
third_party/go/git-appraise/review/ci/ci.go
vendored
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
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 ci defines the internal representation of a continuous integration reports.
|
||||
package ci
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/google/git-appraise/repository"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
// Ref defines the git-notes ref that we expect to contain CI reports.
|
||||
Ref = "refs/notes/devtools/ci"
|
||||
|
||||
// StatusSuccess is the status string representing that a build and/or test passed.
|
||||
StatusSuccess = "success"
|
||||
// StatusFailure is the status string representing that a build and/or test failed.
|
||||
StatusFailure = "failure"
|
||||
|
||||
// FormatVersion defines the latest version of the request format supported by the tool.
|
||||
FormatVersion = 0
|
||||
)
|
||||
|
||||
// Report represents a build/test status report generated by a continuous integration tool.
|
||||
//
|
||||
// Every field is optional.
|
||||
type Report struct {
|
||||
Timestamp string `json:"timestamp,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Agent string `json:"agent,omitempty"`
|
||||
// Version represents the version of the metadata format.
|
||||
Version int `json:"v,omitempty"`
|
||||
}
|
||||
|
||||
// Parse parses a CI report from a git note.
|
||||
func Parse(note repository.Note) (Report, error) {
|
||||
bytes := []byte(note)
|
||||
var report Report
|
||||
err := json.Unmarshal(bytes, &report)
|
||||
return report, err
|
||||
}
|
||||
|
||||
// GetLatestCIReport takes the collection of reports and returns the one with the most recent timestamp.
|
||||
func GetLatestCIReport(reports []Report) (*Report, error) {
|
||||
timestampReportMap := make(map[int]*Report)
|
||||
var timestamps []int
|
||||
|
||||
for _, report := range reports {
|
||||
timestamp, err := strconv.Atoi(report.Timestamp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timestamps = append(timestamps, timestamp)
|
||||
timestampReportMap[timestamp] = &report
|
||||
}
|
||||
if len(timestamps) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
sort.Sort(sort.Reverse(sort.IntSlice(timestamps)))
|
||||
return timestampReportMap[timestamps[0]], nil
|
||||
}
|
||||
|
||||
// ParseAllValid takes collection of git notes and tries to parse a CI report
|
||||
// from each one. Any notes that are not valid CI reports get ignored, as we
|
||||
// expect the git notes to be a heterogenous list, with only some of them
|
||||
// being valid CI status reports.
|
||||
func ParseAllValid(notes []repository.Note) []Report {
|
||||
var reports []Report
|
||||
for _, note := range notes {
|
||||
report, err := Parse(note)
|
||||
if err == nil && report.Version == FormatVersion {
|
||||
if report.Status == "" || report.Status == StatusSuccess || report.Status == StatusFailure {
|
||||
reports = append(reports, report)
|
||||
}
|
||||
}
|
||||
}
|
||||
return reports
|
||||
}
|
||||
85
third_party/go/git-appraise/review/ci/ci_test.go
vendored
Normal file
85
third_party/go/git-appraise/review/ci/ci_test.go
vendored
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
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 ci
|
||||
|
||||
import (
|
||||
"github.com/google/git-appraise/repository"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testCINote1 = `{
|
||||
"Timestamp": "4",
|
||||
"URL": "www.google.com",
|
||||
"Status": "success"
|
||||
}`
|
||||
|
||||
const testCINote2 = `{
|
||||
"Timestamp": "16",
|
||||
"URL": "www.google.com",
|
||||
"Status": "failure"
|
||||
}`
|
||||
|
||||
const testCINote3 = `{
|
||||
"Timestamp": "30",
|
||||
"URL": "www.google.com",
|
||||
"Status": "something else"
|
||||
}`
|
||||
|
||||
const testCINote4 = `{
|
||||
"Timestamp": "28",
|
||||
"URL": "www.google.com",
|
||||
"Status": "success"
|
||||
}`
|
||||
|
||||
const testCINote5 = `{
|
||||
"Timestamp": "27",
|
||||
"URL": "www.google.com",
|
||||
"Status": "success"
|
||||
}`
|
||||
|
||||
func TestCIReport(t *testing.T) {
|
||||
latestReport, err := GetLatestCIReport(ParseAllValid([]repository.Note{
|
||||
repository.Note(testCINote1),
|
||||
repository.Note(testCINote2),
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatal("Failed to properly fetch the latest report", err)
|
||||
}
|
||||
expected, err := Parse(repository.Note(testCINote2))
|
||||
if err != nil {
|
||||
t.Fatal("Failed to parse the expected report", err)
|
||||
}
|
||||
if *latestReport != expected {
|
||||
t.Fatal("This is not the latest ", latestReport)
|
||||
}
|
||||
latestReport, err = GetLatestCIReport(ParseAllValid([]repository.Note{
|
||||
repository.Note(testCINote1),
|
||||
repository.Note(testCINote2),
|
||||
repository.Note(testCINote3),
|
||||
repository.Note(testCINote4),
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatal("Failed to properly fetch the latest report", err)
|
||||
}
|
||||
expected, err = Parse(repository.Note(testCINote4))
|
||||
if err != nil {
|
||||
t.Fatal("Failed to parse the expected report", err)
|
||||
}
|
||||
if *latestReport != expected {
|
||||
t.Fatal("This is not the latest ", latestReport)
|
||||
}
|
||||
}
|
||||
266
third_party/go/git-appraise/review/comment/comment.go
vendored
Normal file
266
third_party/go/git-appraise/review/comment/comment.go
vendored
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
/*
|
||||
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 comment defines the internal representation of a review comment.
|
||||
package comment
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/git-appraise/repository"
|
||||
"github.com/google/git-appraise/review/gpg"
|
||||
)
|
||||
|
||||
// Ref defines the git-notes ref that we expect to contain review comments.
|
||||
const Ref = "refs/notes/devtools/discuss"
|
||||
|
||||
// FormatVersion defines the latest version of the comment format supported by the tool.
|
||||
const FormatVersion = 0
|
||||
|
||||
// ErrInvalidRange inidcates an error during parsing of a user-defined file
|
||||
// range
|
||||
var ErrInvalidRange = errors.New("invalid file location range. The required form is StartLine[+StartColumn][:EndLine[+EndColumn]]. The first line in a file is considered to be line 1")
|
||||
|
||||
// Range represents the range of text that is under discussion.
|
||||
type Range struct {
|
||||
StartLine uint32 `json:"startLine"`
|
||||
StartColumn uint32 `json:"startColumn,omitempty"`
|
||||
EndLine uint32 `json:"endLine,omitempty"`
|
||||
EndColumn uint32 `json:"endColumn,omitempty"`
|
||||
}
|
||||
|
||||
// Location represents the location of a comment within a commit.
|
||||
type Location struct {
|
||||
Commit string `json:"commit,omitempty"`
|
||||
// If the path is omitted, then the comment applies to the entire commit.
|
||||
Path string `json:"path,omitempty"`
|
||||
// If the range is omitted, then the location represents an entire file.
|
||||
Range *Range `json:"range,omitempty"`
|
||||
}
|
||||
|
||||
// Check verifies that this location is valid in the provided
|
||||
// repository.
|
||||
func (location *Location) Check(repo repository.Repo) error {
|
||||
contents, err := repo.Show(location.Commit, location.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lines := strings.Split(contents, "\n")
|
||||
if location.Range.StartLine > uint32(len(lines)) {
|
||||
return fmt.Errorf("Line number %d does not exist in file %q",
|
||||
location.Range.StartLine,
|
||||
location.Path)
|
||||
}
|
||||
if location.Range.StartColumn != 0 &&
|
||||
location.Range.StartColumn > uint32(len(lines[location.Range.StartLine-1])) {
|
||||
return fmt.Errorf("Line %d in %q is too short for column %d",
|
||||
location.Range.StartLine,
|
||||
location.Path,
|
||||
location.Range.StartColumn)
|
||||
}
|
||||
if location.Range.EndLine != 0 &&
|
||||
location.Range.EndLine > uint32(len(lines)) {
|
||||
return fmt.Errorf("End line number %d does not exist in file %q",
|
||||
location.Range.EndLine,
|
||||
location.Path)
|
||||
}
|
||||
if location.Range.EndColumn != 0 &&
|
||||
location.Range.EndColumn > uint32(len(lines[location.Range.EndLine-1])) {
|
||||
return fmt.Errorf("End line %d in %q is too short for column %d",
|
||||
location.Range.EndLine,
|
||||
location.Path,
|
||||
location.Range.EndColumn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Comment represents a review comment, and can occur in any of the following contexts:
|
||||
// 1. As a comment on an entire commit.
|
||||
// 2. As a comment about a specific file in a commit.
|
||||
// 3. As a comment about a specific line in a commit.
|
||||
// 4. As a response to another comment.
|
||||
type Comment struct {
|
||||
// Timestamp and Author are optimizations that allows us to display comment threads
|
||||
// without having to run git-blame over the notes object. This is done because
|
||||
// git-blame will become more and more expensive as the number of code reviews grows.
|
||||
Timestamp string `json:"timestamp,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
// If original is provided, then the comment is an updated version of another comment.
|
||||
Original string `json:"original,omitempty"`
|
||||
// If parent is provided, then the comment is a response to another comment.
|
||||
Parent string `json:"parent,omitempty"`
|
||||
// If location is provided, then the comment is specific to that given location.
|
||||
Location *Location `json:"location,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
// The resolved bit indicates that no further action is needed.
|
||||
//
|
||||
// When the parent of the comment is another comment, this means that comment
|
||||
// has been addressed. Otherwise, the parent is the commit, and this means that the
|
||||
// change has been accepted. If the resolved bit is unset, then the comment is only an FYI.
|
||||
Resolved *bool `json:"resolved,omitempty"`
|
||||
// Version represents the version of the metadata format.
|
||||
Version int `json:"v,omitempty"`
|
||||
|
||||
gpg.Sig
|
||||
}
|
||||
|
||||
// New returns a new comment with the given description message.
|
||||
//
|
||||
// The Timestamp and Author fields are automatically filled in with the current time and user.
|
||||
func New(author string, description string) Comment {
|
||||
return Comment{
|
||||
Timestamp: strconv.FormatInt(time.Now().Unix(), 10),
|
||||
Author: author,
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses a review comment from a git note.
|
||||
func Parse(note repository.Note) (Comment, error) {
|
||||
bytes := []byte(note)
|
||||
var comment Comment
|
||||
err := json.Unmarshal(bytes, &comment)
|
||||
return comment, err
|
||||
}
|
||||
|
||||
// ParseAllValid takes collection of git notes and tries to parse a review
|
||||
// comment from each one. Any notes that are not valid review comments get
|
||||
// ignored, as we expect the git notes to be a heterogenous list, with only
|
||||
// some of them being review comments.
|
||||
func ParseAllValid(notes []repository.Note) map[string]Comment {
|
||||
comments := make(map[string]Comment)
|
||||
for _, note := range notes {
|
||||
comment, err := Parse(note)
|
||||
if err == nil && comment.Version == FormatVersion {
|
||||
hash, err := comment.Hash()
|
||||
if err == nil {
|
||||
comments[hash] = comment
|
||||
}
|
||||
}
|
||||
}
|
||||
return comments
|
||||
}
|
||||
|
||||
func (comment Comment) serialize() ([]byte, error) {
|
||||
if len(comment.Timestamp) < 10 {
|
||||
// To make sure that timestamps from before 2001 appear in the correct
|
||||
// alphabetical order, we reformat the timestamp to be at least 10 characters
|
||||
// and zero-padded.
|
||||
time, err := strconv.ParseInt(comment.Timestamp, 10, 64)
|
||||
if err == nil {
|
||||
comment.Timestamp = fmt.Sprintf("%010d", time)
|
||||
}
|
||||
// We ignore the other case, as the comment timestamp is not in a format
|
||||
// we expected, so we should just leave it alone.
|
||||
}
|
||||
return json.Marshal(comment)
|
||||
}
|
||||
|
||||
// Write writes a review comment as a JSON-formatted git note.
|
||||
func (comment Comment) Write() (repository.Note, error) {
|
||||
bytes, err := comment.serialize()
|
||||
return repository.Note(bytes), err
|
||||
}
|
||||
|
||||
// Hash returns the SHA1 hash of a review comment.
|
||||
func (comment Comment) Hash() (string, error) {
|
||||
bytes, err := comment.serialize()
|
||||
return fmt.Sprintf("%x", sha1.Sum(bytes)), err
|
||||
}
|
||||
|
||||
// Set implenents flag.Value for the Range type
|
||||
func (r *Range) Set(s string) error {
|
||||
var err error
|
||||
*r = Range{}
|
||||
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
startEndParts := strings.Split(s, ":")
|
||||
if len(startEndParts) > 2 {
|
||||
return ErrInvalidRange
|
||||
}
|
||||
|
||||
r.StartLine, r.StartColumn, err = parseRangePart(startEndParts[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(startEndParts) == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
r.EndLine, r.EndColumn, err = parseRangePart(startEndParts[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.StartLine > r.EndLine {
|
||||
return errors.New("start line cannot be greater than end line in range")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRangePart(s string) (uint32, uint32, error) {
|
||||
parts := strings.Split(s, "+")
|
||||
if len(parts) > 2 {
|
||||
return 0, 0, ErrInvalidRange
|
||||
}
|
||||
|
||||
line, err := strconv.ParseUint(parts[0], 10, 32)
|
||||
if err != nil {
|
||||
return 0, 0, ErrInvalidRange
|
||||
}
|
||||
|
||||
if len(parts) == 1 {
|
||||
return uint32(line), 0, nil
|
||||
}
|
||||
|
||||
col, err := strconv.ParseUint(parts[1], 10, 32)
|
||||
if err != nil {
|
||||
return 0, 0, ErrInvalidRange
|
||||
}
|
||||
|
||||
if line == 0 && col != 0 {
|
||||
// line 0 represents the entire file
|
||||
return 0, 0, ErrInvalidRange
|
||||
}
|
||||
|
||||
return uint32(line), uint32(col), nil
|
||||
}
|
||||
|
||||
func (r *Range) String() string {
|
||||
out := ""
|
||||
if r.StartLine != 0 {
|
||||
out = fmt.Sprintf("%d", r.StartLine)
|
||||
}
|
||||
if r.StartColumn != 0 {
|
||||
out = fmt.Sprintf("%s+%d", out, r.StartColumn)
|
||||
}
|
||||
if r.EndLine != 0 {
|
||||
out = fmt.Sprintf("%s:%d", out, r.EndLine)
|
||||
}
|
||||
if r.EndColumn != 0 {
|
||||
out = fmt.Sprintf("%s+%d", out, r.EndColumn)
|
||||
}
|
||||
return out
|
||||
}
|
||||
129
third_party/go/git-appraise/review/gpg/signable.go
vendored
Normal file
129
third_party/go/git-appraise/review/gpg/signable.go
vendored
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// Package gpg provides an interface and an abstraction with which to sign and
|
||||
// verify review requests and comments.
|
||||
package gpg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
const placeholder = "gpgsig"
|
||||
|
||||
// Sig provides an abstraction around shelling out to GPG to sign the
|
||||
// content it's given.
|
||||
type Sig struct {
|
||||
// Sig holds an object's content's signature.
|
||||
Sig string `json:"signature,omitempty"`
|
||||
}
|
||||
|
||||
// Signable is an interfaces which provides the pointer to the signable
|
||||
// object's stringified signature.
|
||||
//
|
||||
// This pointer is used by `Sign` and `Verify` to replace its contents with
|
||||
// `placeholder` or the signature itself for the purposes of signing or
|
||||
// verifying.
|
||||
type Signable interface {
|
||||
Signature() *string
|
||||
}
|
||||
|
||||
// Signature is `Sig`'s implementation of `Signable`. Through this function, an
|
||||
// object which needs to implement `Signable` need only embed `Sig`
|
||||
// anonymously. See, e.g., review/request.go.
|
||||
func (s *Sig) Signature() *string {
|
||||
return &s.Sig
|
||||
}
|
||||
|
||||
// Sign uses gpg to sign the contents of a request and deposit it into the
|
||||
// signature key of the request.
|
||||
func Sign(key string, s Signable) error {
|
||||
// First we retrieve the pointer and write `placeholder` as its value.
|
||||
sigPtr := s.Signature()
|
||||
*sigPtr = placeholder
|
||||
|
||||
// Marshal the content and sign it.
|
||||
content, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sig, err := signContent(key, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the signature as the new value at the pointer.
|
||||
*sigPtr = sig.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func signContent(key string, content []byte) (*bytes.Buffer,
|
||||
error) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd := exec.Command("gpg", "-u", key, "--detach-sign", "--armor")
|
||||
cmd.Stdin = bytes.NewReader(content)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
return &stdout, err
|
||||
}
|
||||
|
||||
// Verify verifies the signatures on the request and its comments with the
|
||||
// given key.
|
||||
func Verify(s Signable) error {
|
||||
// Retrieve the pointer.
|
||||
sigPtr := s.Signature()
|
||||
// Copy its contents.
|
||||
sig := *sigPtr
|
||||
// Overwrite the value with the placeholder.
|
||||
*sigPtr = placeholder
|
||||
|
||||
defer func() { *sigPtr = sig }()
|
||||
|
||||
// 1. Marshal the content into JSON.
|
||||
// 2. Write the signature and the content to temp files.
|
||||
// 3. Use gpg to verify the signature.
|
||||
content, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sigFile, err := ioutil.TempFile("", "sig")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(sigFile.Name())
|
||||
_, err = sigFile.Write([]byte(sig))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = sigFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contentFile, err := ioutil.TempFile("", "content")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(contentFile.Name())
|
||||
_, err = contentFile.Write(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = contentFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd := exec.Command("gpg", "--verify", sigFile.Name(), contentFile.Name())
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s", stderr.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
104
third_party/go/git-appraise/review/request/request.go
vendored
Normal file
104
third_party/go/git-appraise/review/request/request.go
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
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 request defines the internal representation of a review request.
|
||||
package request
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/git-appraise/repository"
|
||||
"github.com/google/git-appraise/review/gpg"
|
||||
)
|
||||
|
||||
// Ref defines the git-notes ref that we expect to contain review requests.
|
||||
const Ref = "refs/notes/devtools/reviews"
|
||||
|
||||
// FormatVersion defines the latest version of the request format supported by the tool.
|
||||
const FormatVersion = 0
|
||||
|
||||
// Request represents an initial request for a code review.
|
||||
//
|
||||
// Every field is optional.
|
||||
type Request struct {
|
||||
// Timestamp and Requester are optimizations that allows us to display reviews
|
||||
// without having to run git-blame over the notes object. This is done because
|
||||
// git-blame will become more and more expensive as the number of reviews grows.
|
||||
Timestamp string `json:"timestamp,omitempty"`
|
||||
ReviewRef string `json:"reviewRef,omitempty"`
|
||||
TargetRef string `json:"targetRef"`
|
||||
Requester string `json:"requester,omitempty"`
|
||||
Reviewers []string `json:"reviewers,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
// Version represents the version of the metadata format.
|
||||
Version int `json:"v,omitempty"`
|
||||
// BaseCommit stores the commit ID of the target ref at the time the review was requested.
|
||||
// This is optional, and only used for submitted reviews which were anchored at a merge commit.
|
||||
// This allows someone viewing that submitted review to find the diff against which the
|
||||
// code was reviewed.
|
||||
BaseCommit string `json:"baseCommit,omitempty"`
|
||||
// Alias stores a post-rebase commit ID for the review. This allows the tool
|
||||
// to track the history of a review even if the commit history changes.
|
||||
Alias string `json:"alias,omitempty"`
|
||||
|
||||
gpg.Sig
|
||||
}
|
||||
|
||||
// New returns a new request.
|
||||
//
|
||||
// The Timestamp and Requester fields are automatically filled in with the current time and user.
|
||||
func New(requester string, reviewers []string, reviewRef, targetRef, description string) Request {
|
||||
return Request{
|
||||
Timestamp: strconv.FormatInt(time.Now().Unix(), 10),
|
||||
Requester: requester,
|
||||
Reviewers: reviewers,
|
||||
ReviewRef: reviewRef,
|
||||
TargetRef: targetRef,
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses a review request from a git note.
|
||||
func Parse(note repository.Note) (Request, error) {
|
||||
bytes := []byte(note)
|
||||
var request Request
|
||||
err := json.Unmarshal(bytes, &request)
|
||||
// TODO(ojarjur): If "requester" is not set, then use git-blame to fill it in.
|
||||
return request, err
|
||||
}
|
||||
|
||||
// ParseAllValid takes collection of git notes and tries to parse a review
|
||||
// request from each one. Any notes that are not valid review requests get
|
||||
// ignored, as we expect the git notes to be a heterogenous list, with only
|
||||
// some of them being review requests.
|
||||
func ParseAllValid(notes []repository.Note) []Request {
|
||||
var requests []Request
|
||||
for _, note := range notes {
|
||||
request, err := Parse(note)
|
||||
if err == nil && request.Version == FormatVersion {
|
||||
requests = append(requests, request)
|
||||
}
|
||||
}
|
||||
return requests
|
||||
}
|
||||
|
||||
// Write writes a review request as a JSON-formatted git note.
|
||||
func (request *Request) Write() (repository.Note, error) {
|
||||
bytes, err := json.Marshal(request)
|
||||
return repository.Note(bytes), err
|
||||
}
|
||||
772
third_party/go/git-appraise/review/review.go
vendored
Normal file
772
third_party/go/git-appraise/review/review.go
vendored
Normal file
|
|
@ -0,0 +1,772 @@
|
|||
/*
|
||||
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 review contains the data structures used to represent code reviews.
|
||||
package review
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/google/git-appraise/repository"
|
||||
"github.com/google/git-appraise/review/analyses"
|
||||
"github.com/google/git-appraise/review/ci"
|
||||
"github.com/google/git-appraise/review/comment"
|
||||
"github.com/google/git-appraise/review/gpg"
|
||||
"github.com/google/git-appraise/review/request"
|
||||
)
|
||||
|
||||
const archiveRef = "refs/devtools/archives/reviews"
|
||||
|
||||
// CommentThread represents the tree-based hierarchy of comments.
|
||||
//
|
||||
// The Resolved field represents the aggregate status of the entire thread. If
|
||||
// it is set to false, then it indicates that there is an unaddressed comment
|
||||
// in the thread. If it is unset, then that means that the root comment is an
|
||||
// FYI only, and that there are no unaddressed comments. If it is set to true,
|
||||
// then that means that there are no unaddressed comments, and that the root
|
||||
// comment has its resolved bit set to true.
|
||||
type CommentThread struct {
|
||||
Hash string `json:"hash,omitempty"`
|
||||
Comment comment.Comment `json:"comment"`
|
||||
Original *comment.Comment `json:"original,omitempty"`
|
||||
Edits []*comment.Comment `json:"edits,omitempty"`
|
||||
Children []CommentThread `json:"children,omitempty"`
|
||||
Resolved *bool `json:"resolved,omitempty"`
|
||||
Edited bool `json:"edited,omitempty"`
|
||||
}
|
||||
|
||||
// Summary represents the high-level state of a code review.
|
||||
//
|
||||
// This high-level state corresponds to the data that can be quickly read
|
||||
// directly from the repo, so other methods that need to operate on a lot
|
||||
// of reviews (such as listing the open reviews) should prefer operating on
|
||||
// the summary rather than the details.
|
||||
//
|
||||
// Review summaries have two status fields which are orthogonal:
|
||||
// 1. Resolved indicates if a reviewer has accepted or rejected the change.
|
||||
// 2. Submitted indicates if the change has been incorporated into the target.
|
||||
type Summary struct {
|
||||
Repo repository.Repo `json:"-"`
|
||||
Revision string `json:"revision"`
|
||||
Request request.Request `json:"request"`
|
||||
AllRequests []request.Request `json:"-"`
|
||||
Comments []CommentThread `json:"comments,omitempty"`
|
||||
Resolved *bool `json:"resolved,omitempty"`
|
||||
Submitted bool `json:"submitted"`
|
||||
}
|
||||
|
||||
// Review represents the entire state of a code review.
|
||||
//
|
||||
// This extends Summary to also include a list of reports for both the
|
||||
// continuous integration status, and the static analysis runs. Those reports
|
||||
// correspond to either the current commit in the review ref (for pending
|
||||
// reviews), or to the last commented-upon commit (for submitted reviews).
|
||||
type Review struct {
|
||||
*Summary
|
||||
Reports []ci.Report `json:"reports,omitempty"`
|
||||
Analyses []analyses.Report `json:"analyses,omitempty"`
|
||||
}
|
||||
|
||||
type commentsByTimestamp []*comment.Comment
|
||||
|
||||
// Interface methods for sorting comment threads by timestamp
|
||||
func (cs commentsByTimestamp) Len() int { return len(cs) }
|
||||
func (cs commentsByTimestamp) Swap(i, j int) { cs[i], cs[j] = cs[j], cs[i] }
|
||||
func (cs commentsByTimestamp) Less(i, j int) bool {
|
||||
return cs[i].Timestamp < cs[j].Timestamp
|
||||
}
|
||||
|
||||
type byTimestamp []CommentThread
|
||||
|
||||
// Interface methods for sorting comment threads by timestamp
|
||||
func (threads byTimestamp) Len() int { return len(threads) }
|
||||
func (threads byTimestamp) Swap(i, j int) { threads[i], threads[j] = threads[j], threads[i] }
|
||||
func (threads byTimestamp) Less(i, j int) bool {
|
||||
return threads[i].Comment.Timestamp < threads[j].Comment.Timestamp
|
||||
}
|
||||
|
||||
type requestsByTimestamp []request.Request
|
||||
|
||||
// Interface methods for sorting review requests by timestamp
|
||||
func (requests requestsByTimestamp) Len() int { return len(requests) }
|
||||
func (requests requestsByTimestamp) Swap(i, j int) {
|
||||
requests[i], requests[j] = requests[j], requests[i]
|
||||
}
|
||||
func (requests requestsByTimestamp) Less(i, j int) bool {
|
||||
return requests[i].Timestamp < requests[j].Timestamp
|
||||
}
|
||||
|
||||
type summariesWithNewestRequestsFirst []Summary
|
||||
|
||||
// Interface methods for sorting review summaries in reverse chronological order
|
||||
func (summaries summariesWithNewestRequestsFirst) Len() int { return len(summaries) }
|
||||
func (summaries summariesWithNewestRequestsFirst) Swap(i, j int) {
|
||||
summaries[i], summaries[j] = summaries[j], summaries[i]
|
||||
}
|
||||
func (summaries summariesWithNewestRequestsFirst) Less(i, j int) bool {
|
||||
return summaries[i].Request.Timestamp > summaries[j].Request.Timestamp
|
||||
}
|
||||
|
||||
// updateThreadsStatus calculates the aggregate status of a sequence of comment threads.
|
||||
//
|
||||
// The aggregate status is the conjunction of all of the non-nil child statuses.
|
||||
//
|
||||
// This has the side-effect of setting the "Resolved" field of all descendant comment threads.
|
||||
func updateThreadsStatus(threads []CommentThread) *bool {
|
||||
sort.Stable(byTimestamp(threads))
|
||||
noUnresolved := true
|
||||
var result *bool
|
||||
for i := range threads {
|
||||
thread := &threads[i]
|
||||
thread.updateResolvedStatus()
|
||||
if thread.Resolved != nil {
|
||||
noUnresolved = noUnresolved && *thread.Resolved
|
||||
result = &noUnresolved
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// updateResolvedStatus calculates the aggregate status of a single comment thread,
|
||||
// and updates the "Resolved" field of that thread accordingly.
|
||||
func (thread *CommentThread) updateResolvedStatus() {
|
||||
resolved := updateThreadsStatus(thread.Children)
|
||||
if resolved == nil {
|
||||
thread.Resolved = thread.Comment.Resolved
|
||||
return
|
||||
}
|
||||
|
||||
if !*resolved {
|
||||
thread.Resolved = resolved
|
||||
return
|
||||
}
|
||||
|
||||
if thread.Comment.Resolved == nil || !*thread.Comment.Resolved {
|
||||
thread.Resolved = nil
|
||||
return
|
||||
}
|
||||
|
||||
thread.Resolved = resolved
|
||||
}
|
||||
|
||||
// Verify verifies the signature on a comment.
|
||||
func (thread *CommentThread) Verify() error {
|
||||
err := gpg.Verify(&thread.Comment)
|
||||
if err != nil {
|
||||
hash, _ := thread.Comment.Hash()
|
||||
return fmt.Errorf("verification of comment [%s] failed: %s", hash, err)
|
||||
}
|
||||
for _, child := range thread.Children {
|
||||
err = child.Verify()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mutableThread is an internal-only data structure used to store partially constructed comment threads.
|
||||
type mutableThread struct {
|
||||
Hash string
|
||||
Comment comment.Comment
|
||||
Edits []*comment.Comment
|
||||
Children []*mutableThread
|
||||
}
|
||||
|
||||
// fixMutableThread is a helper method to finalize a mutableThread struct
|
||||
// (partially constructed comment thread) as a CommentThread struct
|
||||
// (fully constructed comment thread).
|
||||
func fixMutableThread(mutableThread *mutableThread) CommentThread {
|
||||
var children []CommentThread
|
||||
edited := len(mutableThread.Edits) > 0
|
||||
for _, mutableChild := range mutableThread.Children {
|
||||
child := fixMutableThread(mutableChild)
|
||||
if (!edited) && child.Edited {
|
||||
edited = true
|
||||
}
|
||||
children = append(children, child)
|
||||
}
|
||||
comment := &mutableThread.Comment
|
||||
if len(mutableThread.Edits) > 0 {
|
||||
sort.Stable(commentsByTimestamp(mutableThread.Edits))
|
||||
comment = mutableThread.Edits[len(mutableThread.Edits)-1]
|
||||
}
|
||||
|
||||
return CommentThread{
|
||||
Hash: mutableThread.Hash,
|
||||
Comment: *comment,
|
||||
Original: &mutableThread.Comment,
|
||||
Edits: mutableThread.Edits,
|
||||
Children: children,
|
||||
Edited: edited,
|
||||
}
|
||||
}
|
||||
|
||||
// This function builds the comment thread tree from the log-based list of comments.
|
||||
//
|
||||
// Since the comments can be processed in any order, this uses an internal mutable
|
||||
// data structure, and then converts it to the proper CommentThread structure at the end.
|
||||
func buildCommentThreads(commentsByHash map[string]comment.Comment) []CommentThread {
|
||||
threadsByHash := make(map[string]*mutableThread)
|
||||
for hash, comment := range commentsByHash {
|
||||
thread, ok := threadsByHash[hash]
|
||||
if !ok {
|
||||
thread = &mutableThread{
|
||||
Hash: hash,
|
||||
Comment: comment,
|
||||
}
|
||||
threadsByHash[hash] = thread
|
||||
}
|
||||
}
|
||||
var rootHashes []string
|
||||
for hash, thread := range threadsByHash {
|
||||
if thread.Comment.Original != "" {
|
||||
original, ok := threadsByHash[thread.Comment.Original]
|
||||
if ok {
|
||||
original.Edits = append(original.Edits, &thread.Comment)
|
||||
}
|
||||
} else if thread.Comment.Parent == "" {
|
||||
rootHashes = append(rootHashes, hash)
|
||||
} else {
|
||||
parent, ok := threadsByHash[thread.Comment.Parent]
|
||||
if ok {
|
||||
parent.Children = append(parent.Children, thread)
|
||||
}
|
||||
}
|
||||
}
|
||||
var threads []CommentThread
|
||||
for _, hash := range rootHashes {
|
||||
threads = append(threads, fixMutableThread(threadsByHash[hash]))
|
||||
}
|
||||
return threads
|
||||
}
|
||||
|
||||
// loadComments reads in the log-structured sequence of comments for a review,
|
||||
// and then builds the corresponding tree-structured comment threads.
|
||||
func (r *Summary) loadComments(commentNotes []repository.Note) []CommentThread {
|
||||
commentsByHash := comment.ParseAllValid(commentNotes)
|
||||
return buildCommentThreads(commentsByHash)
|
||||
}
|
||||
|
||||
func getSummaryFromNotes(repo repository.Repo, revision string, requestNotes, commentNotes []repository.Note) (*Summary, error) {
|
||||
requests := request.ParseAllValid(requestNotes)
|
||||
if requests == nil {
|
||||
return nil, fmt.Errorf("Could not find any review requests for %q", revision)
|
||||
}
|
||||
sort.Stable(requestsByTimestamp(requests))
|
||||
reviewSummary := Summary{
|
||||
Repo: repo,
|
||||
Revision: revision,
|
||||
Request: requests[len(requests)-1],
|
||||
AllRequests: requests,
|
||||
}
|
||||
reviewSummary.Comments = reviewSummary.loadComments(commentNotes)
|
||||
reviewSummary.Resolved = updateThreadsStatus(reviewSummary.Comments)
|
||||
return &reviewSummary, nil
|
||||
}
|
||||
|
||||
// GetSummary returns the summary of the code review specified by its revision
|
||||
// and the references which contain that reviews summary and comments.
|
||||
//
|
||||
// If no review request exists, the returned review summary is nil.
|
||||
func GetSummaryViaRefs(repo repository.Repo, requestRef, commentRef,
|
||||
revision string) (*Summary, error) {
|
||||
|
||||
if err := repo.VerifyCommit(revision); err != nil {
|
||||
return nil, fmt.Errorf("Could not find a commit named %q", revision)
|
||||
}
|
||||
requestNotes := repo.GetNotes(requestRef, revision)
|
||||
commentNotes := repo.GetNotes(commentRef, revision)
|
||||
summary, err := getSummaryFromNotes(repo, revision, requestNotes, commentNotes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentCommit := revision
|
||||
if summary.Request.Alias != "" {
|
||||
currentCommit = summary.Request.Alias
|
||||
}
|
||||
|
||||
if !summary.IsAbandoned() {
|
||||
submitted, err := repo.IsAncestor(currentCommit, summary.Request.TargetRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
summary.Submitted = submitted
|
||||
}
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
// GetSummary returns the summary of the specified code review.
|
||||
//
|
||||
// If no review request exists, the returned review summary is nil.
|
||||
func GetSummary(repo repository.Repo, revision string) (*Summary, error) {
|
||||
return GetSummaryViaRefs(repo, request.Ref, comment.Ref, revision)
|
||||
}
|
||||
|
||||
// Details returns the detailed review for the given summary.
|
||||
func (r *Summary) Details() (*Review, error) {
|
||||
review := Review{
|
||||
Summary: r,
|
||||
}
|
||||
currentCommit, err := review.GetHeadCommit()
|
||||
if err == nil {
|
||||
review.Reports = ci.ParseAllValid(review.Repo.GetNotes(ci.Ref, currentCommit))
|
||||
review.Analyses = analyses.ParseAllValid(review.Repo.GetNotes(analyses.Ref, currentCommit))
|
||||
}
|
||||
return &review, nil
|
||||
}
|
||||
|
||||
// IsAbandoned returns whether or not the given review has been abandoned.
|
||||
func (r *Summary) IsAbandoned() bool {
|
||||
return r.Request.TargetRef == ""
|
||||
}
|
||||
|
||||
// IsOpen returns whether or not the given review is still open (neither submitted nor abandoned).
|
||||
func (r *Summary) IsOpen() bool {
|
||||
return !r.Submitted && !r.IsAbandoned()
|
||||
}
|
||||
|
||||
// Verify returns whether or not a summary's comments are a) signed, and b)
|
||||
/// that those signatures are verifiable.
|
||||
func (r *Summary) Verify() error {
|
||||
err := gpg.Verify(&r.Request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't verify request targeting: %q: %s",
|
||||
r.Request.TargetRef, err)
|
||||
}
|
||||
for _, thread := range r.Comments {
|
||||
err := thread.Verify()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the specified code review.
|
||||
//
|
||||
// If no review request exists, the returned review is nil.
|
||||
func Get(repo repository.Repo, revision string) (*Review, error) {
|
||||
summary, err := GetSummary(repo, revision)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if summary == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return summary.Details()
|
||||
}
|
||||
|
||||
func getIsSubmittedCheck(repo repository.Repo) func(ref, commit string) bool {
|
||||
refCommitsMap := make(map[string]map[string]bool)
|
||||
|
||||
getRefCommitsMap := func(ref string) map[string]bool {
|
||||
commitsMap, ok := refCommitsMap[ref]
|
||||
if ok {
|
||||
return commitsMap
|
||||
}
|
||||
commitsMap = make(map[string]bool)
|
||||
for _, commit := range repo.ListCommits(ref) {
|
||||
commitsMap[commit] = true
|
||||
}
|
||||
refCommitsMap[ref] = commitsMap
|
||||
return commitsMap
|
||||
}
|
||||
|
||||
return func(ref, commit string) bool {
|
||||
return getRefCommitsMap(ref)[commit]
|
||||
}
|
||||
}
|
||||
|
||||
func unsortedListAll(repo repository.Repo) []Summary {
|
||||
reviewNotesMap, err := repo.GetAllNotes(request.Ref)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
discussNotesMap, err := repo.GetAllNotes(comment.Ref)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
isSubmittedCheck := getIsSubmittedCheck(repo)
|
||||
var reviews []Summary
|
||||
for commit, notes := range reviewNotesMap {
|
||||
summary, err := getSummaryFromNotes(repo, commit, notes, discussNotesMap[commit])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !summary.IsAbandoned() {
|
||||
summary.Submitted = isSubmittedCheck(summary.Request.TargetRef, summary.getStartingCommit())
|
||||
}
|
||||
reviews = append(reviews, *summary)
|
||||
}
|
||||
return reviews
|
||||
}
|
||||
|
||||
// ListAll returns all reviews stored in the git-notes.
|
||||
func ListAll(repo repository.Repo) []Summary {
|
||||
reviews := unsortedListAll(repo)
|
||||
sort.Stable(summariesWithNewestRequestsFirst(reviews))
|
||||
return reviews
|
||||
}
|
||||
|
||||
// ListOpen returns all reviews that are not yet incorporated into their target refs.
|
||||
func ListOpen(repo repository.Repo) []Summary {
|
||||
var openReviews []Summary
|
||||
for _, review := range unsortedListAll(repo) {
|
||||
if review.IsOpen() {
|
||||
openReviews = append(openReviews, review)
|
||||
}
|
||||
}
|
||||
sort.Stable(summariesWithNewestRequestsFirst(openReviews))
|
||||
return openReviews
|
||||
}
|
||||
|
||||
// GetCurrent returns the current, open code review.
|
||||
//
|
||||
// If there are multiple matching reviews, then an error is returned.
|
||||
func GetCurrent(repo repository.Repo) (*Review, error) {
|
||||
reviewRef, err := repo.GetHeadRef()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var matchingReviews []Summary
|
||||
for _, review := range ListOpen(repo) {
|
||||
if review.Request.ReviewRef == reviewRef {
|
||||
matchingReviews = append(matchingReviews, review)
|
||||
}
|
||||
}
|
||||
if matchingReviews == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if len(matchingReviews) != 1 {
|
||||
return nil, fmt.Errorf("There are %d open reviews for the ref \"%s\"", len(matchingReviews), reviewRef)
|
||||
}
|
||||
return matchingReviews[0].Details()
|
||||
}
|
||||
|
||||
// GetBuildStatusMessage returns a string of the current build-and-test status
|
||||
// of the review, or "unknown" if the build-and-test status cannot be determined.
|
||||
func (r *Review) GetBuildStatusMessage() string {
|
||||
statusMessage := "unknown"
|
||||
ciReport, err := ci.GetLatestCIReport(r.Reports)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("unknown: %s", err)
|
||||
}
|
||||
if ciReport != nil {
|
||||
statusMessage = fmt.Sprintf("%s (%q)", ciReport.Status, ciReport.URL)
|
||||
}
|
||||
return statusMessage
|
||||
}
|
||||
|
||||
// GetAnalysesNotes returns all of the notes from the most recent static
|
||||
// analysis run recorded in the git notes.
|
||||
func (r *Review) GetAnalysesNotes() ([]analyses.Note, error) {
|
||||
latestAnalyses, err := analyses.GetLatestAnalysesReport(r.Analyses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if latestAnalyses == nil {
|
||||
return nil, fmt.Errorf("No analyses available")
|
||||
}
|
||||
return latestAnalyses.GetNotes()
|
||||
}
|
||||
|
||||
// GetAnalysesMessage returns a string summarizing the results of the
|
||||
// most recent static analyses.
|
||||
func (r *Review) GetAnalysesMessage() string {
|
||||
latestAnalyses, err := analyses.GetLatestAnalysesReport(r.Analyses)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
if latestAnalyses == nil {
|
||||
return "No analyses available"
|
||||
}
|
||||
status := latestAnalyses.Status
|
||||
if status != "" && status != analyses.StatusNeedsMoreWork {
|
||||
return status
|
||||
}
|
||||
analysesNotes, err := latestAnalyses.GetNotes()
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
if analysesNotes == nil {
|
||||
return "passed"
|
||||
}
|
||||
return fmt.Sprintf("%d warnings\n", len(analysesNotes))
|
||||
// TODO(ojarjur): Figure out the best place to display the actual notes
|
||||
}
|
||||
|
||||
func prettyPrintJSON(jsonBytes []byte) (string, error) {
|
||||
var prettyBytes bytes.Buffer
|
||||
err := json.Indent(&prettyBytes, jsonBytes, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return prettyBytes.String(), nil
|
||||
}
|
||||
|
||||
// GetJSON returns the pretty printed JSON for a review summary.
|
||||
func (r *Summary) GetJSON() (string, error) {
|
||||
jsonBytes, err := json.Marshal(*r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return prettyPrintJSON(jsonBytes)
|
||||
}
|
||||
|
||||
// GetJSON returns the pretty printed JSON for a review.
|
||||
func (r *Review) GetJSON() (string, error) {
|
||||
jsonBytes, err := json.Marshal(*r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return prettyPrintJSON(jsonBytes)
|
||||
}
|
||||
|
||||
// findLastCommit returns the later (newest) commit from the union of the provided commit
|
||||
// and all of the commits that are referenced in the given comment threads.
|
||||
func (r *Review) findLastCommit(startingCommit, latestCommit string, commentThreads []CommentThread) string {
|
||||
isLater := func(commit string) bool {
|
||||
if err := r.Repo.VerifyCommit(commit); err != nil {
|
||||
return false
|
||||
}
|
||||
if t, e := r.Repo.IsAncestor(latestCommit, commit); e == nil && t {
|
||||
return true
|
||||
}
|
||||
if t, e := r.Repo.IsAncestor(startingCommit, commit); e == nil && !t {
|
||||
return false
|
||||
}
|
||||
if t, e := r.Repo.IsAncestor(commit, latestCommit); e == nil && t {
|
||||
return false
|
||||
}
|
||||
ct, err := r.Repo.GetCommitTime(commit)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
lt, err := r.Repo.GetCommitTime(latestCommit)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return ct > lt
|
||||
}
|
||||
updateLatest := func(commit string) {
|
||||
if commit == "" {
|
||||
return
|
||||
}
|
||||
if isLater(commit) {
|
||||
latestCommit = commit
|
||||
}
|
||||
}
|
||||
for _, commentThread := range commentThreads {
|
||||
comment := commentThread.Comment
|
||||
if comment.Location != nil {
|
||||
updateLatest(comment.Location.Commit)
|
||||
}
|
||||
updateLatest(r.findLastCommit(startingCommit, latestCommit, commentThread.Children))
|
||||
}
|
||||
return latestCommit
|
||||
}
|
||||
|
||||
func (r *Summary) getStartingCommit() string {
|
||||
if r.Request.Alias != "" {
|
||||
return r.Request.Alias
|
||||
}
|
||||
return r.Revision
|
||||
}
|
||||
|
||||
// GetHeadCommit returns the latest commit in a review.
|
||||
func (r *Review) GetHeadCommit() (string, error) {
|
||||
currentCommit := r.getStartingCommit()
|
||||
if r.Request.ReviewRef == "" {
|
||||
return currentCommit, nil
|
||||
}
|
||||
|
||||
if r.Submitted {
|
||||
// The review has already been submitted.
|
||||
// Go through the list of comments and find the last commented upon commit.
|
||||
return r.findLastCommit(currentCommit, currentCommit, r.Comments), nil
|
||||
}
|
||||
|
||||
// It is possible that the review ref is no longer an ancestor of the starting
|
||||
// commit (e.g. if a rebase left us in a detached head), in which case we have to
|
||||
// find the head commit without using it.
|
||||
useReviewRef, err := r.Repo.IsAncestor(currentCommit, r.Request.ReviewRef)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if useReviewRef {
|
||||
return r.Repo.ResolveRefCommit(r.Request.ReviewRef)
|
||||
}
|
||||
|
||||
return r.findLastCommit(currentCommit, currentCommit, r.Comments), nil
|
||||
}
|
||||
|
||||
// GetBaseCommit returns the commit against which a review should be compared.
|
||||
func (r *Review) GetBaseCommit() (string, error) {
|
||||
if !r.IsOpen() {
|
||||
if r.Request.BaseCommit != "" {
|
||||
return r.Request.BaseCommit, nil
|
||||
}
|
||||
|
||||
// This means the review has been submitted, but did not specify a base commit.
|
||||
// In this case, we have to treat the last parent commit as the base. This is
|
||||
// usually what we want, since merging a target branch into a feature branch
|
||||
// results in the previous commit to the feature branch being the first parent,
|
||||
// and the latest commit to the target branch being the second parent.
|
||||
return r.Repo.GetLastParent(r.Revision)
|
||||
}
|
||||
|
||||
targetRefHead, err := r.Repo.ResolveRefCommit(r.Request.TargetRef)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
leftHandSide := targetRefHead
|
||||
rightHandSide := r.Revision
|
||||
if r.Request.ReviewRef != "" {
|
||||
if reviewRefHead, err := r.Repo.ResolveRefCommit(r.Request.ReviewRef); err == nil {
|
||||
rightHandSide = reviewRefHead
|
||||
}
|
||||
}
|
||||
|
||||
return r.Repo.MergeBase(leftHandSide, rightHandSide)
|
||||
}
|
||||
|
||||
// ListCommits lists the commits included in a review.
|
||||
func (r *Review) ListCommits() ([]string, error) {
|
||||
baseCommit, err := r.GetBaseCommit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headCommit, err := r.GetHeadCommit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.Repo.ListCommitsBetween(baseCommit, headCommit)
|
||||
}
|
||||
|
||||
// GetDiff returns the diff for a review.
|
||||
func (r *Review) GetDiff(diffArgs ...string) (string, error) {
|
||||
var baseCommit, headCommit string
|
||||
baseCommit, err := r.GetBaseCommit()
|
||||
if err == nil {
|
||||
headCommit, err = r.GetHeadCommit()
|
||||
}
|
||||
if err == nil {
|
||||
return r.Repo.Diff(baseCommit, headCommit, diffArgs...)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// AddComment adds the given comment to the review.
|
||||
func (r *Review) AddComment(c comment.Comment) error {
|
||||
commentNote, err := c.Write()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Repo.AppendNote(comment.Ref, r.Revision, commentNote)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rebase performs an interactive rebase of the review onto its target ref.
|
||||
//
|
||||
// If the 'archivePrevious' argument is true, then the previous head of the
|
||||
// review will be added to the 'refs/devtools/archives/reviews' ref prior
|
||||
// to being rewritten. That ensures the review history is kept from being
|
||||
// garbage collected.
|
||||
func (r *Review) Rebase(archivePrevious bool) error {
|
||||
if archivePrevious {
|
||||
orig, err := r.GetHeadCommit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.Repo.ArchiveRef(orig, archiveRef); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := r.Repo.SwitchToRef(r.Request.ReviewRef); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := r.Repo.RebaseRef(r.Request.TargetRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
alias, err := r.Repo.GetCommitHash("HEAD")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Request.Alias = alias
|
||||
newNote, err := r.Request.Write()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.Repo.AppendNote(request.Ref, r.Revision, newNote)
|
||||
}
|
||||
|
||||
// RebaseAndSign performs an interactive rebase of the review onto its
|
||||
// target ref. It signs the result of the rebase as well as (re)signs
|
||||
// the review request itself.
|
||||
//
|
||||
// If the 'archivePrevious' argument is true, then the previous head of the
|
||||
// review will be added to the 'refs/devtools/archives/reviews' ref prior
|
||||
// to being rewritten. That ensures the review history is kept from being
|
||||
// garbage collected.
|
||||
func (r *Review) RebaseAndSign(archivePrevious bool) error {
|
||||
if archivePrevious {
|
||||
orig, err := r.GetHeadCommit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.Repo.ArchiveRef(orig, archiveRef); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := r.Repo.SwitchToRef(r.Request.ReviewRef); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := r.Repo.RebaseAndSignRef(r.Request.TargetRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
alias, err := r.Repo.GetCommitHash("HEAD")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Request.Alias = alias
|
||||
|
||||
key, err := r.Repo.GetUserSigningKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = gpg.Sign(key, &r.Request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newNote, err := r.Request.Write()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.Repo.AppendNote(request.Ref, r.Revision, newNote)
|
||||
}
|
||||
870
third_party/go/git-appraise/review/review_test.go
vendored
Normal file
870
third_party/go/git-appraise/review/review_test.go
vendored
Normal file
|
|
@ -0,0 +1,870 @@
|
|||
/*
|
||||
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 review
|
||||
|
||||
import (
|
||||
"github.com/google/git-appraise/repository"
|
||||
"github.com/google/git-appraise/review/comment"
|
||||
"github.com/google/git-appraise/review/request"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommentSorting(t *testing.T) {
|
||||
sampleComments := []*comment.Comment{
|
||||
&comment.Comment{
|
||||
Timestamp: "012400",
|
||||
Description: "Fourth",
|
||||
},
|
||||
&comment.Comment{
|
||||
Timestamp: "012400",
|
||||
Description: "Fifth",
|
||||
},
|
||||
&comment.Comment{
|
||||
Timestamp: "012346",
|
||||
Description: "Second",
|
||||
},
|
||||
&comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Description: "First",
|
||||
},
|
||||
&comment.Comment{
|
||||
Timestamp: "012347",
|
||||
Description: "Third",
|
||||
},
|
||||
}
|
||||
sort.Stable(commentsByTimestamp(sampleComments))
|
||||
descriptions := []string{}
|
||||
for _, comment := range sampleComments {
|
||||
descriptions = append(descriptions, comment.Description)
|
||||
}
|
||||
if !(descriptions[0] == "First" && descriptions[1] == "Second" && descriptions[2] == "Third" && descriptions[3] == "Fourth" && descriptions[4] == "Fifth") {
|
||||
t.Fatalf("Comment ordering failed. Got %v", sampleComments)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThreadSorting(t *testing.T) {
|
||||
sampleThreads := []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012400",
|
||||
Description: "Fourth",
|
||||
},
|
||||
},
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012400",
|
||||
Description: "Fifth",
|
||||
},
|
||||
},
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012346",
|
||||
Description: "Second",
|
||||
},
|
||||
},
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Description: "First",
|
||||
},
|
||||
},
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012347",
|
||||
Description: "Third",
|
||||
},
|
||||
},
|
||||
}
|
||||
sort.Stable(byTimestamp(sampleThreads))
|
||||
descriptions := []string{}
|
||||
for _, thread := range sampleThreads {
|
||||
descriptions = append(descriptions, thread.Comment.Description)
|
||||
}
|
||||
if !(descriptions[0] == "First" && descriptions[1] == "Second" && descriptions[2] == "Third" && descriptions[3] == "Fourth" && descriptions[4] == "Fifth") {
|
||||
t.Fatalf("Comment thread ordering failed. Got %v", sampleThreads)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestSorting(t *testing.T) {
|
||||
sampleRequests := []request.Request{
|
||||
request.Request{
|
||||
Timestamp: "012400",
|
||||
Description: "Fourth",
|
||||
},
|
||||
request.Request{
|
||||
Timestamp: "012400",
|
||||
Description: "Fifth",
|
||||
},
|
||||
request.Request{
|
||||
Timestamp: "012346",
|
||||
Description: "Second",
|
||||
},
|
||||
request.Request{
|
||||
Timestamp: "012345",
|
||||
Description: "First",
|
||||
},
|
||||
request.Request{
|
||||
Timestamp: "012347",
|
||||
Description: "Third",
|
||||
},
|
||||
}
|
||||
sort.Stable(requestsByTimestamp(sampleRequests))
|
||||
descriptions := []string{}
|
||||
for _, r := range sampleRequests {
|
||||
descriptions = append(descriptions, r.Description)
|
||||
}
|
||||
if !(descriptions[0] == "First" && descriptions[1] == "Second" && descriptions[2] == "Third" && descriptions[3] == "Fourth" && descriptions[4] == "Fifth") {
|
||||
t.Fatalf("Review request ordering failed. Got %v", sampleRequests)
|
||||
}
|
||||
}
|
||||
|
||||
func validateUnresolved(t *testing.T, resolved *bool) {
|
||||
if resolved != nil {
|
||||
t.Fatalf("Expected resolved status to be unset, but instead it was %v", *resolved)
|
||||
}
|
||||
}
|
||||
|
||||
func validateAccepted(t *testing.T, resolved *bool) {
|
||||
if resolved == nil {
|
||||
t.Fatal("Expected resolved status to be true, but it was unset")
|
||||
}
|
||||
if !*resolved {
|
||||
t.Fatal("Expected resolved status to be true, but it was false")
|
||||
}
|
||||
}
|
||||
|
||||
func validateRejected(t *testing.T, resolved *bool) {
|
||||
if resolved == nil {
|
||||
t.Fatal("Expected resolved status to be false, but it was unset")
|
||||
}
|
||||
if *resolved {
|
||||
t.Fatal("Expected resolved status to be false, but it was true")
|
||||
}
|
||||
}
|
||||
|
||||
func (commentThread *CommentThread) validateUnresolved(t *testing.T) {
|
||||
validateUnresolved(t, commentThread.Resolved)
|
||||
}
|
||||
|
||||
func (commentThread *CommentThread) validateAccepted(t *testing.T) {
|
||||
validateAccepted(t, commentThread.Resolved)
|
||||
}
|
||||
|
||||
func (commentThread *CommentThread) validateRejected(t *testing.T) {
|
||||
validateRejected(t, commentThread.Resolved)
|
||||
}
|
||||
|
||||
func TestSimpleAcceptedThreadStatus(t *testing.T) {
|
||||
resolved := true
|
||||
simpleThread := CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Resolved: &resolved,
|
||||
},
|
||||
}
|
||||
simpleThread.updateResolvedStatus()
|
||||
simpleThread.validateAccepted(t)
|
||||
}
|
||||
|
||||
func TestSimpleRejectedThreadStatus(t *testing.T) {
|
||||
resolved := false
|
||||
simpleThread := CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Resolved: &resolved,
|
||||
},
|
||||
}
|
||||
simpleThread.updateResolvedStatus()
|
||||
simpleThread.validateRejected(t)
|
||||
}
|
||||
|
||||
func TestFYIThenAcceptedThreadStatus(t *testing.T) {
|
||||
accepted := true
|
||||
sampleThread := CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Resolved: nil,
|
||||
},
|
||||
Children: []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: &accepted,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
sampleThread.updateResolvedStatus()
|
||||
sampleThread.validateUnresolved(t)
|
||||
}
|
||||
|
||||
func TestFYIThenFYIThreadStatus(t *testing.T) {
|
||||
sampleThread := CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Resolved: nil,
|
||||
},
|
||||
Children: []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
sampleThread.updateResolvedStatus()
|
||||
sampleThread.validateUnresolved(t)
|
||||
}
|
||||
|
||||
func TestFYIThenRejectedThreadStatus(t *testing.T) {
|
||||
rejected := false
|
||||
sampleThread := CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Resolved: nil,
|
||||
},
|
||||
Children: []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: &rejected,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
sampleThread.updateResolvedStatus()
|
||||
sampleThread.validateRejected(t)
|
||||
}
|
||||
|
||||
func TestAcceptedThenAcceptedThreadStatus(t *testing.T) {
|
||||
accepted := true
|
||||
sampleThread := CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Resolved: &accepted,
|
||||
},
|
||||
Children: []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: &accepted,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
sampleThread.updateResolvedStatus()
|
||||
sampleThread.validateAccepted(t)
|
||||
}
|
||||
|
||||
func TestAcceptedThenFYIThreadStatus(t *testing.T) {
|
||||
accepted := true
|
||||
sampleThread := CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Resolved: &accepted,
|
||||
},
|
||||
Children: []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
sampleThread.updateResolvedStatus()
|
||||
sampleThread.validateAccepted(t)
|
||||
}
|
||||
|
||||
func TestAcceptedThenRejectedThreadStatus(t *testing.T) {
|
||||
accepted := true
|
||||
rejected := false
|
||||
sampleThread := CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Resolved: &accepted,
|
||||
},
|
||||
Children: []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: &rejected,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
sampleThread.updateResolvedStatus()
|
||||
sampleThread.validateRejected(t)
|
||||
}
|
||||
|
||||
func TestRejectedThenAcceptedThreadStatus(t *testing.T) {
|
||||
accepted := true
|
||||
rejected := false
|
||||
sampleThread := CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Resolved: &rejected,
|
||||
},
|
||||
Children: []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: &accepted,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
sampleThread.updateResolvedStatus()
|
||||
sampleThread.validateUnresolved(t)
|
||||
}
|
||||
|
||||
func TestRejectedThenFYIThreadStatus(t *testing.T) {
|
||||
rejected := false
|
||||
sampleThread := CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Resolved: &rejected,
|
||||
},
|
||||
Children: []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
sampleThread.updateResolvedStatus()
|
||||
sampleThread.validateRejected(t)
|
||||
}
|
||||
|
||||
func TestRejectedThenRejectedThreadStatus(t *testing.T) {
|
||||
rejected := false
|
||||
sampleThread := CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Resolved: &rejected,
|
||||
},
|
||||
Children: []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: &rejected,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
sampleThread.updateResolvedStatus()
|
||||
sampleThread.validateRejected(t)
|
||||
}
|
||||
|
||||
func TestRejectedThenAcceptedThreadsStatus(t *testing.T) {
|
||||
accepted := true
|
||||
rejected := false
|
||||
threads := []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: &rejected,
|
||||
},
|
||||
},
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012346",
|
||||
Resolved: &accepted,
|
||||
},
|
||||
},
|
||||
}
|
||||
status := updateThreadsStatus(threads)
|
||||
validateRejected(t, status)
|
||||
}
|
||||
|
||||
func TestRejectedThenFYIThreadsStatus(t *testing.T) {
|
||||
rejected := false
|
||||
threads := []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: &rejected,
|
||||
},
|
||||
},
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012346",
|
||||
Resolved: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
status := updateThreadsStatus(threads)
|
||||
validateRejected(t, status)
|
||||
}
|
||||
|
||||
func TestRejectedThenRejectedThreadsStatus(t *testing.T) {
|
||||
rejected := false
|
||||
threads := []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: &rejected,
|
||||
},
|
||||
},
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012346",
|
||||
Resolved: &rejected,
|
||||
},
|
||||
},
|
||||
}
|
||||
status := updateThreadsStatus(threads)
|
||||
validateRejected(t, status)
|
||||
}
|
||||
|
||||
func TestAcceptedThenAcceptedThreadsStatus(t *testing.T) {
|
||||
accepted := true
|
||||
threads := []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: &accepted,
|
||||
},
|
||||
},
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012346",
|
||||
Resolved: &accepted,
|
||||
},
|
||||
},
|
||||
}
|
||||
status := updateThreadsStatus(threads)
|
||||
validateAccepted(t, status)
|
||||
}
|
||||
|
||||
func TestAcceptedThenFYIThreadsStatus(t *testing.T) {
|
||||
accepted := true
|
||||
threads := []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: &accepted,
|
||||
},
|
||||
},
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012346",
|
||||
Resolved: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
status := updateThreadsStatus(threads)
|
||||
validateAccepted(t, status)
|
||||
}
|
||||
|
||||
func TestAcceptedThenRejectedThreadsStatus(t *testing.T) {
|
||||
accepted := true
|
||||
rejected := false
|
||||
threads := []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: &accepted,
|
||||
},
|
||||
},
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012346",
|
||||
Resolved: &rejected,
|
||||
},
|
||||
},
|
||||
}
|
||||
status := updateThreadsStatus(threads)
|
||||
validateRejected(t, status)
|
||||
}
|
||||
|
||||
func TestFYIThenAcceptedThreadsStatus(t *testing.T) {
|
||||
accepted := true
|
||||
threads := []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: nil,
|
||||
},
|
||||
},
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012346",
|
||||
Resolved: &accepted,
|
||||
},
|
||||
},
|
||||
}
|
||||
status := updateThreadsStatus(threads)
|
||||
validateAccepted(t, status)
|
||||
}
|
||||
|
||||
func TestFYIThenFYIThreadsStatus(t *testing.T) {
|
||||
threads := []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: nil,
|
||||
},
|
||||
},
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012346",
|
||||
Resolved: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
status := updateThreadsStatus(threads)
|
||||
validateUnresolved(t, status)
|
||||
}
|
||||
|
||||
func TestFYIThenRejectedThreadsStatus(t *testing.T) {
|
||||
rejected := false
|
||||
threads := []CommentThread{
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: nil,
|
||||
},
|
||||
},
|
||||
CommentThread{
|
||||
Comment: comment.Comment{
|
||||
Timestamp: "012346",
|
||||
Resolved: &rejected,
|
||||
},
|
||||
},
|
||||
}
|
||||
status := updateThreadsStatus(threads)
|
||||
validateRejected(t, status)
|
||||
}
|
||||
|
||||
func TestBuildCommentThreads(t *testing.T) {
|
||||
rejected := false
|
||||
accepted := true
|
||||
root := comment.Comment{
|
||||
Timestamp: "012345",
|
||||
Resolved: nil,
|
||||
Description: "root",
|
||||
}
|
||||
rootHash, err := root.Hash()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
child := comment.Comment{
|
||||
Timestamp: "012346",
|
||||
Resolved: nil,
|
||||
Parent: rootHash,
|
||||
Description: "child",
|
||||
}
|
||||
childHash, err := child.Hash()
|
||||
updatedChild := comment.Comment{
|
||||
Timestamp: "012346",
|
||||
Resolved: &rejected,
|
||||
Original: childHash,
|
||||
Description: "updated child",
|
||||
}
|
||||
updatedChildHash, err := updatedChild.Hash()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
leaf := comment.Comment{
|
||||
Timestamp: "012347",
|
||||
Resolved: &accepted,
|
||||
Parent: childHash,
|
||||
Description: "leaf",
|
||||
}
|
||||
leafHash, err := leaf.Hash()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
commentsByHash := map[string]comment.Comment{
|
||||
rootHash: root,
|
||||
childHash: child,
|
||||
updatedChildHash: updatedChild,
|
||||
leafHash: leaf,
|
||||
}
|
||||
threads := buildCommentThreads(commentsByHash)
|
||||
if len(threads) != 1 {
|
||||
t.Fatalf("Unexpected threads: %v", threads)
|
||||
}
|
||||
rootThread := threads[0]
|
||||
if rootThread.Comment.Description != "root" {
|
||||
t.Fatalf("Unexpected root thread: %v", rootThread)
|
||||
}
|
||||
if !rootThread.Edited {
|
||||
t.Fatalf("Unexpected root thread edited status: %v", rootThread)
|
||||
}
|
||||
if len(rootThread.Children) != 1 {
|
||||
t.Fatalf("Unexpected root children: %v", rootThread.Children)
|
||||
}
|
||||
rootChild := rootThread.Children[0]
|
||||
if rootChild.Comment.Description != "updated child" {
|
||||
t.Fatalf("Unexpected updated child: %v", rootChild)
|
||||
}
|
||||
if rootChild.Original.Description != "child" {
|
||||
t.Fatalf("Unexpected original child: %v", rootChild)
|
||||
}
|
||||
if len(rootChild.Edits) != 1 {
|
||||
t.Fatalf("Unexpected child history: %v", rootChild.Edits)
|
||||
}
|
||||
if len(rootChild.Children) != 1 {
|
||||
t.Fatalf("Unexpected leaves: %v", rootChild.Children)
|
||||
}
|
||||
threadLeaf := rootChild.Children[0]
|
||||
if threadLeaf.Comment.Description != "leaf" {
|
||||
t.Fatalf("Unexpected leaf: %v", threadLeaf)
|
||||
}
|
||||
if len(threadLeaf.Children) != 0 {
|
||||
t.Fatalf("Unexpected leaf children: %v", threadLeaf.Children)
|
||||
}
|
||||
if threadLeaf.Edited {
|
||||
t.Fatalf("Unexpected leaf edited status: %v", threadLeaf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHeadCommit(t *testing.T) {
|
||||
repo := repository.NewMockRepoForTest()
|
||||
|
||||
submittedSimpleReview, err := Get(repo, repository.TestCommitB)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
submittedSimpleReviewHead, err := submittedSimpleReview.GetHeadCommit()
|
||||
if err != nil {
|
||||
t.Fatal("Unable to compute the head commit for a known review of a simple commit: ", err)
|
||||
}
|
||||
if submittedSimpleReviewHead != repository.TestCommitB {
|
||||
t.Fatal("Unexpected head commit computed for a known review of a simple commit.")
|
||||
}
|
||||
|
||||
submittedModifiedReview, err := Get(repo, repository.TestCommitD)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
submittedModifiedReviewHead, err := submittedModifiedReview.GetHeadCommit()
|
||||
if err != nil {
|
||||
t.Fatal("Unable to compute the head commit for a known, multi-commit review: ", err)
|
||||
}
|
||||
if submittedModifiedReviewHead != repository.TestCommitE {
|
||||
t.Fatal("Unexpected head commit for a known, multi-commit review.")
|
||||
}
|
||||
|
||||
pendingReview, err := Get(repo, repository.TestCommitG)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pendingReviewHead, err := pendingReview.GetHeadCommit()
|
||||
if err != nil {
|
||||
t.Fatal("Unable to compute the head commit for a known review of a merge commit: ", err)
|
||||
}
|
||||
if pendingReviewHead != repository.TestCommitI {
|
||||
t.Fatal("Unexpected head commit computed for a pending review.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBaseCommit(t *testing.T) {
|
||||
repo := repository.NewMockRepoForTest()
|
||||
|
||||
submittedSimpleReview, err := Get(repo, repository.TestCommitB)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
submittedSimpleReviewBase, err := submittedSimpleReview.GetBaseCommit()
|
||||
if err != nil {
|
||||
t.Fatal("Unable to compute the base commit for a known review of a simple commit: ", err)
|
||||
}
|
||||
if submittedSimpleReviewBase != repository.TestCommitA {
|
||||
t.Fatal("Unexpected base commit computed for a known review of a simple commit.")
|
||||
}
|
||||
|
||||
submittedMergeReview, err := Get(repo, repository.TestCommitD)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
submittedMergeReviewBase, err := submittedMergeReview.GetBaseCommit()
|
||||
if err != nil {
|
||||
t.Fatal("Unable to compute the base commit for a known review of a merge commit: ", err)
|
||||
}
|
||||
if submittedMergeReviewBase != repository.TestCommitC {
|
||||
t.Fatal("Unexpected base commit computed for a known review of a merge commit.")
|
||||
}
|
||||
|
||||
pendingReview, err := Get(repo, repository.TestCommitG)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pendingReviewBase, err := pendingReview.GetBaseCommit()
|
||||
if err != nil {
|
||||
t.Fatal("Unable to compute the base commit for a known review of a merge commit: ", err)
|
||||
}
|
||||
if pendingReviewBase != repository.TestCommitF {
|
||||
t.Fatal("Unexpected base commit computed for a pending review.")
|
||||
}
|
||||
|
||||
abandonRequest := pendingReview.Request
|
||||
abandonRequest.TargetRef = ""
|
||||
abandonNote, err := abandonRequest.Write()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repo.AppendNote(request.Ref, repository.TestCommitG, abandonNote); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
abandonedReview, err := Get(repo, repository.TestCommitG)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if abandonedReview.IsOpen() {
|
||||
t.Fatal("Failed to update a review to be abandoned")
|
||||
}
|
||||
abandonedReviewBase, err := abandonedReview.GetBaseCommit()
|
||||
if err != nil {
|
||||
t.Fatal("Unable to compute the base commit for an abandoned review: ", err)
|
||||
}
|
||||
if abandonedReviewBase != repository.TestCommitE {
|
||||
t.Fatal("Unexpected base commit computed for an abandoned review.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRequests(t *testing.T) {
|
||||
repo := repository.NewMockRepoForTest()
|
||||
pendingReview, err := Get(repo, repository.TestCommitG)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pendingReview.AllRequests) != 3 || pendingReview.Request.Description != "Final description of G" {
|
||||
t.Fatal("Unexpected requests for a pending review: ", pendingReview.AllRequests, pendingReview.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRebase(t *testing.T) {
|
||||
repo := repository.NewMockRepoForTest()
|
||||
pendingReview, err := Get(repo, repository.TestCommitG)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Rebase the review and then confirm that it has been updated correctly.
|
||||
if err := pendingReview.Rebase(true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reviewJSON, err := pendingReview.GetJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
headRef, err := repo.GetHeadRef()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if headRef != pendingReview.Request.ReviewRef {
|
||||
t.Fatal("Failed to switch to the review ref during a rebase")
|
||||
}
|
||||
isAncestor, err := repo.IsAncestor(pendingReview.Revision, archiveRef)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !isAncestor {
|
||||
t.Fatalf("Commit %q is not archived", pendingReview.Revision)
|
||||
}
|
||||
reviewCommit, err := repo.GetCommitHash(pendingReview.Request.ReviewRef)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reviewAlias := pendingReview.Request.Alias
|
||||
if reviewAlias == "" || reviewAlias == pendingReview.Revision || reviewCommit != reviewAlias {
|
||||
t.Fatalf("Failed to set the review alias: %q", reviewJSON)
|
||||
}
|
||||
|
||||
// Submit the review.
|
||||
if err := repo.SwitchToRef(pendingReview.Request.TargetRef); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repo.MergeRef(pendingReview.Request.ReviewRef, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Reread the review and confirm that it has been submitted.
|
||||
submittedReview, err := Get(repo, pendingReview.Revision)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
submittedReviewJSON, err := submittedReview.GetJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !submittedReview.Submitted {
|
||||
t.Fatalf("Failed to submit the review: %q", submittedReviewJSON)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRebaseDetachedHead(t *testing.T) {
|
||||
repo := repository.NewMockRepoForTest()
|
||||
pendingReview, err := Get(repo, repository.TestCommitG)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Switch the review to having a review ref that is not a branch.
|
||||
pendingReview.Request.ReviewRef = repository.TestAlternateReviewRef
|
||||
newNote, err := pendingReview.Request.Write()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repo.AppendNote(request.Ref, pendingReview.Revision, newNote); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pendingReview, err = Get(repo, repository.TestCommitG)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Rebase the review and then confirm that it has been updated correctly.
|
||||
if err := pendingReview.Rebase(true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
headRef, err := repo.GetHeadRef()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if headRef != pendingReview.Request.Alias {
|
||||
t.Fatal("Failed to switch to a detached head during a rebase")
|
||||
}
|
||||
isAncestor, err := repo.IsAncestor(pendingReview.Revision, archiveRef)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !isAncestor {
|
||||
t.Fatalf("Commit %q is not archived", pendingReview.Revision)
|
||||
}
|
||||
|
||||
// Submit the review.
|
||||
if err := repo.SwitchToRef(pendingReview.Request.TargetRef); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reviewHead, err := pendingReview.GetHeadCommit()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repo.MergeRef(reviewHead, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Reread the review and confirm that it has been submitted.
|
||||
submittedReview, err := Get(repo, pendingReview.Revision)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
submittedReviewJSON, err := submittedReview.GetJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !submittedReview.Submitted {
|
||||
t.Fatalf("Failed to submit the review: %q", submittedReviewJSON)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue