Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI command for reflog #7032

Merged
merged 13 commits into from
Dec 2, 2023
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
66 changes: 43 additions & 23 deletions go/libraries/doltcore/sqle/reflog_table_function.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ import (
)

type ReflogTableFunction struct {
ctx *sql.Context
database sql.Database
refExpr sql.Expression
tabId sql.TableId
colset sql.ColSet
ctx *sql.Context
database sql.Database
refAndArgExprs []sql.Expression
tabId sql.TableId
colset sql.ColSet
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two members don't seem to be used anywhere, so should be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were added in a separate PR, looks like they're being used a couple lines down.

}

var _ sql.TableFunction = (*ReflogTableFunction)(nil)
Expand Down Expand Up @@ -87,17 +87,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 @@ -131,9 +144,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 @@ -194,14 +213,19 @@ func (rltf *ReflogTableFunction) Schema() sql.Schema {
}

func (rltf *ReflogTableFunction) Resolved() bool {
if rltf.refExpr != nil {
return rltf.refExpr.Resolved()
for _, expr := range rltf.refAndArgExprs {
return expr.Resolved()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't want to always return on the first expression – you need to check that all the expressions are resolved.

You probably want something like if expr.Resolved() == false { return false } instead.

}
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 @@ -226,21 +250,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~
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(minor) It would be nice if we could somehow document why we're skipping some of these. It'll be hard to remember in the future. I know you mentioned we couldn't run reflog.bats through the local-remote test because it depends on dolt gc which won't there yet. That would be a nice note to include, but I'm not sure this format would allow it.

EOM
)

Expand Down
Loading
Loading