UPDATE: split the webapp in a widget and the app itself

This splits the webapp in:

* IronCalc (the widget to be published on npmjs)
* The frontend for our "service"
* Adds "dummy code" for the backend using sqlite
This commit is contained in:
Nicolás Hatcher
2025-01-07 18:17:06 +01:00
committed by Nicolás Hatcher Andrés
parent 378f8351d3
commit 8215cfc9fb
121 changed files with 7997 additions and 1347 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
target/* target/*
.DS_Store **/node_modules/**
.DS_Store

8
Cargo.lock generated
View File

@@ -389,7 +389,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc" name = "ironcalc"
version = "0.2.0" version = "0.3.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -405,7 +405,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.2.0" version = "0.3.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -679,7 +679,7 @@ dependencies = [
[[package]] [[package]]
name = "pyroncalc" name = "pyroncalc"
version = "0.1.2" version = "0.3.0"
dependencies = [ dependencies = [
"ironcalc", "ironcalc",
"pyo3", "pyo3",
@@ -953,7 +953,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm" name = "wasm"
version = "0.1.3" version = "0.3.0"
dependencies = [ dependencies = [
"ironcalc_base", "ironcalc_base",
"serde", "serde",

View File

@@ -10,6 +10,7 @@ members = [
exclude = [ exclude = [
"generate_locale", "generate_locale",
"webapp/app.ironcalc.com/server",
] ]
[profile.release] [profile.release]

View File

@@ -2,7 +2,8 @@
lint: lint:
cargo fmt -- --check cargo fmt -- --check
cargo clippy --all-targets --all-features -- -W clippy::unwrap_used -W clippy::expect_used -W clippy::panic -D warnings cargo clippy --all-targets --all-features -- -W clippy::unwrap_used -W clippy::expect_used -W clippy::panic -D warnings
cd webapp && npm install && npm run check cd webapp/IronCalc/ && npm install && npm run check
cd webapp/app.ironcalc.com/frontend/ && npm install && npm run check
.PHONY: format .PHONY: format
format: format:
@@ -15,7 +16,7 @@ tests: lint
# Regretabbly we need to build the wasm twice, once for the nodejs tests # Regretabbly we need to build the wasm twice, once for the nodejs tests
# and a second one for the vitest. # and a second one for the vitest.
cd bindings/wasm/ && wasm-pack build --target nodejs && node tests/test.mjs && make cd bindings/wasm/ && wasm-pack build --target nodejs && node tests/test.mjs && make
cd webapp && npm run test cd webapp/IronCalc/ && npm run test
cd bindings/python && ./run_tests.sh && ./run_examples.sh cd bindings/python && ./run_tests.sh && ./run_examples.sh
.PHONY: remove-artifacts .PHONY: remove-artifacts

View File

@@ -1,11 +1,11 @@
[package] [package]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.2.0" version = "0.3.0"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"] authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2021" edition = "2021"
homepage = "https://www.ironcalc.com" homepage = "https://www.ironcalc.com"
repository = "https://github.com/ironcalc/ironcalc/" repository = "https://github.com/ironcalc/ironcalc/"
description = "The democratization of spreadsheets" description = "Open source spreadsheet engine"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "pyroncalc" name = "pyroncalc"
version = "0.1.2" version = "0.3.0"
edition = "2021" edition = "2021"
@@ -12,7 +12,7 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.2.0" } xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.3.0" }
pyo3 = { version = "0.22.3", features = ["extension-module"] } pyo3 = { version = "0.22.3", features = ["extension-module"] }

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "ironcalc" name = "ironcalc"
version = "0.1.2" version = "0.3.0"
description = "Create, edit and evaluate Excel spreadsheets" description = "Create, edit and evaluate Excel spreadsheets"
requires-python = ">=3.10" requires-python = ">=3.10"
keywords = [ keywords = [

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "wasm" name = "wasm"
version = "0.1.3" version = "0.3.0"
authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"] authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"]
description = "IronCalc Web bindings" description = "IronCalc Web bindings"
license = "MIT/Apache-2.0" license = "MIT/Apache-2.0"
@@ -14,7 +14,7 @@ crate-type = ["cdylib"]
# Uses `../ironcalc/base` when used locally, and uses # Uses `../ironcalc/base` when used locally, and uses
# the inicated version from crates.io when published. # the inicated version from crates.io when published.
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations # https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
ironcalc_base = { path = "../../base", version = "0.2", features = ["use_regex_lite"] } ironcalc_base = { path = "../../base", version = "0.3", features = ["use_regex_lite"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = "0.2.92" wasm-bindgen = "0.2.92"
serde-wasm-bindgen = "0.4" serde-wasm-bindgen = "0.4"

2
webapp/IronCalc/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/*
dist/*

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,15 @@
{ {
"name": "frontend", "name": "ironcalc",
"private": true, "private": true,
"version": "0.2.0", "version": "0.3.0",
"type": "module", "type": "module",
"main": "./dist/ironcalc.js",
"module": "./dist/ironcalc.js",
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "vite build && tsc",
"preview": "vite preview",
"check": "biome check ./src", "check": "biome check ./src",
"check-write": "biome check --write ./src", "check-write": "biome check --write ./src",
"test": "vitest run" "test": "vitest run"
@@ -14,24 +17,30 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5", "@emotion/styled": "^11.11.5",
"@ironcalc/wasm": "file:../bindings/wasm/pkg", "@ironcalc/wasm": "file:../../bindings/wasm/pkg",
"@mui/material": "^5.15.15", "@mui/material": "^5.15.15",
"i18next": "^23.11.1", "i18next": "^23.11.1",
"lucide-react": "^0.427.0", "lucide-react": "^0.427.0",
"react": "^18.2.0",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-i18next": "^13.5.0" "react-i18next": "^13.5.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.8.3", "@biomejs/biome": "1.8.3",
"@types/react": "^18.2.75",
"@types/react-dom": "^18.2.24",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.4.5", "typescript": "~5.6.2",
"vite": "^5.2.8", "vite": "^6.0.5",
"vite-plugin-svgr": "^4.2.0", "vite-plugin-svgr": "^4.2.0",
"vitest": "^2.0.5" "vitest": "^2.0.5",
"react": "^18.0.0"
},
"peerDependencies": {
"@types/react": "^18.0.0",
"react": "^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
} }
} }

View File

@@ -0,0 +1,21 @@
import "./index.css";
import type { Model } from "@ironcalc/wasm";
import ThemeProvider from "@mui/material/styles/ThemeProvider";
import Workbook from "./components/workbook.tsx";
import { WorkbookState } from "./components/workbookState.ts";
import { theme } from "./theme.ts";
import "./i18n";
interface IronCalcProperties {
model: Model;
}
function IronCalc(properties: IronCalcProperties) {
return (
<ThemeProvider theme={theme}>
<Workbook model={properties.model} workbookState={new WorkbookState()} />
</ThemeProvider>
);
}
export default IronCalc;

View File

@@ -5,9 +5,7 @@ import type {
WorksheetProperties, WorksheetProperties,
} from "@ironcalc/wasm"; } from "@ironcalc/wasm";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
import { PaintRoller } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import ReactDOMServer from "react-dom/server";
import SheetTabBar from "./SheetTabBar/SheetTabBar"; import SheetTabBar from "./SheetTabBar/SheetTabBar";
import { import {
COLUMN_WIDTH_SCALE, COLUMN_WIDTH_SCALE,
@@ -143,16 +141,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
// FIXME: This is so that the cursor indicates there are styles to be pasted // FIXME: This is so that the cursor indicates there are styles to be pasted
const el = rootRef.current?.getElementsByClassName("sheet-container")[0]; const el = rootRef.current?.getElementsByClassName("sheet-container")[0];
if (el) { if (el) {
// Taken from lucide icons: <PaintRoller /> and rotated.
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-paint-roller" style="transform:rotate(-8deg)"><rect width="16" height="6" x="2" y="2" rx="2"></rect><path d="M10 16v-2a2 2 0 0 1 2-2h8a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"></path><rect width="4" height="6" x="8" y="16" rx="1"></rect></svg>`;
(el as HTMLElement).style.cursor = (el as HTMLElement).style.cursor =
`url('data:image/svg+xml;utf8,${encodeURIComponent( `url('data:image/svg+xml;utf8,${encodeURIComponent(svg)}'), auto`;
ReactDOMServer.renderToString(
<PaintRoller
width={24}
height={24}
style={{ transform: "rotate(-8deg)" }}
/>,
),
)}'), auto`;
} }
}; };

View File

Before

Width:  |  Height:  |  Size: 869 B

After

Width:  |  Height:  |  Size: 869 B

View File

Before

Width:  |  Height:  |  Size: 538 B

After

Width:  |  Height:  |  Size: 538 B

View File

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 498 B

View File

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 498 B

View File

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 586 B

View File

Before

Width:  |  Height:  |  Size: 539 B

After

Width:  |  Height:  |  Size: 539 B

View File

Before

Width:  |  Height:  |  Size: 513 B

After

Width:  |  Height:  |  Size: 513 B

View File

Before

Width:  |  Height:  |  Size: 542 B

After

Width:  |  Height:  |  Size: 542 B

View File

Before

Width:  |  Height:  |  Size: 538 B

After

Width:  |  Height:  |  Size: 538 B

View File

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 290 B

View File

Before

Width:  |  Height:  |  Size: 539 B

After

Width:  |  Height:  |  Size: 539 B

View File

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 659 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1004 B

After

Width:  |  Height:  |  Size: 1004 B

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 929 B

After

Width:  |  Height:  |  Size: 929 B

View File

Before

Width:  |  Height:  |  Size: 726 B

After

Width:  |  Height:  |  Size: 726 B

View File

Before

Width:  |  Height:  |  Size: 725 B

After

Width:  |  Height:  |  Size: 725 B

View File

Before

Width:  |  Height:  |  Size: 706 B

After

Width:  |  Height:  |  Size: 706 B

View File

Before

Width:  |  Height:  |  Size: 706 B

After

Width:  |  Height:  |  Size: 706 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -0,0 +1,5 @@
import init, { Model } from "@ironcalc/wasm";
import IronCalc from "./IronCalc";
import { IronCalcIcon, IronCalcLogo } from "./icons";
export { init, Model, IronCalc, IronCalcIcon, IronCalcLogo };

View File

@@ -1,5 +1,4 @@
import { createTheme } from "@mui/material/styles"; import { createTheme } from "@mui/material/styles";
import "./fonts.css";
export const theme = createTheme({ export const theme = createTheme({
typography: { typography: {

View File

@@ -5,13 +5,15 @@
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
"declaration": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "node",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "emitDeclarationOnly": true,
// "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Linting */ /* Linting */
@@ -19,7 +21,8 @@
"noUnusedLocals": false, "noUnusedLocals": false,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"esModuleInterop": true "esModuleInterop": true,
"outDir": "dist",
}, },
"include": ["src"], "include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]

