addons/isl/README.mdblame
View source
b69ab311# Interactive Smartlog
b69ab312
b69ab313Interactive Smartlog (ISL) is an embeddable, web-based GUI for Sapling.
b69ab314[See user documentation here](https://sapling-scm.com/docs/addons/isl).
b69ab315
b69ab316The code for ISL lives in the addons folder:
b69ab317
b69ab318| folder | use |
b69ab319| ---------------- | ---------------------------------------------------------- |
b69ab3110| isl | Front end UI written with React and Jotai |
b69ab3111| isl-server | Back end, which runs sl commands / interacts with the repo |
b69ab3112| isl-server/proxy | `sl web` CLI and server management |
b69ab3113| shared | Utils shared by other projects |
b69ab3114| components | Shareable component library |
b69ab3115| vscode | VS Code extension for Sapling, including ISL as a webview |
b69ab3116
b69ab3117## Development
b69ab3118
b69ab3119First run `yarn` to make sure all of the Node dependencies are installed.
b69ab3120
b69ab3121Use this command from the `addons/` folder to start ISL in development mode:
b69ab3122
b69ab3123```
b69ab3124yarn dev browser --launch .
b69ab3125```
b69ab3126
b69ab3127This does 3 things:
b69ab3128
b69ab3129- Build the client, and watch for changes (equivalent to `yarn start` in `isl/`)
b69ab3130- Build the server, and watch for changes (equivalent to `yarn watch` in `isl-server/`)
b69ab3131- Spawn a local server instance, which opens ISL in your browser (equivalent to `yarn serve` in `isl-server`, with some args). The server will open with `.` as the cwd. Use `--launch /path/to/my/repo` to use a different repository.
b69ab3132
b69ab3133The `yarn dev` command is a shorthand to running each of these in their own terminal.
b69ab3134
b69ab3135Note: the client and server build jobs will watch for changes. The webpage will hot reload as changes are made. The server must be restarted to pick up changes.
b69ab3136Press `R` when running `yarn dev browser --launch CWD` to restart the server while leaving the build running.
b69ab3137
b69ab3138### Launching an ISL Server
b69ab3139
b69ab3140To see more server output, you may sometimes want to use `yarn dev browser` WITHOUT `--launch` to build the client and server, and then launch the server yourself with `yarn serve`. This launches the local ISL server.
b69ab3141
b69ab3142**In the `isl-server/` folder, run `yarn serve --dev` to start the server and open the browser**.
b69ab3143You will have to manually restart it in order to pick up server changes.
b69ab3144This is the development mode equivalent of running `sl web`.
b69ab3145
b69ab3146This launches a WebSocket Server to proxy requests between the server and the
b69ab3147client. The entry point code lives in the `isl-server/proxy/` folder and is a
b69ab3148simple HTTP server that processes `upgrade` requests and forwards
b69ab3149them to the WebSocket Server that expects connections at `/ws`.
b69ab3150
b69ab3151Note: When the server is started, it creates a token to prevent unwanted access.
b69ab3152`--dev` opens the browser on the port used by vite in `yarn start`
b69ab3153to ensure the client connects with the right token.
b69ab3154
b69ab3155**When developing, it's useful to add a few extra arguments to `yarn serve`:**
b69ab3156
b69ab3157```
b69ab3158yarn serve --dev --force --foreground --stdout
b69ab3159```
b69ab3160
b69ab3161- `--dev`: Connect to the vite dev build's hot-reloading front-end server (defaulting to 3000), even though this server will spawn on 3001.
b69ab3162- `--force`: Kill any other active ISL server running on this port, which makes sure it's the latest version of the code.
b69ab3163- `--foreground`: instead of spawning the server in the background, run it in the foreground. `ctrl-c`-ing the `yarn serve` process will kill this server.
b69ab3164- `--stdout`: when combined with `--foreground`, prints the server logs to stdout so you can read them directly in the `yarn serve` terminal output.
b69ab3165- `--command sl`: override the command to use for `sl`, for example you might use `./sl`, or an alias to your local build like `lsl`, or `hg` for Meta-internal uses
b69ab3166
b69ab3167## Production builds
b69ab3168
b69ab3169`build-tar.py` is a script to build production bundles and
b69ab3170package them into a single self-contained `tar.xz` that can be distributed
b69ab3171along with `sl`. It can be launched by the `sl web` command.
b69ab3172
b69ab3173`yarn build` lets you build production bundles without watching for changes, in either
b69ab3174`isl/` or `isl-server/`.
b69ab3175
b69ab3176You can also use `yarn dev --production` to run both client & server `yarn build`.
b69ab3177
b69ab3178## VS Code build
b69ab3179
b69ab3180Similarly to developing in the browser, you can use this command:
b69ab3181
b69ab3182```
b69ab3183yarn dev vscode --launch .
b69ab3184```
b69ab3185
b69ab3186This again does 3 things:
b69ab3187
b69ab3188- Build the webview (client), and watch for changes (equivalent to `yarn watch-webview` in `vscode/`)
b69ab3189- Build the extension (server), and watch for changes (equivalent to `yarn watch-extension` in `vscode/`)
b69ab3190- Start VS Code in extension development mode with the given directory.
b69ab3191
b69ab3192As with the server, you may want to launch vscode yourself. Just use `yarn dev vscode` to build without launching vscode.
b69ab3193
b69ab3194See also `../vscode/CONTRIBUTING.md`.
b69ab3195
b69ab3196## Testing
b69ab3197
b69ab3198Run `yarn test` in the `isl` server to run client-side tests. These generally use
b69ab3199[React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) to "render" the UI in a node process, and check that the fake in-memory DOM is correct.
b69ab31100
b69ab31101Sometimes, this can spit out very long errors, showing the entire DOM when some element is not found.
b69ab31102You can disable this by passing HIDE_RTL_DOM_ERRORS as an env var:
b69ab31103`HIDE_RTL_DOM_ERRORS=1 yarn test`
b69ab31104
b69ab31105# Goals
b69ab31106
b69ab31107ISL is designed to be an opinionated UI. It does not implement every single feature or argument that the CLI supports.
b69ab31108Rather, it implements an intuitive UI by leveraging a subset of features of the `sl` CLI.
b69ab31109
b69ab31110ISL aims to optimize common workflows and provide an intuitive UX around some advanced workflows.
b69ab31111
b69ab31112- **Opinionated**: ISL is opinionated about the "right" way to work.
b69ab31113 This includes using stacks, amending commits, using one-commit-per-PR, rebasing to merge.
b69ab31114- **Simple**: ISL hides unnecessary details and aims to be beginner-friendly.
b69ab31115 Each new button added to the UI makes it more intimidating to new users.
b69ab31116- **User concepts, not machine concepts**:
b69ab31117 ISL hides implementation details to present source control in a way a human would understand it.
b69ab31118 The salient example of this is not showing commit hashes in the UI by default.
b69ab31119 Hashes are needed to refer to commits when typing in a CLI, but
b69ab31120 ISL prefers being able to just click directly on commits, thus we don't need to show the hash by default.
b69ab31121 Other examples of this include drag & drop to rebase, and showing PR info directly under a commit by leaning on one-PR-per-commit.
b69ab31122- **Previews & Smoothness**: The UI should let you preview what action you'll take. It shows an optimistic
b69ab31123 version of the result of each command so the UI feels instant. We aim to avoid the UI _jumping_ between
b69ab31124 states as a result of async data fetches
b69ab31125- **Documentation & Transparency**: The UI uses tooltips and other signals to show you what every button will do.
b69ab31126 It always confirms before running dangerous commands. It shows exactly what CLI command is being run, so you
b69ab31127 could do it yourself and trust what it's doing.
b69ab31128
b69ab31129# Internals
b69ab31130
b69ab31131The following sections describe how ISL is implemented.
b69ab31132
b69ab31133## Build / Bundling
b69ab31134
b69ab31135- All parts of ISL (client, server, vscode extension) are built with vite/rollup, which produces javascript/css bundles.
b69ab31136 This includes node_modules inside the bundle, which means we don't need to worry about including node_modules in builds.
b69ab31137- `sl web` is a normal `sl` python command, which invokes the latest ISL built CLI.
b69ab31138 `isl-server/proxy/run-proxy.ts` is the typescript entry point which is spawned by Python via `node`.
b69ab31139 In development mode, you interact directly with `run-proxy` rather than dealing with `sl web`.
b69ab31140 Note: there are slightly differences between the python `sl web` CLI args and the `run-proxy` CLI args.
b69ab31141 In general, `run-proxy` exposes more options, most of which aren't needed by normal `sl web` users.
b69ab31142
b69ab31143## Architecture
b69ab31144
b69ab31145ISL uses an embeddable Client / Server architecture.
b69ab31146
b69ab31147- The Client runs in a browser-like context (web browser, VS Code webview, Electron renderer)
b69ab31148- The Server runs in a node-like context (node server from `sl web`, VS Code extension host, Electron main)
b69ab31149
b69ab31150The server serves the client's static (html/js/css) files via HTTP.
b69ab31151The client JavaScript then connects back to the server via WebSocket,
b69ab31152where both sides can send and receive messages to communicate.
b69ab31153
b69ab31154### Client
b69ab31155
b69ab31156The client renders the UI and asks the server to actually do stuff. The client has no direct access
b69ab31157to the filesystem or repository. The client can make normal web requests, but does not have access tokens
b69ab31158to make authenticated requests to GitHub.
b69ab31159
b69ab31160The client uses React (for rendering the UI) and [Jotai](https://jotai.org/) (for state management).
b69ab31161We use a combination of regular CSS and [StyleX](https://stylexjs.com/) for styling.
b69ab31162
b69ab31163### Server
b69ab31164
b69ab31165The server is able to interact with the file system, spawn processes, run `sl commands`,
b69ab31166and make authenticated network requests to GitHub.
b69ab31167The server is also responsible for watching the repository for changes.
b69ab31168This will optionally use Watchman if it's installed.
b69ab31169If not, the server falls back to a polling mechanism, which polls on a variable frequency
b69ab31170which depends on if the UI is focused and visible.
b69ab31171
b69ab31172The server shells out to the `gh` CLI to make authenticated requests to GitHub.
b69ab31173
b69ab31174Most of the server's work is done by the `Repository` object, which represents a single Sapling repository.
b69ab31175This object also delegates to manage Watchman subscriptions and GitHub fetching.
b69ab31176
b69ab31177### Server reuse and sharing
b69ab31178
b69ab31179To support running `sl web` in multiple repos / cwds at the same time, ISL supports reusing server instances.
b69ab31180When spawning an ISL server, if the port is already in use by an ISL server, that server will be reused.
b69ab31181
b69ab31182Since the server acts like a normal http web server, it supports multiple clients connecting at the same time,
b69ab31183both the static resources and WebSocket connections.
b69ab31184
b69ab31185`Repository` instances inside the server are cached per repo root.
b69ab31186`RepositoryCache` manages Repositories by reference counting.
b69ab31187A `Repository` does not have its own cwd set. Rather, each reference to a `Repository`
b69ab31188via `RepositoryCache` has an associated cwd. This way, A single `Repository` instance is reused
b69ab31189even if accessed from multiple cwds within the same repo.
b69ab31190We treat each WebSocket connection as its own cwd, and each WebSocket connections has one reference
b69ab31191to a shared Repository via RepositoryCache.
b69ab31192
b69ab31193Connecting multiple clients to the same sever at the same cwd is also supported.
b69ab31194Server-side fetched data is sent to all relevant (same repo) clients, not just the one that made a request.
b69ab31195Note that client-side cached data is not shared, which means optimistic state may not work as well
b69ab31196in a second window for operations triggered in a different window.
b69ab31197
b69ab31198After all clients are disconnected, the server auto-shutdowns after one minute with no remaining repositories
b69ab31199which helps ensure that old ISL servers aren't reused.
b69ab31200
b69ab31201Note that ISL exposes `--kill` and `--force` options to kill old servers and force a fresh server, to make
b69ab31202it easy to work around unexpectedly reusing old ISL servers.
b69ab31203
b69ab31204### Security
b69ab31205
b69ab31206The client sends messages to the server to run `sl` commands.
b69ab31207We must authenticate clients to ensure arbitrary websites or XSS attacks can't connect on localhost:3011 to run commands.
b69ab31208The approach we take is to generate a cryptographic token when a server is started.
b69ab31209Connecting via WebSocket to the server requires this token.
b69ab31210The token is included in the url generated by `sl web`, which allows URLs from `sl web` to connect successfully.
b69ab31211
b69ab31212Because of this token, restarting the ISL server requires clicking a fresh link to use the new token.
b69ab31213Once an ISL server stops running, its token is no longer valid.
b69ab31214
b69ab31215In order to support reusing ISL servers, we must persist the server's token to disk,
b69ab31216so that later `sl web` invocations can find the right token to use.
b69ab31217This persisted data includes the token but also some other metadata about the server,
b69ab31218which is written to a permission-restricted file.
b69ab31219
b69ab31220Detail: we have a second token we use to verify that a server running on a port
b69ab31221is actually an ISL server, to prevent misleading/phishing "reuses" of a server.
b69ab31222
b69ab31223## Embedding
b69ab31224
b69ab31225ISL is designed to be embedded in multiple contexts. `sl web` is the default,
b69ab31226which is also the most complicated due to server reuse and managing tokens.
b69ab31227
b69ab31228The Sapling VS Code extension's ISL webview is another example of an embedding.
b69ab31229Other embeddings are possible, such as an Electron / Tauri standalone app, or
b69ab31230other IDE extensions such as Android Studio.
b69ab31231
b69ab31232### Platform
b69ab31233
b69ab31234To support running in multiple contexts, ISL has the notion of a Platform,
b69ab31235on both the client and server, which contains embedding-specific implementations
b69ab31236of a common API.
b69ab31237
b69ab31238This includes things like opening a file. In the browser, the best we can do is use the OS default.
b69ab31239Inside the VS Code extension, we always want to open with VS Code.
b69ab31240Each platform can implement this to match their UX best.
b69ab31241The Client's platform is where platform-specific code first runs. Some embeddings
b69ab31242have their client platform send platform-specific messages to the server platform.
b69ab31243
b69ab31244The "default" platform is the BrowserPlatform, used by `sl web`.
b69ab31245
b69ab31246Custom platforms can be implemented either by:
b69ab31247
b69ab31248- including platform code in the build process (the VS Code extension does this)
b69ab31249- adding a new platform to isl-server for use by `run-proxy`'s `--platform` option (Android Studio does this)
b69ab31250
b69ab31251## Syncing repository state
b69ab31252
b69ab31253ISL started as a way to automatically re-run `sl status` and `sl smartlog` in a loop.
b69ab31254The UI should always feel up-to-date, even though it needs to run these commands
b69ab31255to actually fetch the data.
b69ab31256The client subscribes to this data, which the server is in charge of fetching automatically.
b69ab31257The server uses Watchman (if installed) to detect when:
b69ab31258
b69ab31259- the `.sl/dirstate` has changed to indicate the list of commits has changed, so we should re-run `sl log`.
b69ab31260- any normal file in the repository has changed, so we should re-run `sl status` to look for uncommitted changes.
b69ab31261 If Watchman is not installed, `sl log` and `sl status` are polled on an interval by `WatchForChanges` and based on window focus.
b69ab31262
b69ab31263Similarly, the server fetches new data from GitHub when the list of PRs changes, and refreshes by polling.
b69ab31264
b69ab31265## Running Operations
b69ab31266
b69ab31267ISL defines an "Operation" as any mutating `sl` command, such as `sl pull`, `sl rebase`, `sl goto`, `sl amend`, `sl add`, etc. Non-examples include `sl status`, `sl log`, `sl cat`, `sl diff`.
b69ab31268
b69ab31269The lifecycle of an operation looks like this:
b69ab31270
b69ab31271```
b69ab31272Ready to run -> Preview -> Queued -> Running -> Optimistic state -> Completed
b69ab31273```
b69ab31274
b69ab31275### Preview Appliers
b69ab31276
b69ab31277Critically, fetching data via `sl log` and `sl status` is separate from running operations.
b69ab31278We only get the "new" state of the world after _both_ the operation has completed _AND_
b69ab31279`sl log` / `sl status` has run to provide us with the latest data.
b69ab31280
b69ab31281This would cause the UI to appear laggy and out of date.
b69ab31282Thus, we support using previews and optimistic to update the UI immediately.
b69ab31283
b69ab31284To support this, ISL defines a "`preview applier`" function for every operation.
b69ab31285The preview applier function describes how the DAG of commits and uncommitted changes
b69ab31286would change as a result of running this operation.
b69ab31287(Detail: there's actually a separate preview applier function for uncommitted changes and the commit DAG
b69ab31288to ensure UI smoothness if `sl log` and `sl status` return data at different times)
b69ab31289
b69ab31290This supports both:
b69ab31291
b69ab31292- **previews**: What would the DAG look like if I ran this command?
b69ab31293 - e.g. Drag & drop rebase preview before clicking "run rebase"
b69ab31294- **optimistic state**: How should we pretend the DAG looks while this command is running?
b69ab31295 - e.g. showing result of a rebase while rebase command is running
b69ab31296
b69ab31297Because `sl log` and `sl status` are run separately from an operation running,
b69ab31298the optimistic state preview applier must be used not just while the operation is running,
b69ab31299but also _after_ it finishes up until we get new data from `sl log` / `sl status`.
b69ab31300
b69ab31301### Queued commands
b69ab31302
b69ab31303Preview Appliers are functions which take a commit DAG and return a new commit DAG.
b69ab31304This allows us to stack the result of preview appliers on top of each other.
b69ab31305This trivially enables _Queued Commands_, which work like `&&` on the CLI.
b69ab31306
b69ab31307If an operation is ongoing, and we click a button to run another,
b69ab31308it is queued up by the server to run next.
b69ab31309The client then renders the DAG resulting from first running Operation 1's preview applier,
b69ab31310then running Operation 2's preview applier.
b69ab31311
b69ab31312Important detail here: if an operation references a commit hash, the queued version
b69ab31313of that operation will not yet know the new hash after the previous operation finishes.
b69ab31314For example, `sl amend` in the middle of a stack, then `sl goto` the top of the stack.
b69ab31315Thus, when telling the server to run an Operation we tag which args are revsets,
b69ab31316so they are replaced with `max(successors(${revset}))` so the hash is replaced
b69ab31317with the latest successor hash. If you intentionally target an obsolete commit, then the hash is used directly.
b69ab31318
b69ab31319## Internationalization
b69ab31320
b69ab31321ISL has a built-in i18n system, however the only language currently implemented is `en-US` English.
b69ab31322`t()` and `<T>` functions convert English strings or keys into values for other languages in the `isl/i18n/${languageCode}` folders. To add support for a new language, add a new `isl/i18n/${languageCode}/common.js`
b69ab31323and provide translations for all the strings found by grepping for `t()` and `<T>` in `isl`.
b69ab31324This system can be improved later as new languages are supported.
b69ab31325
b69ab31326# Debugging
b69ab31327
b69ab31328## ✅ Attaching ISL server to VS Code debugger
b69ab31329
b69ab31330There's a "Run & Debug isl-server" vscode build action which runs `yarn serve --dev` for you with a few additional arguments. When spawned from here, you can use breakpoints in VS Code to step through your server-side code.
b69ab31331
b69ab31332Note that you should have the client & server rollup compilation jobs (described above) running before doing this (it currently won't compile for you, just launch `yarn serve`).
b69ab31333
b69ab31334## ❓ Attaching ISL client to a debugger
b69ab31335
b69ab31336Attaching the client to VS Code debugger does not work as well as the server side.
b69ab31337There is currently no launch task to launch the browser and connect to the debugger.
b69ab31338You can try using "Debug: Open Link" from the command palette, and paste in the ISL server link
b69ab31339(with the token included), but I found breakpoint line numbers don't match up correctly.
b69ab31340
b69ab31341You can open the chrome devtools, go to sources, search for files, and set breakpoints in there,
b69ab31342which will mostly work. `debugger;` statements also work in the dev tools.
b69ab31343
b69ab31344## Stack traces
b69ab31345
b69ab31346If you encounter a stack trace in production, it will be referencing minified line numbers like:
b69ab31347
b69ab31348```txt
b69ab31349Error: something went wrong
b69ab31350 at t (/some/production/path/to/isl-server/dist/run-proxy.js:1:4152)
b69ab31351```
b69ab31352
b69ab31353We build/ship with source maps that sit next to source files, like `isl-server/dist/run-proxy.js.map`.
b69ab31354
b69ab31355You can use these source maps to recover the real stack trace, using a tool like [stacktracify](https://github.com/mifi/stacktracify).
b69ab31356
b69ab31357```sh
b69ab31358$ npm install -g stacktracify
b69ab31359# copy minified stack trace to clipboard, then give the path to the source map:
b69ab31360$ stacktracify /path/to/isl-server/dist/run-proxy.js.map
b69ab31361Error: something went wrong
b69ab31362 at from (webpack://isl-server/proxy/proxyUtils.ts:14:22)
b69ab31363```
b69ab31364
b69ab31365Note that the source map you use must match the version in the original stack trace.
b69ab31366Usually, you can tell the version by the path in the stack trace.
b69ab31367
b69ab31368## Profiling bundle sizes and dependencies
b69ab31369
b69ab31370**Client:**
b69ab31371To analyze the client bundle size (code splitting and dependencies, etc):
b69ab31372
b69ab31373- `cd isl`
b69ab31374- `npx vite-bundle-visualizer`
b69ab31375
b69ab31376Should also work in `vscode/` for the webview code.
b69ab31377
b69ab31378**Server:**
b69ab31379Install [rollup-plugin-visualizer](https://www.npmjs.com/package/rollup-plugin-visualizer)
b69ab31380and add it to the server's rollup.config.mjs, then `yarn build` and inspect the stats.html file.
b69ab31381
b69ab31382Should also work for the vscode extension config.