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 current branch
curBranch := ""
res, err := GetRowsForSql(queryist, sqlCtx, "SELECT active_branch()")
fulghum marked this conversation as resolved.
Show resolved Hide resolved
if err == nil {
// still print the reflog even if we can't get the current branch
curBranch = 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, curBranch)

return 0
}

// reflogToStdOut takes a list of ReflogInfo and prints the reflog to stdout
func reflogToStdOut(reflogInfo []ReflogInfo, curBranch 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, curBranch)
fulghum marked this conversation as resolved.
Show resolved Hide resolved
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, curBranch string) string {
if strings.HasPrefix(fullRef, "refs/heads/") {
branch := strings.TrimPrefix(fullRef, "refs/heads/")
if curBranch != "" && branch == curBranch {
return fmt.Sprintf("\033[36;1mHEAD -> \033[32;1m%s\033[0m", branch) // HEAD in cyan (36;1), branch in green (32;1m)
}
return fmt.Sprintf("\033[32;1m%s\033[0m", branch) // 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 @@ -123,6 +123,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
40 changes: 34 additions & 6 deletions go/libraries/doltcore/sqle/reflog_table_function.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type ReflogTableFunction struct {
ctx *sql.Context
database sql.Database
refExpr sql.Expression
showAll bool
}

var _ sql.TableFunction = (*ReflogTableFunction)(nil)
Expand Down Expand Up @@ -109,9 +110,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 !rltf.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 @@ -179,7 +186,14 @@ func (rltf *ReflogTableFunction) Resolved() bool {
}

func (rltf *ReflogTableFunction) String() string {
return fmt.Sprintf("DOLT_REFLOG(%s)", rltf.refExpr.String())
var args []string
if rltf.showAll {
fulghum marked this conversation as resolved.
Show resolved Hide resolved
args = append(args, "'--all'")
}
if rltf.refExpr != nil {
args = append(args, rltf.refExpr.String())
}
return fmt.Sprintf("DOLT_REFLOG(%s)", strings.Join(args, ", "))
}

func (rltf *ReflogTableFunction) Children() []sql.Node {
Expand Down Expand Up @@ -211,14 +225,28 @@ func (rltf *ReflogTableFunction) Expressions() []sql.Expression {
}

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]

if len(expression) == 2 {
if expression[0].String() == "'--all'" && expression[1].String() == "'--all'" {
return nil, fmt.Errorf("error: multiple values provided for `all`")
}
if expression[0].String() != "'--all'" && expression[1].String() != "'--all'" {
return nil, fmt.Errorf("error: %s has too many positional arguments. Expected at most %d, found %d: %s", rltf.Name(), 1, 2, expression)
}
}
fulghum marked this conversation as resolved.
Show resolved Hide resolved
for _, expr := range expression {
if expr.String() != "'--all'" {
new.refExpr = expr
} else {
new.showAll = true
}
}

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