Open repositories on another machine over ssh; keep the window responsive while git runs #5

Open
szaman wants to merge 14 commits from feature/remote-ssh into master
3 changed files with 39 additions and 11 deletions
Showing only changes of commit 7b902616fe - Show all commits
@@ -35,7 +35,7 @@ one character, and no local folder with exactly that name exists. Every other te
local folder. So `C:\code\x` and `C:/code/x` stay local, `./a:b` stays local, and a local folder. So `C:\code\x` and `C:/code/x` stay local, `./a:b` stays local, and a
folder in the current folder named `notes:2024` stays local when it exists. folder in the current folder named `notes:2024` stays local when it exists.
A path on the server that starts with `~/` is handed to the server's shell with the A path on the server that is `~` or starts with `~/` is handed to the server's shell with the
`~` outside the quotes, so the shell expands it to the home folder there. `~` outside the quotes, so the shell expands it to the home folder there.
## Running git ## Running git
@@ -49,6 +49,7 @@ A path on the server that starts with `~/` is handed to the server's shell with
``` ```
ssh -o BatchMode=yes -o ConnectTimeout=10 ssh -o BatchMode=yes -o ConnectTimeout=10
-o ServerAliveInterval=5 -o ServerAliveCountMax=2
-o ControlMaster=auto -o ControlPath=<socket> -o ControlPersist=10m -o ControlMaster=auto -o ControlPath=<socket> -o ControlPersist=10m
user@myserver "git -C '/srv/app' 'log' '--format=…' …" user@myserver "git -C '/srv/app' 'log' '--format=…' …"
``` ```
@@ -57,12 +58,18 @@ A path on the server that starts with `~/` is handed to the server's shell with
has no terminal to ask in. Keys and the ssh agent work. has no terminal to ask in. Keys and the ssh agent work.
- `ConnectTimeout=10`: a server that does not answer gives an error after 10 seconds - `ConnectTimeout=10`: a server that does not answer gives an error after 10 seconds
instead of a window that waits for ever. instead of a window that waits for ever.
- `ServerAliveInterval=5`, `ServerAliveCountMax=2`: when a server stops answering
on a connection that is already open (also a shared one), ssh notices it in about
10 seconds and gives an error instead of waiting for ever.
- `ControlMaster`, `ControlPath`, `ControlPersist`: the first command opens one - `ControlMaster`, `ControlPath`, `ControlPersist`: the first command opens one
connection and leaves it open; later commands go through it and cost about one connection and leaves it open; later commands go through it and cost about one
network round trip each instead of a new login. The connection closes 10 minutes network round trip each instead of a new login. The connection closes 10 minutes
after the last command. The socket is `gites-%C` in the user's runtime folder after the last command. The socket is `gites-%C` in the user's runtime folder
(`/run/user/<id>` on Linux), or in `/tmp` where there is none; `%C` is a hash ssh (`/run/user/<id>` on Linux). Where there is none (always on macOS), it is in a
makes from the host, port and user, so the path stays short. `gites` folder in the user's cache folder, made so that only the user can open it;
never in `/tmp`. If neither folder can be found or made, these three options are
left out and every command opens a new connection. `%C` is a hash ssh makes from
the host, port and user, so the path stays short.
- On Windows, ssh cannot share connections, so these three options are left out and - On Windows, ssh cannot share connections, so these three options are left out and
every command opens a new connection. every command opens a new connection.
- Quoting: every word of the git command is put in single quotes, and a `'` inside a - Quoting: every word of the git command is put in single quotes, and a `'` inside a
@@ -107,6 +114,12 @@ If the connection drops while a repository is open, the next command shows its e
as a message at the bottom of the window and the graph that is already on screen as a message at the bottom of the window and the graph that is already on screen
stays. The next reload connects again by itself. stays. The next reload connects again by itself.
A message that reports an error stays for 8 seconds, or until it is clicked, and each
hint line above stays on its own line. Other messages go after 1.8 seconds, as before.
The commands that run git run away from the thread that draws the window, so the
window does not freeze while it waits for a slow server.
## Tests ## Tests
- Reading locations: `host:/path`, `user@host:/path`, `host:~/x`, `C:\x`, `C:/x`, a - Reading locations: `host:/path`, `user@host:/path`, `host:~/x`, `C:\x`, `C:/x`, a
+19 -7
View File
@@ -70,7 +70,7 @@ watch(
if (!settingsLoaded) return; if (!settingsLoaded) return;
clearTimeout(settingsTimer); clearTimeout(settingsTimer);
settingsTimer = window.setTimeout(() => { settingsTimer = window.setTimeout(() => {
invoke("save_settings", { value: { ...settings } }).catch((e) => showToast(`could not save settings: ${e}`)); invoke("save_settings", { value: { ...settings } }).catch((e) => showToast(`could not save settings: ${e}`, { error: true }));
}, 300); }, 300);
}, },
{ deep: true }, { deep: true },
@@ -117,7 +117,7 @@ async function load() {
if (data) await applyRepo(data); if (data) await applyRepo(data);
} catch (e) { } catch (e) {
// 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}`); if (repo.value) showToast(`could not reload: ${e}`, { error: true });
else error.value = String(e); else error.value = String(e);
} finally { } finally {
loading = false; loading = false;
@@ -131,7 +131,7 @@ async function openRepo(path: string) {
await applyRepo(await invoke<RepoData>("open_repo", { path })); await applyRepo(await invoke<RepoData>("open_repo", { path }));
} catch (e) { } catch (e) {
// The repository that was open stays open. // The repository that was open stays open.
showToast(`could not open ${path}: ${e}`); showToast(`could not open ${path}: ${e}`, { error: true });
} }
} }
@@ -177,7 +177,7 @@ watch(
invoke("save_filters", { invoke("save_filters", {
repo: repoRoot, repo: repoRoot,
value: { hidden: filters.hidden, rules: filters.rules, hideTags: filters.hideTags, collapseAll: collapseAll.value }, value: { hidden: filters.hidden, rules: filters.rules, hideTags: filters.hideTags, collapseAll: collapseAll.value },
}).catch((e) => showToast(`could not save filters: ${e}`)); }).catch((e) => showToast(`could not save filters: ${e}`, { error: true }));
}, 300); }, 300);
}, },
{ deep: true }, { deep: true },
@@ -366,10 +366,17 @@ async function copyText(text: string) {
// ---- actions ------------------------------------------------------------- // ---- actions -------------------------------------------------------------
let toastTimer: number | undefined; let toastTimer: number | undefined;
function showToast(text: string) { /** Shows a short message. An error message stays longer, 8 seconds, so there is time to
* read it; a click on any message closes it. */
function showToast(text: string, { error = false } = {}) {
toast.value = text; toast.value = text;
clearTimeout(toastTimer); clearTimeout(toastTimer);
toastTimer = window.setTimeout(() => (toast.value = null), 1800); toastTimer = window.setTimeout(() => (toast.value = null), error ? 8000 : 1800);
}
function closeToast() {
clearTimeout(toastTimer);
toast.value = null;
} }
async function copyHash() { async function copyHash() {
@@ -653,7 +660,7 @@ onUnmounted(() => {
</footer> </footer>
<transition name="fade"> <transition name="fade">
<div v-if="toast" class="toast">{{ toast }}</div> <div v-if="toast" class="toast" @click="closeToast">{{ toast }}</div>
</transition> </transition>
</div> </div>
</template> </template>
@@ -760,6 +767,11 @@ onUnmounted(() => {
padding: 6px 14px; padding: 6px 14px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 6px 24px #0008; box-shadow: 0 6px 24px #0008;
/* Keeps each line of a message on its own line, such as the hint under an ssh error. */
white-space: pre-line;
max-width: min(640px, calc(100vw - 32px));
overflow-wrap: anywhere;
cursor: pointer;
} }
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s; } .fade-enter-active, .fade-leave-active { transition: opacity 0.2s; }
.fade-enter-from, .fade-leave-to { opacity: 0; } .fade-enter-from, .fade-leave-to { opacity: 0; }
+4 -1
View File
@@ -56,7 +56,10 @@ const name = (p: string) => p.split(/[\\/]/).filter(Boolean).pop() ?? p;
/> />
<button v-else class="browse" @click="startTyping">Open on another machine…</button> <button v-else class="browse" @click="startTyping">Open on another machine…</button>
</div> </div>
<p class="hint">You can also start gites with a path: <code>gites ~/code/my-project</code></p> <p class="hint">
You can also start gites with a path: <code>gites ~/code/my-project</code>,
or with a repository on another machine: <code>gites myserver:/srv/app</code>
</p>
</div> </div>
</div> </div>
</template> </template>