History graph: split, squash and rebase from the change picker or a right-click menu, confirmed first and undoable; 0.8.0 b4626290e660
closes #1
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-28 16:03 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
b4626290e6607a36264acd6feffe828fa0b0a727b1abc189f904b8cafed86d4a- Depends on
- 35d56b5803d9 9020cf612749 9627171e23e2 ab81294a9ba9 bed495a96408 c8f955891224 d2c9a5843004 f6bda77ecb9f
- Depended on by
- dac6876e4d08 013fefef11e3 cdcffe39dd9f 1f7fc1e3ae4a 44bfd5adfc9a
- In bookmarks
- main
- In tags
- v0.16.0 v0.16.1 v0.17.0 v0.17.1 v0.18.0
- Edits
- 20 atoms, 0 tree operations, 7 files
Browse the tree at this change
modified CHANGELOG.md +8 −0
| 1 | 1 | # Changelog |
|
| 2 | 2 | ||
| 3 | + | ## 0.8.0 |
|
| 4 | + | ||
| 5 | + | - The History graph rewrites history: |
|
| 6 | + | - Split a head: the files you pick become a separate first change. |
|
| 7 | + | - Squash a change into one of its dependencies. |
|
| 8 | + | - Rebase a change onto a bookmark, change or tag. |
|
| 9 | + | - Use them from the change picker or by right-clicking a row. Each asks first, and afterwards offers Undo. |
|
| 10 | + | ||
| 3 | 11 | ## 0.7.1 |
|
| 4 | 12 | ||
| 5 | 13 | - Messages write the forge's name as OxygenHub. |
|
| … | 41 unchanged lines |
modified README.md +2 −1
| … | 39 unchanged lines |
||
| 40 | 40 | - **Views**: Oxygen Operations (undo to any point), Oxygen Bisect |
|
| 41 | 41 | (start, good, bad, skip, run, reset), Oxygen Trust (policy, signatures), |
|
| 42 | 42 | and a History graph with per-change actions (diff, check out, describe, |
|
| 43 | - | tag, abandon). |
|
| 43 | + | tag, abandon, and split, squash and rebase from the picker or by |
|
| 44 | + | right-clicking a row, each confirmed and undoable). |
|
| 44 | 45 | - **Commands**: Show History, File History, Tags, Create Tag, Add Workspace, |
|
| 45 | 46 | Update Subrepos, Verify Signatures, Clone Repository, Initialize Repository, |
|
| 46 | 47 | Show Output. |
|
| … | 10 unchanged lines |
modified package.json +48 −1
| … | 1 unchanged lines |
||
| 2 | 2 | "name": "oxygen-vscode", |
|
| 3 | 3 | "displayName": "Oxygen", |
|
| 4 | 4 | "description": "Oxygen version control: source control view, diffs, decorations, bookmarks and history for oxy repositories", |
|
| 5 | - | "version": "0.7.1", |
|
| 5 | + | "version": "0.8.0", |
|
| 6 | 6 | "publisher": "oxygen", |
|
| 7 | 7 | "license": "MIT OR Apache-2.0", |
|
| 8 | 8 | "icon": "assets/images/oxygen.png", |
|
| … | 283 unchanged lines |
||
| 292 | 292 | "command": "oxygen.openChangeOnRemote", |
|
| 293 | 293 | "title": "Open Change on Remote", |
|
| 294 | 294 | "category": "Oxygen" |
|
| 295 | + | }, |
|
| 296 | + | { |
|
| 297 | + | "command": "oxygen.history.split", |
|
| 298 | + | "title": "Split\u2026", |
|
| 299 | + | "category": "Oxygen", |
|
| 300 | + | "icon": "$(split-horizontal)" |
|
| 301 | + | }, |
|
| 302 | + | { |
|
| 303 | + | "command": "oxygen.history.squash", |
|
| 304 | + | "title": "Squash into a Dependency\u2026", |
|
| 305 | + | "category": "Oxygen", |
|
| 306 | + | "icon": "$(fold)" |
|
| 307 | + | }, |
|
| 308 | + | { |
|
| 309 | + | "command": "oxygen.history.rebase", |
|
| 310 | + | "title": "Rebase onto\u2026", |
|
| 311 | + | "category": "Oxygen", |
|
| 312 | + | "icon": "$(git-pull-request-go-to-changes)" |
|
| 295 | 313 | } |
|
| 296 | 314 | ], |
|
| 297 | 315 | "views": { |
|
| … | 314 unchanged lines |
||
| 612 | 630 | }, |
|
| 613 | 631 | { |
|
| 614 | 632 | "command": "oxygen.refreshTrust", |
|
| 633 | + | "when": "false" |
|
| 634 | + | }, |
|
| 635 | + | { |
|
| 636 | + | "command": "oxygen.history.split", |
|
| 615 | 637 | "when": "false" |
|
| 638 | + | }, |
|
| 639 | + | { |
|
| 640 | + | "command": "oxygen.history.squash", |
|
| 641 | + | "when": "false" |
|
| 642 | + | }, |
|
| 643 | + | { |
|
| 644 | + | "command": "oxygen.history.rebase", |
|
| 645 | + | "when": "false" |
|
| 616 | 646 | } |
|
| 617 | 647 | ], |
|
| 618 | 648 | "editor/title/context": [ |
|
| … | 1 unchanged lines |
||
| 620 | 650 | "command": "oxygen.openOnRemote", |
|
| 621 | 651 | "when": "oxygen.hasRemoteProvider", |
|
| 622 | 652 | "group": "oxygen" |
|
| 653 | + | } |
|
| 654 | + | ], |
|
| 655 | + | "webview/context": [ |
|
| 656 | + | { |
|
| 657 | + | "command": "oxygen.history.split", |
|
| 658 | + | "when": "webviewId == 'oxygen.history' && webviewSection == 'change' && oxygenHead", |
|
| 659 | + | "group": "oxygen@1" |
|
| 660 | + | }, |
|
| 661 | + | { |
|
| 662 | + | "command": "oxygen.history.squash", |
|
| 663 | + | "when": "webviewId == 'oxygen.history' && webviewSection == 'change' && oxygenHasDeps", |
|
| 664 | + | "group": "oxygen@2" |
|
| 665 | + | }, |
|
| 666 | + | { |
|
| 667 | + | "command": "oxygen.history.rebase", |
|
| 668 | + | "when": "webviewId == 'oxygen.history' && webviewSection == 'change'", |
|
| 669 | + | "group": "oxygen@3" |
|
| 623 | 670 | } |
|
| 624 | 671 | ] |
|
| 625 | 672 | }, |
|
| … | 58 unchanged lines |
modified src/extension.ts +124 −1
| … | 3 unchanged lines |
||
| 4 | 4 | import * as vscode from "vscode"; |
|
| 5 | 5 | import { OxygenAPI, RemoteProvider, RemoteTarget, RepositoryHandle, pickProvider, repositoryFor as rootFor } from "./api"; |
|
| 6 | 6 | import { basename } from "node:path"; |
|
| 7 | - | import { Oxy, OxyError, findBinary } from "./oxy"; |
|
| 7 | + | import { Oxy, OxyError, ShowInfo, findBinary } from "./oxy"; |
|
| 8 | 8 | import { Repository, OxyResourceState, SubrepoState } from "./repository"; |
|
| 9 | 9 | import { BisectProvider, TrustProvider } from "./panels"; |
|
| 10 | 10 | import { OxyContentProvider, SCHEME, baseUri, oxyUri } from "./contentProvider"; |
|
| … | 67 unchanged lines |
||
| 78 | 78 | vscode.commands.registerCommand("oxygen.commitSelected", (state?: OxyResourceState, states?: OxyResourceState[]) => commitSelected(states?.length ? states : state ? [state] : [])), |
|
| 79 | 79 | vscode.commands.registerCommand("oxygen.commitHunks", (arg?: Repository | vscode.SourceControl) => commitHunks(arg)), |
|
| 80 | 80 | vscode.commands.registerCommand("oxygen.amend", (arg?: Repository | vscode.SourceControl) => amend(arg)), |
|
| 81 | + | vscode.commands.registerCommand("oxygen.history.split", (ctx?: { root?: string; hash?: string }) => historyContext(ctx, "split")), |
|
| 82 | + | vscode.commands.registerCommand("oxygen.history.squash", (ctx?: { root?: string; hash?: string }) => historyContext(ctx, "squash")), |
|
| 83 | + | vscode.commands.registerCommand("oxygen.history.rebase", (ctx?: { root?: string; hash?: string }) => historyContext(ctx, "rebase")), |
|
| 81 | 84 | vscode.commands.registerCommand("oxygen.undo", (arg?: Repository | vscode.SourceControl) => undo(arg, 1)), |
|
| 82 | 85 | vscode.commands.registerCommand("oxygen.undoTo", (item?: OperationItem) => item && undo(item.repository, item.stepsBack)), |
|
| 83 | 86 | vscode.commands.registerCommand("oxygen.refreshOperations", () => operations.refresh()), |
|
| … | 613 unchanged lines |
||
| 697 | 700 | { label: "$(edit) Describe…", description: "change the message", action: "describe" }, |
|
| 698 | 701 | { label: "$(tag) Tag…", action: "tag" }, |
|
| 699 | 702 | { label: "$(trash) Abandon", description: "drop the change; its content stays on disk if the working copy sat on it", action: "abandon" }, |
|
| 703 | + | ...historyItems(info), |
|
| 700 | 704 | { label: "$(copy) Copy ChangeId", action: "copy" }, |
|
| 701 | 705 | ...info.files.map((f) => ({ label: `$(file) ${f.path}`, description: `${f.kind}${f.from ? ` from ${f.from}` : ""}`, action: "file", path: f.path })), |
|
| 702 | 706 | ]; |
|
| … | 45 unchanged lines |
||
| 748 | 752 | } |
|
| 749 | 753 | case "copy": |
|
| 750 | 754 | await vscode.env.clipboard.writeText(info.change_id); |
|
| 755 | + | return; |
|
| 756 | + | case "split": |
|
| 757 | + | case "squash": |
|
| 758 | + | case "rebase": |
|
| 759 | + | await historyAction(repo, info, pick.action); |
|
| 751 | 760 | return; |
|
| 752 | 761 | } |
|
| 753 | 762 | await repo.refresh(); |
|
| … | 10 unchanged lines |
||
| 764 | 773 | } |
|
| 765 | 774 | return; |
|
| 766 | 775 | } |
|
| 776 | + | showError(e); |
|
| 777 | + | } |
|
| 778 | + | } |
|
| 779 | + | ||
| 780 | + | // ------------------------------------------------ history rewriting (V6) |
|
| 781 | + | ||
| 782 | + | type HistoryAction = "split" | "squash" | "rebase"; |
|
| 783 | + | ||
| 784 | + | /** The rewriting actions a change offers: split for a head with several files, squash when it has dependencies, rebase always. */ |
|
| 785 | + | function historyItems(info: ShowInfo): (vscode.QuickPickItem & { action: string })[] { |
|
| 786 | + | const items: (vscode.QuickPickItem & { action: string })[] = []; |
|
| 787 | + | if (info.head && info.files.length > 1) { |
|
| 788 | + | items.push({ label: "$(split-horizontal) Split…", description: "chosen files become a separate first change", action: "split" }); |
|
| 789 | + | } |
|
| 790 | + | if (info.deps.length > 0) { |
|
| 791 | + | items.push({ label: "$(fold) Squash into a dependency…", description: "fold this change into one it depends on", action: "squash" }); |
|
| 792 | + | } |
|
| 793 | + | items.push({ label: "$(git-pull-request-go-to-changes) Rebase onto…", description: "re-express against another state", action: "rebase" }); |
|
| 794 | + | return items; |
|
| 795 | + | } |
|
| 796 | + | ||
| 797 | + | /** Run `action` on a change: choose, confirm, run, then offer Undo (oxy undo reverses every rewrite). */ |
|
| 798 | + | async function historyAction(repo: Repository, info: ShowInfo, action: HistoryAction): Promise<void> { |
|
| 799 | + | const change = `#${info.hash.slice(0, 12)}`; |
|
| 800 | + | const subject = info.message.split("\n")[0] || "(no description)"; |
|
| 801 | + | let run: () => Promise<string>; |
|
| 802 | + | let question: string; |
|
| 803 | + | if (action === "split") { |
|
| 804 | + | if (!info.head) { |
|
| 805 | + | void vscode.window.showInformationMessage("Oxygen: only a head can be split; nothing may depend on it yet."); |
|
| 806 | + | return; |
|
| 807 | + | } |
|
| 808 | + | const picked = await vscode.window.showQuickPick( |
|
| 809 | + | info.files.map((f) => ({ label: f.path, description: f.kind, picked: false })), |
|
| 810 | + | { title: `Split ${info.change_id.slice(0, 8)}: the files you choose become the first change`, canPickMany: true }, |
|
| 811 | + | ); |
|
| 812 | + | if (!picked || picked.length === 0) { |
|
| 813 | + | return; |
|
| 814 | + | } |
|
| 815 | + | if (picked.length === info.files.length) { |
|
| 816 | + | void vscode.window.showInformationMessage("Oxygen: leave at least one file for the second change."); |
|
| 817 | + | return; |
|
| 818 | + | } |
|
| 819 | + | const paths = picked.map((p) => p.label); |
|
| 820 | + | question = `Split "${subject}"? ${paths.length} file${paths.length === 1 ? "" : "s"} (${paths.join(", ")}) become a separate change; the rest stay.`; |
|
| 821 | + | run = () => repo.oxy.split(repo.root, change, paths); |
|
| 822 | + | } else if (action === "squash") { |
|
| 823 | + | if (info.deps.length === 0) { |
|
| 824 | + | void vscode.window.showInformationMessage("Oxygen: this change has no dependency to fold into."); |
|
| 825 | + | return; |
|
| 826 | + | } |
|
| 827 | + | const log = await repo.oxy.log(repo.root, 500); |
|
| 828 | + | const byHash = new Map(log.changes.map((c) => [c.hash, c])); |
|
| 829 | + | const choices = info.deps.map((d) => { |
|
| 830 | + | const c = byHash.get(d); |
|
| 831 | + | return { label: c ? c.message.split("\n")[0] || "(no description)" : d.slice(0, 12), description: c ? c.change_id.slice(0, 8) : "", hash: d }; |
|
| 832 | + | }); |
|
| 833 | + | const into = choices.length === 1 ? choices[0] : await vscode.window.showQuickPick(choices, { title: `Squash ${info.change_id.slice(0, 8)} into…` }); |
|
| 834 | + | if (!into) { |
|
| 835 | + | return; |
|
| 836 | + | } |
|
| 837 | + | question = `Squash "${subject}" into "${into.label}"? The dependency keeps its ChangeId and absorbs these edits.`; |
|
| 838 | + | run = () => repo.oxy.squash(repo.root, change, `#${into.hash.slice(0, 12)}`); |
|
| 839 | + | } else { |
|
| 840 | + | const bookmarks = await repo.oxy.bookmarks(repo.root); |
|
| 841 | + | type Onto = vscode.QuickPickItem & { onto?: string }; |
|
| 842 | + | const choices: Onto[] = [ |
|
| 843 | + | ...bookmarks.map((b) => ({ label: `$(bookmark) ${b.name}`, description: b.current ? "current bookmark" : "", onto: b.name })), |
|
| 844 | + | { label: "$(edit) Another change, bookmark or tag…" }, |
|
| 845 | + | ]; |
|
| 846 | + | const pick = await vscode.window.showQuickPick(choices, { title: `Rebase ${info.change_id.slice(0, 8)} onto…` }); |
|
| 847 | + | if (!pick) { |
|
| 848 | + | return; |
|
| 849 | + | } |
|
| 850 | + | let onto = pick.onto; |
|
| 851 | + | if (!onto) { |
|
| 852 | + | onto = (await vscode.window.showInputBox({ prompt: "ChangeId prefix, #hash prefix, bookmark or tag" }))?.trim(); |
|
| 853 | + | if (!onto) { |
|
| 854 | + | return; |
|
| 855 | + | } |
|
| 856 | + | } |
|
| 857 | + | const target = onto; |
|
| 858 | + | question = `Rebase "${subject}" onto ${target}? The change is re-expressed against that state; its ChangeId stays.`; |
|
| 859 | + | run = () => repo.oxy.rebase(repo.root, change, [target]); |
|
| 860 | + | } |
|
| 861 | + | const verb = action[0].toUpperCase() + action.slice(1); |
|
| 862 | + | if ((await vscode.window.showWarningMessage(question, { modal: true, detail: "Undo reverses it (Oxygen: Undo, or oxy undo)." }, verb)) !== verb) { |
|
| 863 | + | return; |
|
| 864 | + | } |
|
| 865 | + | try { |
|
| 866 | + | const out = await run(); |
|
| 867 | + | await repo.refresh(); |
|
| 868 | + | HistoryPanel.refresh(repo.root); |
|
| 869 | + | const undo = await vscode.window.showInformationMessage(`Oxygen: ${out.split("\n")[0].replace(/^\* /, "")}`, "Undo"); |
|
| 870 | + | if (undo === "Undo") { |
|
| 871 | + | await repo.oxy.undo(repo.root); |
|
| 872 | + | await repo.refresh(); |
|
| 873 | + | HistoryPanel.refresh(repo.root); |
|
| 874 | + | } |
|
| 875 | + | } catch (e) { |
|
| 876 | + | showError(e); |
|
| 877 | + | } |
|
| 878 | + | } |
|
| 879 | + | ||
| 880 | + | /** A history-graph context menu command: the webview passes `{ root, hash }`. */ |
|
| 881 | + | async function historyContext(ctx: { root?: string; hash?: string } | undefined, action: HistoryAction): Promise<void> { |
|
| 882 | + | const repo = ctx?.root ? repositories.get(ctx.root) : undefined; |
|
| 883 | + | if (!repo || !ctx?.hash) { |
|
| 884 | + | return; |
|
| 885 | + | } |
|
| 886 | + | try { |
|
| 887 | + | const info = await repo.oxy.show(repo.root, `#${ctx.hash.slice(0, 12)}`); |
|
| 888 | + | await historyAction(repo, info, action); |
|
| 889 | + | } catch (e) { |
|
| 767 | 890 | showError(e); |
|
| 768 | 891 | } |
|
| 769 | 892 | } |
|
| … | 355 unchanged lines |
modified src/history.ts +4 −2
| … | 126 unchanged lines |
||
| 127 | 127 | .map((c, i) => { |
|
| 128 | 128 | const subject = c.message.split("\n")[0] || (c.working_copy ? "(working copy)" : "(no description)"); |
|
| 129 | 129 | const sig = c.working_copy ? "" : c.signature?.status === "signed" || c.signature?.status === "sealed" ? (c.signature.revoked ? "⚠" : "✔") : c.signature?.status === "invalid" ? "✖" : "·"; |
|
| 130 | - | return `<div class="row${c.working_copy ? " wc" : ""}" style="top:${i * rowH}px" data-hash="${c.hash}" title="${escape(c.message)}"> |
|
| 130 | + | // Right-click: split, squash and rebase (webview/context menus in package.json). |
|
| 131 | + | const ctx = c.working_copy ? "" : ` data-vscode-context='${escape(JSON.stringify({ webviewSection: "change", root: repo.root, hash: c.hash, oxygenHead: c.head, oxygenHasDeps: c.deps.length > 0, preventDefaultContextMenuItems: true }))}'`; |
|
| 132 | + | return `<div class="row${c.working_copy ? " wc" : ""}" style="top:${i * rowH}px" data-hash="${c.hash}" title="${escape(c.message)}"${ctx}> |
|
| 131 | 133 | <span class="id">${c.change_id.slice(0, 8)}</span> |
|
| 132 | 134 | <span class="subject">${escape(subject)}</span> |
|
| 133 | 135 | <span class="meta">${escape(c.author)} · ${timeAgo(c.when_ms)} <span class="sig" title="${escape(c.signature?.status ?? "")}">${sig}</span></span> |
|
| … | 36 unchanged lines |
||
| 170 | 172 | } |
|
| 171 | 173 | ||
| 172 | 174 | function escape(s: string): string { |
|
| 173 | - | return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); |
|
| 175 | + | return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); |
|
| 174 | 176 | } |
|
| 175 | 177 | ||
| 176 | 178 | export function repositoryLabel(repo: Repository): string { |
|
| … | 2 unchanged lines |
modified src/oxy.ts +15 −0
| … | 246 unchanged lines |
||
| 247 | 247 | return this.check(root, ["abandon", change]).then((r) => r.stdout.toString("utf8").trim()); |
|
| 248 | 248 | } |
|
| 249 | 249 | ||
| 250 | + | /** Split a head: the edits to `paths` become a separate first change. Returns oxy's summary line. */ |
|
| 251 | + | split(root: string, change: string, paths: string[]): Promise<string> { |
|
| 252 | + | return this.check(root, ["split", change, ...paths]).then((r) => r.stdout.toString("utf8").trim()); |
|
| 253 | + | } |
|
| 254 | + | ||
| 255 | + | /** Fold `change` into its dependency `into`, which keeps its ChangeId. */ |
|
| 256 | + | squash(root: string, change: string, into: string): Promise<string> { |
|
| 257 | + | return this.check(root, ["squash", change, "--into", into]).then((r) => r.stdout.toString("utf8").trim()); |
|
| 258 | + | } |
|
| 259 | + | ||
| 260 | + | /** Re-express `change` against the state of `onto` (changes, a bookmark or a tag). */ |
|
| 261 | + | rebase(root: string, change: string, onto: string[]): Promise<string> { |
|
| 262 | + | return this.check(root, ["rebase", change, "--onto", ...onto]).then((r) => r.stdout.toString("utf8").trim()); |
|
| 263 | + | } |
|
| 264 | + | ||
| 250 | 265 | checkout(root: string, changes: string[], discard = false): Promise<void> { |
|
| 251 | 266 | return this.check(root, ["checkout", ...changes, ...(discard ? ["--discard"] : [])]).then(() => undefined); |
|
| 252 | 267 | } |
|
| … | 225 unchanged lines |
modified test/oxy.test.mjs +40 −0
| … | 140 unchanged lines |
||
| 141 | 141 | await oxy.bisectReset(root); |
|
| 142 | 142 | }); |
|
| 143 | 143 | ||
| 144 | + | ||
| 145 | + | test("split, squash and rebase through the history actions, each undoable", async () => { |
|
| 146 | + | const dir = mkdtempSync(join(tmpdir(), "oxy-vscode-hist-")); |
|
| 147 | + | const home = join(dir, "home"); |
|
| 148 | + | mkdirSync(home); |
|
| 149 | + | process.env.OXY_HOME = home; |
|
| 150 | + | await oxy.check(dir, ["init", "repo"]); |
|
| 151 | + | const root = join(dir, "repo"); |
|
| 152 | + | await oxy.check(root, ["identity", "create", "--name", "Test", "--email", "[email protected]"]); |
|
| 153 | + | writeFileSync(join(root, "a.txt"), "a\n"); |
|
| 154 | + | await oxy.commit(root, "one"); |
|
| 155 | + | writeFileSync(join(root, "b.txt"), "b\n"); |
|
| 156 | + | writeFileSync(join(root, "c.txt"), "c\n"); |
|
| 157 | + | await oxy.commit(root, "two files"); |
|
| 158 | + | const find = async (m) => (await oxy.log(root, 50)).changes.find((c) => !c.working_copy && c.message.startsWith(m)); |
|
| 159 | + | ||
| 160 | + | // Split: b.txt becomes its own change, c.txt stays. |
|
| 161 | + | const two = await find("two files"); |
|
| 162 | + | const out = await oxy.split(root, `#${two.hash.slice(0, 12)}`, ["b.txt"]); |
|
| 163 | + | assert.match(out, /Split into/); |
|
| 164 | + | const part = await find("two files (part 1"); |
|
| 165 | + | assert.ok(part, "the first part exists"); |
|
| 166 | + | assert.deepEqual((await oxy.show(root, `#${part.hash.slice(0, 12)}`)).files.map((f) => f.path), ["b.txt"]); |
|
| 167 | + | await oxy.undo(root); |
|
| 168 | + | assert.equal(await find("two files (part 1"), undefined, "undo reverses the split"); |
|
| 169 | + | ||
| 170 | + | // Squash: an edit of a.txt folds into "one", which keeps its ChangeId. |
|
| 171 | + | writeFileSync(join(root, "a.txt"), "a\na2\n"); |
|
| 172 | + | await oxy.commit(root, "edit a"); |
|
| 173 | + | const one = await find("one"); |
|
| 174 | + | const edit = await find("edit a"); |
|
| 175 | + | assert.deepEqual(edit.deps, [one.hash]); |
|
| 176 | + | assert.match(await oxy.squash(root, `#${edit.hash.slice(0, 12)}`, `#${one.hash.slice(0, 12)}`), /Squashed into/); |
|
| 177 | + | const after = await find("one"); |
|
| 178 | + | assert.equal(after.change_id, one.change_id); |
|
| 179 | + | assert.equal(await find("edit a"), undefined); |
|
| 180 | + | ||
| 181 | + | // Rebase onto a state that already has the change is refused, with oxy's reason. |
|
| 182 | + | await assert.rejects(oxy.rebase(root, `#${after.hash.slice(0, 12)}`, ["main"]), /already part of the target state/); |
|
| 183 | + | }); |