Skip to content

Commit 7d1ba2b

Browse files
authored
Merge pull request #10810 from dolthub/elian/10807
Fix remotesapi push fail with "target has uncommitted changes" after any successful push
2 parents 7e74e06 + 0aa83d2 commit 7d1ba2b

File tree

8 files changed

+68
-30
lines changed

8 files changed

+68
-30
lines changed

go/libraries/doltcore/doltdb/doltdb.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -885,10 +885,12 @@ func (ddb *DoltDB) Commit(ctx context.Context, valHash hash.Hash, dref ref.DoltR
885885
return ddb.CommitWithParentSpecs(ctx, valHash, dref, nil, cm)
886886
}
887887

888-
// FastForwardWithWorkspaceCheck will perform a fast forward update of the branch given to the commit given, but only
889-
// if the working set is in sync with the head of the branch given. This is used in the course of pushing to a remote.
890-
// If the target doesn't currently have the working set ref, then no working set change will be made.
891-
func (ddb *DoltDB) FastForwardWithWorkspaceCheck(ctx context.Context, branch ref.DoltRef, commit *Commit) error {
888+
// FastForwardWithWorkspaceCheck performs a fast-forward of |branch| to |commit| only if the remote
889+
// working set is in sync with its HEAD, then updates the working set to match the new HEAD.
890+
// This is used when pushing to a remote. If the remote does not maintain a working set, no working
891+
// set update is performed.
892+
// When |allowDirtyWorking| is true, the working set may have unstaged changes, such as ignored tables.
893+
func (ddb *DoltDB) FastForwardWithWorkspaceCheck(ctx context.Context, branch ref.DoltRef, commit *Commit, allowDirtyWorking bool) error {
892894
ds, err := ddb.db.GetDataset(ctx, branch.String())
893895
if err != nil {
894896
return err
@@ -909,7 +911,7 @@ func (ddb *DoltDB) FastForwardWithWorkspaceCheck(ctx context.Context, branch ref
909911
ws = wsRef.String()
910912
}
911913

912-
_, err = ddb.db.FastForward(ctx, ds, addr, ws)
914+
_, err = ddb.db.FastForward(ctx, ds, addr, ws, allowDirtyWorking)
913915

914916
return err
915917
}
@@ -931,7 +933,7 @@ func (ddb *DoltDB) FastForwardToHash(ctx context.Context, branch ref.DoltRef, ha
931933
return err
932934
}
933935

934-
_, err = ddb.db.FastForward(ctx, ds, hash, "")
936+
_, err = ddb.db.FastForward(ctx, ds, hash, "", false)
935937

936938
return err
937939
}

go/libraries/doltcore/doltdb/hooksdatabase.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,8 @@ func (db hooksDatabase) SetHead(ctx context.Context, ds datas.Dataset, newHeadAd
164164
return ds, err
165165
}
166166

167-
func (db hooksDatabase) FastForward(ctx context.Context, ds datas.Dataset, newHeadAddr hash.Hash, workingSetPath string) (datas.Dataset, error) {
168-
ds, err := db.Database.FastForward(ctx, ds, newHeadAddr, workingSetPath)
167+
func (db hooksDatabase) FastForward(ctx context.Context, ds datas.Dataset, newHeadAddr hash.Hash, workingSetPath string, allowDirtyWorking bool) (datas.Dataset, error) {
168+
ds, err := db.Database.FastForward(ctx, ds, newHeadAddr, workingSetPath, allowDirtyWorking)
169169
if err == nil {
170170
db.ExecuteCommitHooks(ctx, ds, false, false)
171171
}

go/libraries/doltcore/env/actions/remotes.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,13 @@ func Push(ctx context.Context, tempTableDir string, mode ref.UpdateMode, destRef
8686
}
8787
err = srcDB.SetHeadToCommit(ctx, remoteRef, commit)
8888
case ref.FastForwardOnly:
89-
// ResolveBranchRoots fails for remotes that don't maintain a working set (e.g. file://),
90-
// in which case FastForwardWithWorkspaceCheck handles it correctly without the ignored-table check.
89+
// Ignored tables create a staged/working mismatch that blocks fast-forward pushes, so we detect and permit that case.
9190
onlyIgnored := false
9291
roots, err := destDB.ResolveBranchRoots(ctx, destRef)
9392
if err == nil {
9493
onlyIgnored, _ = diff.WorkingSetContainsOnlyIgnoredTables(ctx, roots)
9594
}
96-
if onlyIgnored {
97-
err = destDB.FastForward(ctx, destRef, commit)
98-
} else {
99-
err = destDB.FastForwardWithWorkspaceCheck(ctx, destRef, commit)
100-
}
95+
err = destDB.FastForwardWithWorkspaceCheck(ctx, destRef, commit, onlyIgnored)
10196
if err != nil {
10297
return err
10398
}

go/store/datas/database.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,12 @@ type Database interface {
149149
// is not provided, no working set update will be performed.
150150
SetHead(ctx context.Context, ds Dataset, newHeadAddr hash.Hash, workingSetPath string) (Dataset, error)
151151

152-
// FastForward takes a types.Ref to a Commit object and makes it the new
153-
// Head of ds iff it is a descendant of the current Head. Intended to be
154-
// used e.g. after a call to Pull(). If the update cannot be performed,
155-
// e.g., because another process moved the current Head out from under
156-
// you, err will be non-nil.
157-
// The workingSetPath is the path to the working set that should be used for updating the working set. If one
158-
// is not provided, no working set update will be performed.
159-
FastForward(ctx context.Context, ds Dataset, newHeadAddr hash.Hash, workingSetPath string) (Dataset, error)
152+
// FastForward sets the head of |ds| to the commit at |newHeadAddr| if it is a descendant of
153+
// the current head. If the update cannot be performed, for example because another writer has
154+
// moved the head concurrently, err will be non-nil.
155+
// If |workingSetPath| is non-empty, the working set at that path is updated to match the new head.
156+
// When |allowDirtyWorking| is true, the working set may have unstaged changes, such as ignored tables.
157+
FastForward(ctx context.Context, ds Dataset, newHeadAddr hash.Hash, workingSetPath string, allowDirtyWorking bool) (Dataset, error)
160158

161159
// Stats may return some kind of struct that reports statistics about the
162160
// ChunkStore that backs this Database instance. The type is

go/store/datas/database_common.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -337,13 +337,13 @@ func (db *database) doSetHead(ctx context.Context, ds Dataset, addr hash.Hash, w
337337
})
338338
}
339339

340-
func (db *database) FastForward(ctx context.Context, ds Dataset, newHeadAddr hash.Hash, wsPath string) (Dataset, error) {
340+
func (db *database) FastForward(ctx context.Context, ds Dataset, newHeadAddr hash.Hash, wsPath string, allowDirtyWorking bool) (Dataset, error) {
341341
return db.doHeadUpdate(ctx, ds, func(ds Dataset) error {
342-
return db.doFastForward(ctx, ds, newHeadAddr, wsPath)
342+
return db.doFastForward(ctx, ds, newHeadAddr, wsPath, allowDirtyWorking)
343343
})
344344
}
345345

346-
func (db *database) doFastForward(ctx context.Context, ds Dataset, newHeadAddr hash.Hash, workingSetPath string) error {
346+
func (db *database) doFastForward(ctx context.Context, ds Dataset, newHeadAddr hash.Hash, workingSetPath string, allowDirtyWorking bool) error {
347347
newHead, err := db.readHead(ctx, newHeadAddr)
348348
if err != nil {
349349
return err
@@ -434,7 +434,7 @@ func (db *database) doFastForward(ctx context.Context, ds Dataset, newHeadAddr h
434434

435435
stagedHash := hash.New(msg.StagedRootAddrBytes())
436436
workingSetHash := hash.New(msg.WorkingRootAddrBytes())
437-
if stagedHash != workingSetHash {
437+
if !allowDirtyWorking && stagedHash != workingSetHash {
438438
return prolly.AddressMap{}, ErrDirtyWorkspace
439439
}
440440

go/store/datas/database_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ func (suite *DatabaseSuite) TestFastForward() {
442442
cCommitAddr := mustHeadAddr(ds) // To use in FastForward() below.
443443

444444
// FastForward should disallow this, as |a| is not a descendant of |c|
445-
_, err = suite.db.FastForward(context.Background(), ds, aCommitAddr, "")
445+
_, err = suite.db.FastForward(context.Background(), ds, aCommitAddr, "", false)
446446
suite.Error(err)
447447

448448
// Move Head back to something earlier in the lineage, so we can test FastForward
@@ -451,7 +451,7 @@ func (suite *DatabaseSuite) TestFastForward() {
451451
suite.True(mustHeadValue(ds).Equals(a))
452452

453453
// This should succeed, because while |a| is not a direct parent of |c|, it is an ancestor.
454-
ds, err = suite.db.FastForward(context.Background(), ds, cCommitAddr, "")
454+
ds, err = suite.db.FastForward(context.Background(), ds, cCommitAddr, "", false)
455455
suite.Require().NoError(err)
456456
suite.True(mustHeadValue(ds).Equals(c))
457457
}

go/store/datas/pull/puller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ func testPuller(t *testing.T, makeDB datasFactory) {
412412

413413
sinkDS, err := sinkdb.GetDataset(ctx, "ds")
414414
require.NoError(t, err)
415-
sinkDS, err = sinkdb.FastForward(ctx, sinkDS, rootAddr, "")
415+
sinkDS, err = sinkdb.FastForward(ctx, sinkDS, rootAddr, "", false)
416416
require.NoError(t, err)
417417

418418
require.NoError(t, err)

integration-tests/bats/sql-server-remotesrv.bats

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,3 +839,46 @@ GRANT CLONE_ADMIN ON *.* TO clone_admin_user@'localhost';
839839
[ "$status" -eq 0 ]
840840
[[ "$output" =~ "add rows via remotesapi push" ]] || false
841841
}
842+
843+
# https://github.com/dolthub/dolt/issues/10807
844+
@test "sql-server-remotesrv: consecutive pushes succeed with ignored tables, but fail when remote has real uncommitted changes" {
845+
mkdir remote
846+
cd remote
847+
dolt init
848+
dolt sql -q 'create table names (name varchar(10) primary key);'
849+
dolt sql -q 'insert into names (name) values ("abe"), ("betsy"), ("calvin");'
850+
dolt sql -q "INSERT INTO dolt_ignore VALUES ('tmp_*', true);"
851+
dolt sql -q 'create table tmp_scratch (id int primary key);' # ignored: in working but not staged
852+
dolt add names dolt_ignore
853+
dolt commit -m 'initial names.'
854+
855+
APIPORT=$( definePORT )
856+
dolt sql -q "CREATE USER root@'%' identified by 'rootpass'; GRANT ALL ON *.* to root@'%';"
857+
export DOLT_REMOTE_PASSWORD="rootpass"
858+
export SQL_USER="root"
859+
start_sql_server_with_args --remotesapi-port $APIPORT
860+
861+
cd ../
862+
dolt clone http://localhost:$APIPORT/remote cloned_db -u $SQL_USER
863+
cd cloned_db
864+
865+
dolt sql -q 'insert into names values ("dave");'
866+
dolt commit -am 'add dave'
867+
run dolt push origin --user $SQL_USER main:main
868+
[[ "$status" -eq 0 ]] || false
869+
870+
dolt sql -q 'insert into names values ("eve");'
871+
dolt commit -am 'add eve'
872+
run dolt push origin --user $SQL_USER main:main
873+
[[ "$status" -eq 0 ]] || false
874+
[[ ! "$output" =~ "target has uncommitted changes" ]] || false
875+
876+
# Add a real (non-ignored) uncommitted change to the remote; push must now fail.
877+
dolt --port $PORT --host 127.0.0.1 -u $SQL_USER -p rootpass --no-tls --use-db remote sql -q "INSERT INTO names VALUES ('zeek');"
878+
879+
dolt sql -q 'insert into names values ("frank");'
880+
dolt commit -am 'add frank'
881+
run dolt push origin --user $SQL_USER main:main
882+
[[ "$status" -ne 0 ]] || false
883+
[[ "$output" =~ "target has uncommitted changes" ]] || false
884+
}

0 commit comments

Comments
 (0)