addons/isl/src/stackEdit/__tests__/commitStackState.test.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {RepoPath} from 'shared/types/common';
b69ab319import type {ExportCommit, ExportFile, ExportStack} from 'shared/types/stack';
b69ab3110import type {CommitRev, FileRev} from '../commitStackState';
b69ab3111
b69ab3112import {Map as ImMap, Set as ImSet, List} from 'immutable';
b69ab3113import {nullthrows} from 'shared/utils';
b69ab3114import {WDIR_NODE} from '../../dag/virtualCommit';
b69ab3115import {
b69ab3116 ABSENT_FILE,
b69ab3117 CommitIdx,
b69ab3118 CommitStackState,
b69ab3119 CommitState,
b69ab3120 FileIdx,
b69ab3121 FileState,
b69ab3122} from '../commitStackState';
b69ab3123import {FileStackState} from '../fileStackState';
b69ab3124import {describeAbsorbIdChunkMap} from './absorb.test';
b69ab3125
b69ab3126export const exportCommitDefault: ExportCommit = {
b69ab3127 requested: true,
b69ab3128 immutable: false,
b69ab3129 author: 'test <test@example.com>',
b69ab3130 date: [0, 0],
b69ab3131 node: '',
b69ab3132 text: '',
b69ab3133};
b69ab3134
b69ab3135// In this test we tend to use uppercase for commits (ex. A, B, C),
b69ab3136// and lowercase for files (ex. x, y, z).
b69ab3137
b69ab3138/**
b69ab3139 * Created by `drawdag --no-files`:
b69ab3140 *
b69ab3141 * C # C/z.txt=(removed)
b69ab3142 * |
b69ab3143 * B # B/y.txt=33 (renamed from x.txt)
b69ab3144 * |
b69ab3145 * A # A/x.txt=33
b69ab3146 * | # A/z.txt=22
b69ab3147 * /
b69ab3148 * Z # Z/z.txt=11
b69ab3149 *
b69ab3150 * and exported via `debugexportstack -r 'desc(A)::'`.
b69ab3151 */
b69ab3152const exportStack1: ExportStack = [
b69ab3153 {
b69ab3154 ...exportCommitDefault,
b69ab3155 immutable: true,
b69ab3156 node: 'Z_NODE',
b69ab3157 relevantFiles: {
b69ab3158 'x.txt': null,
b69ab3159 'z.txt': {data: '11'},
b69ab3160 },
b69ab3161 requested: false,
b69ab3162 text: 'Z',
b69ab3163 },
b69ab3164 {
b69ab3165 ...exportCommitDefault,
b69ab3166 files: {
b69ab3167 'x.txt': {data: '33'},
b69ab3168 'z.txt': {data: '22'},
b69ab3169 },
b69ab3170 node: 'A_NODE',
b69ab3171 parents: ['Z_NODE'],
b69ab3172 relevantFiles: {'y.txt': null},
b69ab3173 text: 'A',
b69ab3174 },
b69ab3175 {
b69ab3176 ...exportCommitDefault,
b69ab3177 files: {
b69ab3178 'x.txt': null,
b69ab3179 'y.txt': {copyFrom: 'x.txt', data: '33'},
b69ab3180 },
b69ab3181 node: 'B_NODE',
b69ab3182 parents: ['A_NODE'],
b69ab3183 relevantFiles: {'z.txt': {data: '22'}},
b69ab3184 text: 'B',
b69ab3185 },
b69ab3186 {
b69ab3187 ...exportCommitDefault,
b69ab3188 date: [0.0, 0],
b69ab3189 files: {'z.txt': null},
b69ab3190 node: 'C_NODE',
b69ab3191 parents: ['B_NODE'],
b69ab3192 text: 'C',
b69ab3193 },
b69ab3194];
b69ab3195
b69ab3196/** Construct `CommitStackState` from a stack of files for testing purpose. */
b69ab3197export function linearStackWithFiles(
b69ab3198 stackFiles: Array<{[path: RepoPath]: ExportFile | null}>,
b69ab3199): CommitStackState {
b69ab31100 return new CommitStackState(
b69ab31101 stackFiles.map((files, i) => {
b69ab31102 const nextFiles = stackFiles.at(i + 1) ?? {};
b69ab31103 return {
b69ab31104 ...exportCommitDefault,
b69ab31105 node: `NODE_${i}`,
b69ab31106 parents: i === 0 ? [] : [`NODE_${i - 1}`],
b69ab31107 text: `Commit ${i}`,
b69ab31108 files,
b69ab31109 relevantFiles: Object.fromEntries(
b69ab31110 Object.entries(nextFiles).filter(([path, _file]) => !Object.hasOwn(files, path)),
b69ab31111 ),
b69ab31112 } as ExportCommit;
b69ab31113 }),
b69ab31114 );
b69ab31115}
b69ab31116
b69ab31117describe('CommitStackState', () => {
b69ab31118 it('accepts an empty stack', () => {
b69ab31119 const stack = new CommitStackState([]);
b69ab31120 expect(stack.revs()).toStrictEqual([]);
b69ab31121 });
b69ab31122
b69ab31123 it('accepts a stack without a public commit', () => {
b69ab31124 const stack = new CommitStackState([
b69ab31125 {
b69ab31126 ...exportCommitDefault,
b69ab31127 files: {'a.txt': {data: 'a'}},
b69ab31128 node: 'x',
b69ab31129 parents: [],
b69ab31130 text: 'A',
b69ab31131 },
b69ab31132 ]);
b69ab31133 expect(stack.revs()).toStrictEqual([0]);
b69ab31134 });
b69ab31135
b69ab31136 it('rejects a stack with multiple roots', () => {
b69ab31137 const stack = [
b69ab31138 {...exportCommitDefault, node: 'Z1'},
b69ab31139 {...exportCommitDefault, node: 'Z2'},
b69ab31140 ];
b69ab31141 expect(() => new CommitStackState(stack)).toThrowError(
b69ab31142 'Multiple roots ["Z1","Z2"] is not supported',
b69ab31143 );
b69ab31144 });
b69ab31145
b69ab31146 it('rejects a stack with merges', () => {
b69ab31147 const stack = [
b69ab31148 {...exportCommitDefault, node: 'A', parents: []},
b69ab31149 {...exportCommitDefault, node: 'B', parents: ['A']},
b69ab31150 {...exportCommitDefault, node: 'C', parents: ['A', 'B']},
b69ab31151 ];
b69ab31152 expect(() => new CommitStackState(stack)).toThrowError('Merge commit C is not supported');
b69ab31153 });
b69ab31154
b69ab31155 it('rejects circular stack', () => {
b69ab31156 const stack = [
b69ab31157 {...exportCommitDefault, node: 'A', parents: ['B']},
b69ab31158 {...exportCommitDefault, node: 'B', parents: ['A']},
b69ab31159 ];
b69ab31160 expect(() => new CommitStackState(stack)).toThrowError();
b69ab31161 });
b69ab31162
b69ab31163 it('provides file paths', () => {
b69ab31164 const stack = new CommitStackState(exportStack1);
b69ab31165 expect(stack.getAllPaths()).toStrictEqual(['x.txt', 'y.txt', 'z.txt']);
b69ab31166 });
b69ab31167
b69ab31168 it('logs commit history', () => {
b69ab31169 const stack = new CommitStackState(exportStack1);
b69ab31170 expect(stack.revs()).toStrictEqual([0, 1, 2, 3]);
b69ab31171 expect([...stack.log(1 as CommitRev)]).toStrictEqual([1, 0]);
b69ab31172 expect([...stack.log(3 as CommitRev)]).toStrictEqual([3, 2, 1, 0]);
b69ab31173 });
b69ab31174
b69ab31175 it('finds child commits via childRevs', () => {
b69ab31176 const stack = new CommitStackState(exportStack1);
b69ab31177 expect(stack.childRevs(0 as CommitRev)).toMatchInlineSnapshot(`
b69ab31178 [
b69ab31179 1,
b69ab31180 ]
b69ab31181 `);
b69ab31182 expect(stack.childRevs(1 as CommitRev)).toMatchInlineSnapshot(`
b69ab31183 [
b69ab31184 2,
b69ab31185 ]
b69ab31186 `);
b69ab31187 expect(stack.childRevs(2 as CommitRev)).toMatchInlineSnapshot(`
b69ab31188 [
b69ab31189 3,
b69ab31190 ]
b69ab31191 `);
b69ab31192 expect(stack.childRevs(3 as CommitRev)).toMatchInlineSnapshot(`[]`);
b69ab31193 });
b69ab31194
b69ab31195 describe('log file history', () => {
b69ab31196 // [rev, path] => [rev, path, file]
b69ab31197 const extend = (stack: CommitStackState, revPathPairs: Array<[number, string]>) => {
b69ab31198 return revPathPairs.map(([rev, path]) => {
b69ab31199 const file =
b69ab31200 rev >= 0 ? stack.get(rev as CommitRev)?.files.get(path) : stack.bottomFiles.get(path);
b69ab31201 expect(file).toBe(stack.getFile(rev as CommitRev, path));
b69ab31202 return [rev, path, file];
b69ab31203 });
b69ab31204 };
b69ab31205
b69ab31206 it('logs file history', () => {
b69ab31207 const stack = new CommitStackState(exportStack1);
b69ab31208 expect([...stack.logFile(3 as CommitRev, 'x.txt')]).toStrictEqual(
b69ab31209 extend(stack, [
b69ab31210 [2, 'x.txt'],
b69ab31211 [1, 'x.txt'],
b69ab31212 ]),
b69ab31213 );
b69ab31214 expect([...stack.logFile(3 as CommitRev, 'y.txt')]).toStrictEqual(
b69ab31215 extend(stack, [[2, 'y.txt']]),
b69ab31216 );
b69ab31217 expect([...stack.logFile(3 as CommitRev, 'z.txt')]).toStrictEqual(
b69ab31218 extend(stack, [
b69ab31219 [3, 'z.txt'],
b69ab31220 [1, 'z.txt'],
b69ab31221 ]),
b69ab31222 );
b69ab31223 // Changes in not requested commits (rev 0) are ignored.
b69ab31224 expect([...stack.logFile(3 as CommitRev, 'k.txt')]).toStrictEqual([]);
b69ab31225 });
b69ab31226
b69ab31227 it('logs file history following renames', () => {
b69ab31228 const stack = new CommitStackState(exportStack1);
b69ab31229 expect([...stack.logFile(3 as CommitRev, 'y.txt', true)]).toStrictEqual(
b69ab31230 extend(stack, [
b69ab31231 [2, 'y.txt'],
b69ab31232 [1, 'x.txt'],
b69ab31233 ]),
b69ab31234 );
b69ab31235 });
b69ab31236
b69ab31237 it('logs file history including the bottom', () => {
b69ab31238 const stack = new CommitStackState(exportStack1);
b69ab31239 ['x.txt', 'z.txt'].forEach(path => {
b69ab31240 expect([...stack.logFile(1 as CommitRev, path, true, true)]).toStrictEqual(
b69ab31241 extend(stack, [
b69ab31242 [1, path],
b69ab31243 // rev 0 does not change x.txt or z.txt
b69ab31244 [-1, path],
b69ab31245 ]),
b69ab31246 );
b69ab31247 });
b69ab31248 });
b69ab31249
b69ab31250 it('parentFile follows rename to bottomFile', () => {
b69ab31251 const stack = new CommitStackState([
b69ab31252 {
b69ab31253 ...exportCommitDefault,
b69ab31254 relevantFiles: {
b69ab31255 'x.txt': {data: '11'},
b69ab31256 'z.txt': {data: '22'},
b69ab31257 },
b69ab31258 files: {
b69ab31259 'z.txt': {data: '33', copyFrom: 'x.txt'},
b69ab31260 },
b69ab31261 text: 'Commit Foo',
b69ab31262 },
b69ab31263 ]);
b69ab31264 const file = stack.getFile(0 as CommitRev, 'z.txt');
b69ab31265 expect(stack.getUtf8Data(file)).toBe('33');
b69ab31266 const [, , parentFileWithRename] = stack.parentFile(0 as CommitRev, 'z.txt', true);
b69ab31267 expect(stack.getUtf8Data(parentFileWithRename)).toBe('11');
b69ab31268 const [, , parentFile] = stack.parentFile(0 as CommitRev, 'z.txt', false);
b69ab31269 expect(stack.getUtf8Data(parentFile)).toBe('22');
b69ab31270 });
b69ab31271 });
b69ab31272
b69ab31273 it('provides file contents at given revs', () => {
b69ab31274 const stack = new CommitStackState(exportStack1);
b69ab31275 expect(stack.getFile(0 as CommitRev, 'x.txt')).toBe(ABSENT_FILE);
b69ab31276 expect(stack.getFile(0 as CommitRev, 'y.txt')).toBe(ABSENT_FILE);
b69ab31277 expect(stack.getFile(0 as CommitRev, 'z.txt')).toMatchObject({data: '11'});
b69ab31278 expect(stack.getFile(1 as CommitRev, 'x.txt')).toMatchObject({data: '33'});
b69ab31279 expect(stack.getFile(1 as CommitRev, 'y.txt')).toBe(ABSENT_FILE);
b69ab31280 expect(stack.getFile(1 as CommitRev, 'z.txt')).toMatchObject({data: '22'});
b69ab31281 expect(stack.getFile(2 as CommitRev, 'x.txt')).toBe(ABSENT_FILE);
b69ab31282 expect(stack.getFile(2 as CommitRev, 'y.txt')).toMatchObject({data: '33'});
b69ab31283 expect(stack.getFile(2 as CommitRev, 'z.txt')).toMatchObject({data: '22'});
b69ab31284 expect(stack.getFile(3 as CommitRev, 'x.txt')).toBe(ABSENT_FILE);
b69ab31285 expect(stack.getFile(3 as CommitRev, 'y.txt')).toMatchObject({data: '33'});
b69ab31286 expect(stack.getFile(3 as CommitRev, 'z.txt')).toBe(ABSENT_FILE);
b69ab31287 });
b69ab31288
b69ab31289 describe('builds FileStack', () => {
b69ab31290 it('for double renames', () => {
b69ab31291 // x.txt renamed to both y.txt and z.txt.
b69ab31292 const stack = new CommitStackState([
b69ab31293 {...exportCommitDefault, node: 'A', files: {'x.txt': {data: 'xx'}}},
b69ab31294 {
b69ab31295 ...exportCommitDefault,
b69ab31296 node: 'B',
b69ab31297 parents: ['A'],
b69ab31298 files: {
b69ab31299 'x.txt': null,
b69ab31300 'y.txt': {data: 'yy', copyFrom: 'x.txt'},
b69ab31301 'z.txt': {data: 'zz', copyFrom: 'x.txt'},
b69ab31302 },
b69ab31303 },
b69ab31304 ]);
b69ab31305 expect(stack.describeFileStacks()).toStrictEqual([
b69ab31306 // y.txt inherits x.txt's history.
b69ab31307 '0:./x.txt 1:A/x.txt(xx) 2:B/y.txt(yy)',
b69ab31308 // z.txt does not inherit x.txt's history (but still has a parent for diff rendering purpose).
b69ab31309 '0:A/x.txt(xx) 1:B/z.txt(zz)',
b69ab31310 ]);
b69ab31311 });
b69ab31312
b69ab31313 it('for double copies', () => {
b69ab31314 // x.txt copied to both y.txt and z.txt.
b69ab31315 const stack = new CommitStackState([
b69ab31316 {...exportCommitDefault, node: 'A', files: {'x.txt': {data: 'xx'}}},
b69ab31317 {
b69ab31318 ...exportCommitDefault,
b69ab31319 node: 'B',
b69ab31320 parents: ['A'],
b69ab31321 files: {
b69ab31322 'y.txt': {data: 'yy', copyFrom: 'x.txt'},
b69ab31323 'z.txt': {data: 'zz', copyFrom: 'y.txt'},
b69ab31324 },
b69ab31325 },
b69ab31326 ]);
b69ab31327 expect(stack.describeFileStacks()).toStrictEqual([
b69ab31328 // y.txt connects to x.txt's history.
b69ab31329 '0:./x.txt 1:A/x.txt(xx) 2:B/y.txt(yy)',
b69ab31330 // z.txt does not connect to x.txt's history (but still have one parent for diff).
b69ab31331 '0:./y.txt 1:B/z.txt(zz)',
b69ab31332 ]);
b69ab31333 });
b69ab31334
b69ab31335 it('for changes and copies', () => {
b69ab31336 // x.txt is changed, and copied to both y.txt and z.txt.
b69ab31337 const stack = new CommitStackState([
b69ab31338 {...exportCommitDefault, node: 'A', files: {'x.txt': {data: 'xx'}}},
b69ab31339 {
b69ab31340 ...exportCommitDefault,
b69ab31341 node: 'B',
b69ab31342 parents: ['A'],
b69ab31343 files: {
b69ab31344 'x.txt': {data: 'yy'},
b69ab31345 'y.txt': {data: 'xx', copyFrom: 'x.txt'},
b69ab31346 'z.txt': {data: 'xx', copyFrom: 'x.txt'},
b69ab31347 },
b69ab31348 },
b69ab31349 ]);
b69ab31350 expect(stack.describeFileStacks()).toStrictEqual([
b69ab31351 // x.txt has its own history.
b69ab31352 '0:./x.txt 1:A/x.txt(xx) 2:B/x.txt(yy)',
b69ab31353 // y.txt and z.txt do not share x.txt's history (but still have one parent for diff).
b69ab31354 '0:A/x.txt(xx) 1:B/y.txt(xx)',
b69ab31355 '0:A/x.txt(xx) 1:B/z.txt(xx)',
b69ab31356 ]);
b69ab31357 });
b69ab31358
b69ab31359 it('for the the example stack', () => {
b69ab31360 const stack = new CommitStackState(exportStack1);
b69ab31361 expect(stack.describeFileStacks()).toStrictEqual([
b69ab31362 // x.txt: added by A, modified and renamed by B.
b69ab31363 '0:./x.txt 1:A/x.txt(33) 2:B/y.txt(33)',
b69ab31364 // z.txt: modified by A, deleted by C.
b69ab31365 '0:./z.txt(11) 1:A/z.txt(22) 2:C/z.txt',
b69ab31366 ]);
b69ab31367 });
b69ab31368
b69ab31369 it('with rename tracking disabled', () => {
b69ab31370 const stack = new CommitStackState(exportStack1).buildFileStacks({followRenames: false});
b69ab31371 // no x.txt -> y.txt rename
b69ab31372 expect(stack.describeFileStacks()).toStrictEqual([
b69ab31373 '0:./x.txt 1:A/x.txt(33) 2:B/x.txt',
b69ab31374 '0:./z.txt(11) 1:A/z.txt(22) 2:C/z.txt',
b69ab31375 '0:./y.txt 1:B/y.txt(33)',
b69ab31376 ]);
b69ab31377 });
b69ab31378 });
b69ab31379
b69ab31380 describe('calculates dependencies', () => {
b69ab31381 const e = exportCommitDefault;
b69ab31382
b69ab31383 it('for content changes', () => {
b69ab31384 const stack = new CommitStackState([
b69ab31385 {...e, node: 'Z', requested: false, relevantFiles: {'x.txt': null}},
b69ab31386 {...e, node: 'A', parents: ['Z'], files: {'x.txt': {data: 'b\n'}}},
b69ab31387 {...e, node: 'B', parents: ['A'], files: {'x.txt': {data: 'a\nb\n'}}},
b69ab31388 {...e, node: 'C', parents: ['B'], files: {'x.txt': {data: 'a\nB\n'}}},
b69ab31389 ]);
b69ab31390 expect(stack.calculateDepMap()).toStrictEqual(
b69ab31391 new Map([
b69ab31392 [0, new Set()],
b69ab31393 [1, new Set()],
b69ab31394 [2, new Set()], // insertion at other insertion boundary is dependency-free
b69ab31395 [3, new Set([1])],
b69ab31396 ]),
b69ab31397 );
b69ab31398 });
b69ab31399
b69ab31400 it('for file addition and deletion', () => {
b69ab31401 const stack = new CommitStackState([
b69ab31402 {...e, node: 'Z', requested: false, relevantFiles: {'x.txt': {data: 'a'}}},
b69ab31403 {...e, node: 'A', parents: ['Z'], files: {'x.txt': null}},
b69ab31404 {...e, node: 'B', parents: ['A'], files: {'x.txt': {data: 'a'}}},
b69ab31405 {...e, node: 'C', parents: ['B'], files: {'x.txt': null}},
b69ab31406 ]);
b69ab31407 expect(stack.calculateDepMap()).toStrictEqual(
b69ab31408 new Map([
b69ab31409 [0, new Set()],
b69ab31410 [1, new Set()],
b69ab31411 [2, new Set([1])], // commit B adds x.txt, depends on commit A's deletion.
b69ab31412 [3, new Set([2])], // commit C deletes x.txt, depends on commit B's addition.
b69ab31413 ]),
b69ab31414 );
b69ab31415 });
b69ab31416
b69ab31417 it('for copies', () => {
b69ab31418 const stack = new CommitStackState([
b69ab31419 {...e, node: 'A', files: {'x.txt': {data: 'a'}}},
b69ab31420 {...e, node: 'B', parents: ['A'], files: {'y.txt': {data: 'a', copyFrom: 'x.txt'}}},
b69ab31421 {...e, node: 'C', parents: ['B'], files: {'z.txt': {data: 'a', copyFrom: 'x.txt'}}},
b69ab31422 {
b69ab31423 ...e,
b69ab31424 node: 'D',
b69ab31425 parents: ['C'],
b69ab31426 files: {'p.txt': {data: 'a', copyFrom: 'x.txt'}, 'q.txt': {data: 'a', copyFrom: 'z.txt'}},
b69ab31427 },
b69ab31428 ]);
b69ab31429 expect(stack.calculateDepMap()).toStrictEqual(
b69ab31430 new Map([
b69ab31431 [0, new Set()],
b69ab31432 [1, new Set([0])], // commit B copies commit A's x.txt to y.txt.
b69ab31433 [2, new Set([0])], // commit C copies commit A's x.txt to z.txt.
b69ab31434 [3, new Set([0, 2])], // commit D copies commit A's x.txt to p.txt, and commit C's z.txt to q.txt.
b69ab31435 ]),
b69ab31436 );
b69ab31437 });
b69ab31438 });
b69ab31439
b69ab31440 describe('folding commits', () => {
b69ab31441 const e = exportCommitDefault;
b69ab31442
b69ab31443 it('cannot be used for immutable commits', () => {
b69ab31444 const stack = new CommitStackState([
b69ab31445 {...e, node: 'A', immutable: true},
b69ab31446 {...e, node: 'B', parents: ['A'], immutable: false},
b69ab31447 {...e, node: 'C', parents: ['B'], immutable: false},
b69ab31448 ]);
b69ab31449 expect(stack.canFoldDown(1 as CommitRev)).toBeFalsy();
b69ab31450 expect(stack.canFoldDown(2 as CommitRev)).toBeTruthy();
b69ab31451 });
b69ab31452
b69ab31453 it('cannot be used for out-of-range commits', () => {
b69ab31454 const stack = new CommitStackState([
b69ab31455 {...e, node: 'A'},
b69ab31456 {...e, node: 'B', parents: ['A']},
b69ab31457 ]);
b69ab31458 expect(stack.canFoldDown(0 as CommitRev)).toBeFalsy();
b69ab31459 expect(stack.canFoldDown(1 as CommitRev)).toBeTruthy();
b69ab31460 expect(stack.canFoldDown(2 as CommitRev)).toBeFalsy();
b69ab31461 });
b69ab31462
b69ab31463 it('cannot be used for forks', () => {
b69ab31464 const stack = new CommitStackState([
b69ab31465 {...e, node: 'A'},
b69ab31466 {...e, node: 'B', parents: ['A']},
b69ab31467 {...e, node: 'C', parents: ['A']},
b69ab31468 ]);
b69ab31469 expect(stack.canFoldDown(1 as CommitRev)).toBeFalsy();
b69ab31470 expect(stack.canFoldDown(2 as CommitRev)).toBeFalsy();
b69ab31471 });
b69ab31472
b69ab31473 it('works for simple edits', () => {
b69ab31474 let stack = new CommitStackState([
b69ab31475 {
b69ab31476 ...e,
b69ab31477 node: 'A',
b69ab31478 text: 'Commit A',
b69ab31479 parents: [],
b69ab31480 files: {'x.txt': {data: 'xx'}, 'y.txt': {data: 'yy'}},
b69ab31481 },
b69ab31482 {...e, node: 'B', text: 'Commit B', parents: ['A'], files: {'x.txt': {data: 'yy'}}},
b69ab31483 {...e, node: 'C', text: 'Commit C', parents: ['B'], files: {'x.txt': {data: 'zz'}}},
b69ab31484 ]);
b69ab31485 expect(stack.canFoldDown(1 as CommitRev)).toBeTruthy();
b69ab31486 stack = stack.foldDown(1 as CommitRev);
b69ab31487 expect(stack.stack.size).toBe(2);
b69ab31488 expect(stack.stack.get(0)?.toJS()).toMatchObject({
b69ab31489 key: 'A',
b69ab31490 files: {
b69ab31491 'x.txt': {data: 'yy'},
b69ab31492 'y.txt': {data: 'yy'},
b69ab31493 },
b69ab31494 originalNodes: new Set(['A', 'B']),
b69ab31495 text: 'Commit A, Commit B',
b69ab31496 parents: [],
b69ab31497 });
b69ab31498 expect(stack.stack.get(1)?.toJS()).toMatchObject({
b69ab31499 key: 'C',
b69ab31500 text: 'Commit C',
b69ab31501 parents: [0], // Commit C's parent is updated to Commit A.
b69ab31502 });
b69ab31503 });
b69ab31504
b69ab31505 it('removes copyFrom appropriately', () => {
b69ab31506 let stack = new CommitStackState([
b69ab31507 {...e, node: 'A', parents: [], files: {'x.txt': {data: 'xx'}}},
b69ab31508 {...e, node: 'B', parents: ['A'], files: {'y.txt': {data: 'yy', copyFrom: 'x.txt'}}},
b69ab31509 ]);
b69ab31510 expect(stack.canFoldDown(1 as CommitRev)).toBeTruthy();
b69ab31511 stack = stack.foldDown(1 as CommitRev);
b69ab31512 expect(stack.stack.get(0)?.toJS()).toMatchObject({
b69ab31513 files: {
b69ab31514 'x.txt': {data: 'xx'},
b69ab31515 'y.txt': {data: 'yy'}, // no longer has "copyFrom", since 'x.txt' does not exist in commit A.
b69ab31516 },
b69ab31517 });
b69ab31518 });
b69ab31519
b69ab31520 it('keeps copyFrom appropriately', () => {
b69ab31521 let stack = new CommitStackState([
b69ab31522 {...e, node: 'A', parents: [], files: {xt: {data: 'xx'}, yt: {data: 'yy'}}},
b69ab31523 {...e, node: 'B', parents: ['A'], files: {y1t: {data: 'yy', copyFrom: 'yt'}}},
b69ab31524 {
b69ab31525 ...e,
b69ab31526 node: 'C',
b69ab31527 parents: ['B'],
b69ab31528 files: {x1t: {data: 'x1', copyFrom: 'xt'}, y1t: {data: 'y1'}},
b69ab31529 },
b69ab31530 ]);
b69ab31531 // Fold B+C.
b69ab31532 expect(stack.canFoldDown(2 as CommitRev)).toBeTruthy();
b69ab31533 stack = stack.foldDown(2 as CommitRev);
b69ab31534 expect(stack.stack.get(1)?.toJS()).toMatchObject({
b69ab31535 files: {
b69ab31536 y1t: {data: 'y1', copyFrom: 'yt'}, // reuse copyFrom: 'yt' from commit B.
b69ab31537 x1t: {data: 'x1', copyFrom: 'xt'}, // reuse copyFrom: 'xt' from commit C.
b69ab31538 },
b69ab31539 });
b69ab31540 });
b69ab31541
b69ab31542 it('chains renames', () => {
b69ab31543 let stack = new CommitStackState([
b69ab31544 {...e, node: 'A', parents: [], files: {xt: {data: 'xx'}}},
b69ab31545 {...e, node: 'B', parents: ['A'], files: {yt: {data: 'yy', copyFrom: 'xt'}, xt: null}},
b69ab31546 {...e, node: 'C', parents: ['B'], files: {zt: {data: 'zz', copyFrom: 'yt'}, yt: null}},
b69ab31547 ]);
b69ab31548 // Fold B+C.
b69ab31549 expect(stack.canFoldDown(2 as CommitRev)).toBeTruthy();
b69ab31550 stack = stack.foldDown(2 as CommitRev);
b69ab31551 expect(stack.stack.get(1)?.toJS()).toMatchObject({
b69ab31552 files: {
b69ab31553 xt: ABSENT_FILE.toJS(),
b69ab31554 // 'yt' is no longer considered changed.
b69ab31555 zt: {data: 'zz', copyFrom: 'xt'}, // 'xt'->'yt'->'zt' is folded to 'xt'->'zt'.
b69ab31556 },
b69ab31557 });
b69ab31558 });
b69ab31559
b69ab31560 it('removes cancel-out changes', () => {
b69ab31561 let stack = new CommitStackState([
b69ab31562 {...e, node: 'A', parents: [], files: {xt: {data: 'xx'}}},
b69ab31563 {...e, node: 'B', parents: ['A'], files: {xt: {data: 'yy'}, zt: {data: 'zz'}}},
b69ab31564 {...e, node: 'C', parents: ['B'], files: {xt: {data: 'xx'}}},
b69ab31565 ]);
b69ab31566 // Fold B+C.
b69ab31567 expect(stack.canFoldDown(2 as CommitRev)).toBeTruthy();
b69ab31568 stack = stack.foldDown(2 as CommitRev);
b69ab31569 expect(stack.stack.get(1)?.toJS()).toMatchObject({
b69ab31570 files: {zt: {data: 'zz'}}, // changes to 'yt' is removed.
b69ab31571 });
b69ab31572 });
b69ab31573 });
b69ab31574
b69ab31575 describe('dropping commits', () => {
b69ab31576 const e = exportCommitDefault;
b69ab31577
b69ab31578 it('cannot be used for immutable commits', () => {
b69ab31579 const stack = new CommitStackState([
b69ab31580 {...e, node: 'A', immutable: true},
b69ab31581 {...e, node: 'B', parents: ['A'], immutable: true},
b69ab31582 {...e, node: 'C', parents: ['B'], immutable: false},
b69ab31583 ]);
b69ab31584 expect(stack.canDrop(0 as CommitRev)).toBeFalsy();
b69ab31585 expect(stack.canDrop(1 as CommitRev)).toBeFalsy();
b69ab31586 expect(stack.canDrop(2 as CommitRev)).toBeTruthy();
b69ab31587 });
b69ab31588
b69ab31589 it('detects content dependencies', () => {
b69ab31590 const stack = new CommitStackState([
b69ab31591 {...e, node: 'A', files: {xx: {data: '0\n2\n'}}},
b69ab31592 {...e, node: 'B', parents: ['A'], files: {xx: {data: '0\n1\n2\n'}}},
b69ab31593 {...e, node: 'C', parents: ['B'], files: {xx: {data: '0\n1\n2\n3\n'}}},
b69ab31594 {...e, node: 'D', parents: ['C'], files: {xx: {data: '0\n1\n2\n4\n'}}},
b69ab31595 ]);
b69ab31596 expect(stack.canDrop(0 as CommitRev)).toBeFalsy();
b69ab31597 expect(stack.canDrop(1 as CommitRev)).toBeTruthy();
b69ab31598 expect(stack.canDrop(2 as CommitRev)).toBeFalsy(); // D depends on C
b69ab31599 expect(stack.canDrop(3 as CommitRev)).toBeTruthy();
b69ab31600 });
b69ab31601
b69ab31602 it('detects commit graph dependencies', () => {
b69ab31603 const stack = new CommitStackState([
b69ab31604 {...e, node: 'A', files: {xx: {data: '1'}}},
b69ab31605 {...e, node: 'B', parents: ['A'], files: {xx: {data: '2'}}},
b69ab31606 {...e, node: 'C', parents: ['A'], files: {xx: {data: '3'}}},
b69ab31607 {...e, node: 'D', parents: ['C'], files: {xx: {data: '4'}}},
b69ab31608 ]);
b69ab31609 expect(stack.canDrop(0 as CommitRev)).toBeFalsy();
b69ab31610 expect(stack.canDrop(1 as CommitRev)).toBeTruthy();
b69ab31611 expect(stack.canDrop(2 as CommitRev)).toBeFalsy();
b69ab31612 expect(stack.canDrop(3 as CommitRev)).toBeTruthy();
b69ab31613 });
b69ab31614
b69ab31615 it('for a change in the middle of a stack', () => {
b69ab31616 let stack = new CommitStackState([
b69ab31617 {...e, node: 'A', files: {xx: {data: 'p\ny\n'}}},
b69ab31618 {...e, node: 'B', parents: ['A'], files: {xx: {data: 'p\nx\ny\n'}}},
b69ab31619 {...e, node: 'C', parents: ['B'], files: {xx: {data: 'p\nx\ny\nz\n'}}},
b69ab31620 ]);
b69ab31621 expect(stack.canDrop(0 as CommitRev)).toBeFalsy();
b69ab31622 expect(stack.canDrop(1 as CommitRev)).toBeTruthy();
b69ab31623 expect(stack.canDrop(2 as CommitRev)).toBeTruthy();
b69ab31624 stack = stack.drop(1 as CommitRev);
b69ab31625 expect(stack.stack.size).toBe(2);
b69ab31626 expect(stack.stack.get(1)?.toJS()).toMatchObject({
b69ab31627 originalNodes: ['C'],
b69ab31628 files: {xx: {data: 'p\ny\nz\n'}},
b69ab31629 });
b69ab31630 expect(stack.stack.toArray().map(c => c.key)).toMatchObject(['A', 'C']);
b69ab31631 });
b69ab31632 });
b69ab31633
b69ab31634 describe('reordering commits', () => {
b69ab31635 const e = exportCommitDefault;
b69ab31636
b69ab31637 it('cannot be used for immutable commits', () => {
b69ab31638 const stack = new CommitStackState([
b69ab31639 {...e, node: 'A', immutable: true},
b69ab31640 {...e, node: 'B', parents: ['A'], immutable: true},
b69ab31641 {...e, node: 'C', parents: ['B'], immutable: false},
b69ab31642 ]);
b69ab31643 expect(stack.canReorder([0, 2, 1] as CommitRev[])).toBeFalsy();
b69ab31644 expect(stack.canReorder([1, 0, 2] as CommitRev[])).toBeFalsy();
b69ab31645 expect(stack.canReorder([0, 1, 2] as CommitRev[])).toBeTruthy();
b69ab31646 });
b69ab31647
b69ab31648 it('respects content dependencies', () => {
b69ab31649 const stack = new CommitStackState([
b69ab31650 {...e, node: 'A', files: {xx: {data: '0\n2\n'}}},
b69ab31651 {...e, node: 'B', parents: ['A'], files: {xx: {data: '0\n1\n2\n'}}},
b69ab31652 {...e, node: 'C', parents: ['B'], files: {xx: {data: '0\n1\n2\n3\n'}}},
b69ab31653 {...e, node: 'D', parents: ['C'], files: {xx: {data: '0\n1\n2\n4\n'}}},
b69ab31654 ]);
b69ab31655 expect(stack.canReorder([0, 2, 3, 1] as CommitRev[])).toBeTruthy();
b69ab31656 expect(stack.canReorder([0, 2, 1, 3] as CommitRev[])).toBeTruthy();
b69ab31657 expect(stack.canReorder([0, 3, 2, 1] as CommitRev[])).toBeFalsy();
b69ab31658 expect(stack.canReorder([0, 3, 1, 2] as CommitRev[])).toBeFalsy();
b69ab31659 });
b69ab31660
b69ab31661 it('refuses to reorder non-linear stack', () => {
b69ab31662 const stack = new CommitStackState([
b69ab31663 {...e, node: 'A', files: {xx: {data: '1'}}},
b69ab31664 {...e, node: 'B', parents: ['A'], files: {xx: {data: '2'}}},
b69ab31665 {...e, node: 'C', parents: ['A'], files: {xx: {data: '3'}}},
b69ab31666 {...e, node: 'D', parents: ['C'], files: {xx: {data: '4'}}},
b69ab31667 ]);
b69ab31668 expect(stack.canReorder([0, 2, 3, 1] as CommitRev[])).toBeFalsy();
b69ab31669 expect(stack.canReorder([0, 2, 1, 3] as CommitRev[])).toBeFalsy();
b69ab31670 expect(stack.canReorder([0, 1, 2, 3] as CommitRev[])).toBeFalsy();
b69ab31671 });
b69ab31672
b69ab31673 it('can reorder a long stack', () => {
b69ab31674 const exportStack: ExportStack = [...Array(20).keys()].map(i => {
b69ab31675 return {...e, node: `A${i}`, parents: i === 0 ? [] : [`A${i - 1}`], files: {}};
b69ab31676 });
b69ab31677 const stack = new CommitStackState(exportStack);
b69ab31678 expect(stack.canReorder(stack.revs().reverse())).toBeTruthy();
b69ab31679 });
b69ab31680
b69ab31681 it('reorders adjacent changes', () => {
b69ab31682 // Note: usually rev 0 is a public parent commit, rev 0 is not usually reordered.
b69ab31683 // But this test reorders rev 0 and triggers some interesting code paths.
b69ab31684 let stack = new CommitStackState([
b69ab31685 {...e, node: 'A', files: {xx: {data: '1\n'}}},
b69ab31686 {...e, node: 'B', parents: ['A'], files: {xx: {data: '1\n2\n'}}},
b69ab31687 ]);
b69ab31688 expect(stack.canReorder([1, 0] as CommitRev[])).toBeTruthy();
b69ab31689 stack = stack.reorder([1, 0] as CommitRev[]);
b69ab31690 expect(stack.stack.toArray().map(c => c.files.get('xx')?.data)).toMatchObject([
b69ab31691 '2\n',
b69ab31692 '1\n2\n',
b69ab31693 ]);
b69ab31694 expect(stack.stack.toArray().map(c => c.key)).toMatchObject(['B', 'A']);
b69ab31695 // Reorder back.
b69ab31696 expect(stack.canReorder([1, 0] as CommitRev[])).toBeTruthy();
b69ab31697 stack = stack.reorder([1, 0] as CommitRev[]);
b69ab31698 expect(stack.stack.toArray().map(c => c.files.get('xx')?.data)).toMatchObject([
b69ab31699 '1\n',
b69ab31700 '1\n2\n',
b69ab31701 ]);
b69ab31702 expect(stack.stack.toArray().map(c => c.key)).toMatchObject(['A', 'B']);
b69ab31703 });
b69ab31704
b69ab31705 it('reorders content changes', () => {
b69ab31706 let stack = new CommitStackState([
b69ab31707 {...e, node: 'A', files: {xx: {data: '1\n1\n'}}},
b69ab31708 {...e, node: 'B', parents: ['A'], files: {xx: {data: '0\n1\n1\n'}}},
b69ab31709 {...e, node: 'C', parents: ['B'], files: {yy: {data: '0'}}}, // Does not change 'xx'.
b69ab31710 {...e, node: 'D', parents: ['C'], files: {xx: {data: '0\n1\n1\n2\n'}}},
b69ab31711 {...e, node: 'E', parents: ['D'], files: {xx: {data: '0\n1\n3\n1\n2\n'}}},
b69ab31712 ]);
b69ab31713
b69ab31714 // A-B-C-D-E => A-C-E-B-D.
b69ab31715 let order = [0, 2, 4, 1, 3] as CommitRev[];
b69ab31716 expect(stack.canReorder(order)).toBeTruthy();
b69ab31717 stack = stack.reorder(order);
b69ab31718 const getNode = (r: CommitRev) => stack.stack.get(r)?.originalNodes?.first();
b69ab31719 const getParents = (r: CommitRev) => stack.stack.get(r)?.parents?.toJS();
b69ab31720 expect(stack.revs().map(getNode)).toMatchObject(['A', 'C', 'E', 'B', 'D']);
b69ab31721 expect(stack.revs().map(getParents)).toMatchObject([[], [0], [1], [2], [3]]);
b69ab31722 expect(stack.revs().map(r => stack.getFile(r, 'xx').data)).toMatchObject([
b69ab31723 '1\n1\n',
b69ab31724 '1\n1\n', // Not changed by 'C'.
b69ab31725 '1\n3\n1\n',
b69ab31726 '0\n1\n3\n1\n',
b69ab31727 '0\n1\n3\n1\n2\n',
b69ab31728 ]);
b69ab31729 expect(stack.revs().map(r => stack.getFile(r, 'yy').data)).toMatchObject([
b69ab31730 '',
b69ab31731 '0',
b69ab31732 '0',
b69ab31733 '0',
b69ab31734 '0',
b69ab31735 ]);
b69ab31736
b69ab31737 // Reorder back. A-C-E-B-D => A-B-C-D-E.
b69ab31738 order = [0, 3, 1, 4, 2] as CommitRev[];
b69ab31739 expect(stack.canReorder(order)).toBeTruthy();
b69ab31740 stack = stack.reorder(order);
b69ab31741 expect(stack.revs().map(getNode)).toMatchObject(['A', 'B', 'C', 'D', 'E']);
b69ab31742 expect(stack.revs().map(getParents)).toMatchObject([[], [0], [1], [2], [3]]);
b69ab31743 expect(stack.revs().map(r => stack.getFile(r, 'xx').data)).toMatchObject([
b69ab31744 '1\n1\n',
b69ab31745 '0\n1\n1\n',
b69ab31746 '0\n1\n1\n',
b69ab31747 '0\n1\n1\n2\n',
b69ab31748 '0\n1\n3\n1\n2\n',
b69ab31749 ]);
b69ab31750 });
b69ab31751 });
b69ab31752
b69ab31753 describe('calculating ImportStack', () => {
b69ab31754 it('skips all if nothing changed', () => {
b69ab31755 const stack = new CommitStackState(exportStack1);
b69ab31756 expect(stack.calculateImportStack()).toMatchObject([]);
b69ab31757 });
b69ab31758
b69ab31759 it('skips unchanged commits', () => {
b69ab31760 // Edits B/y.txt, affects descendants C.
b69ab31761 const stack = new CommitStackState(exportStack1).updateEachFile((_rev, file, path) =>
b69ab31762 path === 'y.txt' ? file.set('data', '333') : file,
b69ab31763 );
b69ab31764 expect(stack.calculateImportStack()).toMatchObject([
b69ab31765 [
b69ab31766 'commit',
b69ab31767 {
b69ab31768 mark: ':r2',
b69ab31769 date: [0, 0],
b69ab31770 text: 'B',
b69ab31771 parents: ['A_NODE'],
b69ab31772 predecessors: ['B_NODE'],
b69ab31773 files: {
b69ab31774 'x.txt': null,
b69ab31775 'y.txt': {data: '333', copyFrom: 'x.txt', flags: ''},
b69ab31776 },
b69ab31777 },
b69ab31778 ],
b69ab31779 [
b69ab31780 'commit',
b69ab31781 {
b69ab31782 mark: ':r3',
b69ab31783 date: [0, 0],
b69ab31784 text: 'C',
b69ab31785 parents: [':r2'],
b69ab31786 predecessors: ['C_NODE'],
b69ab31787 files: {'z.txt': null},
b69ab31788 },
b69ab31789 ],
b69ab31790 ]);
b69ab31791 });
b69ab31792
b69ab31793 it('follows reorder', () => {
b69ab31794 // Reorder B and C in the example stack.
b69ab31795 const stack = new CommitStackState(exportStack1).reorder([0, 1, 3, 2] as CommitRev[]);
b69ab31796 expect(stack.calculateImportStack({goto: 'B_NODE', preserveDirtyFiles: true})).toMatchObject([
b69ab31797 ['commit', {text: 'C'}],
b69ab31798 ['commit', {mark: ':r3', text: 'B'}],
b69ab31799 ['reset', {mark: ':r3'}],
b69ab31800 ]);
b69ab31801 });
b69ab31802
b69ab31803 it('stays at the stack top on reorder', () => {
b69ab31804 // Reorder B and C in the example stack.
b69ab31805 const stack = new CommitStackState(exportStack1).reorder([0, 1, 3, 2] as CommitRev[]);
b69ab31806 expect(stack.calculateImportStack({goto: 'C_NODE'})).toMatchObject([
b69ab31807 ['commit', {text: 'C'}],
b69ab31808 ['commit', {mark: ':r3', text: 'B'}],
b69ab31809 ['goto', {mark: ':r3'}],
b69ab31810 ]);
b69ab31811 });
b69ab31812
b69ab31813 it('hides dropped commits', () => {
b69ab31814 let stack = new CommitStackState(exportStack1);
b69ab31815 const revs = stack.revs();
b69ab31816 // Drop the last 2 commits: B and C.
b69ab31817 stack = stack.drop(revs[revs.length - 1]).drop(revs[revs.length - 2]);
b69ab31818 expect(stack.calculateImportStack()).toMatchObject([
b69ab31819 [
b69ab31820 'hide',
b69ab31821 {
b69ab31822 nodes: ['B_NODE', 'C_NODE'],
b69ab31823 },
b69ab31824 ],
b69ab31825 ]);
b69ab31826 });
b69ab31827
b69ab31828 it('produces goto or reset command', () => {
b69ab31829 const stack = new CommitStackState(exportStack1).updateEachFile((_rev, file, path) =>
b69ab31830 path === 'y.txt' ? file.set('data', '333') : file,
b69ab31831 );
b69ab31832 expect(stack.calculateImportStack({goto: 3 as CommitRev})).toMatchObject([
b69ab31833 ['commit', {}],
b69ab31834 ['commit', {}],
b69ab31835 ['goto', {mark: ':r3'}],
b69ab31836 ]);
b69ab31837 expect(
b69ab31838 stack.calculateImportStack({goto: 3 as CommitRev, preserveDirtyFiles: true}),
b69ab31839 ).toMatchObject([
b69ab31840 ['commit', {}],
b69ab31841 ['commit', {}],
b69ab31842 ['reset', {mark: ':r3'}],
b69ab31843 ]);
b69ab31844 });
b69ab31845
b69ab31846 it('optionally rewrites commit date', () => {
b69ab31847 // Swap the last 2 commits.
b69ab31848 const stack = new CommitStackState(exportStack1).reorder([0, 1, 3, 2] as CommitRev[]);
b69ab31849 expect(stack.calculateImportStack({rewriteDate: 40})).toMatchObject([
b69ab31850 ['commit', {date: [40, 0], text: 'C'}],
b69ab31851 ['commit', {date: [40, 0], text: 'B'}],
b69ab31852 ]);
b69ab31853 });
b69ab31854
b69ab31855 it('setFile drops invalid "copyFrom"s', () => {
b69ab31856 // Commit A (x.txt) -> Commit B (y.txt, renamed from x.txt).
b69ab31857 const stack = new CommitStackState([
b69ab31858 {
b69ab31859 ...exportCommitDefault,
b69ab31860 files: {'x.txt': {data: '33'}},
b69ab31861 node: 'A_NODE',
b69ab31862 parents: [],
b69ab31863 relevantFiles: {'y.txt': null},
b69ab31864 text: 'A',
b69ab31865 },
b69ab31866 {
b69ab31867 ...exportCommitDefault,
b69ab31868 files: {'x.txt': null, 'y.txt': {data: '33', copyFrom: 'x.txt'}},
b69ab31869 node: 'B_NODE',
b69ab31870 parents: ['A_NODE'],
b69ab31871 text: 'B',
b69ab31872 },
b69ab31873 ]);
b69ab31874
b69ab31875 // Invalid copyFrom is dropped.
b69ab31876 expect(
b69ab31877 stack
b69ab31878 .setFile(0 as CommitRev, 'x.txt', f => f.set('copyFrom', 'z.txt'))
b69ab31879 .getFile(0 as CommitRev, 'x.txt').copyFrom,
b69ab31880 ).toBeUndefined();
b69ab31881
b69ab31882 // Creating "y.txt" in the parent commit (0) makes the child commit (1) drop copyFrom of "y.txt".
b69ab31883 expect(
b69ab31884 stack
b69ab31885 .setFile(0 as CommitRev, 'y.txt', f => f.merge({data: '33', flags: ''}))
b69ab31886 .getFile(1 as CommitRev, 'y.txt').copyFrom,
b69ab31887 ).toBeUndefined();
b69ab31888
b69ab31889 // Dropping "x.txt" in the parent commit (0) makes the child commit (1) not copying from "x.txt".
b69ab31890 // The content of "y.txt" is not changed.
b69ab31891 const fileY = stack
b69ab31892 .setFile(0 as CommitRev, 'x.txt', _f => ABSENT_FILE)
b69ab31893 .getFile(1 as CommitRev, 'y.txt');
b69ab31894 expect(fileY.copyFrom).toBeUndefined();
b69ab31895 expect(fileY.data).toBe('33');
b69ab31896 });
b69ab31897
b69ab31898 it('optionally skips wdir()', () => {
b69ab31899 const stack = new CommitStackState([
b69ab31900 {
b69ab31901 ...exportCommitDefault,
b69ab31902 files: {
b69ab31903 'x.txt': {data: '11'},
b69ab31904 },
b69ab31905 node: WDIR_NODE,
b69ab31906 parents: [],
b69ab31907 text: 'Temp commit',
b69ab31908 },
b69ab31909 ]).setFile(0 as CommitRev, 'x.txt', f => f.set('data', '22'));
b69ab31910 expect(stack.calculateImportStack()).toMatchInlineSnapshot(`
b69ab31911 [
b69ab31912 [
b69ab31913 "commit",
b69ab31914 {
b69ab31915 "author": "test <test@example.com>",
b69ab31916 "date": [
b69ab31917 0,
b69ab31918 0,
b69ab31919 ],
b69ab31920 "files": {
b69ab31921 "x.txt": {
b69ab31922 "data": "22",
b69ab31923 "flags": "",
b69ab31924 },
b69ab31925 },
b69ab31926 "mark": ":r0",
b69ab31927 "parents": [],
b69ab31928 "predecessors": [],
b69ab31929 "text": "Temp commit",
b69ab31930 },
b69ab31931 ],
b69ab31932 ]
b69ab31933 `);
b69ab31934 expect(stack.calculateImportStack({skipWdir: true})).toMatchInlineSnapshot(`[]`);
b69ab31935 });
b69ab31936 });
b69ab31937
b69ab31938 describe('denseSubStack', () => {
b69ab31939 it('provides bottomFiles', () => {
b69ab31940 const stack = new CommitStackState(exportStack1);
b69ab31941 let subStack = stack.denseSubStack(List([3 as CommitRev])); // C
b69ab31942 // The bottom files contains z (deleted) and its content is before deletion.
b69ab31943 expect([...subStack.bottomFiles.keys()].sort()).toEqual(['z.txt']);
b69ab31944 expect(subStack.bottomFiles.get('z.txt')?.data).toBe('22');
b69ab31945
b69ab31946 subStack = stack.denseSubStack(List([2, 3] as CommitRev[])); // B, C
b69ab31947 // The bottom files contains x (deleted), y (modified) and z (deleted).
b69ab31948 expect([...subStack.bottomFiles.keys()].sort()).toEqual(['x.txt', 'y.txt', 'z.txt']);
b69ab31949 });
b69ab31950
b69ab31951 it('marks all files at every commit as changed', () => {
b69ab31952 const stack = new CommitStackState(exportStack1);
b69ab31953 const subStack = stack.denseSubStack(List([2, 3] as CommitRev[])); // B, C
b69ab31954 // All commits (B, C) should have 3 files (x.txt, y.txt, z.txt) marked as "changed".
b69ab31955 expect(subStack.stack.map(c => c.files.size).toJS()).toEqual([3, 3]);
b69ab31956 // All file stacks (x.txt, y.txt, z.txt) should have 3 revs (bottomFile, B, C).
b69ab31957 expect(subStack.fileStacks.map(f => f.revLength).toJS()).toEqual([3, 3, 3]);
b69ab31958 });
b69ab31959 });
b69ab31960
b69ab31961 describe('insertEmpty', () => {
b69ab31962 const stack = new CommitStackState(exportStack1);
b69ab31963 const getRevs = (stack: CommitStackState) =>
b69ab31964 stack.stack.map(c => [c.rev, c.parents.toArray()]).toArray();
b69ab31965
b69ab31966 it('updates revs of commits', () => {
b69ab31967 expect(getRevs(stack)).toEqual([
b69ab31968 [0, []],
b69ab31969 [1, [0]],
b69ab31970 [2, [1]],
b69ab31971 [3, [2]],
b69ab31972 ]);
b69ab31973 expect(getRevs(stack.insertEmpty(2 as CommitRev, 'foo'))).toEqual([
b69ab31974 [0, []],
b69ab31975 [1, [0]],
b69ab31976 [2, [1]],
b69ab31977 [3, [2]],
b69ab31978 [4, [3]],
b69ab31979 ]);
b69ab31980 });
b69ab31981
b69ab31982 it('inserts at stack top', () => {
b69ab31983 expect(getRevs(stack.insertEmpty(4 as CommitRev, 'foo'))).toEqual([
b69ab31984 [0, []],
b69ab31985 [1, [0]],
b69ab31986 [2, [1]],
b69ab31987 [3, [2]],
b69ab31988 [4, [3]],
b69ab31989 ]);
b69ab31990 });
b69ab31991
b69ab31992 it('uses the provided commit message', () => {
b69ab31993 const msg = 'provided message\nfoobar';
b69ab31994 ([0, 2, 4] as CommitRev[]).forEach(i => {
b69ab31995 expect(stack.insertEmpty(i, msg).stack.get(i)?.text).toBe(msg);
b69ab31996 });
b69ab31997 });
b69ab31998
b69ab31999 it('provides unique keys for inserted commits', () => {
b69ab311000 const newStack = stack
b69ab311001 .insertEmpty(1 as CommitRev, '')
b69ab311002 .insertEmpty(1 as CommitRev, '')
b69ab311003 .insertEmpty(1 as CommitRev, '');
b69ab311004 const keys = newStack.stack.map(c => c.key);
b69ab311005 expect(keys.size).toBe(ImSet(keys).size);
b69ab311006 });
b69ab311007
b69ab311008 // The "originalNodes" are useful for split to set predecessors correctly.
b69ab311009 it('preserves the originalNodes with splitFromRev', () => {
b69ab311010 ([1, 4] as CommitRev[]).forEach(i => {
b69ab311011 const newStack = stack.insertEmpty(i, '', 2 as CommitRev);
b69ab311012 expect(newStack.get(i)?.originalNodes).toBe(stack.get(2 as CommitRev)?.originalNodes);
b69ab311013 expect(newStack.get(i)?.originalNodes?.isEmpty()).toBeFalsy();
b69ab311014 const anotherStack = stack.insertEmpty(i, '');
b69ab311015 expect(anotherStack.get(i)?.originalNodes?.isEmpty()).toBeTruthy();
b69ab311016 });
b69ab311017 });
b69ab311018 });
b69ab311019
b69ab311020 describe('applySubStack', () => {
b69ab311021 const stack = new CommitStackState(exportStack1);
b69ab311022 const subStack = stack.denseSubStack(List([2, 3] as CommitRev[]));
b69ab311023 const emptyStack = subStack.set('stack', List());
b69ab311024
b69ab311025 const getChangedFiles = (state: CommitStackState, rev: number): Array<string> => {
b69ab311026 return [...nullthrows(state.stack.get(rev as CommitRev)).files.keys()].sort();
b69ab311027 };
b69ab311028
b69ab311029 it('optimizes file changes by removing unmodified changes', () => {
b69ab311030 const newStack = stack.applySubStack(2 as CommitRev, 4 as CommitRev, subStack);
b69ab311031 expect(newStack.stack.size).toBe(stack.stack.size);
b69ab311032 // The original `stack` does not have unmodified changes.
b69ab311033 // To verify that `newStack` does not have unmodified changes, check it
b69ab311034 // against the original `stack`.
b69ab311035 stack.revs().forEach(i => {
b69ab311036 expect(getChangedFiles(newStack, i)).toEqual(getChangedFiles(stack, i));
b69ab311037 });
b69ab311038 });
b69ab311039
b69ab311040 it('drops empty commits at the end of subStack', () => {
b69ab311041 // Change the 2nd commit in subStack to empty.
b69ab311042 const newSubStack = subStack.set(
b69ab311043 'stack',
b69ab311044 subStack.stack.setIn([1, 'files'], nullthrows(subStack.stack.get(0)).files),
b69ab311045 );
b69ab311046 // `applySubStack` should drop the 2nd commit in `newSubStack`.
b69ab311047 const newStack = stack.applySubStack(2 as CommitRev, 4 as CommitRev, newSubStack);
b69ab311048 newStack.assertRevOrder();
b69ab311049 expect(newStack.stack.size).toBe(stack.stack.size - 1);
b69ab311050 });
b69ab311051
b69ab311052 it('rewrites revs for the remaining of the stack', () => {
b69ab311053 const newStack = stack.applySubStack(1 as CommitRev, 2 as CommitRev, emptyStack);
b69ab311054 newStack.assertRevOrder();
b69ab311055 [1, 2].forEach(i => {
b69ab311056 expect(newStack.stack.get(i)?.toJS()).toMatchObject({rev: i, parents: [i - 1]});
b69ab311057 });
b69ab311058 });
b69ab311059
b69ab311060 it('rewrites revs for the inserted stack', () => {
b69ab311061 const newStack = stack.applySubStack(2 as CommitRev, 3 as CommitRev, subStack);
b69ab311062 newStack.assertRevOrder();
b69ab311063 [2, 3, 4].forEach(i => {
b69ab311064 expect(newStack.stack.get(i)?.toJS()).toMatchObject({rev: i, parents: [i - 1]});
b69ab311065 });
b69ab311066 });
b69ab311067
b69ab311068 it('preserves file contents of the old stack', () => {
b69ab311069 // Add a file 'x.txt' deleted by the original stack.
b69ab311070 const newSubStack = subStack.set(
b69ab311071 'stack',
b69ab311072 List([
b69ab311073 CommitState({
b69ab311074 key: 'foo',
b69ab311075 files: ImMap([['x.txt', stack.getFile(1 as CommitRev, 'x.txt')]]),
b69ab311076 }),
b69ab311077 ]),
b69ab311078 );
b69ab311079 const newStack = stack.applySubStack(1 as CommitRev, 3 as CommitRev, newSubStack);
b69ab311080
b69ab311081 // 'y.txt' was added by the old stack, not the new stack. So it is re-added
b69ab311082 // to preserve its old content.
b69ab311083 // 'x.txt' was added by the new stack, deleted by the old stack. So it is
b69ab311084 // re-deleted.
b69ab311085 expect(getChangedFiles(newStack, 2)).toEqual(['x.txt', 'y.txt', 'z.txt']);
b69ab311086 expect(newStack.getFile(2 as CommitRev, 'y.txt').data).toBe('33');
b69ab311087 expect(newStack.getFile(2 as CommitRev, 'x.txt')).toBe(ABSENT_FILE);
b69ab311088 });
b69ab311089
b69ab311090 it('update keys to avoid conflict', () => {
b69ab311091 const oldKey = nullthrows(stack.stack.get(1)).key;
b69ab311092 const newSubStack = subStack.set('stack', subStack.stack.setIn([0, 'key'], oldKey));
b69ab311093 const newStack = stack.applySubStack(2 as CommitRev, 3 as CommitRev, newSubStack);
b69ab311094
b69ab311095 // Keys are still unique.
b69ab311096 const keys = newStack.stack.map(c => c.key);
b69ab311097 const keysSet = ImSet(keys);
b69ab311098 expect(keys.size).toBe(keysSet.size);
b69ab311099 });
b69ab311100
b69ab311101 it('drops ABSENT flag if content is not empty', () => {
b69ab311102 // x.txt was deleted by subStack rev 0 (B). We are moving it to be deleted by rev 1 (C).
b69ab311103 expect(subStack.getFile(0 as CommitRev, 'x.txt').flags).toBe(ABSENT_FILE.flags);
b69ab311104 // To break the deletion into done by 2 commits, we edit the file stack of 'x.txt'.
b69ab311105 const fileIdx = nullthrows(
b69ab311106 subStack.commitToFile.get(CommitIdx({rev: 0 as CommitRev, path: 'x.txt'})),
b69ab311107 ).fileIdx;
b69ab311108 const fileStack = nullthrows(subStack.fileStacks.get(fileIdx));
b69ab311109 // The file stack has 3 revs: (base, before deletion), (deleted at rev 0), (deleted at rev 1).
b69ab311110 expect(fileStack.convertToPlainText().toArray()).toEqual(['33', '', '']);
b69ab311111 const newFileStack = new FileStackState(['33', '3', '']);
b69ab311112 const newSubStack = subStack.setFileStack(fileIdx, newFileStack);
b69ab311113 expect(newSubStack.getUtf8Data(newSubStack.getFile(0 as CommitRev, 'x.txt'))).toBe('3');
b69ab311114 // Apply the file stack back to the main stack.
b69ab311115 const newStack = stack.applySubStack(2 as CommitRev, 4 as CommitRev, newSubStack);
b69ab311116 expect(newStack.stack.size).toBe(4);
b69ab311117 // Check that x.txt in rev 2 (B) is '3', not absent.
b69ab311118 const file = newStack.getFile(2 as CommitRev, 'x.txt');
b69ab311119 expect(file.data).toBe('3');
b69ab311120 expect(file.flags ?? '').not.toContain(ABSENT_FILE.flags);
b69ab311121
b69ab311122 // Compare the old and new file stacks.
b69ab311123 // - x.txt deletion is now by commit 'C', not 'B'.
b69ab311124 // - x.txt -> y.txt rename is preserved.
b69ab311125 expect(stack.describeFileStacks(true)).toEqual([
b69ab311126 '0:./x.txt 1:A/x.txt(33) 2:B/y.txt(33)',
b69ab311127 '0:./z.txt(11) 1:A/z.txt(22) 2:C/z.txt',
b69ab311128 ]);
b69ab311129 expect(newStack.describeFileStacks(true)).toEqual([
b69ab311130 '0:./x.txt 1:A/x.txt(33) 2:B/x.txt(3) 3:C/x.txt',
b69ab311131 '0:./z.txt(11) 1:A/z.txt(22) 2:C/z.txt',
b69ab311132 '0:A/x.txt(33) 1:B/y.txt(33)',
b69ab311133 ]);
b69ab311134 });
b69ab311135
b69ab311136 it('does not add ABSENT flag if content becomes empty', () => {
b69ab311137 // This was a herustics when `flags` are not handled properly. Now it is no longer needed.
b69ab311138 // y.txt was added by subStack rev 0 (B). We are moving it to be added by rev 1 (C).
b69ab311139 const fileIdx = nullthrows(
b69ab311140 subStack.commitToFile.get(CommitIdx({rev: 0 as CommitRev, path: 'y.txt'})),
b69ab311141 ).fileIdx;
b69ab311142 const fileStack = nullthrows(subStack.fileStacks.get(fileIdx));
b69ab311143 // The file stack has 3 revs: (base, before add), (add by rev 0), (unchanged by rev 1).
b69ab311144 expect(fileStack.convertToPlainText().toArray()).toEqual(['', '33', '33']);
b69ab311145 const newFileStack = new FileStackState(['', '', '33']);
b69ab311146 const newSubStack = subStack.setFileStack(fileIdx, newFileStack);
b69ab311147 // Apply the file stack back to the main stack.
b69ab311148 const newStack = stack.applySubStack(2 as CommitRev, 4 as CommitRev, newSubStack);
b69ab311149 // Check that y.txt in rev 2 (B) is absent, not just empty.
b69ab311150 const file = newStack.getFile(2 as CommitRev, 'y.txt');
b69ab311151 expect(file.data).toBe('');
b69ab311152 expect(file.flags).toBe('');
b69ab311153 });
b69ab311154 });
b69ab311155
b69ab311156 describe('absorb', () => {
b69ab311157 const absorbStack1: ExportStack = [
b69ab311158 {
b69ab311159 ...exportCommitDefault,
b69ab311160 immutable: true,
b69ab311161 node: 'Z_NODE',
b69ab311162 relevantFiles: {
b69ab311163 'seq.txt': {data: '0\n'},
b69ab311164 'rename_from.txt': null,
b69ab311165 },
b69ab311166 requested: false,
b69ab311167 text: 'PublicCommit',
b69ab311168 },
b69ab311169 {
b69ab311170 ...exportCommitDefault,
b69ab311171 files: {
b69ab311172 'seq.txt': {data: '0\n1\n'},
b69ab311173 'rename_from.txt': {data: '1\n'},
b69ab311174 },
b69ab311175 node: 'A_NODE',
b69ab311176 parents: ['Z_NODE'],
b69ab311177 relevantFiles: {'rename_to.txt': null},
b69ab311178 text: 'CommitA',
b69ab311179 },
b69ab311180 {
b69ab311181 ...exportCommitDefault,
b69ab311182 files: {
b69ab311183 'seq.txt': {data: '0\n1\n2\n'},
b69ab311184 'rename_to.txt': {copyFrom: 'rename_from.txt', data: '1\n'},
b69ab311185 'rename_from.txt': null,
b69ab311186 },
b69ab311187 node: 'B_NODE',
b69ab311188 parents: ['A_NODE'],
b69ab311189 text: 'CommitB',
b69ab311190 },
b69ab311191 {
b69ab311192 ...exportCommitDefault,
b69ab311193 // Working copy changes. 012 => xyz.
b69ab311194 files: {
b69ab311195 'seq.txt': {data: 'x\ny\nz\n'},
b69ab311196 'rename_to.txt': {data: 'p\n'},
b69ab311197 },
b69ab311198 node: 'WDIR',
b69ab311199 parents: ['B_NODE'],
b69ab311200 text: 'Wdir',
b69ab311201 },
b69ab311202 ];
b69ab311203
b69ab311204 it('can prepare for absorb', () => {
b69ab311205 const stack = new CommitStackState(absorbStack1);
b69ab311206 expect(stack.describeFileStacks()).toMatchInlineSnapshot(`
b69ab311207 [
b69ab311208 "0:./rename_from.txt 1:CommitA/rename_from.txt(1↵) 2:CommitB/rename_to.txt(1↵) 3:Wdir/rename_to.txt(p↵)",
b69ab311209 "0:./seq.txt(0↵) 1:CommitA/seq.txt(0↵1↵) 2:CommitB/seq.txt(0↵1↵2↵) 3:Wdir/seq.txt(x↵y↵z↵)",
b69ab311210 ]
b69ab311211 `);
b69ab311212 const stackWithAbsorb = stack.analyseAbsorb();
b69ab311213 expect(stackWithAbsorb.hasPendingAbsorb()).toBeTruthy();
b69ab311214 // The "1 => p" change in "rename_to.txt" is absorbed following file renames into rename_from.txt.
b69ab311215 // The "1 => y", "2 => z" changes in "seq.txt" are absorbed to CommitA and CommitB.
b69ab311216 // The "0 => x" change in "seq.txt" is left in the working copy as "0" is an immutable line (public commit).
b69ab311217 expect(stackWithAbsorb.describeFileStacks()).toMatchInlineSnapshot(`
b69ab311218 [
b69ab311219 "0:./rename_from.txt 1:CommitA/rename_from.txt(1↵;absorbed:p↵)",
b69ab311220 "0:./seq.txt(0↵) 1:CommitA/seq.txt(0↵1↵;absorbed:0↵y↵) 2:CommitB/seq.txt(0↵y↵2↵;absorbed:0↵y↵z↵) 3:Wdir/seq.txt(0↵y↵z↵;absorbed:x↵y↵z↵)",
b69ab311221 ]
b69ab311222 `);
b69ab311223 expect(describeAbsorbExtra(stackWithAbsorb)).toMatchInlineSnapshot(`
b69ab311224 {
b69ab311225 "0": [
b69ab311226 "0: -1↵ +p↵ Selected=1 Introduced=1",
b69ab311227 ],
b69ab311228 "1": [
b69ab311229 "0: -0↵ +x↵ Introduced=0",
b69ab311230 "1: -1↵ +y↵ Selected=1 Introduced=1",
b69ab311231 "2: -2↵ +z↵ Selected=2 Introduced=2",
b69ab311232 ],
b69ab311233 }
b69ab311234 `);
b69ab311235 });
b69ab311236
b69ab311237 const absorbStack2: ExportStack = [
b69ab311238 {
b69ab311239 ...exportCommitDefault,
b69ab311240 immutable: true,
b69ab311241 node: 'Z_NODE',
b69ab311242 relevantFiles: {
b69ab311243 'a.txt': null,
b69ab311244 },
b69ab311245 requested: false,
b69ab311246 text: 'PublicCommit',
b69ab311247 },
b69ab311248 {
b69ab311249 ...exportCommitDefault,
b69ab311250 files: {
b69ab311251 'a.txt': {data: 'a1\na2\na3\n'},
b69ab311252 },
b69ab311253 node: 'A_NODE',
b69ab311254 parents: ['Z_NODE'],
b69ab311255 relevantFiles: {'b.txt': null},
b69ab311256 text: 'CommitA',
b69ab311257 },
b69ab311258 {
b69ab311259 ...exportCommitDefault,
b69ab311260 files: {
b69ab311261 'b.txt': {data: 'b1\nb2\nb3\n'},
b69ab311262 },
b69ab311263 relevantFiles: {
b69ab311264 'a.txt': {data: 'a1\na2\na3\n'},
b69ab311265 'c.txt': {data: 'c1\nc2\nc3\n'},
b69ab311266 },
b69ab311267 node: 'B_NODE',
b69ab311268 parents: ['A_NODE'],
b69ab311269 text: 'CommitB',
b69ab311270 },
b69ab311271 {
b69ab311272 ...exportCommitDefault,
b69ab311273 files: {
b69ab311274 'a.txt': {data: 'a1\na2\na3\nx1\n'},
b69ab311275 'b.txt': {data: 'b1\nb2\nb3\ny1\n'},
b69ab311276 'c.txt': {data: 'c1\nc2\nc3\nz1\n'},
b69ab311277 },
b69ab311278 node: 'C_NODE',
b69ab311279 parents: ['B_NODE'],
b69ab311280 text: 'CommitC',
b69ab311281 },
b69ab311282 {
b69ab311283 ...exportCommitDefault,
b69ab311284 files: {
b69ab311285 'a.txt': {data: 'A1\na2\na3\nX1\n'},
b69ab311286 'b.txt': {data: 'B1\nb2\nb3\nY1\n'},
b69ab311287 'c.txt': {data: 'C1\nC2\nc3\nz1\n'},
b69ab311288 },
b69ab311289 node: 'WDIR',
b69ab311290 parents: ['C_NODE'],
b69ab311291 text: 'Wdir',
b69ab311292 },
b69ab311293 ];
b69ab311294
b69ab311295 it('provides absorb candidate revs', () => {
b69ab311296 const stack = new CommitStackState(absorbStack2).analyseAbsorb();
b69ab311297 expect(describeAbsorbExtra(stack)).toMatchInlineSnapshot(`
b69ab311298 {
b69ab311299 "0": [
b69ab311300 "0: -a1↵ +A1↵ Selected=1 Introduced=1",
b69ab311301 "1: -x1↵ +X1↵ Selected=2 Introduced=2",
b69ab311302 ],
b69ab311303 "1": [
b69ab311304 "0: -b1↵ +B1↵ Selected=1 Introduced=1",
b69ab311305 "1: -y1↵ +Y1↵ Selected=2 Introduced=2",
b69ab311306 ],
b69ab311307 "2": [
b69ab311308 "0: -c1↵ c2↵ +C1↵ C2↵ Introduced=0",
b69ab311309 ],
b69ab311310 }
b69ab311311 `);
b69ab311312 expect(describeAbsorbEditCommits(stack)).toEqual([
b69ab311313 {
b69ab311314 // The "a1 -> A1" change is applied to "CommitA" which introduced "a".
b69ab311315 // It can be applied to "CommitC" which changes "a.txt" too.
b69ab311316 // It cannot be applied to "CommitB" which didn't change "a.txt" (and
b69ab311317 // therefore not tracked by linelog).
b69ab311318 id: 'a.txt/0',
b69ab311319 diff: ['a1↵', 'A1↵'],
b69ab311320 selected: 'CommitA',
b69ab311321 candidates: ['CommitA', 'CommitC', 'Wdir'],
b69ab311322 },
b69ab311323 {
b69ab311324 // The "x1 -> X1" change is applied to "CommitC" which introduced "c".
b69ab311325 id: 'a.txt/1',
b69ab311326 diff: ['x1↵', 'X1↵'],
b69ab311327 selected: 'CommitC',
b69ab311328 candidates: ['CommitC', 'Wdir'],
b69ab311329 },
b69ab311330 {
b69ab311331 // The "b1 -> B1" change belongs to CommitB.
b69ab311332 id: 'b.txt/0',
b69ab311333 diff: ['b1↵', 'B1↵'],
b69ab311334 selected: 'CommitB',
b69ab311335 candidates: ['CommitB', 'CommitC', 'Wdir'],
b69ab311336 },
b69ab311337 {
b69ab311338 // The "y1 -> Y1" change belongs to CommitB.
b69ab311339 id: 'b.txt/1',
b69ab311340 diff: ['y1↵', 'Y1↵'],
b69ab311341 selected: 'CommitC',
b69ab311342 candidates: ['CommitC', 'Wdir'],
b69ab311343 },
b69ab311344 {
b69ab311345 // The "c1c2 -> C1C2" change is not automatically absorbed, since
b69ab311346 // "ccc" is public/immutable.
b69ab311347 id: 'c.txt/0',
b69ab311348 diff: ['c1↵c2↵', 'C1↵C2↵'],
b69ab311349 selected: undefined,
b69ab311350 // CommitC is a candidate because it modifies c.txt.
b69ab311351 candidates: ['CommitC', 'Wdir'],
b69ab311352 },
b69ab311353 ]);
b69ab311354 });
b69ab311355
b69ab311356 it('updates absorb destination commit', () => {
b69ab311357 const stack = new CommitStackState(absorbStack2).analyseAbsorb();
b69ab311358 // Current state. Note the "-a1 +A1" has "Selected=1" where 1 is the "file stack rev".
b69ab311359 expect(stack.absorbExtra.get(0)?.get(0)?.selectedRev).toBe(1);
b69ab311360 // Move the "a1 -> A1" change from CommitA to CommitC.
b69ab311361 // See the above test's "describeAbsorbExtra" to confirm that "a1 -> A1"
b69ab311362 // has fileIdx=0 and absorbEditId=0.
b69ab311363 // CommitC has rev=3.
b69ab311364 const newStack = stack.setAbsorbEditDestination(0, 0, 3 as CommitRev);
b69ab311365 // "-a1 +A1" now has "Selected=2":
b69ab311366 expect(newStack.absorbExtra.get(0)?.get(0)?.selectedRev).toBe(2);
b69ab311367 expect(describeAbsorbExtra(newStack)).toMatchInlineSnapshot(`
b69ab311368 {
b69ab311369 "0": [
b69ab311370 "0: -a1↵ +A1↵ Selected=2 Introduced=1",
b69ab311371 "1: -x1↵ +X1↵ Selected=2 Introduced=2",
b69ab311372 ],
b69ab311373 "1": [
b69ab311374 "0: -b1↵ +B1↵ Selected=1 Introduced=1",
b69ab311375 "1: -y1↵ +Y1↵ Selected=2 Introduced=2",
b69ab311376 ],
b69ab311377 "2": [
b69ab311378 "0: -c1↵ c2↵ +C1↵ C2↵ Introduced=0",
b69ab311379 ],
b69ab311380 }
b69ab311381 `);
b69ab311382 // The A1 is now absorbed at CommitC.
b69ab311383 expect(newStack.describeFileStacks()).toMatchInlineSnapshot(`
b69ab311384 [
b69ab311385 "0:./a.txt 1:CommitA/a.txt(a1↵a2↵a3↵) 2:CommitC/a.txt(a1↵a2↵a3↵x1↵;absorbed:A1↵a2↵a3↵X1↵)",
b69ab311386 "0:./b.txt 1:CommitB/b.txt(b1↵b2↵b3↵;absorbed:B1↵b2↵b3↵) 2:CommitC/b.txt(B1↵b2↵b3↵y1↵;absorbed:B1↵b2↵b3↵Y1↵)",
b69ab311387 "0:./c.txt(c1↵c2↵c3↵) 1:CommitC/c.txt(c1↵c2↵c3↵z1↵) 2:Wdir/c.txt(c1↵c2↵c3↵z1↵;absorbed:C1↵C2↵c3↵z1↵)",
b69ab311388 ]
b69ab311389 `);
b69ab311390 // It can be moved back.
b69ab311391 const newStack2 = newStack.setAbsorbEditDestination(0, 0, 1 as CommitRev);
b69ab311392 expect(newStack2.absorbExtra.get(0)?.get(0)?.selectedRev).toBe(1);
b69ab311393 // It can be moved to wdir(), the top rev.
b69ab311394 const topRev = nullthrows(newStack2.revs().at(-1));
b69ab311395 const newStack3 = newStack2.setAbsorbEditDestination(0, 0, topRev);
b69ab311396 expect(newStack3.getAbsorbCommitRevs(0, 0).selectedRev).toBe(topRev);
b69ab311397 });
b69ab311398
b69ab311399 it('updates getUtf8 with pending absorb edits', () => {
b69ab311400 const stack1 = new CommitStackState(absorbStack2).useFileStack();
b69ab311401 const get = (
b69ab311402 stack: CommitStackState,
b69ab311403 fileIdx: number,
b69ab311404 fileRev: number,
b69ab311405 considerAbsorb?: boolean,
b69ab311406 ) =>
b69ab311407 replaceNewLines(
b69ab311408 stack.getUtf8Data(
b69ab311409 FileState({data: FileIdx({fileIdx, fileRev: fileRev as FileRev})}),
b69ab311410 considerAbsorb,
b69ab311411 ),
b69ab311412 );
b69ab311413 expect(get(stack1, 0, 1)).toMatchInlineSnapshot(`"a1↵a2↵a3↵"`);
b69ab311414 // getUtf8Data considers the pending absorb (a1 -> A1).
b69ab311415 const stack2 = stack1.analyseAbsorb();
b69ab311416 expect(get(stack2, 0, 1)).toMatchInlineSnapshot(`"A1↵a2↵a3↵"`);
b69ab311417 // Can still ask for the content without absorb explicitly.
b69ab311418 expect(get(stack2, 0, 1, false)).toMatchInlineSnapshot(`"a1↵a2↵a3↵"`);
b69ab311419 });
b69ab311420
b69ab311421 it('can apply absorb edits', () => {
b69ab311422 const beforeStack = new CommitStackState(absorbStack2).useFileStack().analyseAbsorb();
b69ab311423 expect(beforeStack.useFileStack().describeFileStacks()).toMatchInlineSnapshot(`
b69ab311424 [
b69ab311425 "0:./a.txt 1:CommitA/a.txt(a1↵a2↵a3↵;absorbed:A1↵a2↵a3↵) 2:CommitC/a.txt(A1↵a2↵a3↵x1↵;absorbed:A1↵a2↵a3↵X1↵)",
b69ab311426 "0:./b.txt 1:CommitB/b.txt(b1↵b2↵b3↵;absorbed:B1↵b2↵b3↵) 2:CommitC/b.txt(B1↵b2↵b3↵y1↵;absorbed:B1↵b2↵b3↵Y1↵)",
b69ab311427 "0:./c.txt(c1↵c2↵c3↵) 1:CommitC/c.txt(c1↵c2↵c3↵z1↵) 2:Wdir/c.txt(c1↵c2↵c3↵z1↵;absorbed:C1↵C2↵c3↵z1↵)",
b69ab311428 ]
b69ab311429 `);
b69ab311430 // After `applyAbsorbEdits`, "absorbed:" contents become real contents.
b69ab311431 const afterStack = beforeStack.applyAbsorbEdits();
b69ab311432 expect(afterStack.hasPendingAbsorb()).toBeFalsy();
b69ab311433 expect(describeAbsorbExtra(afterStack)).toMatchInlineSnapshot(`{}`);
b69ab311434 expect(afterStack.useFileStack().describeFileStacks()).toMatchInlineSnapshot(`
b69ab311435 [
b69ab311436 "0:./a.txt 1:CommitA/a.txt(A1↵a2↵a3↵) 2:CommitC/a.txt(A1↵a2↵a3↵X1↵) 3:Wdir/a.txt(A1↵a2↵a3↵X1↵)",
b69ab311437 "0:./b.txt 1:CommitB/b.txt(B1↵b2↵b3↵) 2:CommitC/b.txt(B1↵b2↵b3↵Y1↵) 3:Wdir/b.txt(B1↵b2↵b3↵Y1↵)",
b69ab311438 "0:./c.txt(c1↵c2↵c3↵) 1:CommitC/c.txt(c1↵c2↵c3↵z1↵) 2:Wdir/c.txt(C1↵C2↵c3↵z1↵)",
b69ab311439 ]
b69ab311440 `);
b69ab311441 });
b69ab311442
b69ab311443 function describeAbsorbExtra(stack: CommitStackState) {
b69ab311444 return stack.absorbExtra.map(describeAbsorbIdChunkMap).toJS();
b69ab311445 }
b69ab311446
b69ab311447 function replaceNewLines(text: string): string {
b69ab311448 return text.replaceAll('\n', '↵');
b69ab311449 }
b69ab311450
b69ab311451 function describeAbsorbEditCommits(stack: CommitStackState) {
b69ab311452 const describeCommit = (rev: CommitRev) => nullthrows(stack.get(rev)).text;
b69ab311453 const result: object[] = [];
b69ab311454 stack.absorbExtra.forEach((absorbEdits, fileIdx) => {
b69ab311455 absorbEdits.forEach((absorbEdit, absorbEditId) => {
b69ab311456 const {candidateRevs, selectedRev} = stack.getAbsorbCommitRevs(fileIdx, absorbEditId);
b69ab311457 result.push({
b69ab311458 id: `${stack.getFileStackPath(fileIdx, absorbEdit.introductionRev)}/${absorbEditId}`,
b69ab311459 diff: [
b69ab311460 replaceNewLines(absorbEdit.oldLines.join('')),
b69ab311461 replaceNewLines(absorbEdit.newLines.join('')),
b69ab311462 ],
b69ab311463 candidates: candidateRevs.map(describeCommit),
b69ab311464 selected: selectedRev && describeCommit(selectedRev),
b69ab311465 });
b69ab311466 });
b69ab311467 });
b69ab311468 return result;
b69ab311469 }
b69ab311470 });
b69ab311471});