Skip to content

Commit

Permalink
Merge pull request #7032 from dolthub/steph/reflog
Browse files Browse the repository at this point in the history
CLI command for reflog
  • Loading branch information
stephkyou authored Dec 2, 2023
2 parents 7572724 + d73c8fd commit bbe8c32
Show file tree
Hide file tree
Showing 11 changed files with 558 additions and 42 deletions.
6 changes: 6 additions & 0 deletions go/cmd/dolt/cli/arg_parser_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ func CreateCountCommitsArgParser() *argparser.ArgParser {
return ap
}

func CreateReflogArgParser() *argparser.ArgParser {
ap := argparser.NewArgParserWithMaxArgs("reflog", 1)
ap.SupportsFlag(AllFlag, "", "Show all refs, including hidden refs, such as DoltHub workspace refs")
return ap
}

func CreateGlobalArgParser(name string) *argparser.ArgParser {
ap := argparser.NewArgParserWithVariableArgs(name)
if name == "dolt" {
Expand Down
197 changes: 197 additions & 0 deletions go/cmd/dolt/commands/reflog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// Copyright 2023 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 (
"context"
"fmt"
"strings"

"github.com/dolthub/go-mysql-server/sql"
"github.com/gocraft/dbr/v2"
"github.com/gocraft/dbr/v2/dialect"

"github.com/dolthub/dolt/go/cmd/dolt/cli"
"github.com/dolthub/dolt/go/cmd/dolt/errhand"
eventsapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi/v1alpha1"
"github.com/dolthub/dolt/go/libraries/doltcore/env"
"github.com/dolthub/dolt/go/libraries/utils/argparser"
"github.com/dolthub/dolt/go/store/util/outputpager"
)

var reflogDocs = cli.CommandDocumentationContent{
ShortDesc: "Shows a history of named refs",
LongDesc: `Shows the history of named refs (e.g. branches and tags), which is useful for understanding how a branch
or tag changed over time to reference different commits, particularly for information not surfaced through {{.EmphasisLeft}}dolt log{{.EmphasisRight}}.
The data from Dolt's reflog comes from [Dolt's journaling chunk store](https://www.dolthub.com/blog/2023-03-08-dolt-chunk-journal/).
This data is local to a Dolt database and never included when pushing, pulling, or cloning a Dolt database. This means when you clone a Dolt database, it will not have any reflog data until you perform operations that change what commit branches or tags reference.
Dolt's reflog is similar to [Git's reflog](https://git-scm.com/docs/git-reflog), but there are a few differences:
- The Dolt reflog currently only supports named references, such as branches and tags, and not any of Git's special refs (e.g. {{.EmphasisLeft}}HEAD{{.EmphasisRight}}, {{.EmphasisLeft}}FETCH-HEAD{{.EmphasisRight}}, {{.EmphasisLeft}}MERGE-HEAD{{.EmphasisRight}}).
- The Dolt reflog can be queried for the log of references, even after a reference has been deleted. In Git, once a branch or tag is deleted, the reflog for that ref is also deleted and to find the last commit a branch or tag pointed to you have to use Git's special {{.EmphasisLeft}}HEAD{{.EmphasisRight}} reflog to find the commit, which can sometimes be challenging. Dolt makes this much easier by allowing you to see the history for a deleted ref so you can easily see the last commit a branch or tag pointed to before it was deleted.`,
Synopsis: []string{
`[--all] {{.LessThan}}ref{{.GreaterThan}}`,
},
}

type ReflogCmd 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 ReflogCmd) Name() string {
return "reflog"
}

// Description returns a description of the command
func (cmd ReflogCmd) Description() string {
return "Show history of named refs."
}

// EventType returns the type of the event to log
func (cmd ReflogCmd) EventType() eventsapi.ClientEventType {
return eventsapi.ClientEventType_REFLOG
}

func (cmd ReflogCmd) Docs() *cli.CommandDocumentation {
ap := cmd.ArgParser()
return cli.NewCommandDocumentation(reflogDocs, ap)
}

func (cmd ReflogCmd) ArgParser() *argparser.ArgParser {
return cli.CreateReflogArgParser()
}

func (cmd ReflogCmd) RequiresRepo() bool {
return false
}

// Exec executes the command
func (cmd ReflogCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cmd.ArgParser()
help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, reflogDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)

queryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx)
if err != nil {
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}
if closeFunc != nil {
defer closeFunc()
}

query, err := constructInterpolatedDoltReflogQuery(apr)
if err != nil {
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}

rows, err := GetRowsForSql(queryist, sqlCtx, query)
if err != nil {
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}

