Skip to content

Commit

Permalink
👍 Add prompts/editors proxy feature
Browse files Browse the repository at this point in the history
Gin does what guise.vim and askpass.vim does so that users don't need to
install guise.vim nor askpass.vim

It uses `GIT_EDITOR` and `GIT_ASKPASS` enviroment variables
  • Loading branch information
lambdalisue committed Mar 15, 2022
1 parent 94af015 commit b434dbc
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 22 deletions.
10 changes: 1 addition & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ without announcements.**

## Features

- Proxy the prompts/editor used by git commands to Vim
- `Gin` to call a raw git command
- `GinBranch` to see `git branch` of a repository
- `GinChaperon` to solve git conflicts (like `git mergetool`)
Expand All @@ -29,16 +30,7 @@ Gin is written in denops thus users need to install denops.vim
- [vim-denops/denops.vim][vim-denops/denops.vim]<br> An ecosystem for writing
Vim/Neovim plugin in Deno.

Additionally, the following Vim/Neovim plugins are highly recommended to use:

- [lambdalisue/guise.vim][lambdalisue/guise.vim]<br> To open a new tabpage to
edit a commit message on `Gin commit`.
- [lambdalisue/askpass.vim][lambdalisue/askpass.vim]<br> To input SSH key
passphrase on `Gin push` or so on.

[vim-denops/denops.vim]: https://github.com/vim-denops/denops.vim
[lambdalisue/guise.vim]: https://github.com/lambdalisue/guise.vim
[lambdalisue/askpass.vim]: https://github.com/lambdalisue/askpass.vim

## Similar projects

Expand Down
42 changes: 42 additions & 0 deletions denops/gin/core/proxy/askpass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env -S deno run --no-check --allow-env=GIN_PROXY_ADDRESS --allow-net=127.0.0.1
import { streams } from "../../deps.ts";

const resultPattern = /^([^:]+):(.*)$/;

const addr = JSON.parse(Deno.env.get("GIN_PROXY_ADDRESS") ?? "null");
if (!addr) {
throw new Error("GIN_PROXY_ADDRESS environment variable is required");
}

const prompt = Deno.args[0];
if (!prompt) {
throw new Error("No prompt is specified to the askpass");
}

const encoder = new TextEncoder();
const decoder = new TextDecoder();

const conn = await Deno.connect(addr);
await streams.writeAll(conn, encoder.encode(`askpass:${prompt}`));
await conn.closeWrite();
const result = decoder.decode(await streams.readAll(conn));
conn.close();

const m = result.match(resultPattern);
if (!m) {
throw new Error(`Unexpected result '${result}' is received`);
}

const [status, value] = m.slice(1);
switch (status) {
case "ok":
console.log(value);
Deno.exit(0);
/* fall through */
case "err":
console.error(value);
Deno.exit(1);
/* fall through */
default:
throw new Error(`Unexpected status '${status}' is received`);
}
41 changes: 41 additions & 0 deletions denops/gin/core/proxy/editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env -S deno run --no-check --allow-env=GIN_PROXY_ADDRESS --allow-net=127.0.0.1
import { streams } from "../../deps.ts";

const resultPattern = /^([^:]+):(.*)$/;

const addr = JSON.parse(Deno.env.get("GIN_PROXY_ADDRESS") ?? "null");
if (!addr) {
throw new Error("GIN_PROXY_ADDRESS environment variable is required");
}

const filename = Deno.args[0];
if (!filename) {
throw new Error("No filename is specified to the editor");
}

const encoder = new TextEncoder();
const decoder = new TextDecoder();

const conn = await Deno.connect(addr);
await streams.writeAll(conn, encoder.encode(`editor:${filename}`));
await conn.closeWrite();
const result = decoder.decode(await streams.readAll(conn));
conn.close();

const m = result.match(resultPattern);
if (!m) {
throw new Error(`Unexpected result '${result}' is received`);
}

