Open repositories on another machine over ssh; keep the window responsive while git runs #5
+18
-16
@@ -11,7 +11,7 @@ import { layout } from "./lib/layout";
|
|||||||
import { textWidth } from "./lib/measure";
|
import { textWidth } from "./lib/measure";
|
||||||
import { colorOf } from "./lib/palette";
|
import { colorOf } from "./lib/palette";
|
||||||
import { addRecent, mergeRecents } from "./lib/recents";
|
import { addRecent, mergeRecents } from "./lib/recents";
|
||||||
import { tickets } from "./lib/latest";
|
import { repoRequests, type Request } from "./lib/latest";
|
||||||
import type { FileDiff, Layout } from "./lib/diff";
|
import type { FileDiff, Layout } from "./lib/diff";
|
||||||
import type { ChangedFile, CommitDetails, FilterState, Ref, RefKind, RepoData } from "./lib/types";
|
import type { ChangedFile, CommitDetails, FilterState, Ref, RefKind, RepoData } from "./lib/types";
|
||||||
import DiffView from "./components/DiffView.vue";
|
import DiffView from "./components/DiffView.vue";
|
||||||
@@ -107,10 +107,10 @@ let filtersLoadedFor: string | null = null;
|
|||||||
let loading = false;
|
let loading = false;
|
||||||
/**
|
/**
|
||||||
* Git runs away from the window, so replies can arrive in another order than the requests
|
* Git runs away from the window, so replies can arrive in another order than the requests
|
||||||
* went out. Every request that can put a new repository on screen (a reload or opening one)
|
* went out. Every reload and every open is noted here, and its reply is used only if
|
||||||
* takes a ticket; a reply is used only if no newer such request was sent after it.
|
* `requests.isCurrent` allows it: opening a repository wins over a reload.
|
||||||
*/
|
*/
|
||||||
const repoRequests = tickets();
|
const requests = repoRequests();
|
||||||
/** False until the first load answers, so the welcome screen does not flash up at start. */
|
/** False until the first load answers, so the welcome screen does not flash up at start. */
|
||||||
const started = ref(false);
|
const started = ref(false);
|
||||||
|
|
||||||
@@ -118,14 +118,14 @@ async function load() {
|
|||||||
// The window gets focus often; one reload at a time is enough.
|
// The window gets focus often; one reload at a time is enough.
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
const ticket = repoRequests.take();
|
const request = requests.startReload();
|
||||||
try {
|
try {
|
||||||
// null: gites was started outside a repository and none is open yet.
|
// null: gites was started outside a repository and none is open yet.
|
||||||
const data = await invoke<RepoData | null>("load_repo");
|
const data = await invoke<RepoData | null>("load_repo");
|
||||||
if (data) await applyRepo(data, ticket);
|
if (data) await applyRepo(data, request);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// A newer request replaced this one; its error no longer matters.
|
// An open replaced this reload; its error no longer matters.
|
||||||
if (!repoRequests.isLatest(ticket)) return;
|
if (!requests.isCurrent(request)) return;
|
||||||
// A repository already on screen stays, for example when a connection drops.
|
// A repository already on screen stays, for example when a connection drops.
|
||||||
if (repo.value) showToast(`could not reload: ${e}`, { error: true });
|
if (repo.value) showToast(`could not reload: ${e}`, { error: true });
|
||||||
else error.value = String(e);
|
else error.value = String(e);
|
||||||
@@ -137,14 +137,16 @@ async function load() {
|
|||||||
|
|
||||||
async function openRepo(path: string) {
|
async function openRepo(path: string) {
|
||||||
menuOpen.value = false;
|
menuOpen.value = false;
|
||||||
const ticket = repoRequests.take();
|
const request = requests.startOpen();
|
||||||
try {
|
try {
|
||||||
await applyRepo(await invoke<RepoData>("open_repo", { path }), ticket);
|
await applyRepo(await invoke<RepoData>("open_repo", { path }), request);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// A newer request replaced this one; its error no longer matters.
|
// A newer open replaced this one; its error no longer matters.
|
||||||
if (!repoRequests.isLatest(ticket)) return;
|
if (!requests.isCurrent(request)) return;
|
||||||
// The repository that was open stays open.
|
// The repository that was open stays open.
|
||||||
showToast(`could not open ${path}: ${e}`, { error: true });
|
showToast(`could not open ${path}: ${e}`, { error: true });
|
||||||
|
} finally {
|
||||||
|
requests.finish(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,16 +159,16 @@ async function browse() {
|
|||||||
|
|
||||||
const menuOpen = ref(false);
|
const menuOpen = ref(false);
|
||||||
|
|
||||||
/** Puts `data` on screen, unless a request newer than the one holding `ticket` was sent. */
|
/** Puts `data` on screen, unless `request` was replaced (see `requests`). */
|
||||||
async function applyRepo(data: RepoData, ticket: number) {
|
async function applyRepo(data: RepoData, request: Request) {
|
||||||
if (!repoRequests.isLatest(ticket)) return;
|
if (!requests.isCurrent(request)) return;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
if (filtersLoadedFor !== data.root) {
|
if (filtersLoadedFor !== data.root) {
|
||||||
const saved = await invoke<Partial<FilterState & { collapseAll: boolean }> | null>("load_filters", {
|
const saved = await invoke<Partial<FilterState & { collapseAll: boolean }> | null>("load_filters", {
|
||||||
repo: data.root,
|
repo: data.root,
|
||||||
});
|
});
|
||||||
// Another repository may have been asked for while the filters were read.
|
// Another repository may have been asked for while the filters were read.
|
||||||
if (!repoRequests.isLatest(ticket)) return;
|
if (!requests.isCurrent(request)) return;
|
||||||
filters.hidden = saved?.hidden ?? [];
|
filters.hidden = saved?.hidden ?? [];
|
||||||
filters.rules = saved?.rules ?? [];
|
filters.rules = saved?.rules ?? [];
|
||||||
filters.hideTags = saved?.hideTags ?? false;
|
filters.hideTags = saved?.hideTags ?? false;
|
||||||
|
|||||||
+50
-26
@@ -1,33 +1,57 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { tickets } from "./latest";
|
import { repoRequests } from "./latest";
|
||||||
|
|
||||||
describe("tickets", () => {
|
describe("repoRequests", () => {
|
||||||
it("treats the only ticket taken as the newest", () => {
|
it("applies a reload with no open around", () => {
|
||||||
const t = tickets();
|
const r = repoRequests();
|
||||||
const a = t.take();
|
const reload = r.startReload();
|
||||||
expect(t.isLatest(a)).toBe(true);
|
expect(r.isCurrent(reload)).toBe(true);
|
||||||
});
|
});
|
||||||
it("treats an older ticket as out of date once a newer one is taken", () => {
|
it("applies a reload that starts after an open has finished", () => {
|
||||||
const t = tickets();
|
const r = repoRequests();
|
||||||
const older = t.take();
|
const open = r.startOpen();
|
||||||
const newer = t.take();
|
r.finish(open);
|
||||||
expect(t.isLatest(older)).toBe(false);
|
const reload = r.startReload();
|
||||||
expect(t.isLatest(newer)).toBe(true);
|
expect(r.isCurrent(reload)).toBe(true);
|
||||||
});
|
});
|
||||||
it("keeps an older ticket out of date after more tickets are taken", () => {
|
it("drops a reload started during a running open, and applies the open", () => {
|
||||||
const t = tickets();
|
const r = repoRequests();
|
||||||
const first = t.take();
|
const open = r.startOpen();
|
||||||
t.take();
|
const reload = r.startReload();
|
||||||
const last = t.take();
|
// The reload answers first, while the open is still running.
|
||||||
expect(t.isLatest(first)).toBe(false);
|
expect(r.isCurrent(reload)).toBe(false);
|
||||||
expect(t.isLatest(last)).toBe(true);
|
expect(r.isCurrent(open)).toBe(true);
|
||||||
|
r.finish(open);
|
||||||
|
// Its reply may hold the repository from before the open, so it stays dropped.
|
||||||
|
expect(r.isCurrent(reload)).toBe(false);
|
||||||
});
|
});
|
||||||
it("keeps separate sets of tickets apart", () => {
|
it("drops a reload when an open starts after it", () => {
|
||||||
const a = tickets();
|
const r = repoRequests();
|
||||||
const b = tickets();
|
const reload = r.startReload();
|
||||||
const fromA = a.take();
|
const open = r.startOpen();
|
||||||
b.take();
|
expect(r.isCurrent(reload)).toBe(false);
|
||||||
b.take();
|
r.finish(open);
|
||||||
expect(a.isLatest(fromA)).toBe(true);
|
expect(r.isCurrent(reload)).toBe(false);
|
||||||
|
expect(r.isCurrent(open)).toBe(true);
|
||||||
|
});
|
||||||
|
it("applies only the newer of two opens", () => {
|
||||||
|
const r = repoRequests();
|
||||||
|
const older = r.startOpen();
|
||||||
|
const newer = r.startOpen();
|
||||||
|
expect(r.isCurrent(older)).toBe(false);
|
||||||
|
expect(r.isCurrent(newer)).toBe(true);
|
||||||
|
});
|
||||||
|
it("applies an open even when a reload started after it", () => {
|
||||||
|
const r = repoRequests();
|
||||||
|
const open = r.startOpen();
|
||||||
|
r.startReload();
|
||||||
|
expect(r.isCurrent(open)).toBe(true);
|
||||||
|
});
|
||||||
|
it("keeps separate sets of requests apart", () => {
|
||||||
|
const a = repoRequests();
|
||||||
|
const b = repoRequests();
|
||||||
|
const reload = a.startReload();
|
||||||
|
b.startOpen();
|
||||||
|
expect(a.isCurrent(reload)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+34
-10
@@ -1,17 +1,41 @@
|
|||||||
/**
|
/**
|
||||||
* Numbers requests so that only the reply to the newest one is used. Take a ticket before
|
* Decides which replies may put a repository on screen. Git runs away from the window, so
|
||||||
* sending a request; when its reply arrives, use it only if `isLatest` says no newer ticket
|
* replies can arrive in another order than the requests went out. Opening a repository wins
|
||||||
* was taken in the meantime.
|
* over a reload:
|
||||||
|
* - a reload's reply is dropped if an open was running when the reload started, if an open
|
||||||
|
* started after it, or if an open is still running when the reply arrives;
|
||||||
|
* - an open's reply is dropped only if a newer open started after it.
|
||||||
*/
|
*/
|
||||||
export interface Tickets {
|
export type Request = { kind: "open" | "reload"; number: number; duringOpen: boolean };
|
||||||
take(): number;
|
|
||||||
isLatest(ticket: number): boolean;
|
export interface RepoRequests {
|
||||||
|
startOpen(): Request;
|
||||||
|
startReload(): Request;
|
||||||
|
/** Call when an open has finished, whether its reply was used or not. */
|
||||||
|
finish(request: Request): void;
|
||||||
|
isCurrent(request: Request): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tickets(): Tickets {
|
export function repoRequests(): RepoRequests {
|
||||||
let newest = 0;
|
let counter = 0;
|
||||||
|
let newestOpen = 0;
|
||||||
|
const runningOpens = new Set<number>();
|
||||||
return {
|
return {
|
||||||
take: () => ++newest,
|
startOpen() {
|
||||||
isLatest: (ticket) => ticket === newest,
|
const number = ++counter;
|
||||||
|
newestOpen = number;
|
||||||
|
runningOpens.add(number);
|
||||||
|
return { kind: "open", number, duringOpen: false };
|
||||||
|
},
|
||||||
|
startReload() {
|
||||||
|
return { kind: "reload", number: ++counter, duringOpen: runningOpens.size > 0 };
|
||||||
|
},
|
||||||
|
finish(request) {
|
||||||
|
runningOpens.delete(request.number);
|
||||||
|
},
|
||||||
|
isCurrent(request) {
|
||||||
|
if (request.kind === "open") return request.number === newestOpen;
|
||||||
|
return !request.duringOpen && newestOpen < request.number && runningOpens.size === 0;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user