return printReflog(rows, queryist, sqlCtx)
}

// constructInterpolatedDoltReflogQuery generates the sql query necessary to call the DOLT_REFLOG() function.
// Also interpolates this query to prevent sql injection
func constructInterpolatedDoltReflogQuery(apr *argparser.ArgParseResults) (string, error) {
var params []interface{}
var args []string

if apr.NArg() == 1 {
params = append(params, apr.Arg(0))
args = append(args, "?")
}
if apr.Contains(cli.AllFlag) {
args = append(args, "'--all'")
}

query := fmt.Sprintf("SELECT ref, commit_hash, commit_message FROM DOLT_REFLOG(%s)", strings.Join(args, ", "))
interpolatedQuery, err := dbr.InterpolateForDialect(query, params, dialect.MySQL)
if err != nil {
return "", err
}

return interpolatedQuery, nil
}

type ReflogInfo struct {
ref string
commitHash string
commitMessage string
}

// printReflog takes a list of sql rows with columns ref, commit hash, commit message. Prints the reflog to stdout
func printReflog(rows []sql.Row, queryist cli.Queryist, sqlCtx *sql.Context) int {
var reflogInfo []ReflogInfo

// Get the hash of HEAD for the `HEAD ->` decoration
headHash := ""
res, err := GetRowsForSql(queryist, sqlCtx, "SELECT hashof('HEAD')")
if err == nil {
// still print the reflog even if we can't get the hash
headHash = res[0][0].(string)
}

for _, row := range rows {
ref := row[0].(string)
commitHash := row[1].(string)
commitMessage := row[2].(string)
reflogInfo = append(reflogInfo, ReflogInfo{ref, commitHash, commitMessage})
}

reflogToStdOut(reflogInfo, headHash)

return 0
}

// reflogToStdOut takes a list of ReflogInfo and prints the reflog to stdout
func reflogToStdOut(reflogInfo []ReflogInfo, headHash string) {
if cli.ExecuteWithStdioRestored == nil {
return
}
cli.ExecuteWithStdioRestored(func() {
pager := outputpager.Start()
defer pager.Stop()

for _, info := range reflogInfo {
// TODO: use short hash instead
line := []string{fmt.Sprintf("\033[33m%s\033[0m", info.commitHash)} // commit hash in yellow (33m)

processedRef := processRefForReflog(info.ref)
if headHash != "" && headHash == info.commitHash {
line = append(line, fmt.Sprintf("\033[33m(\033[36;1mHEAD -> %s\033[33m)\033[0m", processedRef)) // HEAD in cyan (36;1)
} else {
line = append(line, fmt.Sprintf("\033[33m(%s\033[33m)\033[0m", processedRef)) // () in yellow (33m)
}
line = append(line, fmt.Sprintf("%s\n", info.commitMessage))
pager.Writer.Write([]byte(strings.Join(line, " ")))
}
})
}

// processRefForReflog takes a full ref (e.g. refs/heads/master) or tag name and returns the ref name (e.g. master) with relevant decoration.
func processRefForReflog(fullRef string) string {
if strings.HasPrefix(fullRef, "refs/heads/") {
return fmt.Sprintf("\033[32;1m%s\033[0m", strings.TrimPrefix(fullRef, "refs/heads/")) // branch in green (32;1m)
} else if strings.HasPrefix(fullRef, "refs/tags/") {
return fmt.Sprintf("\033[33mtag: %s\033[0m", strings.TrimPrefix(fullRef, "refs/tags/")) // tag in yellow (33m)
} else if strings.HasPrefix(fullRef, "refs/remotes/") {
return fmt.Sprintf("\033[31;1m%s\033[0m", strings.TrimPrefix(fullRef, "refs/remotes/")) // remote in red (31;1m)
} else if strings.HasPrefix(fullRef, "refs/workspaces/") {
return fmt.Sprintf("\033[35;1mworkspace: %s\033[0m", strings.TrimPrefix(fullRef, "refs/workspaces/")) // workspace in magenta (35;1m)
} else {
return fullRef
}
}
1 change: 1 addition & 0 deletions go/cmd/dolt/dolt.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ var doltSubCommands = []cli.Command{
&commands.Assist{},
commands.ProfileCmd{},
commands.QueryDiff{},
commands.ReflogCmd{},
}

