517 lines
16 KiB
Go
517 lines
16 KiB
Go
// Copyright 2019 Dolthub, Inc.
|
|
//
|
|
// 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 commands
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
"github.com/fatih/color"
|
|
"github.com/gocraft/dbr/v2"
|
|
"github.com/gocraft/dbr/v2/dialect"
|
|
|
|
"github.com/dolthub/dolt/go/cmd/dolt/cli"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/env"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
|
|
"github.com/dolthub/dolt/go/libraries/utils/argparser"
|
|
"github.com/dolthub/dolt/go/store/util/outputpager"
|
|
eventsapi "github.com/dolthub/eventsapi_schema/dolt/services/eventsapi/v1alpha1"
|
|
)
|
|
|
|
var logDocs = cli.CommandDocumentationContent{
|
|
ShortDesc: `Show commit logs`,
|
|
LongDesc: `Shows the commit logs
|
|
|
|
The command takes options to control what is shown and how.
|
|
|
|
{{.EmphasisLeft}}dolt log{{.EmphasisRight}}
|
|
Lists commit logs from current HEAD when no options provided.
|
|
|
|
{{.EmphasisLeft}}dolt log [<revisions>...]{{.EmphasisRight}}
|
|
Lists commit logs starting from revision. If multiple revisions provided, lists logs reachable by all revisions.
|
|
|
|
{{.EmphasisLeft}}dolt log [<revisions>...] -- <table>{{.EmphasisRight}}
|
|
Lists commit logs starting from revisions, only including commits with changes to table.
|
|
|
|
{{.EmphasisLeft}}dolt log <revisionB>..<revisionA>{{.EmphasisRight}}
|
|
{{.EmphasisLeft}}dolt log <revisionA> --not <revisionB>{{.EmphasisRight}}
|
|
{{.EmphasisLeft}}dolt log ^<revisionB> <revisionA>{{.EmphasisRight}}
|
|
Different ways to list two dot logs. These will list commit logs for revisionA, while excluding commits from revisionB. The table option is not supported for two dot log.
|
|
|
|
{{.EmphasisLeft}}dolt log <revisionB>...<revisionA>{{.EmphasisRight}}
|
|
{{.EmphasisLeft}}dolt log <revisionA> <revisionB> --not $(dolt merge-base <revisionA> <revisionB>){{.EmphasisRight}}
|
|
Different ways to list three dot logs. These will list commit logs reachable by revisionA OR revisionB, while excluding commits reachable by BOTH revisionA AND revisionB.`,
|
|
Synopsis: []string{
|
|
`[-n {{.LessThan}}num_commits{{.GreaterThan}}] [{{.LessThan}}revision-range{{.GreaterThan}}] [[--] {{.LessThan}}table{{.GreaterThan}}]`,
|
|
},
|
|
}
|
|
|
|
type LogCmd struct{}
|
|
|
|
// Name is returns the name of the Dolt cli command. This is what is used on the command line to invoke the command
|
|
func (cmd LogCmd) Name() string {
|
|
return "log"
|
|
}
|
|
|
|
// Description returns a description of the command
|
|
func (cmd LogCmd) Description() string {
|
|
return "Show commit logs."
|
|
}
|
|
|
|
// EventType returns the type of the event to log
|
|
func (cmd LogCmd) EventType() eventsapi.ClientEventType {
|
|
return eventsapi.ClientEventType_LOG
|
|
}
|
|
|
|
func (cmd LogCmd) Docs() *cli.CommandDocumentation {
|
|
ap := cmd.ArgParser()
|
|
return cli.NewCommandDocumentation(logDocs, ap)
|
|
}
|
|
|
|
func (cmd LogCmd) ArgParser() *argparser.ArgParser {
|
|
return cli.CreateLogArgParser(false)
|
|
}
|
|
|
|
// Exec executes the command
|
|
func (cmd LogCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
|
|
return cmd.logWithLoggerFunc(ctx, commandStr, args, dEnv, cliCtx)
|
|
}
|
|
|
|
func (cmd LogCmd) logWithLoggerFunc(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
|
|
ap := cmd.ArgParser()
|
|
apr, _, terminate, status := ParseArgsOrPrintHelp(ap, commandStr, args, logDocs)
|
|
if terminate {
|
|
return status
|
|
}
|
|
|
|
queryist, err := cliCtx.QueryEngine(ctx)
|
|
if err != nil {
|
|
return handleErrAndExit(err)
|
|
}
|
|
|
|
query, err := constructInterpolatedDoltLogQuery(apr, queryist.Queryist, queryist.Context)
|
|
if err != nil {
|
|
return handleErrAndExit(err)
|
|
}
|
|
logRows, err := cli.GetRowsForSql(queryist.Queryist, queryist.Context, query)
|
|
if err != nil {
|
|
return handleErrAndExit(err)
|
|
}
|
|
|
|
return handleErrAndExit(logCommits(apr, logRows, queryist.Queryist, queryist.Context))
|
|
}
|
|
|
|
func collectRevisions(apr *argparser.ArgParseResults, queryist cli.Queryist, sqlCtx *sql.Context) ([]string, int, error) {
|
|
revisions := make(map[string]bool, apr.NArg())
|
|
tablesIndex := 0
|
|
|
|
if apr.PositionalArgsSeparatorIndex >= 0 {
|
|
for i := 0; i < apr.PositionalArgsSeparatorIndex; i++ {
|
|
revisions[apr.Args[i]] = true
|
|
}
|
|
tablesIndex = apr.PositionalArgsSeparatorIndex
|
|
} else {
|
|
for _, arg := range apr.Args {
|
|
if strings.Contains(arg, "..") || strings.HasPrefix(arg, "^") || strings.HasPrefix(arg, "refs/") || strings.HasPrefix(arg, "remotes/") {
|
|
tablesIndex++
|
|
revisions[arg] = true
|
|
} else {
|
|
_, err := cli.GetRowsForSql(queryist, sqlCtx, "select hashof('"+arg+"')")
|
|
|
|
// Once we get a non-revision argument, we treat the remaining args as tables
|
|
if _, ok := revisions[arg]; ok || err != nil {
|
|
break
|
|
} else {
|
|
tablesIndex++
|
|
revisions[arg] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if apr.Contains(cli.AllFlag) {
|
|
branches, err := getBranches(sqlCtx, queryist, false, true)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
for _, branch := range branches {
|
|
revisions[branch.name] = true
|
|
}
|
|
}
|
|
|
|
var revisionNames []string
|
|
for arg, ok := range revisions {
|
|
if ok {
|
|
revisionNames = append(revisionNames, arg)
|
|
}
|
|
}
|
|
|
|
return revisionNames, tablesIndex, nil
|
|
}
|
|
|
|
func collectTables(apr *argparser.ArgParseResults, queryist cli.Queryist, sqlCtx *sql.Context, selectedBranches []string, startIndex int) ([]string, error) {
|
|
var existingTables map[string]bool
|
|
var tableNames []string
|
|
|
|
existingTables, err := getExistingTables(selectedBranches, queryist, sqlCtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := startIndex; i < apr.NArg(); i++ {
|
|
if _, ok := existingTables[apr.Args[i]]; !ok && apr.PositionalArgsSeparatorIndex < 0 {
|
|
return nil, fmt.Errorf("error: table %s does not exist", apr.Args[i])
|
|
}
|
|
tableNames = append(tableNames, apr.Args[i])
|
|
}
|
|
|
|
return tableNames, nil
|
|
|
|
}
|
|
|
|
// constructInterpolatedDoltLogQuery generates the sql query necessary to call the DOLT_LOG() function.
|
|
// Also interpolates this query to prevent sql injection.
|
|
func constructInterpolatedDoltLogQuery(apr *argparser.ArgParseResults, queryist cli.Queryist, sqlCtx *sql.Context) (string, error) {
|
|
|
|
var buffer bytes.Buffer
|
|
var first bool
|
|
first = true
|
|
|
|
buffer.WriteString("select commit_hash from dolt_log(")
|
|
|
|
writeToBuffer := func(s string) {
|
|
if !first {
|
|
buffer.WriteString(", ")
|
|
}
|
|
buffer.WriteString(s)
|
|
first = false
|
|
}
|
|
|
|
params, tablesIndex, err := collectRevisions(apr, queryist, sqlCtx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for range params {
|
|
writeToBuffer("?")
|
|
}
|
|
|
|
if tablesIndex < len(apr.Args) {
|
|
tableNames, err := collectTables(apr, queryist, sqlCtx, params, tablesIndex)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(tableNames) > 0 {
|
|
params = append(params, strings.Join(tableNames, ","))
|
|
writeToBuffer("'--tables'")
|
|
writeToBuffer("?")
|
|
}
|
|
}
|
|
|
|
if minParents, hasMinParents := apr.GetValue(cli.MinParentsFlag); hasMinParents {
|
|
writeToBuffer("?")
|
|
params = append(params, "--min-parents="+minParents)
|
|
}
|
|
|
|
if hasMerges := apr.Contains(cli.MergesFlag); hasMerges {
|
|
writeToBuffer("'--merges'")
|
|
}
|
|
|
|
if excludedCommits, hasExcludedCommits := apr.GetValueList(cli.NotFlag); hasExcludedCommits {
|
|
writeToBuffer("'--not'")
|
|
for _, commit := range excludedCommits {
|
|
writeToBuffer("?")
|
|
params = append(params, commit)
|
|
}
|
|
}
|
|
|
|
if decorate, hasDecorate := apr.GetValue(cli.DecorateFlag); hasDecorate {
|
|
if err := cli.ValidateDecorateOption(decorate); err != nil {
|
|
return "", err
|
|
}
|
|
writeToBuffer("?")
|
|
params = append(params, "--decorate="+resolveDecorateAuto(decorate))
|
|
}
|
|
|
|
buffer.WriteString(")")
|
|
|
|
if numLines, hasNumLines := apr.GetValue(cli.NumberFlag); hasNumLines {
|
|
num, err := strconv.Atoi(numLines)
|
|
if err != nil || num < 0 {
|
|
return "", fmt.Errorf("fatal: invalid --number argument: %s", numLines)
|
|
}
|
|
buffer.WriteString(" limit " + numLines)
|
|
}
|
|
|
|
interfaceParams := make([]interface{}, len(params))
|
|
for i, param := range params {
|
|
interfaceParams[i] = param
|
|
}
|
|
|
|
interpolatedQuery, err := dbr.InterpolateForDialect(buffer.String(), interfaceParams, dialect.MySQL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return interpolatedQuery, nil
|
|
}
|
|
|
|
// getExistingTables returns a map of table names that exist in the commit history of the given revisions
|
|
func getExistingTables(revisions []string, queryist cli.Queryist, sqlCtx *sql.Context) (map[string]bool, error) {
|
|
tableNames := make(map[string]bool)
|
|
|
|
if len(revisions) == 0 {
|
|
revisions = []string{"HEAD"}
|
|
}
|
|
|
|
for _, rev := range revisions {
|
|
rows, err := cli.GetRowsForSql(queryist, sqlCtx, "show tables as of '"+rev+"'")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, r := range rows {
|
|
tableNames[r[0].(string)] = true
|
|
}
|
|
}
|
|
|
|
return tableNames, nil
|
|
}
|
|
|
|
// logCommits takes a list of sql rows that have only 1 column, commit hash, and retrieves the commit info for each hash to be printed to std out
|
|
func logCommits(apr *argparser.ArgParseResults, commitHashes []sql.Row, queryist cli.Queryist, sqlCtx *sql.Context) error {
|
|
opts := commitInfoOptions{showSignature: apr.Contains(cli.ShowSignatureFlag)}
|
|
var commitsInfo []CommitInfo
|
|
for _, hash := range commitHashes {
|
|
cmHash := hash[0].(string)
|
|
commit, err := getCommitInfoWithOptions(sqlCtx, queryist, cmHash, opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if commit == nil {
|
|
return fmt.Errorf("no commits found for ref %s", cmHash)
|
|
}
|
|
commitsInfo = append(commitsInfo, *commit)
|
|
}
|
|
|
|
// Resolve auto before opening the pager. checkIsTerminal uses ExecuteWithStdioRestored,
|
|
// which mutates os.Stdout, and the pager block calls ExecuteWithStdioRestored too.
|
|
// Calling it from inside the pager block leaves the pager holding a stale stdout handle.
|
|
decoration := resolveDecorateAuto(apr.GetValueOrDefault(cli.DecorateFlag, cli.DecorateAuto))
|
|
return logToStdOut(apr, commitsInfo, sqlCtx, queryist, decoration)
|
|
}
|
|
|
|
// resolveDecorateAuto returns DecorateShort when stdout is a terminal and DecorateNo
|
|
// otherwise. Other |decorate| values pass through unchanged.
|
|
func resolveDecorateAuto(decorate string) string {
|
|
if decorate != cli.DecorateAuto {
|
|
return decorate
|
|
}
|
|
if checkIsTerminal() {
|
|
return cli.DecorateShort
|
|
}
|
|
return cli.DecorateNo
|
|
}
|
|
|
|
func logCompact(pager *outputpager.Pager, apr *argparser.ArgParseResults, commits []CommitInfo, sqlCtx *sql.Context, queryist cli.Queryist, decoration string) error {
|
|
color.NoColor = false
|
|
for _, comm := range commits {
|
|
if len(comm.parentHashes) < apr.GetIntOrDefault(cli.MinParentsFlag, 0) {
|
|
return nil
|
|
}
|
|
|
|
chStr := comm.commitHash
|
|
if apr.Contains(cli.ParentsFlag) {
|
|
for _, h := range comm.parentHashes {
|
|
chStr += " " + h
|
|
}
|
|
}
|
|
|
|
// TODO: use short hash instead
|
|
// Write commit hash
|
|
pager.Writer.Write([]byte(color.YellowString("%s ", chStr)))
|
|
|
|
if decoration != cli.DecorateNo {
|
|
printRefs(pager, &comm, decoration)
|
|
}
|
|
|
|
formattedDesc := strings.Replace(comm.commitMeta.Description, "\n", " ", -1) + "\n"
|
|
pager.Writer.Write([]byte(formattedDesc))
|
|
|
|
if apr.Contains(cli.StatFlag) {
|
|
if comm.parentHashes != nil && len(comm.parentHashes) == 1 { // don't print stats for merge commits
|
|
diffStats := make(map[string]*merge.MergeStats)
|
|
diffStats, _, err := calculateMergeStats(queryist, sqlCtx, diffStats, comm.parentHashes[0], comm.commitHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printDiffStats(diffStats, pager)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func logDefault(pager *outputpager.Pager, apr *argparser.ArgParseResults, commits []CommitInfo, sqlCtx *sql.Context, queryist cli.Queryist, decoration string) error {
|
|
for _, comm := range commits {
|
|
PrintCommitInfo(pager, apr.GetIntOrDefault(cli.MinParentsFlag, 0), apr.Contains(cli.ParentsFlag), apr.Contains(cli.ShowSignatureFlag), decoration, &comm)
|
|
if apr.Contains(cli.StatFlag) {
|
|
if comm.parentHashes != nil && len(comm.parentHashes) == 1 { // don't print stats for merge commits
|
|
diffStats := make(map[string]*merge.MergeStats)
|
|
diffStats, _, err := calculateMergeStats(queryist, sqlCtx, diffStats, comm.parentHashes[0], comm.commitHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printDiffStats(diffStats, pager)
|
|
pager.Writer.Write([]byte("\n"))
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func logToStdOut(apr *argparser.ArgParseResults, commits []CommitInfo, sqlCtx *sql.Context, queryist cli.Queryist, decoration string) (err error) {
|
|
if cli.ExecuteWithStdioRestored == nil {
|
|
return nil
|
|
}
|
|
cli.ExecuteWithStdioRestored(func() {
|
|
pager := outputpager.Start()
|
|
defer pager.Stop()
|
|
if apr.Contains(cli.GraphFlag) {
|
|
logGraph(pager, apr, commits)
|
|
} else if apr.Contains(cli.OneLineFlag) {
|
|
err = logCompact(pager, apr, commits, sqlCtx, queryist, decoration)
|
|
} else {
|
|
err = logDefault(pager, apr, commits, sqlCtx, queryist, decoration)
|
|
}
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
// printDiffStats prints the diff stats for a commit to a pager
|
|
func printDiffStats(diffStats map[string]*merge.MergeStats, pager *outputpager.Pager) {
|
|
maxNameLen := 0
|
|
maxModCount := 0
|
|
rowsAdded := 0
|
|
rowsDeleted := 0
|
|
rowsChanged := 0
|
|
var tbls []string
|
|
for tblName, stats := range diffStats {
|
|
if stats.Operation == merge.TableModified {
|
|
tbls = append(tbls, tblName)
|
|
nameLen := len(tblName)
|
|
modCount := stats.Adds + stats.Modifications + stats.Deletes
|
|
|
|
if nameLen > maxNameLen {
|
|
maxNameLen = nameLen
|
|
}
|
|
|
|
if modCount > maxModCount {
|
|
maxModCount = modCount
|
|
}
|
|
|
|
rowsAdded += stats.Adds
|
|
rowsChanged += stats.Modifications
|
|
rowsDeleted += stats.Deletes
|
|
}
|
|
}
|
|
|
|
if len(tbls) > 0 {
|
|
sort.Strings(tbls)
|
|
modCountStrLen := len(strconv.FormatInt(int64(maxModCount), 10))
|
|
format := fmt.Sprintf(" %%-%ds | %%-%ds %%s\n", maxNameLen, modCountStrLen)
|
|
|
|
for _, tbl := range tbls {
|
|
stats := diffStats[tbl]
|
|
if stats.Operation == merge.TableModified {
|
|
modCount := stats.Adds + stats.Modifications + stats.Deletes
|
|
modCountStr := strconv.FormatInt(int64(modCount), 10)
|
|
visualizedChanges := visualizeChangesForLog(stats, maxModCount)
|
|
|
|
pager.Writer.Write([]byte(fmt.Sprintf(format, tbl, modCountStr, visualizedChanges)))
|
|
}
|
|
}
|
|
|
|
details := fmt.Sprintf(" %d tables changed, %d rows added(+), %d rows modified(*), %d rows deleted(-)\n", len(tbls), rowsAdded, rowsChanged, rowsDeleted)
|
|
pager.Writer.Write([]byte(details))
|
|
}
|
|
|
|
for tblName, stats := range diffStats {
|
|
if stats.Operation == merge.TableAdded {
|
|
pager.Writer.Write([]byte(" " + tblName + " added\n"))
|
|
}
|
|
}
|
|
for tblName, stats := range diffStats {
|
|
if stats.Operation == merge.TableRemoved {
|
|
pager.Writer.Write([]byte(" " + tblName + " deleted\n"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// visualizeChangesForLog generates the string with the appropriate symbols to represent the changes in a commit with
|
|
// the corresponding color suitable for writing to a pager
|
|
func visualizeChangesForLog(stats *merge.MergeStats, maxMods int) string {
|
|
color.NoColor = false
|
|
const maxVisLen = 30 //can be a bit longer due to min len and rounding
|
|
|
|
resultStr := ""
|
|
if stats.Adds > 0 {
|
|
addLen := int(maxVisLen * (float64(stats.Adds) / float64(maxMods)))
|
|
if addLen > stats.Adds {
|
|
addLen = stats.Adds
|
|
}
|
|
addStr := fillStringWithChar('+', addLen)
|
|
resultStr += color.HiGreenString("%s", addStr)
|
|
|
|
}
|
|
|
|
if stats.Modifications > 0 {
|
|
modLen := int(maxVisLen * (float64(stats.Modifications) / float64(maxMods)))
|
|
if modLen > stats.Modifications {
|
|
modLen = stats.Modifications
|
|
}
|
|
modStr := fillStringWithChar('*', modLen)
|
|
resultStr += color.HiYellowString("%s", modStr)
|
|
}
|
|
|
|
if stats.Deletes > 0 {
|
|
delLen := int(maxVisLen * (float64(stats.Deletes) / float64(maxMods)))
|
|
if delLen > stats.Deletes {
|
|
delLen = stats.Deletes
|
|
}
|
|
delStr := fillStringWithChar('-', delLen)
|
|
resultStr += color.HiRedString("%s", delStr)
|
|
}
|
|
|
|
return resultStr
|
|
}
|
|
|
|
func handleErrAndExit(err error) int {
|
|
if err != nil {
|
|
cli.PrintErrln(strings.ReplaceAll(err.Error(), "Invalid argument to dolt_log: ", ""))
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
}
|