const [status, value] = m.slice(1);
switch (status) {
case "ok":
Deno.exit(0);
/* fall through */
case "err":
console.error(value);
Deno.exit(1);
/* fall through */
default:
throw new Error(`Unexpected status '${status}' is received`);
}
8 changes: 8 additions & 0 deletions denops/gin/core/proxy/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Denops } from "../../deps.ts";
import { listen } from "./server.ts";

export function main(denops: Denops): void {
listen(denops).catch((e) =>
console.error(`Unexpected error occured in the proxy server: ${e}`)
);
}
147 changes: 147 additions & 0 deletions denops/gin/core/proxy/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {
anonymous,
autocmd,
batch,
deferred,
Denops,
fn,
option,
path,
streams,
unknownutil,
vars,
} from "../../deps.ts";
import { decodeUtf8, encodeUtf8 } from "../../util/text.ts";
import * as buffer from "../../util/buffer.ts";

const recordPattern = /^([^:]+?):(.*)$/;

export async function listen(denops: Denops): Promise<void> {
const listener = Deno.listen({
hostname: "127.0.0.1",
port: 0,
});
const [disableAskpass, disableEditor] = await batch.gather(
denops,
async (denops) => {
await vars.g.get(denops, "gin_proxy_disable_askpass");
await vars.g.get(denops, "gin_proxy_disable_editor");
},
);
await batch.batch(denops, async (denops) => {
await vars.e.set(
denops,
"GIN_PROXY_ADDRESS",
JSON.stringify(listener.addr),
);
if (!unknownutil.ensureBoolean(disableAskpass ?? false)) {
await vars.e.set(
denops,
"GIT_ASKPASS",
path.fromFileUrl(new URL("askpass.ts", import.meta.url)),
);
}
if (!unknownutil.ensureBoolean(disableEditor ?? false)) {
await vars.e.set(
denops,
"GIT_EDITOR",
path.fromFileUrl(new URL("editor.ts", import.meta.url)),
);
}
});
for await (const conn of listener) {
handleConnection(denops, conn).catch((e) => console.error(e));
}
}

async function handleConnection(
denops: Denops,
conn: Deno.Conn,
): Promise<void> {
const record = decodeUtf8(await streams.readAll(conn));
const m = record.match(recordPattern);
if (!m) {
throw new Error(`Unexpected record '${record}' received`);
}
const [name, value] = m.slice(1);
try {
switch (name) {
case "askpass":
await handleAskpass(denops, conn, value);
break;
case "editor":
await handleEditor(denops, conn, value);
break;
default:
throw new Error(`Unexpected record prefix '${name}' received`);
}
} finally {
conn.close();
}
}

async function handleAskpass(
denops: Denops,
conn: Deno.Conn,
prompt: string,
): Promise<void> {
try {
const value = await fn.inputsecret(denops, prompt);
await streams.writeAll(conn, encodeUtf8(`ok:${value}`));
} catch (e: unknown) {
await streams.writeAll(conn, encodeUtf8(`err:${e}`));
}
}

async function handleEditor(
denops: Denops,
conn: Deno.Conn,
filename: string,
): Promise<void> {
try {
await edit(denops, filename);
await streams.writeAll(conn, encodeUtf8("ok:"));
} catch (e: unknown) {
await streams.writeAll(conn, encodeUtf8(`err:${e}`));
}
}

async function edit(
denops: Denops,
filename: string,
): Promise<void> {
await denops.cmd("silent noswapfile tab drop `=filename` | edit", {
filename,
});
const [winid, bufnr] = await batch.gather(denops, async (denops) => {
await fn.win_getid(denops);
await fn.bufnr(denops);
});
unknownutil.assertNumber(winid);
unknownutil.assertNumber(bufnr);
const auname = `gin_editor_${winid}_${bufnr}`;
const waiter = deferred<void>();
const [waiterId] = anonymous.once(denops, async () => {
await autocmd.group(denops, auname, (helper) => {
helper.remove();
});
waiter.resolve();
});
await buffer.ensure(denops, bufnr, async () => {
await batch.batch(denops, async (denops) => {
await option.bufhidden.setLocal(denops, "wipe");
await autocmd.group(denops, auname, (helper) => {
helper.remove();
helper.define(
["BufWipeout", "VimLeave"],
"*",
`call denops#request('gin', '${waiterId}', [])`,
{
once: true,
},
);
});
});
});
await waiter;
}
4 changes: 3 additions & 1 deletion denops/gin/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ export * as vars from "https://deno.land/x/denops_std@v3.1.4/variable/mod.ts";
export * as unknownutil from "https://deno.land/x/unknownutil@v2.0.0/mod.ts";

