Compare commits

..

10 commits
v0.1.1 ... main

Author SHA1 Message Date
inx
a1da786ed1
chore: update README 2026-01-28 23:09:36 +08:00
inx
665774a981
chore: include tests in release artifacts for verification 2026-01-28 23:07:00 +08:00
inx
b980a6907b
chore: add install method in README 2026-01-28 22:06:26 +08:00
inx
692800c5d9
chore: bump version 2026-01-28 21:59:36 +08:00
inx
a735c7cf84
chore: finish rename 2026-01-28 21:50:08 +08:00
inx
9a16f9a22e
chore: extend cargo informations 2026-01-28 21:20:59 +08:00
inx
adce0a5e0d
chore(rename): rename project from 'gosh' to 'naj'
This rebrands the CLI tool to 'naj' (Old Chinese reconstruction for "Me/I").
This name was chosen for better typing ergonomics (R-L-R alternation) and
availability on crates.io.

Changes:
- Update `Cargo.toml` package name to `naj`.
- Update binary name target to `naj`.
- Update documentation and README to reflect the new identity.

BREAKING CHANGE: The binary name is now `naj`. Users must update their
scripts and usage from `gosh` to `naj`.
2026-01-28 21:08:09 +08:00
inx
bb41d4acda
fix(mocking): some moking will leak
Fix leakage for the config while using `git config`.
2026-01-28 21:01:48 +08:00
inx
1bc109ae64
fix(security): sanitize gpg.ssh.program to prevent global config leaks
Explicitly inject `gpg.ssh.program=ssh-keygen` during blind injection.
Leaving this unset would allow global configuration (potentially malicious)
to bleed into the ephemeral session. Setting it to empty string causes
Git to crash. Using the system default `ssh-keygen` ensures both
stability and isolation.
2026-01-28 17:52:46 +08:00
inx
e512bd9e2a
chore: modify workflow scripts
calc hash for dist.
2026-01-28 16:36:20 +08:00
18 changed files with 453 additions and 311 deletions

View file

@ -1,16 +1,29 @@
[package]
name = "gosh"
version = "0.1.1"
edition = "2021"
[dependencies]
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
dirs = "5.0"
anyhow = "1.0"
[dev-dependencies]
assert_cmd = "2.0"
tempfile = "3.10"
predicates = "3.1"
[package]
name = "naj"
version = "0.1.3"
edition = "2021"
description = "Manage your digital selves. (Derived from Old Chinese /*ŋˤajʔ/ 'I/Me')"
authors = ["INX <inx@inx.wf>"]
license = "BSD-2-Clause"
readme = "README.md"
homepage = "https://github.com/dotinx/naj"
repository = "https://github.com/dotinx/naj"
categories = ["command-line-utilities"]
exclude = ["scripts/","assets/"]
[dependencies]
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
dirs = "5.0"
anyhow = "1.0"
[dev-dependencies]
assert_cmd = "2.1"
tempfile = "3.24"
predicates = "3.1"
[[bin]]
name = "naj"
path = "src/main.rs"

View file

