-
-
Notifications
You must be signed in to change notification settings - Fork 535
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
CLI command for reflog #7032
Changes from 11 commits
adcf3c1
fccec02
d3e7c7d
37158e0
6ba1d09
3e5c6b6
5d24edd
0f2e926
e9c63ac
b47eeb4
e2f18ee
a99b52d
d73c8fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
} | ||
|
||
var _ sql.TableFunction = (*ReflogTableFunction)(nil) | ||
|
@@ -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 | ||
|
@@ -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)) { | ||
|
@@ -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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} | ||
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 { | ||
|
@@ -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 | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -135,6 +135,7 @@ SKIP_SERVER_TESTS=$(cat <<-EOM | |
~cli-hosted.bats~ | ||
~profile.bats~ | ||
~ls.bats~ | ||
~reflog.bats~ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
EOM | ||
) | ||
|
||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.