export * as fs from "https://deno.land/std@0.128.0/fs/mod.ts";
export * as io from "https://deno.land/std@0.128.0/io/mod.ts";
export * as path from "https://deno.land/std@0.128.0/path/mod.ts";
export { deadline } from "https://deno.land/std@0.128.0/async/mod.ts";
export * as streams from "https://deno.land/std@0.128.0/streams/mod.ts";
export { deadline, deferred } from "https://deno.land/std@0.128.0/async/mod.ts";
2 changes: 2 additions & 0 deletions denops/gin/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Denops } from "./deps.ts";

import { main as mainAction } from "./core/action/main.ts";
import { main as mainBare } from "./core/bare/main.ts";
import { main as mainProxy } from "./core/proxy/main.ts";
import { main as mainUtil } from "./core/util/main.ts";

import { main as mainChaperon } from "./feat/chaperon/main.ts";
Expand All @@ -14,6 +15,7 @@ import { main as mainStatus } from "./feat/status/main.ts";
export function main(denops: Denops): void {
mainAction(denops);
mainBare(denops);
mainProxy(denops);
mainUtil(denops);

mainChaperon(denops);
Expand Down
46 changes: 34 additions & 12 deletions doc/gin.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,6 @@ denops.vim~
An ecosystem for writing Vim/Neovim plugin in Deno.
https://github.com/vim-denops/denops.vim

Additionally, the following Vim/Neovim plugins are highly recommended to use:

*gin-guise*
guise.vim~
To open a new |tabpage| to edit a commit message on "Gin commit".
https://github.com/lambdalisue/guise.vim~

*gin-askpass*
askpass.vim~
To input SSH key passphrase on "Gin push" or so on.
https://github.com/lambdalisue/askpass.vim


=============================================================================
USAGE *gin-usage*
Expand Down Expand Up @@ -72,6 +60,30 @@ Users have to define alternative mappings to disable default mappings like:
See |gin-actions| for all actions available.

-----------------------------------------------------------------------------
PROXY *gin-proxy*

Gin proxies prompts and editors used by git commands to running Vim by
overriding "GIT_ASKPASS" and "GIT_EDITOR" environment variables.
This means that whether you invoke "git commit" via the "Gin" command or via
the Vim |terminal|, a new buffer will be opened instead of Vim being launched
nested.

This feature is almost equivalent to askpass.vim and/or guise.vim, but
simplified to focus on git.

askpass.vim~
https://github.com/lambdalisue/askpass.vim

guise.vim~
https://github.com/lambdalisue/guise.vim

It can live together with the plugins mentioned above because the environment
variable names used are different.

Use |g:gin_proxy_disable_askpass| and/or |g:gin_proxy_disable_editor| to
disable this proxy feature.


=============================================================================
INTERFACE *gin-interface*
Expand Down Expand Up @@ -287,6 +299,16 @@ VARIABLES *gin-variables*

Default: 0

*g:gin_proxy_disable_askpass*
Disable overriding "GIT_ASKPASS" to proxy prompts.

Default: 0

*g:gin_proxy_disable_editor*
Disable overriding "GIT_EDITOR" to proxy editors.

Default: 0

*g:gin_status_disable_default_mappings*
Disable default mappings on `:GinStatus` buffer.

Expand Down

0 comments on commit b434dbc

Please sign in to comment.