Git用户管理进阶:Deno CLI的应用实践
为什么要切换Git用户呢?
通常来说,我们在GitLab上使用工作邮箱,而GitHub、Gitee等网站使用我们的个人邮箱。我们的电脑上,Git账号一般都是全局设置个人邮箱,在公司的项目里,再设置工作邮箱。
而有时候,有的电脑上是配置的这个,有的配置是那个,在提交记录里就有些混乱了,个人项目还好,团队协作中就显得很不专业了。
怎么切换Git用户呢?
切换Git用户的几种方法
方法1:Git命令
我们知道,全局设置用户是这样的:
git config --global user.name XXX
git config --global user.email XXX@xxx.com
那么某个工程设置用户只需要去掉--global
即可:
git config user.name XXX
git config user.email XXX@xxx.com
但每次都执行这两句命令也挺麻烦的,有没有更便捷的方法呢?
方法2:sh脚本
最简单是建个sh的脚本文件,每次都执行一次。
#!/bin/sh
git config user.name "XXX"
git config user.email "XXX@xxx.com"
缺点是每次都得把这个文件复制一遍,那样跟把两个命令合并成一行有什么区别?
方法3:Deno脚本
另一种就是用Deno写个脚本,在子任务里执行这两句:
export async function runTask(str: string): Promise<string> {
const [cmd, ...args] = str.split(" ");
const command = new Deno.Command(cmd, {
args,
});
const { code, stdout, stderr } = await command.output();
const te = new TextDecoder();
if (code !== 0) {
throw new Error(te.decode(stderr));
}
return te.decode(stdout);
}
export async function runTasks(arr: string[]) {
for (const str of arr) {
await runTask(str);
console.log(`运行任务success:${str}`);
}
}
if (import.meta.main) {
await runTasks([
`git config user.name "XXX"`,
`git config user.email "XXX@xxx.com"`,
]);
}
我们使用deno install
把它安装为全局的命令,然后就可以到处使用了。
缺点是这个文件在本地还好,如果把它发布到CDN上,里面再包含我们的用户名,有种暴露隐私的感觉。如果Git全局设置的是工作邮箱,用它来切换个人邮箱可能比较合适。
方法4:Git includeif
Git配置文件支持include
和includeif
关键字来包含其它配置文件,对我们这种需求,可以使用includeif
。
首先,打开当前用户的根目录下,找到.gitconfig
文件:
$ cat ~/.gitconfig
# 下面是我的文件内容,之前git config --global user.name修改的就是这里
[user]
name = jw397
email = jw397@126.com
[init]
defaultBranch = main
在下面加入以下代码:
[includeIf "gitdir:~/wk/gitlab/"]
path = .gitconfig-gitlab
在当前目录下,新建一个.gitconfig-gitlab
文件:
$ vim .gitconfig-gitlab
[user]
name = 测试
email = test@test.com
这样之后在~/wk/gitlab/
这个目录下,所有的项目默认就是新配置的用户名和邮箱了,除非你单独使用git config user.name
设置过,在当前目录下使用这个命令,本质上修改的是.git/config
文件。
手写一个CLI进行切换
本来上面的方法4已经很完美了,为什么还要折腾一个新的CLI呢?
因为我是按代码类型分的文件夹,这样的话需要重新整理一遍(虽然也不是不可以):
上面Deno脚本的方案,用户名、邮箱如果修改为可以动态配置的,就比较合理了。对于这个简单的功能,我们可以选择将数据存储到JSON文件里,也可以存储到LocalStorage
里,当然,也可以使用目前Deno正在主推的本地数据库Deno.kv
。
刚好我想试用一个Deno.kv
,所以决定模仿nrm
实现一个简单的切换Git脚本管理器,就叫gum
(Git User Manager)吧。
命令无非是增(add)、删(del)、改(改就算了,直接用add替换)、查(list)、切换(use)。
数据存储
存储3个字段,使用alias作为key值:
interface GitUser {
username: string;
email: string;
alias: string; // 用作key值
}
先打开Deno.kv:
import os from "node:os";
import { join } from "https://deno.land/std@0.202.0/path/mod.ts";
import { ensureFile } from "https://deno.land/std@0.202.0/fs/ensure_file.ts";
const PREFIX = "git_user";
const kvPath = join(os.homedir(), ".deno", "kv", PREFIX);
await ensureFile(kvPath); // 如果不存在则创建
const kv = await Deno.openKv(kvPath); // 指定路径后,版本升级数据也不会丢失
之所以这么复杂,是因为需要指定kv文件的存储路径,如果不指定的话,默认位置在Deno的缓存目录的location_data/${hash}/kv.sqlite3
,后续版本升级hash值变化,数据就丢失了。
再封装几个函数:
async function getUserList(): Promise<GitUser[]> {
const entries = kv.list<GitUser>({ prefix: [PREFIX] });
const users: GitUser[] = [];
for await (const entry of entries) {
users.push(entry.value);
}
return users;
}
async function addUser(user: GitUser) {
await kv.set([PREFIX, user.alias], user);
}
async function getUser(alias: string): Promise<GitUser | null> {
const entry = await kv.get<GitUser>([PREFIX, alias]);
if (entry) {
return entry.value;
}
return null;
}
async function removeUser(alias: string) {
await kv.delete([PREFIX, alias]);
}
add
我们先实现add命令,把它制作为可交互的,也就是让用户自己输入用户名、邮箱和alias。
为了方便,我们选用cliffy来进行交互。
import { Input } from "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/mod.ts";
import { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.3/command/mod.ts";
const add = new Command()
.description("Add one git user.")
.action(async () => {
const name: string = await Input.prompt({
message: `Set a username`,
});
const email: string = await Input.prompt({
message: `Set a email`,
});
const alias: string = await Input.prompt({
message: `Set an alias`,
default: name,
});
console.log(`${name} ${email} ${alias}`);
});
await new Command()
.name("gum")
.description("Change the git user in current project")
.command("add", add)
.parse(Deno.args);
执行命令:
$ deno run mod.ts add
? Set a username › aa
? Set a email › bb
? Set an alias (aa) › cc
aa bb cc
上面已经写好了添加的接口,所以直接调用即可:
const user: GitUser = {
username: name,
email,
alias,
};
await addUser(user);
list
增加之后,我们需要显示所有用户信息与当前用户。
先调用Git命令获取当前用户名称,再匹配出来记录的用户信息:
async function getCurrentUser(): Promise<GitUser | undefined> {
const username = await runTask("git config user.name", false);
const allUsers = await getUserList();
return allUsers.find((user) => user.username === username.trim());
}
将用户信息绘制成一个表格:
import {
Row,
Table,
} from "https://deno.land/x/cliffy@v1.0.0-rc.3/table/mod.ts";
async function showGitList() {
const users = await getUserList();
const currentUser = await getCurrentUser();
const rows = users.map((user) => {
const isCurrent = currentUser?.alias === user.alias;
return new Row(
user.alias,
user.username,
user.email,
isCurrent ? "✓" : "",
).border();
});
new Table()
.header(Row.from(["Alias", "UserName", "Email", "Current"]).border())
.body(rows)
.render();
}
这样list命令就有了:
const list = new Command()
.description("List all git users.")
.action(showGitList);
await new Command()
.name("gum")
.default("list")
.command("list ls", list)
.parse(Deno.args);
效果是这样的:
$ gum
# 等同于
$ gum list
┌───────┬──────────┬───────────────┬─────────┐
│ Alias │ UserName │ Email │ Current │
├───────┼──────────┼───────────────┼─────────┤
│ jw │ jw397 │ jw397@126.com │ ✓ │
├───────┼──────────┼───────────────┼─────────┤
│ test │ test │ test@test.cn │ │
└───────┴──────────┴───────────────┴─────────┘
use
下来就该切换用户信息了,直接调用这俩Git命令即可:
async function changeGitUser(username: string, email: string) {
await runTasks([
`git config user.name ${username}`,
`git config user.email ${email}`,
]);
}
添加use命令:
const use = new Command()
.description("Change current git user.")
.arguments("<alias:string>")
.action(async (_options: unknown, alias: string) => {
const user = await getUser(alias);
if (!user) {
console.log(error(`Not find user by alias: ${alias}`));
return;
}
console.log(`${user.alias} ------ ${user.username} ---- ${user.email}`);
await changeGitUser(user.username, user.email);
console.log(info("Git user changed"));
await showGitList();
});
await new Command()
.name("gum")
.default("list")
.command("list ls", list)
.command("add", add)
.command("use", use)
.parse(Deno.args);
del
删除时需要注意下如果是当前用户,不允许删除。
const del = new Command()
.description("Remove one git user.")
.arguments("<alias:string>")
.action(async (_options: unknown, alias: string) => {
const user = await getUser(alias);
if (!user) {
console.log(error(`Not find user by alias: ${alias}`));
return;
}
const currentUser = await getCurrentUser();
if (currentUser?.alias === alias) {
console.log(error(`Can't remove current git user`));
return;
}
console.log(`${user.alias} ------ ${user.username} ---- ${user.email}`);
await removeUser(alias);
console.log(info("Git user has been removed"));
await showGitList();
});
await new Command()
.name("gum")
.default("list")
.command("list ls", list)
.command("add", add)
.command("delete del remove", del)
.command("use", use)
.parse(Deno.args);
upgrade
本来上面就算结束了,但看cliffy还提供了一个更新的处理,就把它加进来了:
import {
DenoLandProvider,
UpgradeCommand,
} from "https://deno.land/x/cliffy@v1.0.0-rc.3/command/upgrade/mod.ts";
import pkg from "../../deno.json" with { type: "json" };
const upgrade = new UpgradeCommand({
main: "cli/git/user_change.ts",
args: [
"--allow-net",
"--allow-run",
"--allow-env",
"--allow-write",
"--allow-read",
"--unstable",
],
provider: new DenoLandProvider({ name: "jw_cli" }),
});
await new Command()
.name("gum")
.version("v" + pkg.version)
.command("upgrade", upgrade)
.parse(Deno.args);
这里有几处需要注意:
- version是发布到
deno.land
的版本号,我的发布都带了v,记录在deno.json里,所以需要读取这个文件 - upgrade的main参数是这个脚本的相对路径,args是脚本需要的权限,DenoLandProvider的参数是发布到
deno.land
中的包名。底层是调用的这个接口http://cdn.deno.land/jw_cli/meta/versions.json
,会得到所有的包的版本信息:
{"latest":"v0.9.2","versions":["v0.9.2","v0.9.1","v0.9.0","v0.8.0","v0.7.0","v0.6.0","v0.5.2","v0.5.1","v0.5.0","v0.4.0","v0.3.1","v0.3.0","v0.2.7","v0.2.6","v0.2.5","v0.2.4","v0.2.3","v0.2.2","v0.2.1","v0.2.1","v0.2.0","v0.1.6","v0.1.5","v0.1.4","v0.1.3","v0.1.2","v0.1.1","v0.1.0","v0.0.10","v0.0.9","v0.0.8","v0.0.7","v0.0.6","v0.0.5","v0.0.4","v0.0.3","v0.0.2","0.0.2","v0.0.1"]}
这样gum upgrade -l
就会展示所有版本:
可以使用gum upgrade --version v0.9.2
来降级到指定版本。
gum --help
每次都会请求这个版本,如果有新版本,会提示你更新:
Usage: gum
Version: v0.9.2 (New version available: v0.9.3. Run 'gum upgrade' to upgrade to the latest version!)
deno: 1.37.0
v8: 11.8.172.3
typescript: 5.2.2
Description:
Change the git user in current project
Options:
-h, --help - Show this help.
-V, --version - Show the version number for this program.
Commands:
list, ls - List all git users.
add - Add one git user.
delete, del, remove <alias> - Remove one git user.
use <alias> - Change current git user.
upgrade - Upgrade gum executable to latest or given version.
不过,如果新的版本有权限申请,这样升级后不会成功,每次命令行都会提示让你添加允许:
$ gum
┌ ⚠️ Deno requests env access to "HOME".
├ Run again with --allow-env to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all env permissions)
这时就需要手动重新安装下了。
安装
我们的CLI就算完成了。 我本地开发时,使用命令安装:
deno install --allow-run --allow-net --allow-read --allow-write --allow-env --unstable -n gum -f ./cli/git/user_change.ts
而现在可以使用deno.land
上的文件:
deno install --allow-run --allow-net --allow-read --allow-write --allow-env --unstable -n gum -f https://deno.land/x/jw_cli@v0.9.3/cli/git/user_change.ts
有兴趣的同学可以试试。
总结
本文介绍了几种切换Git用户的方法,核心说起来只有2种:
- 在工程下调用
git config user.xxx
修改当前工程的配置项(.git/config
文件)。 - 使用
includeif
,读取全局配置时,某路径下读取另一个配置文件。
我们使用Deno封装了一个CLI,可以便捷地在某工程下切换Git用户,更多目的是为了熟悉Deno.kv
与CLI交互。
Deno开发CLI的优势还是很大的,不像Node.js一样需要做额外的配置,TypeScript也是开箱即用,又有完善的权限管理,对于第三方的代码也能直接查看源码不用担心投毒,推荐大家平时寻找合适的场景玩一玩。
转载自:https://juejin.cn/post/7283532551472332857