View File

@@ -1,6 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"declaration": true,
"skipLibCheck": true, "skipLibCheck": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",

View File

@@ -0,0 +1,37 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import { resolve } from 'node:path';
// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'IronCalc',
// the proper extensions will be added
fileName: 'ironcalc',
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ['react', 'react-dom', '@ironcalc/wasm'],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
react: 'React',
'react-dom': 'ReactDOM',
'@ironcalc/wasm': 'IronCalc',
},
},
},
},
plugins: [react(), svgr()],
server: {
fs: {
// Allow serving files from one level up to the project root
allow: ['..'],
},
},
});

View File

@@ -0,0 +1,7 @@
:2080
# rocket API
reverse_proxy /api/* 127.0.0.1:8000
# everything else is the frontend
reverse_proxy :5173

View File

@@ -0,0 +1,44 @@
# IronCalc service application
This directory contains the code (frontend and backend) to run the code deployed at https://app.ironcalc.com
## Development build:
1. Run in this folder `caddy run` (that just just a proxy for the front end and backend).
You will need to leave it running all the time.
2. In the server folder run `cargo run`
3. In the frontend folder `npm install` and `npm run dev`
That's it if you point your browser to localhost:2080 you should see the app.
Note that step three involves alo building thw wasm bindings and the widget
## Deployment
The development environment is very close to a deployment environment.
### Build the server binary:
In the server directory run:
```
cargo build --release
```
You will find a single binary in target/release/ironcalc_server
### Build the frontend files
In the frontend folder:
```
npm install
npm run build
```
That will create a bunch of files that you should copy to your server
## TODO
Deployment details, brotli, logs, stats, Postgres, systemctl files, ...

View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist/*
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,17 @@
# IronCalc application
This is the front end deployed at https:://app.ironcalc.com
To build for production:
```
npm install
npm run build
```
A development build:
```
npm run dev
```

View File

@@ -0,0 +1,19 @@
{
"organizeImports": { "enabled": true },
"linter": {
"enabled": true,
"rules": {
"correctness": { "noUnusedImports": "error" }
}
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2
},
"css": {
"formatter": {
"enabled": true
}
}
}

View File

@@ -12,6 +12,5 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
<script defer data-domain="app.ironcalc.com" src="https://plausible.io/js/script.js"></script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
{
"name": "frontend",
"private": true,
"version": "0.3.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"check": "biome check ./src",
"check-write": "biome check --write ./src"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@ironcalc/ironcalc": "file:../../IronCalc",
"@mui/material": "^6.3.1",
"lucide": "^0.469.0",
"lucide-react": "^0.469.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "~5.6.2",
"vite": "^6.0.5",
"vite-plugin-svgr": "^4.2.0"
}
}

View File

@@ -1,15 +1,12 @@
import "./App.css"; import "./App.css";
import Workbook from "./components/workbook";
import "./i18n";
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import init, { Model } from "@ironcalc/wasm";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FileBar } from "./AppComponents/FileBar"; import { FileBar } from "./components/FileBar";
import { import {
get_documentation_model, get_documentation_model,
get_model, get_model,
uploadFile, uploadFile,
} from "./AppComponents/rpc"; } from "./components/rpc";
import { import {
createNewModel, createNewModel,
deleteSelectedModel, deleteSelectedModel,
@@ -17,15 +14,13 @@ import {
saveModelToStorage, saveModelToStorage,
saveSelectedModelInStorage, saveSelectedModelInStorage,
selectModelFromStorage, selectModelFromStorage,
} from "./AppComponents/storage"; } from "./components/storage";
import { WorkbookState } from "./components/workbookState";
import { IronCalcIcon } from "./icons"; // From IronCalc
import { IronCalc, IronCalcIcon, Model, init } from "@ironcalc/ironcalc";
function App() { function App() {
const [model, setModel] = useState<Model | null>(null); const [model, setModel] = useState<Model | null>(null);
const [workbookState, setWorkbookState] = useState<WorkbookState | null>(
null,
);
useEffect(() => { useEffect(() => {
async function start() { async function start() {
@@ -60,12 +55,11 @@ function App() {
const newModel = loadModelFromStorageOrCreate(); const newModel = loadModelFromStorageOrCreate();
setModel(newModel); setModel(newModel);
} }
setWorkbookState(new WorkbookState());
} }
start(); start();
}, []); }, []);
if (!model || !workbookState) { if (!model) {
return ( return (
<Loading> <Loading>
<IronCalcIcon style={{ width: 24, height: 24, marginBottom: 16 }} /> <IronCalcIcon style={{ width: 24, height: 24, marginBottom: 16 }} />
@@ -114,7 +108,7 @@ function App() {
} }
}} }}
/> />
<Workbook model={model} workbookState={workbookState} /> <IronCalc model={model} />
</Wrapper> </Wrapper>
); );
} }

View File

@@ -1,8 +1,9 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import type { Model } from "@ironcalc/wasm"; import type { Model } from "@ironcalc/ironcalc";
import { IronCalcIcon, IronCalcLogo } from "@ironcalc/ironcalc";
import { CircleCheck } from "lucide-react"; import { CircleCheck } from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { IronCalcIcon, IronCalcLogo } from "./../icons"; // import { IronCalcIcon, IronCalcLogo } from "./../icons";
import { FileMenu } from "./FileMenu"; import { FileMenu } from "./FileMenu";
import { ShareButton } from "./ShareButton"; import { ShareButton } from "./ShareButton";
import { WorkbookTitle } from "./WorkbookTitle"; import { WorkbookTitle } from "./WorkbookTitle";

View File

@@ -14,7 +14,6 @@ function UploadFileDialog(properties: {
const { onModelUpload } = properties; const { onModelUpload } = properties;
useEffect(() => { useEffect(() => {
const root = document.getElementById("root");
if (crossRef.current) { if (crossRef.current) {
crossRef.current.focus(); crossRef.current.focus();
} }

View File

@@ -1,4 +1,4 @@
import { Model } from "@ironcalc/wasm"; import { Model } from "@ironcalc/ironcalc";
import { base64ToBytes, bytesToBase64 } from "./util"; import { base64ToBytes, bytesToBase64 } from "./util";
const MAX_WORKBOOKS = 50; const MAX_WORKBOOKS = 50;

Some files were not shown because too many files have changed in this diff Show More