| 1 |
// The process wrapper against the real oxy binary on a scratch repository. |
| 2 |
// Runs with `npm test`; needs oxy on PATH, at ~/.local/bin/oxy, or in OXY_BIN. |
| 3 |
|
| 4 |
import { test } from "node:test"; |
| 5 |
import assert from "node:assert/strict"; |
| 6 |
import { mkdtempSync, writeFileSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs"; |
| 7 |
import { tmpdir } from "node:os"; |
| 8 |
import { join } from "node:path"; |
| 9 |
import { Oxy, findBinary, isRemoteBookmark, mergeChoices, parseKeptBoth } from "../out/oxy.mjs"; |
| 10 |
|
| 11 |
const binary = process.env.OXY_BIN ?? findBinary(undefined); |
| 12 |
const oxy = new Oxy(binary); |
| 13 |
|
| 14 |
/** Whether the oxy under test is at least `min` (a feature newer than the oldest oxy the tests run with). */ |
| 15 |
const oxyVersion = (await oxy.version()).version; |
| 16 |
function atLeast(min) { |
| 17 |
const [a, b] = [oxyVersion, min].map((v) => v.split(/[.-]/).slice(0, 3).map(Number)); |
| 18 |
for (let i = 0; i < 3; i++) { |
| 19 |
if (a[i] !== b[i]) { |
| 20 |
return a[i] > b[i]; |
| 21 |
} |
| 22 |
} |
| 23 |
return true; |
| 24 |
} |
| 25 |
|
| 26 |
test("root, status, cat, log and commit round trip", async () => { |
| 27 |
const dir = mkdtempSync(join(tmpdir(), "oxy-vscode-")); |
| 28 |
const home = join(dir, "home"); |
| 29 |
mkdirSync(home); |
| 30 |
process.env.OXY_HOME = home; |
| 31 |
await oxy.check(dir, ["init", "repo"]); |
| 32 |
const root = join(dir, "repo"); |
| 33 |
await oxy.check(root, ["identity", "create", "--name", "Test", "--email", "[email protected]"]); |
| 34 |
|
| 35 |
const version = await oxy.version(); |
| 36 |
assert.equal(version.protocol, 1); |
| 37 |
assert.equal(await oxy.root(dir), null, "outside a repository"); |
| 38 |
const info = await oxy.root(root); |
| 39 |
assert.equal(info?.bookmark, "main"); |
| 40 |
assert.ok(info?.root.endsWith("repo")); |
| 41 |
|
| 42 |
writeFileSync(join(root, "a.txt"), "one\n"); |
| 43 |
let status = await oxy.status(root); |
| 44 |
assert.deepEqual(status.paths, [{ path: "a.txt", status: "Added" }]); |
| 45 |
assert.equal(status.clean, false); |
| 46 |
await oxy.commit(root, "first"); |
| 47 |
status = await oxy.status(root); |
| 48 |
assert.equal(status.clean, true); |
| 49 |
|
| 50 |
writeFileSync(join(root, "a.txt"), "one\ntwo\n"); |
| 51 |
assert.equal((await oxy.cat(root, "a.txt")).toString(), "one\n", "the base version"); |
| 52 |
const log = await oxy.log(root, 10); |
| 53 |
assert.equal(log.changes.filter((c) => !c.working_copy).length, 1); |
| 54 |
assert.equal(log.changes.find((c) => !c.working_copy)?.signature?.status, "unsigned"); |
| 55 |
assert.ok((await oxy.prompt(root)).includes("main")); |
| 56 |
const bookmarks = await oxy.bookmarks(root); |
| 57 |
assert.deepEqual(bookmarks.map((b) => [b.name, b.current]), [["main", true]]); |
| 58 |
await assert.rejects(oxy.cat(root, "missing.txt"), /not in that state/); |
| 59 |
|
| 60 |
// Amend folds the edit into the single head. |
| 61 |
await oxy.amend(root, "first, amended"); |
| 62 |
assert.equal((await oxy.status(root)).clean, true); |
| 63 |
assert.ok((await oxy.log(root, 10)).changes.some((c) => c.message === "first, amended")); |
| 64 |
|
| 65 |
// Hunks: commit only b.txt's, leave a.txt's edit, then discard it. |
| 66 |
writeFileSync(join(root, "a.txt"), "one\ntwo\nthree\n"); |
| 67 |
writeFileSync(join(root, "b.txt"), "b\n"); |
| 68 |
const hunks = await oxy.hunks(root); |
| 69 |
assert.ok(hunks.length >= 2, JSON.stringify(hunks)); |
| 70 |
const bHunk = hunks.find((h) => h.path === "b.txt"); |
| 71 |
await oxy.commitHunks(root, "b only", [bHunk.index]); |
| 72 |
status = await oxy.status(root); |
| 73 |
assert.deepEqual(status.paths.map((p) => p.path), ["a.txt"], "a.txt stays uncommitted"); |
| 74 |
assert.equal((await oxy.cat(root, "b.txt")).toString(), "b\n", "committed with its content"); |
| 75 |
await oxy.discard(root, ["a.txt"]); |
| 76 |
assert.equal((await oxy.status(root)).clean, true); |
| 77 |
|
| 78 |
// The operation log, and undo of the last operation (the commit). |
| 79 |
const ops = await oxy.opLog(root); |
| 80 |
assert.ok(ops[0].current && ops.length > 3); |
| 81 |
assert.match(await oxy.undo(root), /Undone/); |
| 82 |
status = await oxy.status(root); |
| 83 |
assert.ok(status.paths.some((p) => p.path === "b.txt"), "the commit is undone; b.txt is uncommitted work again"); |
| 84 |
}); |
| 85 |
|
| 86 |
test("bookmarks, remotes and sync against a local origin", async () => { |
| 87 |
const dir = mkdtempSync(join(tmpdir(), "oxy-vscode-sync-")); |
| 88 |
const home = join(dir, "home"); |
| 89 |
mkdirSync(home); |
| 90 |
process.env.OXY_HOME = home; |
| 91 |
await oxy.check(dir, ["init", "origin"]); |
| 92 |
await oxy.clone(dir, join(dir, "origin"), join(dir, "work")); |
| 93 |
const root = join(dir, "work"); |
| 94 |
writeFileSync(join(root, "a.txt"), "a\n"); |
| 95 |
await oxy.commit(root, "a"); |
| 96 |
const remotes = await oxy.remotes(root); |
| 97 |
assert.equal(remotes[0].name, "origin"); |
| 98 |
let push = await oxy.push(root); |
| 99 |
assert.equal(push[0].ok, true); |
| 100 |
let bookmarks = await oxy.bookmarks(root); |
| 101 |
assert.deepEqual(bookmarks.map((b) => [b.name, b.current, b.ahead, b.behind]), [["main", true, 0, 0]]); |
| 102 |
|
| 103 |
await oxy.bookmarkCreate(root, "feature"); |
| 104 |
await oxy.bookmarkSwitch(root, "feature"); |
| 105 |
writeFileSync(join(root, "a.txt"), "a\nfeature\n"); |
| 106 |
await oxy.commit(root, "feature work"); |
| 107 |
bookmarks = await oxy.bookmarks(root); |
| 108 |
assert.equal(bookmarks.find((b) => b.current)?.name, "feature"); |
| 109 |
await oxy.bookmarkSwitch(root, "main"); |
| 110 |
assert.equal((await oxy.cat(root, "a.txt")).toString(), "a\n"); |
| 111 |
writeFileSync(join(root, "b.txt"), "b\n"); |
| 112 |
await assert.rejects(oxy.bookmarkSwitch(root, "feature"), /has changes/); |
| 113 |
await oxy.commit(root, "b on main"); |
| 114 |
assert.match(await oxy.merge(root, "feature"), /Merged/); |
| 115 |
assert.equal((await oxy.cat(root, "a.txt")).toString(), "a\nfeature\n"); |
| 116 |
push = await oxy.push(root, "origin", "feature"); |
| 117 |
assert.equal(push[0].ok, true); |
| 118 |
push = await oxy.push(root); |
| 119 |
assert.equal(push[0].ok, true); |
| 120 |
const fetch = await oxy.fetch(root); |
| 121 |
assert.equal(fetch.remote, "origin"); |
| 122 |
const pull = await oxy.pull(root); |
| 123 |
assert.equal(pull.changed, false); |
| 124 |
await oxy.bookmarkDelete(root, "feature"); |
| 125 |
bookmarks = await oxy.bookmarks(root); |
| 126 |
assert.deepEqual(bookmarks.map((b) => b.name), ["main"]); |
| 127 |
|
| 128 |
// Blame, show, diff text, and file history. |
| 129 |
const blame = await oxy.blame(root, "a.txt"); |
| 130 |
assert.deepEqual(blame.map((l) => l.line), [1, 2]); |
| 131 |
assert.notEqual(blame[0].hash, blame[1].hash, "two changes introduced the two lines"); |
| 132 |
const show = await oxy.show(root, `#${blame[1].hash.slice(0, 12)}`); |
| 133 |
assert.equal(show.message, "feature work"); |
| 134 |
assert.deepEqual(show.files.map((f) => f.path), ["a.txt"]); |
| 135 |
assert.match(await oxy.diffText(root, `#${blame[1].hash.slice(0, 12)}`), /\+feature/); |
| 136 |
const history = await oxy.log(root, 50, "a.txt"); |
| 137 |
assert.equal(history.changes.filter((c) => !c.working_copy).length, 2); |
| 138 |
|
| 139 |
// Tags, workspaces, trust, bisect status, subrepos. |
| 140 |
await oxy.tagCreate(root, "v1", [], "first"); |
| 141 |
const tags = await oxy.tags(root); |
| 142 |
assert.deepEqual(tags.map((t) => [t.name, t.message]), [["v1", "first"]]); |
| 143 |
const workspaces = await oxy.workspaces(root); |
| 144 |
assert.equal(workspaces.length, 1); |
| 145 |
assert.equal(workspaces[0].current, true); |
| 146 |
assert.equal(await oxy.policyShow(root), null, "no policy yet"); |
| 147 |
const verify = await oxy.verify(root); |
| 148 |
assert.ok(verify.every((v) => v.status === "unsigned")); |
| 149 |
assert.equal(await oxy.bisectStatus(root), null); |
| 150 |
assert.deepEqual(await oxy.submodules(root), []); |
| 151 |
await oxy.bisectStart(root); |
| 152 |
assert.equal((await oxy.bisectStatus(root))?.state, "need-marks"); |
| 153 |
await oxy.bisectReset(root); |
| 154 |
}); |
| 155 |
|
| 156 |
|
| 157 |
test("split, squash and rebase through the history actions, each undoable", async () => { |
| 158 |
const dir = mkdtempSync(join(tmpdir(), "oxy-vscode-hist-")); |
| 159 |
const home = join(dir, "home"); |
| 160 |
mkdirSync(home); |
| 161 |
process.env.OXY_HOME = home; |
| 162 |
await oxy.check(dir, ["init", "repo"]); |
| 163 |
const root = join(dir, "repo"); |
| 164 |
await oxy.check(root, ["identity", "create", "--name", "Test", "--email", "[email protected]"]); |
| 165 |
writeFileSync(join(root, "a.txt"), "a\n"); |
| 166 |
await oxy.commit(root, "one"); |
| 167 |
writeFileSync(join(root, "b.txt"), "b\n"); |
| 168 |
writeFileSync(join(root, "c.txt"), "c\n"); |
| 169 |
await oxy.commit(root, "two files"); |
| 170 |
const find = async (m) => (await oxy.log(root, 50)).changes.find((c) => !c.working_copy && c.message.startsWith(m)); |
| 171 |
|
| 172 |
// Split: b.txt becomes its own change, c.txt stays. |
| 173 |
const two = await find("two files"); |
| 174 |
const out = await oxy.split(root, `#${two.hash.slice(0, 12)}`, ["b.txt"]); |
| 175 |
assert.match(out, /Split into/); |
| 176 |
const part = await find("two files (part 1"); |
| 177 |
assert.ok(part, "the first part exists"); |
| 178 |
assert.deepEqual((await oxy.show(root, `#${part.hash.slice(0, 12)}`)).files.map((f) => f.path), ["b.txt"]); |
| 179 |
await oxy.undo(root); |
| 180 |
assert.equal(await find("two files (part 1"), undefined, "undo reverses the split"); |
| 181 |
|
| 182 |
// Squash: an edit of a.txt folds into "one", which keeps its ChangeId. |
| 183 |
writeFileSync(join(root, "a.txt"), "a\na2\n"); |
| 184 |
await oxy.commit(root, "edit a"); |
| 185 |
const one = await find("one"); |
| 186 |
const edit = await find("edit a"); |
| 187 |
assert.deepEqual(edit.deps, [one.hash]); |
| 188 |
assert.match(await oxy.squash(root, `#${edit.hash.slice(0, 12)}`, `#${one.hash.slice(0, 12)}`), /Squashed into/); |
| 189 |
const after = await find("one"); |
| 190 |
assert.equal(after.change_id, one.change_id); |
| 191 |
assert.equal(await find("edit a"), undefined); |
| 192 |
|
| 193 |
// Rebase onto a state that already has the change is refused, with oxy's reason. |
| 194 |
await assert.rejects(oxy.rebase(root, `#${after.hash.slice(0, 12)}`, ["main"]), /already part of the target state/); |
| 195 |
}); |
| 196 |
|
| 197 |
test("identity reports how commits are signed", async () => { |
| 198 |
const dir = mkdtempSync(join(tmpdir(), "oxy-vscode-id-")); |
| 199 |
const home = join(dir, "home"); |
| 200 |
mkdirSync(home); |
| 201 |
process.env.OXY_HOME = home; |
| 202 |
await oxy.check(dir, ["init", "repo"]); |
| 203 |
const root = join(dir, "repo"); |
| 204 |
assert.equal(await oxy.identity(root), null, "none yet"); |
| 205 |
await oxy.check(root, ["identity", "create", "--name", "Test", "--email", "[email protected]"]); |
| 206 |
const id = await oxy.identity(root); |
| 207 |
assert.equal(id?.who, "Test <[email protected]>"); |
| 208 |
assert.equal(id?.secret, true); |
| 209 |
assert.ok(id?.mode === "change" || id?.mode === "seal"); |
| 210 |
}); |
| 211 |
|
| 212 |
|
| 213 |
// ---------------------------------------------------------------- oxy 0.5 to 0.9 (oxygen-vscode#11) |
| 214 |
|
| 215 |
test("oxy resolve --both's report is read", () => { |
| 216 |
const text = [ |
| 217 |
"kept both CHANGELOG.md: conflict 1, 3", |
| 218 |
"markers CHANGELOG.md: conflict 2: both sides changed these lines; edit the markers", |
| 219 |
"resolved CHANGELOG.md", |
| 220 |
"markers src/a.rs", |
| 221 |
"All conflicts are resolved; `oxy commit` will record the resolution.", |
| 222 |
].join("\n"); |
| 223 |
assert.deepEqual(parseKeptBoth(text), { |
| 224 |
kept: [{ path: "CHANGELOG.md", conflicts: [1, 3] }], |
| 225 |
left: [{ path: "CHANGELOG.md", conflict: 2, why: "both sides changed these lines; edit the markers" }], |
| 226 |
}); |
| 227 |
assert.deepEqual(parseKeptBoth(""), { kept: [], left: [] }); |
| 228 |
}); |
| 229 |
|
| 230 |
test("Merge Bookmark offers the other local bookmarks, then remotes' bookmarks", () => { |
| 231 |
const list = [ |
| 232 |
{ name: "main", heads: ["a"], current: true, ahead: 0, behind: 0, remote: "origin" }, |
| 233 |
{ name: "feature/x", heads: ["b"], current: false, ahead: null, behind: null, remote: null }, |
| 234 |
{ name: "origin/develop", heads: ["c", "d"], current: false, ahead: null, behind: null, remote: "origin" }, |
| 235 |
]; |
| 236 |
assert.deepEqual(list.map(isRemoteBookmark), [false, false, true]); |
| 237 |
assert.deepEqual(mergeChoices(list), [ |
| 238 |
{ name: "feature/x", remote: null, heads: 1 }, |
| 239 |
{ name: "origin/develop", remote: "origin", heads: 2 }, |
| 240 |
]); |
| 241 |
}); |
| 242 |
|
| 243 |
test("tags are withdrawn, signed and never reused", async () => { |
| 244 |
const dir = mkdtempSync(join(tmpdir(), "oxy-vscode-tag-")); |
| 245 |
const home = join(dir, "home"); |
| 246 |
mkdirSync(home); |
| 247 |
process.env.OXY_HOME = home; |
| 248 |
await oxy.check(dir, ["init", "repo"]); |
| 249 |
const root = join(dir, "repo"); |
| 250 |
await oxy.check(root, ["identity", "create", "--name", "Test", "--email", "[email protected]"]); |
| 251 |
writeFileSync(join(root, "a.txt"), "a\n"); |
| 252 |
await oxy.commit(root, "one"); |
| 253 |
await oxy.tagCreate(root, "v1", [], "first"); |
| 254 |
assert.match(await oxy.tagWithdraw(root, "v1", "built from the wrong state"), /Withdrew v1/); |
| 255 |
assert.deepEqual(await oxy.tags(root), [], "a withdrawn tag is not listed"); |
| 256 |
await assert.rejects(oxy.tagCreate(root, "v1", [], "again"), /withdrawn/); |
| 257 |
// No remote, so no hub whose transparency log to check: oxy says so. |
| 258 |
await assert.rejects(oxy.tlogVerify(root), /hub/); |
| 259 |
}); |
| 260 |
|
| 261 |
test("shallow clone, remote bookmarks merged, amend and commit by paths", async () => { |
| 262 |
const dir = mkdtempSync(join(tmpdir(), "oxy-vscode-paths-")); |
| 263 |
const home = join(dir, "home"); |
| 264 |
mkdirSync(home); |
| 265 |
process.env.OXY_HOME = home; |
| 266 |
await oxy.check(dir, ["init", "origin"]); |
| 267 |
const origin = join(dir, "origin"); |
| 268 |
await oxy.check(origin, ["identity", "create", "--name", "Test", "--email", "[email protected]"]); |
| 269 |
writeFileSync(join(origin, "a.txt"), "a\n"); |
| 270 |
await oxy.commit(origin, "one"); |
| 271 |
await oxy.clone(dir, origin, join(dir, "work"), 0); |
| 272 |
const root = join(dir, "work"); |
| 273 |
assert.equal(readFileSync(join(root, "a.txt"), "utf8"), "a\n", "the shallow clone has the latest state"); |
| 274 |
|
| 275 |
// Amend only one path: the other stays uncommitted work. |
| 276 |
writeFileSync(join(root, "a.txt"), "a\nmore\n"); |
| 277 |
await oxy.commit(root, "more"); |
| 278 |
writeFileSync(join(root, "a.txt"), "a\nmore\nagain\n"); |
| 279 |
writeFileSync(join(root, "c.txt"), "c\n"); |
| 280 |
await oxy.amend(root, undefined, ["a.txt"]); |
| 281 |
const status = await oxy.status(root); |
| 282 |
assert.deepEqual(status.paths.map((p) => p.path), ["c.txt"], "c.txt was not folded in"); |
| 283 |
assert.equal((await oxy.cat(root, "a.txt")).toString(), "a\nmore\nagain\n"); |
| 284 |
rmSync(join(root, "c.txt")); |
| 285 |
|
| 286 |
// A bookmark made on the origin is merged here as origin/feat, without a local bookmark. |
| 287 |
await oxy.bookmarkCreate(origin, "feat"); |
| 288 |
await oxy.bookmarkSwitch(origin, "feat"); |
| 289 |
writeFileSync(join(origin, "f.txt"), "f\n"); |
| 290 |
await oxy.commit(origin, "feat work"); |
| 291 |
await oxy.fetch(root); |
| 292 |
const all = await oxy.bookmarks(root, true); |
| 293 |
assert.deepEqual(mergeChoices(all).map((c) => [c.name, c.remote]), [["origin/feat", "origin"]]); |
| 294 |
assert.deepEqual((await oxy.bookmarks(root)).map((b) => b.name), ["main"], "without --all, local bookmarks only"); |
| 295 |
assert.match(await oxy.merge(root, "origin/feat"), /Merged/); |
| 296 |
assert.equal(readFileSync(join(root, "f.txt"), "utf8"), "f\n"); |
| 297 |
}); |
| 298 |
|
| 299 |
/** A repository whose main and other bookmarks changed `t.txt` (inserting after the same line) and `bin.dat` two ways, merged. */ |
| 300 |
async function conflicted(prefix, text) { |
| 301 |
const dir = mkdtempSync(join(tmpdir(), prefix)); |
| 302 |
const home = join(dir, "home"); |
| 303 |
mkdirSync(home); |
| 304 |
process.env.OXY_HOME = home; |
| 305 |
await oxy.check(dir, ["init", "repo"]); |
| 306 |
const root = join(dir, "repo"); |
| 307 |
await oxy.check(root, ["identity", "create", "--name", "Test", "--email", "[email protected]"]); |
| 308 |
writeFileSync(join(root, "t.txt"), "a\nb\n"); |
| 309 |
writeFileSync(join(root, "u.txt"), "a\nb\n"); |
| 310 |
writeFileSync(join(root, "bin.dat"), Buffer.from([0, 1, 2, 3])); |
| 311 |
await oxy.commit(root, "base"); |
| 312 |
await oxy.bookmarkCreate(root, "other"); |
| 313 |
writeFileSync(join(root, "t.txt"), text("X")); |
| 314 |
writeFileSync(join(root, "u.txt"), "a\nX\n"); |
| 315 |
writeFileSync(join(root, "bin.dat"), Buffer.from([0, 1, 2, 4])); |
| 316 |
await oxy.commit(root, "main side"); |
| 317 |
await oxy.bookmarkSwitch(root, "other"); |
| 318 |
writeFileSync(join(root, "t.txt"), text("Y")); |
| 319 |
writeFileSync(join(root, "u.txt"), "a\nY\n"); |
| 320 |
writeFileSync(join(root, "bin.dat"), Buffer.from([0, 1, 2, 5])); |
| 321 |
await oxy.commit(root, "other side"); |
| 322 |
await oxy.bookmarkSwitch(root, "main"); |
| 323 |
await oxy.merge(root, "other"); |
| 324 |
return root; |
| 325 |
} |
| 326 |
|
| 327 |
test("a binary file's conflict is settled by keeping one side, then its resolution committed by path", async () => { |
| 328 |
const root = await conflicted("oxy-vscode-keep-", (s) => `a\n${s}\n`); |
| 329 |
let status = await oxy.status(root); |
| 330 |
const bin = status.conflicts.find((c) => c.path === "bin.dat"); |
| 331 |
assert.equal(bin.kind, "replace"); |
| 332 |
assert.equal(bin.changes.length, 2); |
| 333 |
// Keep side 2 as Keep a Side does: its version in place, candidates gone, oxy resolve PATH. |
| 334 |
const side = await oxy.cat(root, "bin.dat", bin.changes[1]); |
| 335 |
writeFileSync(join(root, "bin.dat"), side); |
| 336 |
for (const c of readdirSync(root).filter((e) => e.startsWith("bin.dat.oxy-conflict-"))) { |
| 337 |
rmSync(join(root, c)); |
| 338 |
} |
| 339 |
status = await oxy.resolvePaths(root, ["bin.dat"]); |
| 340 |
assert.equal(status.conflicts.find((c) => c.path === "bin.dat").resolved, true); |
| 341 |
assert.ok(status.conflicts.some((c) => c.path !== "bin.dat" && c.resolved !== true), "the text conflicts remain"); |
| 342 |
await assert.rejects(oxy.resolvePaths(root, ["u.txt"]), /no replace conflict/); |
| 343 |
|
| 344 |
// Commit Resolution of Selected Paths: only bin.dat; the text conflicts stay for later. |
| 345 |
await oxy.commit(root, "keep the other logo", ["bin.dat"]); |
| 346 |
status = await oxy.status(root); |
| 347 |
assert.equal(status.conflicts.some((c) => c.path === "bin.dat"), false); |
| 348 |
assert.deepEqual(status.conflicts.map((c) => c.path).sort(), ["t.txt", "u.txt"]); |
| 349 |
assert.deepEqual([...(await oxy.cat(root, "bin.dat"))], [...side]); |
| 350 |
}); |
| 351 |
|
| 352 |
test("two changes adding lines at the same place are settled with one command", { skip: !atLeast("0.8.0") && `oxy ${oxyVersion} has no resolve --both` }, async () => { |
| 353 |
const root = await conflicted("oxy-vscode-both-", (s) => `a\n${s}\nb\n`); |
| 354 |
const result = await oxy.resolveBoth(root, ["t.txt", "u.txt"]); |
| 355 |
assert.deepEqual(result.kept, [{ path: "t.txt", conflicts: [1] }]); |
| 356 |
assert.deepEqual(result.left.map((l) => [l.path, l.conflict]), [["u.txt", 1]], "u.txt's sides changed the same line"); |
| 357 |
assert.match(result.left[0].why, /same lines|edit the markers/); |
| 358 |
assert.equal(readFileSync(join(root, "t.txt"), "utf8"), "a\nX\nY\nb\n", "earliest change first"); |
| 359 |
const status = await oxy.status(root); |
| 360 |
assert.equal(status.conflicts.find((c) => c.path === "t.txt").resolved, true); |
| 361 |
}); |