@ -1,20 +1,30 @@
# Gosh (Git Operations SHell)
# Naj
**Gosh** is a lightweight, secure, and idempotent wrapper for Git, written in Rust. It solves the chaos of managing multiple Git identities (Work vs. Personal) by strictly isolating configurations and preventing accidental identity leaks.
![Naj Banner](assets/naj-banner.jpg)
**Naj** is a lightweight, secure, and idempotent wrapper for Git, written in Rust. It solves the chaos of managing multiple Git identities (Work vs. Personal) by strictly isolating configurations and preventing accidental identity leaks.
## 🚀 Features
* **🛡️ Fail-Safe Security**: Uses "Blind Injection" to forcibly wipe global identity keys before applying a profile. If your profile lacks a key, Gosh fails securely rather than falling back to your global `~/.gitconfig`.
* **⚡ Ephemeral Execution**: Run commands like `gosh work commit` without modifying any files on disk. Perfect for one-off fixes.
* **🛡️ Fail-Safe Security**: Uses "Blind Injection" to forcibly wipe global identity keys before applying a profile. If your profile lacks a key, Naj fails securely rather than falling back to your global `~/.gitconfig`.
* **⚡ Ephemeral Execution**: Run commands like `naj work commit` without modifying any files on disk. Perfect for one-off fixes.
* **💾 Persistent Switching**: Permanently bind a repository to an identity using Git's native `[include]` directive.
* **🛠️ Zero Config Setup**: Automatically handles `clone` and `init` setup, applying the correct identity immediately.
* **📂 Portable Profiles**: Profiles are stored in `~/.config/gosh/profiles/`, designed to be synced via a private Git repository.
* **📂 Portable Profiles**: Profiles are stored in `~/.config/naj/profiles/`, designed to be synced via a private Git repository.
## 📦 Installation
### From Cargo
```bash
cargo install naj
```
### From Source
```bash
git clone https://github.com/dotinx/naj.git
cd naj
cargo install --path .
```
@ -22,28 +32,28 @@ cargo install --path .
### 1. Management: Create Identities
Gosh manages identities as "Profiles".
Naj manages identities as "Profiles".
```bash
# Syntax: gosh -c <Name> <Email> <ProfileID>
gosh -c "Alice Work" "alice@company.com" "work"
gosh -c "Alice Hobby" "alice@gmail.com" "personal"
# Syntax: naj -c <Name> <Email> <ProfileID>
naj -c "Alice Work" "alice@company.com" "work"
naj -c "Alice Hobby" "alice@gmail.com" "personal"
# List all profiles
gosh -l
naj -l
# Edit a profile (e.g., to add signingkey or sshCommand)
gosh -e work
naj -e work
```
### 2. Workflow A: Setup New Projects (Recommended)
When you clone or init a repository, Gosh automatically sets up the local config.
When you clone or init a repository, Naj automatically sets up the local config.
```bash
# Clones the repo and immediately binds it to the "work" profile
gosh work clone git@github.com:company/backend.git
naj work clone git@github.com:company/backend.git
# Inside the repo, you can now just use standard git
cd backend
@ -57,10 +67,10 @@ Run a command with a specific identity *without* modifying the repository config
```bash
# Temporarily commit as "personal" in a work repo (e.g., fixing a typo)
gosh personal commit -m "Fix typo"
naj personal commit -m "Fix typo"
# Verification
gosh personal config user.email
naj personal config user.email
```
@ -70,32 +80,32 @@ Change the identity bound to an existing repository.
```bash
cd my-repo
gosh work
naj work
# If the repo has "dirty" config (manually set user.name), force overwrite it:
gosh work -f
naj work -f
```
## ⚙️ Configuration
Gosh follows the XDG Base Directory specification.
Naj follows the XDG Base Directory specification.
* **Config File**: `~/.config/gosh/gosh.toml`
* **Profiles**: `~/.config/gosh/profiles/*.gitconfig`
* **Config File**: `~/.config/naj/naj.toml`
* **Profiles**: `~/.config/naj/profiles/*.gitconfig`
On the first run, Gosh will automatically create these directories and a default configuration file.
On the first run, Naj will automatically create these directories and a default configuration file.
### Environment Variables
* `GOSH_CONFIG_PATH`: Override the config directory (Useful for NixOS or testing).
* `GOSH_MOCKING=1`: Dry-run mode. Prints the constructed `git` command to stderr instead of executing it.
* `NAJ_CONFIG_PATH`: Override the config directory (Useful for NixOS or testing).
* `NAJ_MOCKING=1`: Dry-run mode. Prints the constructed `git` command to stderr instead of executing it.
## 🔒 Security Design: Blind Injection
In **Exec Mode**, Gosh does **not** read your local configuration to decide what to override. Instead, it aggressively injects empty values for sensitive keys before applying your profile.
In **Exec Mode**, Naj does **not** read your local configuration to decide what to override. Instead, it aggressively injects empty values for sensitive keys before applying your profile.
**Example command generated by Gosh:**
**Example command generated by Naj:**
```bash
git \
@ -103,7 +113,7 @@ git \
-c user.email="" \
-c user.signingkey="" \
-c commit.gpgsign=false \
-c include.path=~/.config/gosh/profiles/work.gitconfig \ # 2. Apply Profile
-c include.path=~/.config/naj/profiles/work.gitconfig \ # 2. Apply Profile
commit ...
```

BIN
assets/naj-banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

200
scripts/build.sh Normal file → Executable file
View file

@ -1,101 +1,101 @@
#!/bin/bash
set -e # 遇到错误立即停止
# --- 配置部分 ---
APP_NAME="gosh"
OUTPUT_DIR="dist"
# 新增:获取 dist 目录的绝对物理路径
mkdir -p "$OUTPUT_DIR"
ABS_OUTPUT_DIR="$(realpath "$OUTPUT_DIR")"
# 你想要支持的 Target 列表
# cross 支持的列表可见: https://github.com/cross-rs/cross#supported-targets
TARGETS=(
"x86_64-unknown-linux-gnu" # 标准 Linux x64
"x86_64-unknown-linux-musl" # 静态链接 Linux x64 (推荐: 无依赖,兼容性最好)
# "i686-unknown-linux-gnu" # Linux x86
# "i586-unknown-linux-musl" # Linux x86 (静态链接)
"aarch64-unknown-linux-gnu" # Linux ARM64 (如树莓派 4, docker ARM 容器)
# "x86_64-unknown-freebsd" # FreeBSD x64
# "x86_64-pc-windows-gnu" # Windows x64 (使用 MinGW)
# "aarch64-apple-darwin" # macOS ARM64 (注意: cross 对 macOS 支持有限,通常建议在 Mac 上原生编译)
)
# --- 检查依赖 ---
if ! command -v cross &> /dev/null; then
echo "❌ Error: 'cross' is not installed."
echo "👉 Please install it: cargo install cross"
exit 1
fi
if ! command -v podman &> /dev/null; then
echo "❌ Error: 'podman' is not installed or not in PATH."
exit 1
fi
# --- 获取版本号 ---
# 简单地从 Cargo.toml 提取版本号
VERSION=$(grep "^version" Cargo.toml | head -n 1 | cut -d '"' -f 2)
echo "🚀 Preparing to build $APP_NAME v$VERSION..."
# 清理并创建输出目录
rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
# --- 循环编译 ---
for target in "${TARGETS[@]}"; do
echo "------------------------------------------------"
echo "🔨 Building target: $target"
echo "------------------------------------------------"
# 1. 使用 cross 编译 release 版本
cross build --target "$target" --release
# 2. 准备打包
BINARY_NAME="$APP_NAME"
if [[ $target == *"windows"* ]]; then
BINARY_NAME="${APP_NAME}.exe"
fi
# 查找编译生成的二进制文件位置
BUILD_BIN_PATH="target/$target/release/$BINARY_NAME"
if [ ! -f "$BUILD_BIN_PATH" ]; then
echo "❌ Error: Binary not found at $BUILD_BIN_PATH"
exit 1
fi
# 3. 打包文件名格式: gosh-v0.1.0-x86_64-unknown-linux-musl.tar.gz
ARCHIVE_NAME="${APP_NAME}-v${VERSION}-${target}"
# 进入输出目录进行打包操作
# 创建一个临时目录来存放二进制文件和文档(如果有 README/LICENSE
TMP_DIR=$(mktemp -d)
cp "$BUILD_BIN_PATH" "$TMP_DIR/"
# 如果有 README 或 LICENSE也可以 cp 到 TMP_DIR
# cp README.md LICENSE "$TMP_DIR/"
echo "📦 Packaging $ARCHIVE_NAME..."
# if [[ $target == *"windows"* ]]; then
# # Windows 使用 zip
# ARCHIVE_FILE="${ARCHIVE_NAME}.zip"
# (cd "$TMP_DIR" && zip -r "../../$OUTPUT_DIR/$ARCHIVE_FILE" .)
# else
# Linux/Unix 使用 tar.gz
ARCHIVE_FILE="${ARCHIVE_NAME}.tar.gz"
(cd "$TMP_DIR" && tar -czf "$ABS_OUTPUT_DIR/$ARCHIVE_FILE" .)
# fi
# 清理临时目录
rm -rf "$TMP_DIR"
# 4. 生成校验和 (SHA256)
(cd "$ABS_OUTPUT_DIR" && shasum -a 256 "$ARCHIVE_FILE" > "${ARCHIVE_FILE}.sha256")
echo "✅ Success: $OUTPUT_DIR/$ARCHIVE_FILE"
done
echo "------------------------------------------------"
echo "🎉 Build finished! All artifacts are in '$OUTPUT_DIR/'"
#!/bin/bash
set -e # 遇到错误立即停止
# --- 配置部分 ---
APP_NAME="naj"
OUTPUT_DIR="dist"
# 新增:获取 dist 目录的绝对物理路径
mkdir -p "$OUTPUT_DIR"
ABS_OUTPUT_DIR="$(realpath "$OUTPUT_DIR")"
# 你想要支持的 Target 列表
# cross 支持的列表可见: https://github.com/cross-rs/cross#supported-targets
TARGETS=(
"x86_64-unknown-linux-gnu" # 标准 Linux x64
"x86_64-unknown-linux-musl" # 静态链接 Linux x64 (推荐: 无依赖,兼容性最好)
# "i686-unknown-linux-gnu" # Linux x86
# "i586-unknown-linux-musl" # Linux x86 (静态链接)
"aarch64-unknown-linux-gnu" # Linux ARM64 (如树莓派 4, docker ARM 容器)
# "x86_64-unknown-freebsd" # FreeBSD x64
# "x86_64-pc-windows-gnu" # Windows x64 (使用 MinGW)
# "aarch64-apple-darwin" # macOS ARM64 (注意: cross 对 macOS 支持有限,通常建议在 Mac 上原生编译)
)
# --- 检查依赖 ---
if ! command -v cross &> /dev/null; then
echo "❌ Error: 'cross' is not installed."
echo "👉 Please install it: cargo install cross"
exit 1
fi
if ! command -v podman &> /dev/null; then
echo "❌ Error: 'podman' is not installed or not in PATH."
exit 1
fi
# --- 获取版本号 ---
# 简单地从 Cargo.toml 提取版本号
VERSION=$(grep "^version" Cargo.toml | head -n 1 | cut -d '"' -f 2)
echo "🚀 Preparing to build $APP_NAME v$VERSION..."
# 清理并创建输出目录
rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
# --- 循环编译 ---
for target in "${TARGETS[@]}"; do
echo "------------------------------------------------"
echo "🔨 Building target: $target"
echo "------------------------------------------------"
# 1. 使用 cross 编译 release 版本
cross build --target "$target" --release
# 2. 准备打包
BINARY_NAME="$APP_NAME"
if [[ $target == *"windows"* ]]; then
BINARY_NAME="${APP_NAME}.exe"
fi
# 查找编译生成的二进制文件位置
BUILD_BIN_PATH="target/$target/release/$BINARY_NAME"
if [ ! -f "$BUILD_BIN_PATH" ]; then
echo "❌ Error: Binary not found at $BUILD_BIN_PATH"
exit 1
fi
# 3. 打包文件名格式: naj-v0.1.0-x86_64-unknown-linux-musl.tar.gz
ARCHIVE_NAME="${APP_NAME}-v${VERSION}-${target}"
# 进入输出目录进行打包操作
# 创建一个临时目录来存放二进制文件和文档(如果有 README/LICENSE
TMP_DIR=$(mktemp -d)
cp "$BUILD_BIN_PATH" "$TMP_DIR/"
# 如果有 README 或 LICENSE也可以 cp 到 TMP_DIR
# cp README.md LICENSE "$TMP_DIR/"
echo "📦 Packaging $ARCHIVE_NAME..."
# if [[ $target == *"windows"* ]]; then
# # Windows 使用 zip
# ARCHIVE_FILE="${ARCHIVE_NAME}.zip"
# (cd "$TMP_DIR" && zip -r "../../$OUTPUT_DIR/$ARCHIVE_FILE" .)
# else
# Linux/Unix 使用 tar.gz
ARCHIVE_FILE="${ARCHIVE_NAME}.tar.gz"
(cd "$TMP_DIR" && tar -czf "$ABS_OUTPUT_DIR/$ARCHIVE_FILE" .)
# fi
# 清理临时目录
rm -rf "$TMP_DIR"
# 4. 生成校验和 (SHA256)
(cd "$ABS_OUTPUT_DIR" && shasum -a 256 "$ARCHIVE_FILE" > "${ARCHIVE_FILE}.sha256")
echo "✅ Success: $OUTPUT_DIR/$ARCHIVE_FILE"
done
echo "------------------------------------------------"
echo "🎉 Build finished! All artifacts are in '$OUTPUT_DIR/'"
ls -lh "$OUTPUT_DIR"

44
scripts/hash.sh Executable file
View file

@ -0,0 +1,44 @@
#!/bin/bash
# 定义目标目录
TARGET_DIR="$PWD/dist"
# 检查目录是否存在
if [ ! -d "$TARGET_DIR" ]; then
echo "Error: Directory $TARGET_DIR does not exist."
exit 1
fi
# 输出表格表头
echo "| Filename | SHA256 | Status |"
echo "| :--- | :--- | :--- |"
# 遍历所有的 .tar.gz 文件
for file in "$TARGET_DIR"/*.tar.gz; do
# 检查是否有匹配的文件,防止 glob 失败
[ -e "$file" ] || continue
filename=$(basename "$file")
sha_file="${file}.sha256"
# 计算当前的 SHA256
# 使用 awk 只取第一列(哈希值)
actual_sha=$(sha256sum "$file" | awk '{print $1}')
# 校验逻辑
if [ -f "$sha_file" ]; then
# 从文件中读取预期的 SHA256
expected_sha=$(cat "$sha_file" | awk '{print $1}')
if [ "$actual_sha" == "$expected_sha" ]; then
status="✅ OK"
else
status="❌ Mismatch"
fi
else
status="⚠️ No .sha256 file"
fi
# 输出表格行
echo "| $filename | \`$actual_sha\` | $status |"
done

View file

@ -1,10 +1,10 @@
# Gosh Integration & Scenario Tests
# Naj Integration & Scenario Tests
This directory contains the integration test suite and scenario simulations for `gosh`. These scripts are designed to validate the core functionality, security isolation, and developer workflows in a controlled, sandbox environment.
This directory contains the integration test suite and scenario simulations for `naj`. These scripts are designed to validate the core functionality, security isolation, and developer workflows in a controlled, sandbox environment.
## Overview
The tests in this directory focus on end-to-end (E2E) validation, ensuring that `gosh` correctly interacts with Git configurations, SSH keys, and system environments without contaminating the user's global settings.
The tests in this directory focus on end-to-end (E2E) validation, ensuring that `naj` correctly interacts with Git configurations, SSH keys, and system environments without contaminating the user's global settings.
## Test Matrix
@ -21,7 +21,7 @@ The tests in this directory focus on end-to-end (E2E) validation, ensuring that
To run these tests locally, ensure you have the following installed:
- **Bash**: Most scripts use standard Bash features.
- **Git**: Version 2.34+ is required for SSH signing tests.
- **Gosh**: The `gosh` binary must be available in your PATH or accessible via the `GOSH_CMD` environment variable.
- **Naj**: The `naj` binary must be available in your PATH or accessible via the `NAJ_CMD` environment variable.
## Running Tests
@ -42,11 +42,11 @@ You can run individual test scripts directly:
bash scripts/tests/alice.sh
```
Each script initializes a sandbox in `/tmp/gosh_test_*` or similar, ensuring that your `~/.gitconfig` and `~/.ssh` remain untouched.
Each script initializes a sandbox in `/tmp/naj_test_*` or similar, ensuring that your `~/.gitconfig` and `~/.ssh` remain untouched.
## Design Principles
1. **Isolation**: All tests use a dedicated `GOSH_CONFIG_PATH` and temporary directories to ensure side-effect-free execution.
1. **Isolation**: All tests use a dedicated `NAJ_CONFIG_PATH` and temporary directories to ensure side-effect-free execution.
2. **Assertions**: Scripts use exit codes and explicit checks to verify expected outcomes (e.g., checking `git cat-file` for signatures).
3. **Readability**: Log levels (STEP, INFO, ERROR) are used to provide clear feedback during execution.
@ -55,5 +55,5 @@ Each script initializes a sandbox in `/tmp/gosh_test_*` or similar, ensuring tha
When adding a new test script:
- Use `set -e` to ensure the script fails on the first error.
- Use a dedicated sandbox directory for all temporary files.
- Export `GOSH_CONFIG_PATH` to point into your sandbox.
- Export `NAJ_CONFIG_PATH` to point into your sandbox.
- Document the scenario and expected results in the script header.

View file

@ -2,11 +2,11 @@
set -e
# --- 0. 环境与工具准备 ---
GOSH_CMD="gosh" # 确保已编译或 alias 到 cargo run
NAJ_CMD="naj" # 确保已编译或 alias 到 cargo run
BASE_DIR="/tmp/alice_demo_signed"
# 隔离 Gosh 配置
export GOSH_CONFIG_PATH="$BASE_DIR/config"
# 隔离 Naj 配置
export NAJ_CONFIG_PATH="$BASE_DIR/config"
# 隔离 SSH 密钥目录
SSH_DIR="$BASE_DIR/ssh_keys"
# 模拟仓库目录
@ -29,7 +29,7 @@ info "Git Version: $GIT_VERSION (SSH Signing requires 2.34+)"
# --- 1. 清理与沙盒初始化 ---
log "Initializing Sandbox at $BASE_DIR..."
rm -rf "$BASE_DIR"
mkdir -p "$GOSH_CONFIG_PATH"
mkdir -p "$NAJ_CONFIG_PATH"
mkdir -p "$SSH_DIR"
mkdir -p "$REPO_DIR"
@ -44,15 +44,15 @@ info "Generated Work Key: $SSH_DIR/id_work"
ssh-keygen -t ed25519 -C "alice@alice.com" -f "$SSH_DIR/id_personal" -N "" -q
info "Generated Personal Key: $SSH_DIR/id_personal"
# --- 3. 使用 Gosh 创建 Profile 并注入签名配置 ---
log "Creating Gosh Profiles..."
# --- 3. 使用 Naj 创建 Profile 并注入签名配置 ---
log "Creating Naj Profiles..."
# 3.1 创建基础 Work Profile
$GOSH_CMD -c "Alice Work" "alice@contoso.com" "work"
$NAJ_CMD -c "Alice Work" "alice@contoso.com" "work"
# 3.2 手动追加 SSH 签名配置到 Work Profile
# 这里演示了 Gosh 的灵活性:你可以手动编辑生成的 .gitconfig
WORK_PROFILE="$GOSH_CONFIG_PATH/profiles/work.gitconfig"
# 这里演示了 Naj 的灵活性:你可以手动编辑生成的 .gitconfig
WORK_PROFILE="$NAJ_CONFIG_PATH/profiles/work.gitconfig"
cat >> "$WORK_PROFILE" <<EOF
[gpg]
format = ssh
@ -67,8 +67,8 @@ EOF
info "Configured Work Profile with SSH Signing"
# 3.3 创建并配置 Personal Profile
$GOSH_CMD -c "Alice Personal" "alice@alice.com" "personal"
PERSONAL_PROFILE="$GOSH_CONFIG_PATH/profiles/personal.gitconfig"
$NAJ_CMD -c "Alice Personal" "alice@alice.com" "personal"
PERSONAL_PROFILE="$NAJ_CONFIG_PATH/profiles/personal.gitconfig"
cat >> "$PERSONAL_PROFILE" <<EOF
[gpg]
format = ssh
@ -90,9 +90,9 @@ cd "$REPO_DIR"
# 模拟远程仓库
git init --bare --quiet "backend.git"
# 使用 Gosh 克隆 (Clone -> Infer -> Switch)
# 使用 Naj 克隆 (Clone -> Infer -> Switch)
# 注意:这里我们 Clone 本地路径,但 core.sshCommand 依然会被配置进去,这是符合预期的
$GOSH_CMD work clone "$REPO_DIR/backend.git" work-backend
$NAJ_CMD work clone "$REPO_DIR/backend.git" work-backend
cd work-backend
# 提交代码
@ -123,7 +123,7 @@ git init --quiet "oss-project"
cd oss-project
# 切换到 Personal
$GOSH_CMD personal
$NAJ_CMD personal
# 提交
touch fun.txt
@ -148,12 +148,12 @@ fi
log "Scenario C: Ephemeral Execution (Security Check)"
# 当前在 oss-project (Personal),我们想用 Work 身份签个名
# 执行 gosh work commit
$GOSH_CMD work commit --allow-empty -m "Hotfix via Exec" > /dev/null
# 执行 naj work commit
$NAJ_CMD work commit --allow-empty -m "Hotfix via Exec" > /dev/null
# 验证最后一次提交的签名
# 注意Exec 模式下,Gosh 会通过 -c user.signingkey="" 先清空,再注入 work profile
# 如果这一步成功且签名了,说明 Gosh 正确注入了 id_work.pub
# 注意Exec 模式下,Naj 会通过 -c user.signingkey="" 先清空,再注入 work profile
# 如果这一步成功且签名了,说明 Naj 正确注入了 id_work.pub
LATEST_COMMIT_MSG=$(git log -1 --pretty=%B)
info "Latest commit: $LATEST_COMMIT_MSG"

View file

@ -2,11 +2,11 @@
set -e
# --- 0. 环境与工具准备 ---
GOSH_CMD="gosh" # 确保已编译或 alias 到 cargo run
NAJ_CMD="naj" # 确保已编译或 alias 到 cargo run
BASE_DIR="/tmp/alice_demo_debug"
# 隔离 Gosh 配置
export GOSH_CONFIG_PATH="$BASE_DIR/config"
# 隔离 Naj 配置
export NAJ_CONFIG_PATH="$BASE_DIR/config"
# 隔离 SSH 密钥目录
SSH_DIR="$BASE_DIR/ssh_keys"
# 模拟仓库目录
@ -43,7 +43,7 @@ info "Git Version: $GIT_VERSION (SSH Signing requires 2.34+)"
# --- 1. 清理与沙盒初始化 ---
log "Initializing Sandbox at $BASE_DIR..."
rm -rf "$BASE_DIR"
mkdir -p "$GOSH_CONFIG_PATH"
mkdir -p "$NAJ_CONFIG_PATH"
mkdir -p "$SSH_DIR"
mkdir -p "$REPO_DIR"
@ -54,12 +54,12 @@ info "Generated Work Key: .../id_work"
ssh-keygen -t ed25519 -C "alice@alice.com" -f "$SSH_DIR/id_personal" -N "" -q
info "Generated Personal Key: .../id_personal"
# --- 3. 使用 Gosh 创建 Profile ---
log "Creating Gosh Profiles..."
# --- 3. 使用 Naj 创建 Profile ---
log "Creating Naj Profiles..."
# 3.1 Work Profile
$GOSH_CMD -c "Alice Work" "alice@contoso.com" "work"
WORK_PROFILE="$GOSH_CONFIG_PATH/profiles/work.gitconfig"
$NAJ_CMD -c "Alice Work" "alice@contoso.com" "work"
WORK_PROFILE="$NAJ_CONFIG_PATH/profiles/work.gitconfig"
cat >> "$WORK_PROFILE" <<EOF
[gpg]
format = ssh
@ -73,8 +73,8 @@ EOF
info "Configured Work Profile (SSH Signing Enabled)"
# 3.2 Personal Profile
$GOSH_CMD -c "Alice Personal" "alice@alice.com" "personal"
PERSONAL_PROFILE="$GOSH_CONFIG_PATH/profiles/personal.gitconfig"
$NAJ_CMD -c "Alice Personal" "alice@alice.com" "personal"
PERSONAL_PROFILE="$NAJ_CONFIG_PATH/profiles/personal.gitconfig"
cat >> "$PERSONAL_PROFILE" <<EOF
[gpg]
format = ssh
@ -94,9 +94,9 @@ log "Scenario A: Setup Mode (Work Repo)"
cd "$REPO_DIR"
git init --bare --quiet "backend.git"
# 使用 Gosh 克隆
info "Running: gosh work clone ..."
$GOSH_CMD work clone "$REPO_DIR/backend.git" work-backend
# 使用 Naj 克隆
info "Running: naj work clone ..."
$NAJ_CMD work clone "$REPO_DIR/backend.git" work-backend
cd work-backend
# 提交代码
@ -114,8 +114,8 @@ git init --quiet "oss-project"
cd oss-project
# 切换到 Personal
info "Running: gosh personal (Switching...)"
$GOSH_CMD personal
info "Running: naj personal (Switching...)"
$NAJ_CMD personal
# 提交
touch fun.txt
@ -128,11 +128,11 @@ debug_inspect
# === 场景 C: 临时执行与密钥隔离 (Exec Mode) ===
log "Scenario C: Ephemeral Execution (Security Check)"
info "Current Profile is: Personal (oss-project)"
info "Executing 'gosh work commit' (Should use Work Identity temporarily)..."
info "Executing 'naj work commit' (Should use Work Identity temporarily)..."
# 执行 gosh work commit
# 执行 naj work commit
# 注意:这里我们不再重定向到 /dev/null我们要看 git 的原生输出
$GOSH_CMD work commit --allow-empty -m "Hotfix via Exec (Scenario C)"
$NAJ_CMD work commit --allow-empty -m "Hotfix via Exec (Scenario C)"
# 🔍 查看日志
# 这里的重点是:

View file

@ -2,11 +2,11 @@
set -e
# --- 0. 全局配置 ---
GOSH_CMD="gosh"
BASE_DIR="/tmp/gosh_collab_demo"
NAJ_CMD="naj"
BASE_DIR="/tmp/naj_collab_demo"
# 隔离 Gosh 配置
export GOSH_CONFIG_PATH="$BASE_DIR/config"
# 隔离 Naj 配置
export NAJ_CONFIG_PATH="$BASE_DIR/config"
# 隔离 SSH 密钥目录
SSH_DIR="$BASE_DIR/ssh_keys"
# 模拟仓库目录
@ -58,7 +58,7 @@ verify_last_commit() {
# --- 1. 初始化沙盒 ---
log "Initializing Sandbox..."
rm -rf "$BASE_DIR"
mkdir -p "$GOSH_CONFIG_PATH/profiles"
mkdir -p "$NAJ_CONFIG_PATH/profiles"
mkdir -p "$SSH_DIR"
mkdir -p "$REPO_DIR"
@ -79,12 +79,12 @@ ssh-keygen -t ed25519 -C "bob@partner.org" -f "$SSH_DIR/id_bob" -N "" -q
echo "bob@partner.org $(cat $SSH_DIR/id_bob.pub)" >> "$ALLOWED_SIGNERS"
ok "Generated Bob's Key & Added to Trust Store"
# --- 3. 配置 Gosh Profiles ---
log "Configuring Gosh Profiles..."
# --- 3. 配置 Naj Profiles ---
log "Configuring Naj Profiles..."
# --> Alice Profile
$GOSH_CMD -c "Alice Work" "alice@contoso.com" "alice_work"
cat >> "$GOSH_CONFIG_PATH/profiles/alice_work.gitconfig" <<EOF
$NAJ_CMD -c "Alice Work" "alice@contoso.com" "alice_work"
cat >> "$NAJ_CONFIG_PATH/profiles/alice_work.gitconfig" <<EOF
[gpg]
format = ssh
[user]
@ -97,8 +97,8 @@ EOF
ok "Profile 'alice_work' created"
# --> Bob Profile
$GOSH_CMD -c "Bob Partner" "bob@partner.org" "bob_partner"
cat >> "$GOSH_CONFIG_PATH/profiles/bob_partner.gitconfig" <<EOF
$NAJ_CMD -c "Bob Partner" "bob@partner.org" "bob_partner"
cat >> "$NAJ_CONFIG_PATH/profiles/bob_partner.gitconfig" <<EOF
[gpg]
format = ssh
[user]
@ -121,7 +121,7 @@ cd project-alpha
# 1. Alice 初始化项目
echo ">>> [Commit 1] Alice starts the project"
$GOSH_CMD alice_work
$NAJ_CMD alice_work
touch README.md
git add README.md
git commit -m "Init Project Alpha" > /dev/null
@ -129,14 +129,14 @@ verify_last_commit "alice@contoso.com" "alice_work"
# 2. Bob 进来修改 (模拟同一台机器切换身份)
echo ">>> [Commit 2] Bob adds features"
$GOSH_CMD bob_partner
$NAJ_CMD bob_partner
echo "Feature by Bob" >> README.md
git commit -am "Bob adds feature" > /dev/null
verify_last_commit "bob@partner.org" "bob_partner"
# 3. Alice 审查并修改
echo ">>> [Commit 3] Alice reviews and updates"
$GOSH_CMD alice_work
$NAJ_CMD alice_work
echo "Reviewed by Alice" >> README.md
git commit -am "Alice review" > /dev/null
verify_last_commit "alice@contoso.com" "alice_work"
@ -149,7 +149,7 @@ git init --quiet project-beta
cd project-beta
# 1. Bob 拥有这个项目
$GOSH_CMD bob_partner
$NAJ_CMD bob_partner
touch main.rs
git add main.rs
git commit -m "Bob starts Beta" > /dev/null
@ -157,13 +157,13 @@ verify_last_commit "bob@partner.org" "bob_partner"
# 2. Alice 临时修复 (不切换 Profile直接用 Exec)
# 当前 Profile 依然是 bob_partner (可以通过 .git/config 验证)
# Alice 用 gosh alice_work exec 临时提交
# Alice 用 naj alice_work exec 临时提交
echo ">>> [Commit 4] Alice hotfixes via Exec Mode"
echo "// Hotfix" >> main.rs
git add main.rs
# 这里是关键测试Exec 模式下的签名注入
$GOSH_CMD alice_work commit -m "Alice hotfix" > /dev/null
$NAJ_CMD alice_work commit -m "Alice hotfix" > /dev/null
# 验证:虽然此时 .git/config 指向 Bob但这个 Commit 必须是 Alice 签名的
verify_last_commit "alice@contoso.com" "alice_work"

View file

@ -2,9 +2,9 @@
set -e
# --- 配置 ---
GOSH_CMD="gosh"
BASE_DIR="/tmp/gosh_edge_test"
export GOSH_CONFIG_PATH="$BASE_DIR/config"
NAJ_CMD="naj"
BASE_DIR="/tmp/naj_edge_test"
export NAJ_CONFIG_PATH="$BASE_DIR/config"
REPO_DIR="$BASE_DIR/repos"
# 颜色
@ -15,11 +15,11 @@ log() { echo -e "\n\033[0;34m[TEST] $1\033[0m"; }
# --- 初始化 ---
rm -rf "$BASE_DIR"
mkdir -p "$GOSH_CONFIG_PATH" "$REPO_DIR"
mkdir -p "$NAJ_CONFIG_PATH" "$REPO_DIR"
# 创建一个 Profile
log "Creating Profile..."
$GOSH_CMD -c "Edge User" "edge@test.com" "edge"
$NAJ_CMD -c "Edge User" "edge@test.com" "edge"
# --- 测试 1: 子目录执行 ---
log "Scenario 1: Running from a deep subdirectory"
@ -30,10 +30,10 @@ mkdir -p src/deep/level
cd src/deep/level
echo "Current dir: $(pwd)"
echo "Executing 'gosh edge' from subdirectory..."
echo "Executing 'naj edge' from subdirectory..."
# 执行 switch
$GOSH_CMD edge
$NAJ_CMD edge
# 验证
# 我们需要回到根目录看 config或者直接用 git config
@ -55,9 +55,9 @@ cd "$DIR_WITH_SPACE"
git init --quiet
echo "Current dir: $(pwd)"
echo "Executing 'gosh edge'..."
echo "Executing 'naj edge'..."
$GOSH_CMD edge
$NAJ_CMD edge
# 验证
CONFIG_EMAIL=$(git config user.email)

View file

@ -1,8 +1,8 @@
#!/bin/bash
# --- 准备 ---
GOSH_CMD="gosh" # 确保已编译或 alias
BASE_DIR="/tmp/gosh_security_test"
NAJ_CMD="naj" # 确保已编译或 alias
BASE_DIR="/tmp/naj_security_test"
UNSAFE_REPO="$BASE_DIR/root_owned_repo"
# 1. 初始化一个归属于 root 的仓库 (对当前用户来说是不安全的)
@ -17,29 +17,29 @@ sudo touch "$UNSAFE_REPO/testfile"
# 确保当前用户对目录有读写权限(以便能进入),但 .git 依然属于 root
sudo chmod -R 777 "$UNSAFE_REPO"
echo "[TEST] Running 'gosh' in a dubious ownership repo..."
echo "[TEST] Running 'naj' in a dubious ownership repo..."
cd "$UNSAFE_REPO"
# 2. 尝试运行 gosh (期望失败)
if $GOSH_CMD -l > /dev/null 2>&1; then
# 注意:gosh -l 不需要 git 仓库,所以应该成功。
# 2. 尝试运行 naj (期望失败)
if $NAJ_CMD -l > /dev/null 2>&1; then
# 注意:naj -l 不需要 git 仓库,所以应该成功。
# 我们需要测 switch 或 exec这需要 git 上下文
echo " (gosh list works, which is fine)"
echo " (naj list works, which is fine)"
fi
echo "Attempting to switch profile..."
# 捕获输出
OUTPUT=$($GOSH_CMD testprofile 2>&1 || true)
OUTPUT=$($NAJ_CMD testprofile 2>&1 || true)
# 3. 验证结果
if echo "$OUTPUT" | grep -q "fatal: detected dubious ownership"; then
echo "✅ PASS: Gosh propagated Git's security error."
echo "✅ PASS: Naj propagated Git's security error."
echo " Git said: 'detected dubious ownership'"
echo " Gosh refused to act."
echo " Naj refused to act."
elif echo "$OUTPUT" | grep -q "Not a git repository"; then
echo "✅ PASS: Gosh treated it as invalid (Git rev-parse failed)."
echo "✅ PASS: Naj treated it as invalid (Git rev-parse failed)."
else
echo "❌ FAIL: Gosh tried to execute! This is dangerous."
echo "❌ FAIL: Naj tried to execute! This is dangerous."
echo "Output was: $OUTPUT"
exit 1
fi

View file

@ -1,7 +1,7 @@
use clap::{Parser, Args};
#[derive(Parser, Debug)]
#[command(name = "gosh")]
#[command(name = "naj")]
#[command(about = "Git Operations Shell", long_about = None)]
pub struct Cli {
#[command(flatten)]

View file

@ -11,58 +11,58 @@ pub struct Strategies {
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GoshConfig {
pub struct NajConfig {
pub strategies: Strategies,
pub profile_dir: String,
}
impl Default for GoshConfig {
impl Default for NajConfig {
fn default() -> Self {
GoshConfig {
NajConfig {
strategies: Strategies {
clone: "INCLUDE".to_string(),
switch: "include".to_string(),
},
profile_dir: "~/.config/gosh/profiles".to_string(),
profile_dir: "~/.config/naj/profiles".to_string(),
}
}
}
pub fn get_config_root() -> Result<PathBuf> {
if let Ok(path) = std::env::var("GOSH_CONFIG_PATH") {
if let Ok(path) = std::env::var("NAJ_CONFIG_PATH") {
return Ok(PathBuf::from(path));
}
let config_dir = dirs::config_dir().ok_or_else(|| anyhow::anyhow!("Could not find config directory"))?;
Ok(config_dir.join("gosh"))
Ok(config_dir.join("naj"))
}
pub fn load_config() -> Result<GoshConfig> {
pub fn load_config() -> Result<NajConfig> {
let root = get_config_root()?;
let config_path = root.join("gosh.toml");
let config_path = root.join("naj.toml");
if !config_path.exists() {
return initialize_config(&root, &config_path);
}
let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
let config: GoshConfig = toml::from_str(&content).context("Failed to parse config file")?;
let config: NajConfig = toml::from_str(&content).context("Failed to parse config file")?;
Ok(config)
}
fn initialize_config(root: &Path, config_path: &Path) -> Result<GoshConfig> {
fn initialize_config(root: &Path, config_path: &Path) -> Result<NajConfig> {
// Ensure root exists
fs::create_dir_all(root).context("Failed to create config root")?;
// Determine default profile_dir based on environment to support testing isolation
let profile_dir_str = if let Ok(env_path) = std::env::var("GOSH_CONFIG_PATH") {
// If GOSH_CONFIG_PATH is set, default profile dir should be inside it for isolation
let profile_dir_str = if let Ok(env_path) = std::env::var("NAJ_CONFIG_PATH") {
// If NAJ_CONFIG_PATH is set, default profile dir should be inside it for isolation
let p = PathBuf::from(env_path).join("profiles");
// Use forward slashes for TOML consistency if possible, though PathBuf handles it.
// On Windows, replace backslashes to avoid escape issues in TOML string if not raw
// We'll just rely on to_string_lossy but be careful with escaping in the format macro
p.to_string_lossy().to_string()
} else {
"~/.config/gosh/profiles".to_string()
"~/.config/naj/profiles".to_string()
};
// Use toml serialization to ensure string is escaped properly?
@ -70,7 +70,7 @@ fn initialize_config(root: &Path, config_path: &Path) -> Result<GoshConfig> {
// If path contains backslashes (Windows), we need to escape them for the TOML string literal: "C:\\Foo"
let escaped_profile_dir = profile_dir_str.replace("\\", "\\\\");
let generated_toml = format!(r#"# Gosh Configuration
let generated_toml = format!(r#"# Naj Configuration
profile_dir = "{}"
@ -85,6 +85,6 @@ switch = "include" # Soft strategy
let expanded_profile_dir = expand_path(&profile_dir_str)?;
fs::create_dir_all(&expanded_profile_dir).context("Failed to create profiles directory")?;
let config: GoshConfig = toml::from_str(&generated_toml)?;
let config: NajConfig = toml::from_str(&generated_toml)?;
Ok(config)
}

View file

@ -1,4 +1,4 @@
use crate::config::GoshConfig;
use crate::config::NajConfig;
use crate::sanitizer;
use crate::utils::expand_path;
use anyhow::{Result, anyhow, Context};
@ -11,7 +11,7 @@ enum Action {
Switch,
}
pub fn run(config: &GoshConfig, profile_id: &str, args: &[String], force: bool) -> Result<()> {
pub fn run(config: &NajConfig, profile_id: &str, args: &[String], force: bool) -> Result<()> {
// Determine Action
let action = if args.is_empty() {
Action::Switch
@ -28,7 +28,7 @@ pub fn run(config: &GoshConfig, profile_id: &str, args: &[String], force: bool)
}
}
fn get_profile_path(config: &GoshConfig, id: &str) -> Result<PathBuf> {
fn get_profile_path(config: &NajConfig, id: &str) -> Result<PathBuf> {
let profile_dir = expand_path(&config.profile_dir)?;
let p = profile_dir.join(format!("{}.gitconfig", id));
if !p.exists() {
@ -38,7 +38,7 @@ fn get_profile_path(config: &GoshConfig, id: &str) -> Result<PathBuf> {
}
fn is_mocking() -> bool {
std::env::var("GOSH_MOCKING").is_ok()
std::env::var("NAJ_MOCKING").is_ok()
}
fn run_command(cmd: &mut Command) -> Result<()> {
@ -70,7 +70,7 @@ fn run_command(cmd: &mut Command) -> Result<()> {
Ok(())
}
fn get_profile_dir(config: &GoshConfig) -> Result<PathBuf> {
fn get_profile_dir(config: &NajConfig) -> Result<PathBuf> {
expand_path(&config.profile_dir)
}
@ -90,23 +90,27 @@ fn clean_existing_profiles(profile_dir: &Path) -> Result<()> {
// Check if the path belongs to our profile directory
// We use string containment as a heuristic since paths might be canonicalized differently
// but typically we add absolute paths.
let is_gosh_profile = val.contains(&profile_dir.to_string_lossy().to_string())
let is_naj_profile = val.contains(&profile_dir.to_string_lossy().to_string())
|| (val.contains("/profiles/") && val.ends_with(".gitconfig"));
if is_gosh_profile {
if is_naj_profile {
let mut cmd = Command::new("git");
cmd.args(&["config", "--local", "--unset", "include.path", val]);
// We tolerate failure here (e.g. if key doesn't exist anymore for some reason)
let _ = cmd.output();
if is_mocking() {
eprintln!("[DRY-RUN] {:?}", cmd);
} else {
let _ = cmd.output();
}
}
}
Ok(())
}
fn run_exec(config: &GoshConfig, profile_id: &str, args: &[String]) -> Result<()> {
fn run_exec(config: &NajConfig, profile_id: &str, args: &[String]) -> Result<()> {
let profile_path = get_profile_path(config, profile_id)?;
let injections = sanitizer::get_blind_injections();
let injections = sanitizer::BLIND_INJECTIONS;
let mut cmd = Command::new("git");
@ -128,7 +132,7 @@ fn run_exec(config: &GoshConfig, profile_id: &str, args: &[String]) -> Result<()
run_command(&mut cmd)
}
fn run_switch(config: &GoshConfig, profile_id: &str, force: bool) -> Result<()> {
fn run_switch(config: &NajConfig, profile_id: &str, force: bool) -> Result<()> {
let status = Command::new("git")
.args(&["rev-parse", "--is-inside-work-tree"])
.stdout(std::process::Stdio::null())
@ -165,7 +169,11 @@ fn run_switch(config: &GoshConfig, profile_id: &str, force: bool) -> Result<()>
let mut cmd = Command::new("git");
cmd.args(&["config", "--remove-section", section]);
// Ignore errors
let _ = cmd.output();
if is_mocking() {
eprintln!("[DRY-RUN] {:?}", cmd);
} else {
let _ = cmd.output();
}
}
// Unset keys
@ -173,11 +181,15 @@ fn run_switch(config: &GoshConfig, profile_id: &str, force: bool) -> Result<()>
for key in keys {
let mut cmd = Command::new("git");
cmd.args(&["config", "--unset", key]);
let _ = cmd.output();
if is_mocking() {
eprintln!("[DRY-RUN] {:?}", cmd);
} else {
let _ = cmd.output();
}
}
}
// 0. Clean existing gosh profiles
// 0. Clean existing naj profiles
let profiles_dir = get_profile_dir(config)?;
clean_existing_profiles(&profiles_dir)?;
@ -188,10 +200,12 @@ fn run_switch(config: &GoshConfig, profile_id: &str, force: bool) -> Result<()>
run_command(&mut cmd)?;
println!("Switched to profile '{}'", profile_id);
warn_if_dirty_config(profile_id)?;
Ok(())
}
fn run_setup(config: &GoshConfig, profile_id: &str, args: &[String]) -> Result<()> {
fn run_setup(config: &NajConfig, profile_id: &str, args: &[String]) -> Result<()> {
// 1. Pass raw args to git (no injection)
let mut cmd = Command::new("git");
cmd.args(args);
@ -272,3 +286,28 @@ fn extract_basename(url: &str) -> PathBuf {
let path = Path::new(s);
path.file_name().map(|n| PathBuf::from(n)).unwrap_or_else(|| PathBuf::from("repo"))
}
fn warn_if_dirty_config(profile_id: &str) -> Result<()> {
let config_path = Path::new(".git/config");
if config_path.exists() {
let content = std::fs::read_to_string(config_path)?;
// Check for sections that typically contain identity or dangerous settings
// We look for [user], [author], [gpg] sections, or specific keys like sshCommand/gpgsign
let is_dirty = content.contains("[user]")
|| content.contains("[author]")
|| content.contains("[gpg]")
|| content.contains("sshCommand")
|| content.contains("gpgsign");
if is_dirty {
println!("\n⚠️ WARNING: Dirty Local Config Detected!");
println!(" Your .git/config contains hardcoded '[user]' or '[core]' settings.");
println!(" These settings (like signing keys) are merging with your profile");
println!(" and causing a \"Frankenstein Identity\".");
println!("");
println!(" 👉 Fix it: Run 'naj {} -f' to force clean.\n", profile_id);
}
}
Ok(())
}

View file

@ -2,15 +2,15 @@ use anyhow::{bail, Context, Result};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use crate::config::GoshConfig;
use crate::config::NajConfig;
use crate::utils::expand_path;
fn get_profile_path(config: &GoshConfig, id: &str) -> Result<PathBuf> {
fn get_profile_path(config: &NajConfig, id: &str) -> Result<PathBuf> {
let profile_dir = expand_path(&config.profile_dir)?;
Ok(profile_dir.join(format!("{}.gitconfig", id)))
}
pub fn create_profile(config: &GoshConfig, name: &str, email: &str, id: &str) -> Result<()> {
pub fn create_profile(config: &NajConfig, name: &str, email: &str, id: &str) -> Result<()> {
let file_path = get_profile_path(config, id)?;
if file_path.exists() {
@ -28,7 +28,7 @@ pub fn create_profile(config: &GoshConfig, name: &str, email: &str, id: &str) ->
Ok(())
}
pub fn remove_profile(config: &GoshConfig, id: &str) -> Result<()> {
pub fn remove_profile(config: &NajConfig, id: &str) -> Result<()> {
let file_path = get_profile_path(config, id)?;
if !file_path.exists() {
@ -40,7 +40,7 @@ pub fn remove_profile(config: &GoshConfig, id: &str) -> Result<()> {
Ok(())
}
pub fn edit_profile(config: &GoshConfig, id: &str) -> Result<()> {
pub fn edit_profile(config: &NajConfig, id: &str) -> Result<()> {
let file_path = get_profile_path(config, id)?;
if !file_path.exists() {
@ -60,7 +60,7 @@ pub fn edit_profile(config: &GoshConfig, id: &str) -> Result<()> {
Ok(())
}
pub fn list_profiles(config: &GoshConfig) -> Result<()> {
pub fn list_profiles(config: &NajConfig) -> Result<()> {
let profile_dir = expand_path(&config.profile_dir)?;
if !profile_dir.exists() {

View file

@ -7,16 +7,14 @@ pub const BLACKLIST_KEYS: &[&str] = &[
"http.cookieFile",
];
pub fn get_blind_injections() -> Vec<(&'static str, &'static str)> {
vec![
("user.name", ""),
("user.email", ""),
("user.signingkey", ""),
("core.sshCommand", ""),
("gpg.format", "openpgp"),
("gpg.ssh.program", "ssh-keygen"),
("gpg.program", "gpg"),
("commit.gpgsign", "false"),
("tag.gpgsign", "false"),
]
}
pub const BLIND_INJECTIONS: &[(&str, &str)] = &[
("user.name", ""),
("user.email", ""),
("user.signingkey", ""),
("core.sshCommand", ""),
("gpg.format", "openpgp"),
("gpg.ssh.program", "ssh-keygen"),
("gpg.program", "gpg"),
("commit.gpgsign", "false"),
("tag.gpgsign", "false"),
];

View file

@ -7,15 +7,15 @@ fn test_config_initialization() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let config_path = temp_dir.path().join("config");
// Run gosh with GOSH_CONFIG_PATH set to temp dir
let mut cmd = Command::new(env!("CARGO_BIN_EXE_gosh"));
cmd.env("GOSH_CONFIG_PATH", &config_path)
// Run naj with NAJ_CONFIG_PATH set to temp dir
let mut cmd = Command::new(env!("CARGO_BIN_EXE_naj"));
cmd.env("NAJ_CONFIG_PATH", &config_path)
.arg("-l") // Trigger config load using list flag (not positional "list" profile)
.assert()
.success();
// Verify config file exists
assert!(config_path.join("gosh.toml").exists());
assert!(config_path.join("naj.toml").exists());
assert!(config_path.join("profiles").exists());
Ok(())
@ -26,8 +26,8 @@ fn test_profile_creation_and_listing() -> Result<(), Box<dyn std::error::Error>>
let temp_dir = TempDir::new()?;
let config_path = temp_dir.path();
let mut cmd = Command::new(env!("CARGO_BIN_EXE_gosh"));
cmd.env("GOSH_CONFIG_PATH", config_path)
let mut cmd = Command::new(env!("CARGO_BIN_EXE_naj"));
cmd.env("NAJ_CONFIG_PATH", config_path)
.args(&["-c", "Test User", "test@example.com", "test_user"])
.assert()
.success();
@ -40,8 +40,8 @@ fn test_profile_creation_and_listing() -> Result<(), Box<dyn std::error::Error>>
assert!(content.contains("email = test@example.com"));
// Verify list
let mut cmd_list = Command::new(env!("CARGO_BIN_EXE_gosh"));
cmd_list.env("GOSH_CONFIG_PATH", config_path)
let mut cmd_list = Command::new(env!("CARGO_BIN_EXE_naj"));
cmd_list.env("NAJ_CONFIG_PATH", config_path)
.arg("-l")
.assert()
.success()
@ -56,15 +56,15 @@ fn test_duplicate_creation_failure() -> Result<(), Box<dyn std::error::Error>> {
let config_path = temp_dir.path();
// Create first
Command::new(env!("CARGO_BIN_EXE_gosh"))
.env("GOSH_CONFIG_PATH", config_path)
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", config_path)
.args(&["-c", "User", "u@e.com", "dup_test"])
.assert()
.success();
// Create duplicate
Command::new(env!("CARGO_BIN_EXE_gosh"))
.env("GOSH_CONFIG_PATH", config_path)
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", config_path)
.args(&["-c", "User2", "u2@e.com", "dup_test"])
.assert()
.failure(); // Should fail
@ -79,24 +79,24 @@ fn test_remove_profile() -> Result<(), Box<dyn std::error::Error>> {
let profile_path = config_path.join("profiles").join("rem_test.gitconfig");
// Create
Command::new(env!("CARGO_BIN_EXE_gosh"))
.env("GOSH_CONFIG_PATH", config_path)
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", config_path)
.args(&["-c", "User", "u@e.com", "rem_test"])
.assert()
.success();
assert!(profile_path.exists());
// Remove
Command::new(env!("CARGO_BIN_EXE_gosh"))
.env("GOSH_CONFIG_PATH", config_path)
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", config_path)
.args(&["-r", "rem_test"])
.assert()
.success();
assert!(!profile_path.exists());
// Remove non-existent
Command::new(env!("CARGO_BIN_EXE_gosh"))
.env("GOSH_CONFIG_PATH", config_path)
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", config_path)
.args(&["-r", "rem_test"])
.assert()
.failure();
@ -110,16 +110,16 @@ fn test_exec_dry_run_injection_strict() -> Result<(), Box<dyn std::error::Error>
let config_path = temp_dir.path();
// Create a profile first
Command::new(env!("CARGO_BIN_EXE_gosh"))
.env("GOSH_CONFIG_PATH", config_path)
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", config_path)
.args(&["-c", "Test", "test@e.com", "p1"])
.assert()
.success();
// Run exec with mocking
let mut cmd = Command::new(env!("CARGO_BIN_EXE_gosh"));
cmd.env("GOSH_CONFIG_PATH", config_path)
.env("GOSH_MOCKING", "1")
let mut cmd = Command::new(env!("CARGO_BIN_EXE_naj"));
cmd.env("NAJ_CONFIG_PATH", config_path)
.env("NAJ_MOCKING", "1")
.args(&["p1", "commit", "-m", "foo"])
.assert()
.success()
@ -150,15 +150,15 @@ fn test_switch_mode_persistent() -> Result<(), Box<dyn std::error::Error>> {
.output()?;
// Create profile
Command::new(env!("CARGO_BIN_EXE_gosh"))
.env("GOSH_CONFIG_PATH", &config_path)
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", &config_path)
.args(&["-c", "Switch User", "s@e.com", "switch_test"])
.assert()
.success();
// Switch
Command::new(env!("CARGO_BIN_EXE_gosh"))
.env("GOSH_CONFIG_PATH", &config_path)
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", &config_path)
.current_dir(&repo_dir)
.arg("switch_test")
.assert()
@ -197,15 +197,15 @@ fn test_switch_force_mode_sanitization() -> Result<(), Box<dyn std::error::Error
fs::write(&config_file, current_config)?;
// Create profile
Command::new(env!("CARGO_BIN_EXE_gosh"))
.env("GOSH_CONFIG_PATH", &config_path)
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", &config_path)
.args(&["-c", "Force User", "f@e.com", "force_test"])
.assert()
.success();
// Force Switch
Command::new(env!("CARGO_BIN_EXE_gosh"))
.env("GOSH_CONFIG_PATH", &config_path)
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", &config_path)
.current_dir(&repo_dir)
.args(&["force_test", "-f"])
.assert()
@ -240,22 +240,22 @@ fn test_setup_mode_local_clone() -> Result<(), Box<dyn std::error::Error>> {
// Git allows cloning an empty repo, so this is enough
// 2. Create Profile
Command::new(env!("CARGO_BIN_EXE_gosh"))
.env("GOSH_CONFIG_PATH", &config_path)
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", &config_path)
.args(&["-c", "CloneUser", "c@e.com", "clone_test"])
.assert()
.success();
// 3. Run Gosh Clone (Setup Mode)
// 3. Run Naj Clone (Setup Mode)
// We clone from local source to a folder named "dest_repo"
// Gosh should:
// Naj should:
// a) Run git clone
// b) Infer directory is "dest_repo"
// c) Run switch logic on "dest_repo"
let dest_repo_name = "dest_repo";
Command::new(env!("CARGO_BIN_EXE_gosh"))
.env("GOSH_CONFIG_PATH", &config_path)
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", &config_path)
.current_dir(temp_dir.path()) // Execute in temp root
.args(&["clone_test", "clone", source_repo.to_str().unwrap(), dest_repo_name])
.assert()
@ -270,7 +270,7 @@ fn test_setup_mode_local_clone() -> Result<(), Box<dyn std::error::Error>> {
assert!(dest_git_config.exists(), "Cloned repo config should exist");
let content = fs::read_to_string(dest_git_config)?;
// Verify that gosh automatically switched the profile after cloning
// Verify that naj automatically switched the profile after cloning
assert!(content.contains("[include]"));
assert!(content.contains("clone_test.gitconfig"));

View file

@ -0,0 +1,38 @@
use assert_cmd::Command;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_switch_force_mocking() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let config_path = temp_dir.path().join("config");
let repo_dir = temp_dir.path().join("repo");
fs::create_dir_all(&repo_dir)?;
std::process::Command::new("git")
.arg("init")
.current_dir(&repo_dir)
.output()?;
// Create profile
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", &config_path)
.args(&["-c", "User", "u@e.com", "mock_test"])
.assert()
.success();
// Run force switch with mocking
Command::new(env!("CARGO_BIN_EXE_naj"))
.env("NAJ_CONFIG_PATH", &config_path)
.env("NAJ_MOCKING", "1")
.current_dir(&repo_dir)
.args(&["mock_test", "-f"])
.assert()
.success()
// Check for dry-run output of cleanup commands
.stderr(predicates::str::contains("config"))
.stderr(predicates::str::contains("--remove-section"))
.stderr(predicates::str::contains("user"));
Ok(())
}