diff --git a/pyproject.toml b/pyproject.toml index aa541c3..e4b56ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "webdiff" -version = "1.3.0b1" +version = "1.3.0b2" description = "Two-column web-based git difftool" license = "Apache-2.0" readme = "README.md" diff --git a/testdata/jsondiffs/left/ospd5.summary.json b/testdata/jsondiffs/left/ospd5.summary.json new file mode 100644 index 0000000..faca24c --- /dev/null +++ b/testdata/jsondiffs/left/ospd5.summary.json @@ -0,0 +1 @@ +{"id":8503055,"num_reps":5505497728300038000000,"elapsed_s":27775981.582439058,"failures":["sredetiltnapsres","stngeiaedrlpsdes","gresenapdtilsres","dresenalstipares","gresenalstipares","gresenalstipores","dresenilstapares","dresenilstapores","dresenilstapures","gressnaletipdres","gntseaieslrdopes","sttseiaenrlpsges","plsdeaiertnrsges","plsteaiertnrsges","psldeaiertnrsges","spldeaiertnrsges","spldeiaetrntsges","splseaiertnrsdes","splseaiertnrsges","slpseaierntrsdes","slpseaierntrsges","slpseaiertnrsges","slpseiaerntrsdes","dresenapstilares","gresenapstilares","gresenapstilores","dplcseainrtngies","dresenilgtapsres","stndeiaenrlpsges","stngeiaenrlpsges","gsedtnileratspes","slpseaierntrgdes","gntseaieplrdseas","gntseaieplrdsees","plcdeaiertnrsges","dplseaiertnrsges","mplseaiertnrsges","sresetipdnalsges","plcseaiertnrsges","dlpseaiertnrsges","gredenalstipares","gredenalstipores","hsebetalsripgnes","hsedetalsripgnes","tsebetalsripgnes","tsedetalsripgnes"],"elim_level":[0,15455,177058,5063716,76688548,461765945,746413471,1341313341,1163044169,683351596,332798842,146849412,75414967,51931121],"secs_by_level":[3674055.3433500067,410511.63218999945,1173934.825350049,3012839.132659852,5235705.436329976,4241101.513749921,4354658.159489981,2102788.1823800365,669439.2463199788,109737.61249999885,25531.954199997952,4647.2012399998175,1373.4651100000929],"bounds":[17529004948,13770052786,11002135202,8716185865,6917888192,5476876187,4192563351,3051811248,2128082619,1431049972,956438815,668040946,500412197],"depth":[0,19950,615593,14344534,148852615,670318189,1798495841,2584907222,2213849795,1122244756,511943456,205264471,96606475,62028585],"boards_to_test":16527857205,"init_nodes":21169090330328,"total_nodes":216126848966789,"tree_bytes":426388045692928,"total_bytes":574688434585600,"n_bound":9429491482,"n_force":2101423756,"max_multi":1312378509,"bound_secs":[0,26.31909999999997,762.3841000000707,13895.171989995355,96300.35142999486,278184.6050899924,509398.4086700136,489545.5812600387,261858.04687000782,96741.63683000114,42098.825499994186,16502.548799997923,8129.411579999221,6838.873439999473],"test_secs":859209.9650399276,"best_board":[3827,"splseaiertnrsges"]} diff --git a/testdata/jsondiffs/right/ospd5.summary.json b/testdata/jsondiffs/right/ospd5.summary.json new file mode 100644 index 0000000..c6d6c96 --- /dev/null +++ b/testdata/jsondiffs/right/ospd5.summary.json @@ -0,0 +1,144 @@ +{ + "id": 8503055, + "num_reps": 5505497728300038000000, + "elapsed_s": 27775981.582439058, + "failures": [ + "sredetiltnapsres", + "stngeiaedrlpsdes", + "gresenapdtilsres", + "dresenalstipares", + "gresenalstipares", + "gresenalstipores", + "dresenilstapares", + "dresenilstapores", + "dresenilstapures", + "gressnaletipdres", + "gntseaieslrdopes", + "sttseiaenrlpsges", + "plsdeaiertnrsges", + "plsteaiertnrsges", + "psldeaiertnrsges", + "spldeaiertnrsges", + "spldeiaetrntsges", + "splseaiertnrsdes", + "splseaiertnrsges", + "slpseaierntrsdes", + "slpseaierntrsges", + "slpseaiertnrsges", + "slpseiaerntrsdes", + "dresenapstilares", + "gresenapstilares", + "gresenapstilores", + "dplcseainrtngies", + "dresenilgtapsres", + "stndeiaenrlpsges", + "stngeiaenrlpsges", + "gsedtnileratspes", + "slpseaierntrgdes", + "gntseaieplrdseas", + "gntseaieplrdsees", + "plcdeaiertnrsges", + "dplseaiertnrsges", + "mplseaiertnrsges", + "sresetipdnalsges", + "plcseaiertnrsges", + "dlpseaiertnrsges", + "gredenalstipares", + "gredenalstipores", + "hsebetalsripgnes", + "hsedetalsripgnes", + "tsebetalsripgnes", + "tsedetalsripgnes" + ], + "elim_level": [ + 0, + 15455, + 177058, + 5063716, + 76688548, + 461765945, + 746413471, + 1341313341, + 1163044169, + 683351596, + 332798842, + 146849412, + 75414967, + 51931121 + ], + "secs_by_level": [ + 3674055.3433500067, + 410511.63218999945, + 1173934.825350049, + 3012839.132659852, + 5235705.436329976, + 4241101.513749921, + 4354658.159489981, + 2102788.1823800365, + 669439.2463199788, + 109737.61249999885, + 25531.954199997952, + 4647.2012399998175, + 1373.4651100000929 + ], + "bounds": [ + 17529004948, + 13770052786, + 11002135202, + 8716185865, + 6917888192, + 5476876187, + 4192563351, + 3051811248, + 2128082619, + 1431049972, + 956438815, + 668040946, + 500412197 + ], + "depth": [ + 0, + 19950, + 615593, + 14344534, + 148852615, + 670318189, + 1798495841, + 2584907222, + 2213849795, + 1122244756, + 511943456, + 205264471, + 96606475, + 62028585 + ], + "boards_to_test": 16527857205, + "init_nodes": 21169090330328, + "total_nodes": 216126848966789, + "tree_bytes": 426388045692928, + "total_bytes": 574688434585600, + "n_bound": 9429491482, + "n_force": 2101423756, + "max_multi": 1312378509, + "bound_secs": [ + 0, + 26.31909999999997, + 762.3841000000707, + 13895.171989995355, + 96300.35142999486, + 278184.6050899924, + 509398.4086700136, + 489545.5812600387, + 261858.04687000782, + 96741.63683000114, + 42098.825499994186, + 16502.548799997923, + 8129.411579999221, + 6838.873439999473 + ], + "test_secs": 859209.9650399276, + "best_board": [ + 3827, + "splseaiertnrsges" + ] +} diff --git a/testdata/jsondiffs/right/tsconfig.json b/testdata/jsondiffs/right/tsconfig.json index 4971d4b..79a6927 100644 --- a/testdata/jsondiffs/right/tsconfig.json +++ b/testdata/jsondiffs/right/tsconfig.json @@ -1,6 +1,6 @@ { + /* Note that this file isn't really JSON! It's JSONC. */ "compilerOptions": { - /* Note that this file isn't really JSON! */ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ @@ -8,7 +8,7 @@ "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, /* Specify library files to be included in the compilation. */ "lib": [ - "es2021", + "es2021", "dom" ] , // "allowJs": true, /* Allow javascript files to be compiled. */ diff --git a/ts/CodeDiffContainer.tsx b/ts/CodeDiffContainer.tsx index 7aa8ac1..05d93ce 100644 --- a/ts/CodeDiffContainer.tsx +++ b/ts/CodeDiffContainer.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {DiffOptions, encodeDiffOptions} from './diff-options'; +import {GitDiffOptions, gitDiffOptionsToFlags} from './diff-options'; import {CodeDiff, PatchOptions} from './codediff/codediff'; import {guessLanguageUsingContents, guessLanguageUsingFileName} from './codediff/language'; import {GitConfig} from './options'; @@ -53,18 +53,19 @@ export interface ImageDiffData { } // A "no changes" sign which only appears when applicable. -export function NoChanges(props: {filePair: FilePair}) { - const {filePair} = props; +export function NoChanges(props: {filePair: FilePair; isEqualAfterNormalization: boolean}) { + const {filePair, isEqualAfterNormalization} = props; + let msg = null; if (filePair.no_changes) { - return
(File content is identical)
; + msg = <>(File content is identical); + } else if (isEqualAfterNormalization) { + msg = <>(File content is identical after normalization); } else if (filePair.is_image_diff && filePair.are_same_pixels) { - return ( -
- Pixels are the same, though file content differs (perhaps the headers are different?) -
+ msg = ( + <>Pixels are the same, though file content differs (perhaps the headers are different?) ); } - return null; + return msg ?
{msg}
: null; } // Either side can be empty (i.e. an add or a delete), in which case getOrNull resolves to null. @@ -84,7 +85,7 @@ async function getOrNull(side: string, path: string, normalizeJSON: boolean) { export interface CodeDiffContainerProps { filePair: FilePair; - diffOptions: Partial; + diffOptions: Partial; normalizeJSON: boolean; } @@ -96,6 +97,8 @@ export function CodeDiffContainer(props: CodeDiffContainerProps) { >(); React.useEffect(() => { + // It would be more correct to set contents=undefined here to get a loading state, + // but this produces an unnecessary flash for rapid transitions. const getDiff = async () => { const response = await fetch(`/diff/${filePair.idx}`, { method: 'POST', @@ -104,7 +107,7 @@ export function CodeDiffContainer(props: CodeDiffContainerProps) { 'Content-Type': 'application/json', }, body: JSON.stringify({ - options: encodeDiffOptions(diffOptions), + options: gitDiffOptionsToFlags(diffOptions), normalize_json: normalizeJSON, }), }); @@ -127,6 +130,10 @@ export function CodeDiffContainer(props: CodeDiffContainerProps) { }); }, [filePair, diffOptions, normalizeJSON]); + const isEqualAfterNormalization = React.useMemo(() => { + return !filePair.no_changes && normalizeJSON && contents && contents.before == contents.after; + }, [contents, filePair.no_changes, normalizeJSON]); + return (
@@ -136,6 +143,7 @@ export function CodeDiffContainer(props: CodeDiffContainerProps) { contentsBefore={contents.before} contentsAfter={contents.after} diffOps={contents.diffOps} + isEqualAfterNormalization={!!isEqualAfterNormalization} /> ) : ( 'Loading…' @@ -150,6 +158,7 @@ interface FileDiffProps { contentsBefore: string | null; contentsAfter: string | null; diffOps: DiffRange[]; + isEqualAfterNormalization: boolean; } function extractFilename(path: string) { @@ -164,7 +173,7 @@ function lengthOrZero(data: unknown[] | string | null | undefined) { } function FileDiff(props: FileDiffProps) { - const {filePair, contentsBefore, contentsAfter, diffOps} = props; + const {filePair, contentsBefore, contentsAfter, diffOps, isEqualAfterNormalization} = props; const pathBefore = filePair.a; const pathAfter = filePair.b; // build the diff view and add it to the current DOM @@ -201,7 +210,7 @@ function FileDiff(props: FileDiffProps) { return (
- + ; - setOptions: (newOptions: Partial) => void; + options: Partial; + updateOptions: UpdateOptionsFn; defaultMaxDiffWidth: number; - maxDiffWidth: number; - setMaxDiffWidth: (maxDiffWidth: number) => void; isVisible: boolean; setIsVisible: (isVisible: boolean) => void; } @@ -51,47 +51,44 @@ const popupStyle: React.CSSProperties = { fontFamily: 'sans-serif', }; +type BooleanOptions = Extract, {v: boolean}>['k']; + export function DiffOptionsControl(props: Props) { - const {options, setOptions, isVisible, setIsVisible, maxDiffWidth, setMaxDiffWidth} = props; + const {options, updateOptions, isVisible, setIsVisible} = props; + const maxDiffWidth = options.maxDiffWidth ?? props.defaultMaxDiffWidth; const togglePopup = () => { setIsVisible(!isVisible); }; - const toggleIgnoreAllSpace = () => { - setOptions({...options, ignoreAllSpace: !options.ignoreAllSpace}); - }; - const toggleIgnoreSpaceChange = () => { - setOptions({...options, ignoreSpaceChange: !options.ignoreSpaceChange}); - }; - const toggleFunctionContext = () => { - setOptions({...options, functionContext: !options.functionContext}); + const toggleField = (k: BooleanOptions) => () => { + updateOptions(options => ({[k]: !options[k]})); }; const setUnifiedContext: React.ChangeEventHandler = e => { - setOptions({...options, unified: e.currentTarget.valueAsNumber}); + updateOptions({unified: e.currentTarget.valueAsNumber}); }; const changeDiffAlgorithm: React.ChangeEventHandler = e => { - setOptions({...options, diffAlgorithm: e.currentTarget.value as DiffAlgorithm}); + updateOptions({diffAlgorithm: e.currentTarget.value as DiffAlgorithm}); }; const changeMaxDiffWidth: React.ChangeEventHandler = e => { - setMaxDiffWidth(e.currentTarget.valueAsNumber); + updateOptions({maxDiffWidth: e.currentTarget.valueAsNumber}); }; React.useEffect(() => { const handleKeydown = (e: KeyboardEvent) => { if (!isLegitKeypress(e)) return; if (e.code == 'KeyW') { - setOptions({...options, ignoreAllSpace: !options.ignoreAllSpace}); + updateOptions(options => ({ignoreAllSpace: !options.ignoreAllSpace})); } else if (e.code == 'KeyB') { - setOptions({...options, ignoreSpaceChange: !options.ignoreSpaceChange}); + updateOptions(options => ({ignoreSpaceChange: !options.ignoreSpaceChange})); } }; document.addEventListener('keydown', handleKeydown); return () => { document.removeEventListener('keydown', handleKeydown); }; - }, [options, setOptions]); + }, [options, updateOptions]); - const diffOptsStr = encodeDiffOptions(options).join(' '); + const diffOptsStr = gitDiffOptionsToFlags(options).join(' '); return ( <> @@ -108,7 +105,7 @@ export function DiffOptionsControl(props: Props) { - + + +
+ Whitespace: @@ -116,7 +113,7 @@ export function DiffOptionsControl(props: Props) { type="checkbox" checked={!!options.ignoreAllSpace} id="ignore-all-space" - onChange={toggleIgnoreAllSpace} + onChange={toggleField('ignoreAllSpace')} />{' '}
+ {' '} + +
Context: @@ -157,7 +165,7 @@ export function DiffOptionsControl(props: Props) { type="checkbox" checked={!!options.functionContext} id="function-context" - onChange={toggleFunctionContext} + onChange={toggleField('functionContext')} />{' '}