var commandsWithoutCliCtx = []cli.Command{
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go/libraries/doltcore/sqle/enginetest/dolt_queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -4317,7 +4317,7 @@ var DoltReflogTestScripts = []queries.ScriptTest{
Assertions: []queries.ScriptTestAssertion{
{
Query: "select * from dolt_reflog('foo', 'bar');",
ExpectedErrStr: "function 'dolt_reflog' expected 0 or 1 arguments, 2 received",
ExpectedErrStr: "error: dolt_reflog has too many positional arguments. Expected at most 1, found 2: ['foo' 'bar']",
},
{
Query: "select * from dolt_reflog(NULL);",
Expand Down
64 changes: 43 additions & 21 deletions go/libraries/doltcore/sqle/reflog_table_function.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ import (
)

type ReflogTableFunction struct {
ctx *sql.Context
database sql.Database
refExpr sql.Expression
ctx *sql.Context
database sql.Database
refAndArgExprs []sql.Expression
}

var _ sql.TableFunction = (*ReflogTableFunction)(nil)
Expand Down Expand Up @@ -65,17 +65,30 @@ func (rltf *ReflogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.Row
}

var refName string
if rltf.refExpr != nil {
target, err := rltf.refExpr.Eval(ctx, row)
showAll := false
for _, expr := range rltf.refAndArgExprs {
target, err := expr.Eval(ctx, row)
if err != nil {
return nil, fmt.Errorf("error evaluating expression (%s): %s",
rltf.refExpr.String(), err.Error())
expr.String(), err.Error())
}

refName, ok = target.(string)
targetStr, ok := target.(string)
if !ok {
return nil, fmt.Errorf("argument (%v) is not a string value, but a %T", target, target)
}

if targetStr == "--all" {
if showAll {
return nil, fmt.Errorf("error: multiple values provided for `all`")
}
showAll = true
} else {
if refName != "" {
return nil, fmt.Errorf("error: %s has too many positional arguments. Expected at most %d, found %d: %s",
rltf.Name(), 1, 2, rltf.refAndArgExprs)
}
refName = targetStr
}
}

ddb := sqlDb.DbData().Ddb
Expand Down Expand Up @@ -109,9 +122,15 @@ func (rltf *ReflogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.Row
if doltRef.GetType() == ref.InternalRefType {
return nil
}
// skip workspace refs by default
if doltRef.GetType() == ref.WorkspaceRefType {
if !showAll {
return nil
}
}

// If a ref expression to filter on was specified, see if we match the current ref
if rltf.refExpr != nil {
if refName != "" {
// If the caller has supplied a branch or tag name, without the fully qualified ref path,
// take the first match and use that as the canonical ref to filter on
if strings.HasSuffix(strings.ToLower(id), "/"+strings.ToLower(refName)) {
Expand Down Expand Up @@ -172,14 +191,21 @@ func (rltf *ReflogTableFunction) Schema() sql.Schema {
}

func (rltf *ReflogTableFunction) Resolved() bool {
if rltf.refExpr != nil {
return rltf.refExpr.Resolved()
for _, expr := range rltf.refAndArgExprs {
if !expr.Resolved() {
return false
}
}
return true
}

func (rltf *ReflogTableFunction) String() string {
return fmt.Sprintf("DOLT_REFLOG(%s)", rltf.refExpr.String())
var args []string

for _, expr := range rltf.refAndArgExprs {
args = append(args, expr.String())
}
return fmt.Sprintf("DOLT_REFLOG(%s)", strings.Join(args, ", "))
}

func (rltf *ReflogTableFunction) Children() []sql.Node {
Expand All @@ -204,21 +230,17 @@ func (rltf *ReflogTableFunction) IsReadOnly() bool {
}

func (rltf *ReflogTableFunction) Expressions() []sql.Expression {
if rltf.refExpr != nil {
return []sql.Expression{rltf.refExpr}
}
return []sql.Expression{}
return rltf.refAndArgExprs
}

func (rltf *ReflogTableFunction) WithExpressions(expression ...sql.Expression) (sql.Node, error) {
if len(expression) > 1 {
return nil, sql.ErrInvalidArgumentNumber.New(rltf.Name(), "0 or 1", len(expression))
if len(expression) > 2 {
return nil, sql.ErrInvalidArgumentNumber.New(rltf.Name(), "0 to 2", len(expression))
}

new := *rltf
if len(expression) > 0 {
new.refExpr = expression[0]
}
new.refAndArgExprs = expression

return &new, nil
}

Expand Down
1 change: 1 addition & 0 deletions integration-tests/bats/helper/local-remote.bash
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ SKIP_SERVER_TESTS=$(cat <<-EOM
~cli-hosted.bats~
~profile.bats~
~ls.bats~
~reflog.bats~
EOM
)

Expand Down
Loading

0 comments on commit bbe8c32

Please sign in to comment.