From f7802511a1ca51db71e96685f59bc4bd22c51c7b Mon Sep 17 00:00:00 2001 From: superlzyguy Date: Mon, 19 Oct 2026 04:39:17 +0000 Subject: [PATCH] Report native fast-forward merge outcomes accurately --- src/database.cpp | 32 ++++++++++++++---- test/dolt.test.ts | 84 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/database.cpp b/src/database.cpp index 1537104..43ad106 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -246,6 +246,18 @@ Napi::Value Database::DoltCheckout(const Napi::CallbackInfo& info) { return env.Undefined(); } +// Commit hashes are 40 lowercase hex characters; every other dolt_merge +// result (for example "Already up to date") is a status string. +static bool IsCommitHash(const std::string& value) { + if (value.size() != 40) return false; + for (char c : value) { + const bool digit = c >= '0' && c <= '9'; + const bool lower = c >= 'a' && c <= 'f'; + if (!digit && !lower) return false; + } + return true; +} + Napi::Value Database::DoltMerge(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (!db_) { Napi::Error::New(env, "Database is closed").ThrowAsJavaScriptException(); return env.Undefined(); } @@ -254,15 +266,23 @@ Napi::Value Database::DoltMerge(const Napi::CallbackInfo& info) { return env.Undefined(); } std::string branch = info[0].As().Utf8Value(); - // dolt_merge is a scalar function returning the merge commit hash (or NULL on conflict). - ExecScalar(env, "SELECT dolt_merge(?)", {branch}); + // Resolve once, then merge that immutable commit. Re-reading the mutable + // branch after a merge can misclassify the result or throw if a peer has + // advanced/deleted the branch. The captured hash is the actual merge input, + // not an inference about unlocked state before or after the operation. + std::string target = ExecScalar(env, "SELECT dolt_hashof(?)", {branch}); if (env.IsExceptionPending()) return env.Undefined(); - // Derive fast_forward and conflicts from post-merge state. + std::string outcome = ExecScalar(env, + "SELECT dolt_merge(?, '-m', 'Merge branch ''' || ? || ''' into ' || active_branch())", + {target, branch}); + if (env.IsExceptionPending()) return env.Undefined(); + // Fast-forward returns the chosen input commit; a three-way merge returns + // a new commit, and an up-to-date merge returns a status string. + int fastForward = IsCommitHash(outcome) && outcome == target ? 1 : 0; std::string nConflicts = ExecScalar(env, "SELECT COUNT(*) FROM dolt_conflicts", {}); - std::string logLen = ExecScalar(env, - "SELECT COUNT(*) FROM dolt_log LIMIT 2", {}); + if (env.IsExceptionPending()) return env.Undefined(); auto result = Napi::Object::New(env); - result.Set("fast_forward", Napi::Number::New(env, 0)); + result.Set("fast_forward", Napi::Number::New(env, fastForward)); result.Set("conflicts", Napi::Number::New(env, (double)std::stoi(nConflicts.empty() ? "0" : nConflicts))); return result; diff --git a/test/dolt.test.ts b/test/dolt.test.ts index 76f11d5..44962eb 100644 --- a/test/dolt.test.ts +++ b/test/dolt.test.ts @@ -217,6 +217,90 @@ describe("doltMerge()", () => { expect(cfVal).toBe(0) }) + test("reports a real fast-forward as fast_forward 1", () => { + db.doltBranch("ff-branch") + db.doltCheckout("ff-branch") + db.exec("INSERT INTO items VALUES (7, 'from ff-branch')") + db.doltCommit("ff commit") + db.doltCheckout("main") + const result = db.doltMerge("ff-branch") + const ffVal = result.fast_forward + const cfVal = result.conflicts + expect(ffVal).toBe(1) + expect(cfVal).toBe(0) + // HEAD advanced to exactly the incoming branch head. + const head = db.prepare("SELECT dolt_hashof('HEAD') AS h").get() as any + const branch = db.prepare("SELECT dolt_hashof('ff-branch') AS h").get() as any + expect(head.h).toBe(branch.h) + }) + + test("reports a three-way merge as fast_forward 0", () => { + db.doltBranch("three-way") + db.doltCheckout("three-way") + db.exec("INSERT INTO items VALUES (8, 'from branch')") + db.doltCommit("branch commit") + db.doltCheckout("main") + db.exec("INSERT INTO items VALUES (9, 'from main')") + db.doltCommit("main commit") + const result = db.doltMerge("three-way") + const ffVal = result.fast_forward + const cfVal = result.conflicts + expect(ffVal).toBe(0) + expect(cfVal).toBe(0) + // The merge landed on a new commit, not on the branch head. + const head = db.prepare("SELECT dolt_hashof('HEAD') AS h").get() as any + const branch = db.prepare("SELECT dolt_hashof('three-way') AS h").get() as any + expect(head.h).not.toBe(branch.h) + expect((db.prepare("SELECT COUNT(*) AS n FROM items").get() as any).n).toBe(2) + }) + + test("reports an up-to-date merge as fast_forward 0", () => { + db.doltBranch("stale") + db.exec("INSERT INTO items VALUES (10, 'ahead')") + db.doltCommit("ahead of stale") + const before = db.prepare("SELECT dolt_hashof('HEAD') AS h").get() as any + const result = db.doltMerge("stale") + const ffVal = result.fast_forward + expect(ffVal).toBe(0) + const after = db.prepare("SELECT dolt_hashof('HEAD') AS h").get() as any + expect(after.h).toBe(before.h) + }) + + test("fast-forwards to an incoming commit that is itself a merge", () => { + const base = (db.prepare("SELECT dolt_hashof('HEAD') AS h").get() as any).h + db.doltBranch("side") + db.doltCheckout("side") + db.exec("INSERT INTO items VALUES (11, 'side')") + db.doltCommit("side commit") + db.doltCheckout("main") + db.exec("INSERT INTO items VALUES (12, 'main')") + db.doltCommit("main commit") + db.doltMerge("side") + const merged = (db.prepare("SELECT dolt_hashof('HEAD') AS h").get() as any).h + db.doltBranch("behind", base) + db.doltCheckout("behind") + expect(db.doltMerge("main").fast_forward).toBe(1) + expect((db.prepare("SELECT dolt_hashof('HEAD') AS h").get() as any).h).toBe(merged) + }) + + test("keeps the caller's branch label in the three-way commit message", () => { + db.doltBranch("editor's-take") + db.doltCheckout("editor's-take") + db.exec("INSERT INTO items VALUES (13, 'take')") + db.doltCommit("take commit") + db.doltCheckout("main") + db.exec("INSERT INTO items VALUES (14, 'main')") + db.doltCommit("main commit") + expect(db.doltMerge("editor's-take").fast_forward).toBe(0) + expect(db.doltLog({ limit: 1 })[0]?.message).toBe("Merge branch 'editor's-take' into main") + }) + + test("rejects an unknown merge source without changing HEAD", () => { + const head = (db.prepare("SELECT dolt_hashof('HEAD') AS h").get() as any).h + expect(() => db.doltMerge("does-not-exist")).toThrow() + expect((db.prepare("SELECT dolt_hashof('HEAD') AS h").get() as any).h).toBe(head) + }) + test("merged data is visible on main after merge", () => { db.doltBranch("data-branch") db.doltCheckout("data-branch")