Compare commits
10 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1da786ed1 | |||
| 665774a981 | |||
| b980a6907b | |||
| 692800c5d9 | |||
| a735c7cf84 | |||
| 9a16f9a22e | |||
| adce0a5e0d | |||
| bb41d4acda | |||
| 1bc109ae64 | |||
| e512bd9e2a |
18 changed files with 453 additions and 311 deletions
45
Cargo.toml
45
Cargo.toml
|
|
@ -1,16 +1,29 @@
|
||||||
[package]
|
[package]
|
||||||
name = "gosh"
|
name = "naj"
|
||||||
version = "0.1.1"
|
version = "0.1.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
description = "Manage your digital selves. (Derived from Old Chinese /*ŋˤajʔ/ 'I/Me')"
|
||||||
[dependencies]
|
authors = ["INX <inx@inx.wf>"]
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
license = "BSD-2-Clause"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
readme = "README.md"
|
||||||
toml = "0.8"
|
homepage = "https://github.com/dotinx/naj"
|
||||||
dirs = "5.0"
|
repository = "https://github.com/dotinx/naj"
|
||||||
anyhow = "1.0"
|
categories = ["command-line-utilities"]
|
||||||
|
exclude = ["scripts/","assets/"]
|
||||||
[dev-dependencies]
|
|
||||||
assert_cmd = "2.0"
|
|
||||||
tempfile = "3.10"
|
[dependencies]
|
||||||
predicates = "3.1"
|
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"
|
||||||
|
|
|
||||||
62
README.md
62
README.md
|
|
@ -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** 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
|
## 🚀 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`.
|
* **🛡️ 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 `gosh work commit` without modifying any files on disk. Perfect for one-off fixes.
|
* **⚡ 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.
|
* **💾 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.
|
* **🛠️ 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
|
## 📦 Installation
|
||||||
|
|
||||||
|
### From Cargo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install naj
|
||||||
|
```
|
||||||
|
|
||||||
### From Source
|
### From Source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
git clone https://github.com/dotinx/naj.git
|
||||||
|
cd naj
|
||||||
cargo install --path .
|
cargo install --path .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -22,28 +32,28 @@ cargo install --path .
|
||||||
|
|
||||||
### 1. Management: Create Identities
|
### 1. Management: Create Identities
|
||||||
|
|
||||||
Gosh manages identities as "Profiles".
|
Naj manages identities as "Profiles".
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Syntax: gosh -c <Name> <Email> <ProfileID>
|
# Syntax: naj -c <Name> <Email> <ProfileID>
|
||||||
gosh -c "Alice Work" "alice@company.com" "work"
|
naj -c "Alice Work" "alice@company.com" "work"
|
||||||
gosh -c "Alice Hobby" "alice@gmail.com" "personal"
|
naj -c "Alice Hobby" "alice@gmail.com" "personal"
|
||||||
|
|
||||||
# List all profiles
|
# List all profiles
|
||||||
gosh -l
|
naj -l
|
||||||
|
|
||||||
# Edit a profile (e.g., to add signingkey or sshCommand)
|
# Edit a profile (e.g., to add signingkey or sshCommand)
|
||||||
gosh -e work
|
naj -e work
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Workflow A: Setup New Projects (Recommended)
|
### 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
|
```bash
|
||||||
# Clones the repo and immediately binds it to the "work" profile
|
# 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
|
# Inside the repo, you can now just use standard git
|
||||||
cd backend
|
cd backend
|
||||||
|
|
@ -57,10 +67,10 @@ Run a command with a specific identity *without* modifying the repository config
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Temporarily commit as "personal" in a work repo (e.g., fixing a typo)
|
# 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
|
# Verification
|
||||||
gosh personal config user.email
|
naj personal config user.email
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -70,32 +80,32 @@ Change the identity bound to an existing repository.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd my-repo
|
cd my-repo
|
||||||
gosh work
|
naj work
|
||||||
|
|
||||||
# If the repo has "dirty" config (manually set user.name), force overwrite it:
|
# If the repo has "dirty" config (manually set user.name), force overwrite it:
|
||||||
gosh work -f
|
naj work -f
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
||||||
Gosh follows the XDG Base Directory specification.
|
Naj follows the XDG Base Directory specification.
|
||||||
|
|
||||||
* **Config File**: `~/.config/gosh/gosh.toml`
|
* **Config File**: `~/.config/naj/naj.toml`
|
||||||
* **Profiles**: `~/.config/gosh/profiles/*.gitconfig`
|
* **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
|
### Environment Variables
|
||||||
|
|
||||||
* `GOSH_CONFIG_PATH`: Override the config directory (Useful for NixOS or testing).
|
* `NAJ_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_MOCKING=1`: Dry-run mode. Prints the constructed `git` command to stderr instead of executing it.
|
||||||
|
|
||||||
## 🔒 Security Design: Blind Injection
|
## 🔒 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
|
```bash
|
||||||
git \
|
git \
|
||||||
|
|
@ -103,7 +113,7 @@ git \
|
||||||
-c user.email="" \
|
-c user.email="" \
|
||||||
-c user.signingkey="" \
|
-c user.signingkey="" \
|
||||||
-c commit.gpgsign=false \
|
-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 ...
|
commit ...
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
BIN
assets/naj-banner.jpg
Normal file
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
200
scripts/build.sh
Normal file → Executable file
|
|
@ -1,101 +1,101 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e # 遇到错误立即停止
|
set -e # 遇到错误立即停止
|
||||||
|
|
||||||
# --- 配置部分 ---
|
# --- 配置部分 ---
|
||||||
APP_NAME="gosh"
|
APP_NAME="naj"
|
||||||
OUTPUT_DIR="dist"
|
OUTPUT_DIR="dist"
|
||||||
# 新增:获取 dist 目录的绝对物理路径
|
# 新增:获取 dist 目录的绝对物理路径
|
||||||
mkdir -p "$OUTPUT_DIR"
|
mkdir -p "$OUTPUT_DIR"
|
||||||
ABS_OUTPUT_DIR="$(realpath "$OUTPUT_DIR")"
|
ABS_OUTPUT_DIR="$(realpath "$OUTPUT_DIR")"
|
||||||
|
|
||||||
# 你想要支持的 Target 列表
|
# 你想要支持的 Target 列表
|
||||||
# cross 支持的列表可见: https://github.com/cross-rs/cross#supported-targets
|
# cross 支持的列表可见: https://github.com/cross-rs/cross#supported-targets
|
||||||
TARGETS=(
|
TARGETS=(
|
||||||
"x86_64-unknown-linux-gnu" # 标准 Linux x64
|
"x86_64-unknown-linux-gnu" # 标准 Linux x64
|
||||||
"x86_64-unknown-linux-musl" # 静态链接 Linux x64 (推荐: 无依赖,兼容性最好)
|
"x86_64-unknown-linux-musl" # 静态链接 Linux x64 (推荐: 无依赖,兼容性最好)
|
||||||
# "i686-unknown-linux-gnu" # Linux x86
|
# "i686-unknown-linux-gnu" # Linux x86
|
||||||
# "i586-unknown-linux-musl" # Linux x86 (静态链接)
|
# "i586-unknown-linux-musl" # Linux x86 (静态链接)
|
||||||
"aarch64-unknown-linux-gnu" # Linux ARM64 (如树莓派 4, docker ARM 容器)
|
"aarch64-unknown-linux-gnu" # Linux ARM64 (如树莓派 4, docker ARM 容器)
|
||||||
# "x86_64-unknown-freebsd" # FreeBSD x64
|
# "x86_64-unknown-freebsd" # FreeBSD x64
|
||||||
# "x86_64-pc-windows-gnu" # Windows x64 (使用 MinGW)
|
# "x86_64-pc-windows-gnu" # Windows x64 (使用 MinGW)
|
||||||
# "aarch64-apple-darwin" # macOS ARM64 (注意: cross 对 macOS 支持有限,通常建议在 Mac 上原生编译)
|
# "aarch64-apple-darwin" # macOS ARM64 (注意: cross 对 macOS 支持有限,通常建议在 Mac 上原生编译)
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- 检查依赖 ---
|
# --- 检查依赖 ---
|
||||||
if ! command -v cross &> /dev/null; then
|
if ! command -v cross &> /dev/null; then
|
||||||
echo "❌ Error: 'cross' is not installed."
|
echo "❌ Error: 'cross' is not installed."
|
||||||
echo "👉 Please install it: cargo install cross"
|
echo "👉 Please install it: cargo install cross"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v podman &> /dev/null; then
|
if ! command -v podman &> /dev/null; then
|
||||||
echo "❌ Error: 'podman' is not installed or not in PATH."
|
echo "❌ Error: 'podman' is not installed or not in PATH."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- 获取版本号 ---
|
# --- 获取版本号 ---
|
||||||
# 简单地从 Cargo.toml 提取版本号
|
# 简单地从 Cargo.toml 提取版本号
|
||||||
VERSION=$(grep "^version" Cargo.toml | head -n 1 | cut -d '"' -f 2)
|
VERSION=$(grep "^version" Cargo.toml | head -n 1 | cut -d '"' -f 2)
|
||||||
echo "🚀 Preparing to build $APP_NAME v$VERSION..."
|
echo "🚀 Preparing to build $APP_NAME v$VERSION..."
|
||||||
|
|
||||||
# 清理并创建输出目录
|
# 清理并创建输出目录
|
||||||
rm -rf "$OUTPUT_DIR"
|
rm -rf "$OUTPUT_DIR"
|
||||||
mkdir -p "$OUTPUT_DIR"
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
# --- 循环编译 ---
|
# --- 循环编译 ---
|
||||||
for target in "${TARGETS[@]}"; do
|
for target in "${TARGETS[@]}"; do
|
||||||
echo "------------------------------------------------"
|
echo "------------------------------------------------"
|
||||||
echo "🔨 Building target: $target"
|
echo "🔨 Building target: $target"
|
||||||
echo "------------------------------------------------"
|
echo "------------------------------------------------"
|
||||||
|
|
||||||
# 1. 使用 cross 编译 release 版本
|
# 1. 使用 cross 编译 release 版本
|
||||||
cross build --target "$target" --release
|
cross build --target "$target" --release
|
||||||
|
|
||||||
# 2. 准备打包
|
# 2. 准备打包
|
||||||
BINARY_NAME="$APP_NAME"
|
BINARY_NAME="$APP_NAME"
|
||||||
if [[ $target == *"windows"* ]]; then
|
if [[ $target == *"windows"* ]]; then
|
||||||
BINARY_NAME="${APP_NAME}.exe"
|
BINARY_NAME="${APP_NAME}.exe"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 查找编译生成的二进制文件位置
|
# 查找编译生成的二进制文件位置
|
||||||
BUILD_BIN_PATH="target/$target/release/$BINARY_NAME"
|
BUILD_BIN_PATH="target/$target/release/$BINARY_NAME"
|
||||||
|
|
||||||
if [ ! -f "$BUILD_BIN_PATH" ]; then
|
if [ ! -f "$BUILD_BIN_PATH" ]; then
|
||||||
echo "❌ Error: Binary not found at $BUILD_BIN_PATH"
|
echo "❌ Error: Binary not found at $BUILD_BIN_PATH"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. 打包文件名格式: gosh-v0.1.0-x86_64-unknown-linux-musl.tar.gz
|
# 3. 打包文件名格式: naj-v0.1.0-x86_64-unknown-linux-musl.tar.gz
|
||||||
ARCHIVE_NAME="${APP_NAME}-v${VERSION}-${target}"
|
ARCHIVE_NAME="${APP_NAME}-v${VERSION}-${target}"
|
||||||
|
|
||||||
# 进入输出目录进行打包操作
|
# 进入输出目录进行打包操作
|
||||||
# 创建一个临时目录来存放二进制文件和文档(如果有 README/LICENSE)
|
# 创建一个临时目录来存放二进制文件和文档(如果有 README/LICENSE)
|
||||||
TMP_DIR=$(mktemp -d)
|
TMP_DIR=$(mktemp -d)
|
||||||
cp "$BUILD_BIN_PATH" "$TMP_DIR/"
|
cp "$BUILD_BIN_PATH" "$TMP_DIR/"
|
||||||
# 如果有 README 或 LICENSE,也可以 cp 到 TMP_DIR
|
# 如果有 README 或 LICENSE,也可以 cp 到 TMP_DIR
|
||||||
# cp README.md LICENSE "$TMP_DIR/"
|
# cp README.md LICENSE "$TMP_DIR/"
|
||||||
|
|
||||||
echo "📦 Packaging $ARCHIVE_NAME..."
|
echo "📦 Packaging $ARCHIVE_NAME..."
|
||||||
|
|
||||||
# if [[ $target == *"windows"* ]]; then
|
# if [[ $target == *"windows"* ]]; then
|
||||||
# # Windows 使用 zip
|
# # Windows 使用 zip
|
||||||
# ARCHIVE_FILE="${ARCHIVE_NAME}.zip"
|
# ARCHIVE_FILE="${ARCHIVE_NAME}.zip"
|
||||||
# (cd "$TMP_DIR" && zip -r "../../$OUTPUT_DIR/$ARCHIVE_FILE" .)
|
# (cd "$TMP_DIR" && zip -r "../../$OUTPUT_DIR/$ARCHIVE_FILE" .)
|
||||||
# else
|
# else
|
||||||
# Linux/Unix 使用 tar.gz
|
# Linux/Unix 使用 tar.gz
|
||||||
ARCHIVE_FILE="${ARCHIVE_NAME}.tar.gz"
|
ARCHIVE_FILE="${ARCHIVE_NAME}.tar.gz"
|
||||||
(cd "$TMP_DIR" && tar -czf "$ABS_OUTPUT_DIR/$ARCHIVE_FILE" .)
|
(cd "$TMP_DIR" && tar -czf "$ABS_OUTPUT_DIR/$ARCHIVE_FILE" .)
|
||||||
# fi
|
# fi
|
||||||
|
|
||||||
# 清理临时目录
|
# 清理临时目录
|
||||||
rm -rf "$TMP_DIR"
|
rm -rf "$TMP_DIR"
|
||||||
|
|
||||||
# 4. 生成校验和 (SHA256)
|
# 4. 生成校验和 (SHA256)
|
||||||
(cd "$ABS_OUTPUT_DIR" && shasum -a 256 "$ARCHIVE_FILE" > "${ARCHIVE_FILE}.sha256")
|
(cd "$ABS_OUTPUT_DIR" && shasum -a 256 "$ARCHIVE_FILE" > "${ARCHIVE_FILE}.sha256")
|
||||||
|
|
||||||
echo "✅ Success: $OUTPUT_DIR/$ARCHIVE_FILE"
|
echo "✅ Success: $OUTPUT_DIR/$ARCHIVE_FILE"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "------------------------------------------------"
|
echo "------------------------------------------------"
|
||||||
echo "🎉 Build finished! All artifacts are in '$OUTPUT_DIR/'"
|
echo "🎉 Build finished! All artifacts are in '$OUTPUT_DIR/'"
|
||||||
ls -lh "$OUTPUT_DIR"
|
ls -lh "$OUTPUT_DIR"
|
||||||
44
scripts/hash.sh
Executable file
44
scripts/hash.sh
Executable 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
|
||||||
|
|
@ -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
|
## 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
|
## 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:
|
To run these tests locally, ensure you have the following installed:
|
||||||
- **Bash**: Most scripts use standard Bash features.
|
- **Bash**: Most scripts use standard Bash features.
|
||||||
- **Git**: Version 2.34+ is required for SSH signing tests.
|
- **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
|
## Running Tests
|
||||||
|
|
||||||
|
|
@ -42,11 +42,11 @@ You can run individual test scripts directly:
|
||||||
bash scripts/tests/alice.sh
|
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
|
## 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).
|
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.
|
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:
|
When adding a new test script:
|
||||||
- Use `set -e` to ensure the script fails on the first error.
|
- Use `set -e` to ensure the script fails on the first error.
|
||||||
- Use a dedicated sandbox directory for all temporary files.
|
- 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.
|
- Document the scenario and expected results in the script header.
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# --- 0. 环境与工具准备 ---
|
# --- 0. 环境与工具准备 ---
|
||||||
GOSH_CMD="gosh" # 确保已编译或 alias 到 cargo run
|
NAJ_CMD="naj" # 确保已编译或 alias 到 cargo run
|
||||||
BASE_DIR="/tmp/alice_demo_signed"
|
BASE_DIR="/tmp/alice_demo_signed"
|
||||||
|
|
||||||
# 隔离 Gosh 配置
|
# 隔离 Naj 配置
|
||||||
export GOSH_CONFIG_PATH="$BASE_DIR/config"
|
export NAJ_CONFIG_PATH="$BASE_DIR/config"
|
||||||
# 隔离 SSH 密钥目录
|
# 隔离 SSH 密钥目录
|
||||||
SSH_DIR="$BASE_DIR/ssh_keys"
|
SSH_DIR="$BASE_DIR/ssh_keys"
|
||||||
# 模拟仓库目录
|
# 模拟仓库目录
|
||||||
|
|
@ -29,7 +29,7 @@ info "Git Version: $GIT_VERSION (SSH Signing requires 2.34+)"
|
||||||
# --- 1. 清理与沙盒初始化 ---
|
# --- 1. 清理与沙盒初始化 ---
|
||||||
log "Initializing Sandbox at $BASE_DIR..."
|
log "Initializing Sandbox at $BASE_DIR..."
|
||||||
rm -rf "$BASE_DIR"
|
rm -rf "$BASE_DIR"
|
||||||
mkdir -p "$GOSH_CONFIG_PATH"
|
mkdir -p "$NAJ_CONFIG_PATH"
|
||||||
mkdir -p "$SSH_DIR"
|
mkdir -p "$SSH_DIR"
|
||||||
mkdir -p "$REPO_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
|
ssh-keygen -t ed25519 -C "alice@alice.com" -f "$SSH_DIR/id_personal" -N "" -q
|
||||||
info "Generated Personal Key: $SSH_DIR/id_personal"
|
info "Generated Personal Key: $SSH_DIR/id_personal"
|
||||||
|
|
||||||
# --- 3. 使用 Gosh 创建 Profile 并注入签名配置 ---
|
# --- 3. 使用 Naj 创建 Profile 并注入签名配置 ---
|
||||||
log "Creating Gosh Profiles..."
|
log "Creating Naj Profiles..."
|
||||||
|
|
||||||
# 3.1 创建基础 Work Profile
|
# 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
|
# 3.2 手动追加 SSH 签名配置到 Work Profile
|
||||||
# 这里演示了 Gosh 的灵活性:你可以手动编辑生成的 .gitconfig
|
# 这里演示了 Naj 的灵活性:你可以手动编辑生成的 .gitconfig
|
||||||
WORK_PROFILE="$GOSH_CONFIG_PATH/profiles/work.gitconfig"
|
WORK_PROFILE="$NAJ_CONFIG_PATH/profiles/work.gitconfig"
|
||||||
cat >> "$WORK_PROFILE" <<EOF
|
cat >> "$WORK_PROFILE" <<EOF
|
||||||
[gpg]
|
[gpg]
|
||||||
format = ssh
|
format = ssh
|
||||||
|
|
@ -67,8 +67,8 @@ EOF
|
||||||
info "Configured Work Profile with SSH Signing"
|
info "Configured Work Profile with SSH Signing"
|
||||||
|
|
||||||
# 3.3 创建并配置 Personal Profile
|
# 3.3 创建并配置 Personal Profile
|
||||||
$GOSH_CMD -c "Alice Personal" "alice@alice.com" "personal"
|
$NAJ_CMD -c "Alice Personal" "alice@alice.com" "personal"
|
||||||
PERSONAL_PROFILE="$GOSH_CONFIG_PATH/profiles/personal.gitconfig"
|
PERSONAL_PROFILE="$NAJ_CONFIG_PATH/profiles/personal.gitconfig"
|
||||||
cat >> "$PERSONAL_PROFILE" <<EOF
|
cat >> "$PERSONAL_PROFILE" <<EOF
|
||||||
[gpg]
|
[gpg]
|
||||||
format = ssh
|
format = ssh
|
||||||
|
|
@ -90,9 +90,9 @@ cd "$REPO_DIR"
|
||||||
# 模拟远程仓库
|
# 模拟远程仓库
|
||||||
git init --bare --quiet "backend.git"
|
git init --bare --quiet "backend.git"
|
||||||
|
|
||||||
# 使用 Gosh 克隆 (Clone -> Infer -> Switch)
|
# 使用 Naj 克隆 (Clone -> Infer -> Switch)
|
||||||
# 注意:这里我们 Clone 本地路径,但 core.sshCommand 依然会被配置进去,这是符合预期的
|
# 注意:这里我们 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
|
cd work-backend
|
||||||
|
|
||||||
# 提交代码
|
# 提交代码
|
||||||
|
|
@ -123,7 +123,7 @@ git init --quiet "oss-project"
|
||||||
cd oss-project
|
cd oss-project
|
||||||
|
|
||||||
# 切换到 Personal
|
# 切换到 Personal
|
||||||
$GOSH_CMD personal
|
$NAJ_CMD personal
|
||||||
|
|
||||||
# 提交
|
# 提交
|
||||||
touch fun.txt
|
touch fun.txt
|
||||||
|
|
@ -148,12 +148,12 @@ fi
|
||||||
log "Scenario C: Ephemeral Execution (Security Check)"
|
log "Scenario C: Ephemeral Execution (Security Check)"
|
||||||
# 当前在 oss-project (Personal),我们想用 Work 身份签个名
|
# 当前在 oss-project (Personal),我们想用 Work 身份签个名
|
||||||
|
|
||||||
# 执行 gosh work commit
|
# 执行 naj work commit
|
||||||
$GOSH_CMD work commit --allow-empty -m "Hotfix via Exec" > /dev/null
|
$NAJ_CMD work commit --allow-empty -m "Hotfix via Exec" > /dev/null
|
||||||
|
|
||||||
# 验证最后一次提交的签名
|
# 验证最后一次提交的签名
|
||||||
# 注意:Exec 模式下,Gosh 会通过 -c user.signingkey="" 先清空,再注入 work profile
|
# 注意:Exec 模式下,Naj 会通过 -c user.signingkey="" 先清空,再注入 work profile
|
||||||
# 如果这一步成功且签名了,说明 Gosh 正确注入了 id_work.pub
|
# 如果这一步成功且签名了,说明 Naj 正确注入了 id_work.pub
|
||||||
|
|
||||||
LATEST_COMMIT_MSG=$(git log -1 --pretty=%B)
|
LATEST_COMMIT_MSG=$(git log -1 --pretty=%B)
|
||||||
info "Latest commit: $LATEST_COMMIT_MSG"
|
info "Latest commit: $LATEST_COMMIT_MSG"
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# --- 0. 环境与工具准备 ---
|
# --- 0. 环境与工具准备 ---
|
||||||
GOSH_CMD="gosh" # 确保已编译或 alias 到 cargo run
|
NAJ_CMD="naj" # 确保已编译或 alias 到 cargo run
|
||||||
BASE_DIR="/tmp/alice_demo_debug"
|
BASE_DIR="/tmp/alice_demo_debug"
|
||||||
|
|
||||||
# 隔离 Gosh 配置
|
# 隔离 Naj 配置
|
||||||
export GOSH_CONFIG_PATH="$BASE_DIR/config"
|
export NAJ_CONFIG_PATH="$BASE_DIR/config"
|
||||||
# 隔离 SSH 密钥目录
|
# 隔离 SSH 密钥目录
|
||||||
SSH_DIR="$BASE_DIR/ssh_keys"
|
SSH_DIR="$BASE_DIR/ssh_keys"
|
||||||
# 模拟仓库目录
|
# 模拟仓库目录
|
||||||
|
|
@ -43,7 +43,7 @@ info "Git Version: $GIT_VERSION (SSH Signing requires 2.34+)"
|
||||||
# --- 1. 清理与沙盒初始化 ---
|
# --- 1. 清理与沙盒初始化 ---
|
||||||
log "Initializing Sandbox at $BASE_DIR..."
|
log "Initializing Sandbox at $BASE_DIR..."
|
||||||
rm -rf "$BASE_DIR"
|
rm -rf "$BASE_DIR"
|
||||||
mkdir -p "$GOSH_CONFIG_PATH"
|
mkdir -p "$NAJ_CONFIG_PATH"
|
||||||
mkdir -p "$SSH_DIR"
|
mkdir -p "$SSH_DIR"
|
||||||
mkdir -p "$REPO_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
|
ssh-keygen -t ed25519 -C "alice@alice.com" -f "$SSH_DIR/id_personal" -N "" -q
|
||||||
info "Generated Personal Key: .../id_personal"
|
info "Generated Personal Key: .../id_personal"
|
||||||
|
|
||||||
# --- 3. 使用 Gosh 创建 Profile ---
|
# --- 3. 使用 Naj 创建 Profile ---
|
||||||
log "Creating Gosh Profiles..."
|
log "Creating Naj Profiles..."
|
||||||
|
|
||||||
# 3.1 Work Profile
|
# 3.1 Work Profile
|
||||||
$GOSH_CMD -c "Alice Work" "alice@contoso.com" "work"
|
$NAJ_CMD -c "Alice Work" "alice@contoso.com" "work"
|
||||||
WORK_PROFILE="$GOSH_CONFIG_PATH/profiles/work.gitconfig"
|
WORK_PROFILE="$NAJ_CONFIG_PATH/profiles/work.gitconfig"
|
||||||
cat >> "$WORK_PROFILE" <<EOF
|
cat >> "$WORK_PROFILE" <<EOF
|
||||||
[gpg]
|
[gpg]
|
||||||
format = ssh
|
format = ssh
|
||||||
|
|
@ -73,8 +73,8 @@ EOF
|
||||||
info "Configured Work Profile (SSH Signing Enabled)"
|
info "Configured Work Profile (SSH Signing Enabled)"
|
||||||
|
|
||||||
# 3.2 Personal Profile
|
# 3.2 Personal Profile
|
||||||
$GOSH_CMD -c "Alice Personal" "alice@alice.com" "personal"
|
$NAJ_CMD -c "Alice Personal" "alice@alice.com" "personal"
|
||||||
PERSONAL_PROFILE="$GOSH_CONFIG_PATH/profiles/personal.gitconfig"
|
PERSONAL_PROFILE="$NAJ_CONFIG_PATH/profiles/personal.gitconfig"
|
||||||
cat >> "$PERSONAL_PROFILE" <<EOF
|
cat >> "$PERSONAL_PROFILE" <<EOF
|
||||||
[gpg]
|
[gpg]
|
||||||
format = ssh
|
format = ssh
|
||||||
|
|
@ -94,9 +94,9 @@ log "Scenario A: Setup Mode (Work Repo)"
|
||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
git init --bare --quiet "backend.git"
|
git init --bare --quiet "backend.git"
|
||||||
|
|
||||||
# 使用 Gosh 克隆
|
# 使用 Naj 克隆
|
||||||
info "Running: gosh work clone ..."
|
info "Running: naj work clone ..."
|
||||||
$GOSH_CMD work clone "$REPO_DIR/backend.git" work-backend
|
$NAJ_CMD work clone "$REPO_DIR/backend.git" work-backend
|
||||||
cd work-backend
|
cd work-backend
|
||||||
|
|
||||||
# 提交代码
|
# 提交代码
|
||||||
|
|
@ -114,8 +114,8 @@ git init --quiet "oss-project"
|
||||||
cd oss-project
|
cd oss-project
|
||||||
|
|
||||||
# 切换到 Personal
|
# 切换到 Personal
|
||||||
info "Running: gosh personal (Switching...)"
|
info "Running: naj personal (Switching...)"
|
||||||
$GOSH_CMD personal
|
$NAJ_CMD personal
|
||||||
|
|
||||||
# 提交
|
# 提交
|
||||||
touch fun.txt
|
touch fun.txt
|
||||||
|
|
@ -128,11 +128,11 @@ debug_inspect
|
||||||
# === 场景 C: 临时执行与密钥隔离 (Exec Mode) ===
|
# === 场景 C: 临时执行与密钥隔离 (Exec Mode) ===
|
||||||
log "Scenario C: Ephemeral Execution (Security Check)"
|
log "Scenario C: Ephemeral Execution (Security Check)"
|
||||||
info "Current Profile is: Personal (oss-project)"
|
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 的原生输出
|
# 注意:这里我们不再重定向到 /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)"
|
||||||
|
|
||||||
# 🔍 查看日志
|
# 🔍 查看日志
|
||||||
# 这里的重点是:
|
# 这里的重点是:
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# --- 0. 全局配置 ---
|
# --- 0. 全局配置 ---
|
||||||
GOSH_CMD="gosh"
|
NAJ_CMD="naj"
|
||||||
BASE_DIR="/tmp/gosh_collab_demo"
|
BASE_DIR="/tmp/naj_collab_demo"
|
||||||
|
|
||||||
# 隔离 Gosh 配置
|
# 隔离 Naj 配置
|
||||||
export GOSH_CONFIG_PATH="$BASE_DIR/config"
|
export NAJ_CONFIG_PATH="$BASE_DIR/config"
|
||||||
# 隔离 SSH 密钥目录
|
# 隔离 SSH 密钥目录
|
||||||
SSH_DIR="$BASE_DIR/ssh_keys"
|
SSH_DIR="$BASE_DIR/ssh_keys"
|
||||||
# 模拟仓库目录
|
# 模拟仓库目录
|
||||||
|
|
@ -58,7 +58,7 @@ verify_last_commit() {
|
||||||
# --- 1. 初始化沙盒 ---
|
# --- 1. 初始化沙盒 ---
|
||||||
log "Initializing Sandbox..."
|
log "Initializing Sandbox..."
|
||||||
rm -rf "$BASE_DIR"
|
rm -rf "$BASE_DIR"
|
||||||
mkdir -p "$GOSH_CONFIG_PATH/profiles"
|
mkdir -p "$NAJ_CONFIG_PATH/profiles"
|
||||||
mkdir -p "$SSH_DIR"
|
mkdir -p "$SSH_DIR"
|
||||||
mkdir -p "$REPO_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"
|
echo "bob@partner.org $(cat $SSH_DIR/id_bob.pub)" >> "$ALLOWED_SIGNERS"
|
||||||
ok "Generated Bob's Key & Added to Trust Store"
|
ok "Generated Bob's Key & Added to Trust Store"
|
||||||
|
|
||||||
# --- 3. 配置 Gosh Profiles ---
|
# --- 3. 配置 Naj Profiles ---
|
||||||
log "Configuring Gosh Profiles..."
|
log "Configuring Naj Profiles..."
|
||||||
|
|
||||||
# --> Alice Profile
|
# --> Alice Profile
|
||||||
$GOSH_CMD -c "Alice Work" "alice@contoso.com" "alice_work"
|
$NAJ_CMD -c "Alice Work" "alice@contoso.com" "alice_work"
|
||||||
cat >> "$GOSH_CONFIG_PATH/profiles/alice_work.gitconfig" <<EOF
|
cat >> "$NAJ_CONFIG_PATH/profiles/alice_work.gitconfig" <<EOF
|
||||||
[gpg]
|
[gpg]
|
||||||
format = ssh
|
format = ssh
|
||||||
[user]
|
[user]
|
||||||
|
|
@ -97,8 +97,8 @@ EOF
|
||||||
ok "Profile 'alice_work' created"
|
ok "Profile 'alice_work' created"
|
||||||
|
|
||||||
# --> Bob Profile
|
# --> Bob Profile
|
||||||
$GOSH_CMD -c "Bob Partner" "bob@partner.org" "bob_partner"
|
$NAJ_CMD -c "Bob Partner" "bob@partner.org" "bob_partner"
|
||||||
cat >> "$GOSH_CONFIG_PATH/profiles/bob_partner.gitconfig" <<EOF
|
cat >> "$NAJ_CONFIG_PATH/profiles/bob_partner.gitconfig" <<EOF
|
||||||
[gpg]
|
[gpg]
|
||||||
format = ssh
|
format = ssh
|
||||||
[user]
|
[user]
|
||||||
|
|
@ -121,7 +121,7 @@ cd project-alpha
|
||||||
|
|
||||||
# 1. Alice 初始化项目
|
# 1. Alice 初始化项目
|
||||||
echo ">>> [Commit 1] Alice starts the project"
|
echo ">>> [Commit 1] Alice starts the project"
|
||||||
$GOSH_CMD alice_work
|
$NAJ_CMD alice_work
|
||||||
touch README.md
|
touch README.md
|
||||||
git add README.md
|
git add README.md
|
||||||
git commit -m "Init Project Alpha" > /dev/null
|
git commit -m "Init Project Alpha" > /dev/null
|
||||||
|
|
@ -129,14 +129,14 @@ verify_last_commit "alice@contoso.com" "alice_work"
|
||||||
|
|
||||||
# 2. Bob 进来修改 (模拟同一台机器切换身份)
|
# 2. Bob 进来修改 (模拟同一台机器切换身份)
|
||||||
echo ">>> [Commit 2] Bob adds features"
|
echo ">>> [Commit 2] Bob adds features"
|
||||||
$GOSH_CMD bob_partner
|
$NAJ_CMD bob_partner
|
||||||
echo "Feature by Bob" >> README.md
|
echo "Feature by Bob" >> README.md
|
||||||
git commit -am "Bob adds feature" > /dev/null
|
git commit -am "Bob adds feature" > /dev/null
|
||||||
verify_last_commit "bob@partner.org" "bob_partner"
|
verify_last_commit "bob@partner.org" "bob_partner"
|
||||||
|
|
||||||
# 3. Alice 审查并修改
|
# 3. Alice 审查并修改
|
||||||
echo ">>> [Commit 3] Alice reviews and updates"
|
echo ">>> [Commit 3] Alice reviews and updates"
|
||||||
$GOSH_CMD alice_work
|
$NAJ_CMD alice_work
|
||||||
echo "Reviewed by Alice" >> README.md
|
echo "Reviewed by Alice" >> README.md
|
||||||
git commit -am "Alice review" > /dev/null
|
git commit -am "Alice review" > /dev/null
|
||||||
verify_last_commit "alice@contoso.com" "alice_work"
|
verify_last_commit "alice@contoso.com" "alice_work"
|
||||||
|
|
@ -149,7 +149,7 @@ git init --quiet project-beta
|
||||||
cd project-beta
|
cd project-beta
|
||||||
|
|
||||||
# 1. Bob 拥有这个项目
|
# 1. Bob 拥有这个项目
|
||||||
$GOSH_CMD bob_partner
|
$NAJ_CMD bob_partner
|
||||||
touch main.rs
|
touch main.rs
|
||||||
git add main.rs
|
git add main.rs
|
||||||
git commit -m "Bob starts Beta" > /dev/null
|
git commit -m "Bob starts Beta" > /dev/null
|
||||||
|
|
@ -157,13 +157,13 @@ verify_last_commit "bob@partner.org" "bob_partner"
|
||||||
|
|
||||||
# 2. Alice 临时修复 (不切换 Profile,直接用 Exec)
|
# 2. Alice 临时修复 (不切换 Profile,直接用 Exec)
|
||||||
# 当前 Profile 依然是 bob_partner (可以通过 .git/config 验证)
|
# 当前 Profile 依然是 bob_partner (可以通过 .git/config 验证)
|
||||||
# Alice 用 gosh alice_work exec 临时提交
|
# Alice 用 naj alice_work exec 临时提交
|
||||||
echo ">>> [Commit 4] Alice hotfixes via Exec Mode"
|
echo ">>> [Commit 4] Alice hotfixes via Exec Mode"
|
||||||
echo "// Hotfix" >> main.rs
|
echo "// Hotfix" >> main.rs
|
||||||
git add main.rs
|
git add main.rs
|
||||||
|
|
||||||
# 这里是关键测试:Exec 模式下的签名注入
|
# 这里是关键测试: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 签名的
|
# 验证:虽然此时 .git/config 指向 Bob,但这个 Commit 必须是 Alice 签名的
|
||||||
verify_last_commit "alice@contoso.com" "alice_work"
|
verify_last_commit "alice@contoso.com" "alice_work"
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# --- 配置 ---
|
# --- 配置 ---
|
||||||
GOSH_CMD="gosh"
|
NAJ_CMD="naj"
|
||||||
BASE_DIR="/tmp/gosh_edge_test"
|
BASE_DIR="/tmp/naj_edge_test"
|
||||||
export GOSH_CONFIG_PATH="$BASE_DIR/config"
|
export NAJ_CONFIG_PATH="$BASE_DIR/config"
|
||||||
REPO_DIR="$BASE_DIR/repos"
|
REPO_DIR="$BASE_DIR/repos"
|
||||||
|
|
||||||
# 颜色
|
# 颜色
|
||||||
|
|
@ -15,11 +15,11 @@ log() { echo -e "\n\033[0;34m[TEST] $1\033[0m"; }
|
||||||
|
|
||||||
# --- 初始化 ---
|
# --- 初始化 ---
|
||||||
rm -rf "$BASE_DIR"
|
rm -rf "$BASE_DIR"
|
||||||
mkdir -p "$GOSH_CONFIG_PATH" "$REPO_DIR"
|
mkdir -p "$NAJ_CONFIG_PATH" "$REPO_DIR"
|
||||||
|
|
||||||
# 创建一个 Profile
|
# 创建一个 Profile
|
||||||
log "Creating Profile..."
|
log "Creating Profile..."
|
||||||
$GOSH_CMD -c "Edge User" "edge@test.com" "edge"
|
$NAJ_CMD -c "Edge User" "edge@test.com" "edge"
|
||||||
|
|
||||||
# --- 测试 1: 子目录执行 ---
|
# --- 测试 1: 子目录执行 ---
|
||||||
log "Scenario 1: Running from a deep subdirectory"
|
log "Scenario 1: Running from a deep subdirectory"
|
||||||
|
|
@ -30,10 +30,10 @@ mkdir -p src/deep/level
|
||||||
cd src/deep/level
|
cd src/deep/level
|
||||||
|
|
||||||
echo "Current dir: $(pwd)"
|
echo "Current dir: $(pwd)"
|
||||||
echo "Executing 'gosh edge' from subdirectory..."
|
echo "Executing 'naj edge' from subdirectory..."
|
||||||
|
|
||||||
# 执行 switch
|
# 执行 switch
|
||||||
$GOSH_CMD edge
|
$NAJ_CMD edge
|
||||||
|
|
||||||
# 验证
|
# 验证
|
||||||
# 我们需要回到根目录看 config,或者直接用 git config
|
# 我们需要回到根目录看 config,或者直接用 git config
|
||||||
|
|
@ -55,9 +55,9 @@ cd "$DIR_WITH_SPACE"
|
||||||
git init --quiet
|
git init --quiet
|
||||||
|
|
||||||
echo "Current dir: $(pwd)"
|
echo "Current dir: $(pwd)"
|
||||||
echo "Executing 'gosh edge'..."
|
echo "Executing 'naj edge'..."
|
||||||
|
|
||||||
$GOSH_CMD edge
|
$NAJ_CMD edge
|
||||||
|
|
||||||
# 验证
|
# 验证
|
||||||
CONFIG_EMAIL=$(git config user.email)
|
CONFIG_EMAIL=$(git config user.email)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# --- 准备 ---
|
# --- 准备 ---
|
||||||
GOSH_CMD="gosh" # 确保已编译或 alias
|
NAJ_CMD="naj" # 确保已编译或 alias
|
||||||
BASE_DIR="/tmp/gosh_security_test"
|
BASE_DIR="/tmp/naj_security_test"
|
||||||
UNSAFE_REPO="$BASE_DIR/root_owned_repo"
|
UNSAFE_REPO="$BASE_DIR/root_owned_repo"
|
||||||
|
|
||||||
# 1. 初始化一个归属于 root 的仓库 (对当前用户来说是不安全的)
|
# 1. 初始化一个归属于 root 的仓库 (对当前用户来说是不安全的)
|
||||||
|
|
@ -17,29 +17,29 @@ sudo touch "$UNSAFE_REPO/testfile"
|
||||||
# 确保当前用户对目录有读写权限(以便能进入),但 .git 依然属于 root
|
# 确保当前用户对目录有读写权限(以便能进入),但 .git 依然属于 root
|
||||||
sudo chmod -R 777 "$UNSAFE_REPO"
|
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"
|
cd "$UNSAFE_REPO"
|
||||||
|
|
||||||
# 2. 尝试运行 gosh (期望失败)
|
# 2. 尝试运行 naj (期望失败)
|
||||||
if $GOSH_CMD -l > /dev/null 2>&1; then
|
if $NAJ_CMD -l > /dev/null 2>&1; then
|
||||||
# 注意:gosh -l 不需要 git 仓库,所以应该成功。
|
# 注意:naj -l 不需要 git 仓库,所以应该成功。
|
||||||
# 我们需要测 switch 或 exec,这需要 git 上下文
|
# 我们需要测 switch 或 exec,这需要 git 上下文
|
||||||
echo " (gosh list works, which is fine)"
|
echo " (naj list works, which is fine)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Attempting to switch profile..."
|
echo "Attempting to switch profile..."
|
||||||
# 捕获输出
|
# 捕获输出
|
||||||
OUTPUT=$($GOSH_CMD testprofile 2>&1 || true)
|
OUTPUT=$($NAJ_CMD testprofile 2>&1 || true)
|
||||||
|
|
||||||
# 3. 验证结果
|
# 3. 验证结果
|
||||||
if echo "$OUTPUT" | grep -q "fatal: detected dubious ownership"; then
|
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 " 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
|
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
|
else
|
||||||
echo "❌ FAIL: Gosh tried to execute! This is dangerous."
|
echo "❌ FAIL: Naj tried to execute! This is dangerous."
|
||||||
echo "Output was: $OUTPUT"
|
echo "Output was: $OUTPUT"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use clap::{Parser, Args};
|
use clap::{Parser, Args};
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(name = "gosh")]
|
#[command(name = "naj")]
|
||||||
#[command(about = "Git Operations Shell", long_about = None)]
|
#[command(about = "Git Operations Shell", long_about = None)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
|
|
|
||||||
|
|
@ -11,58 +11,58 @@ pub struct Strategies {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct GoshConfig {
|
pub struct NajConfig {
|
||||||
pub strategies: Strategies,
|
pub strategies: Strategies,
|
||||||
pub profile_dir: String,
|
pub profile_dir: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GoshConfig {
|
impl Default for NajConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
GoshConfig {
|
NajConfig {
|
||||||
strategies: Strategies {
|
strategies: Strategies {
|
||||||
clone: "INCLUDE".to_string(),
|
clone: "INCLUDE".to_string(),
|
||||||
switch: "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> {
|
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));
|
return Ok(PathBuf::from(path));
|
||||||
}
|
}
|
||||||
let config_dir = dirs::config_dir().ok_or_else(|| anyhow::anyhow!("Could not find config directory"))?;
|
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 root = get_config_root()?;
|
||||||
let config_path = root.join("gosh.toml");
|
let config_path = root.join("naj.toml");
|
||||||
|
|
||||||
if !config_path.exists() {
|
if !config_path.exists() {
|
||||||
return initialize_config(&root, &config_path);
|
return initialize_config(&root, &config_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
|
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)
|
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
|
// Ensure root exists
|
||||||
fs::create_dir_all(root).context("Failed to create config root")?;
|
fs::create_dir_all(root).context("Failed to create config root")?;
|
||||||
|
|
||||||
// Determine default profile_dir based on environment to support testing isolation
|
// 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") {
|
let profile_dir_str = if let Ok(env_path) = std::env::var("NAJ_CONFIG_PATH") {
|
||||||
// If GOSH_CONFIG_PATH is set, default profile dir should be inside it for isolation
|
// If NAJ_CONFIG_PATH is set, default profile dir should be inside it for isolation
|
||||||
let p = PathBuf::from(env_path).join("profiles");
|
let p = PathBuf::from(env_path).join("profiles");
|
||||||
// Use forward slashes for TOML consistency if possible, though PathBuf handles it.
|
// 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
|
// 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
|
// We'll just rely on to_string_lossy but be careful with escaping in the format macro
|
||||||
p.to_string_lossy().to_string()
|
p.to_string_lossy().to_string()
|
||||||
} else {
|
} else {
|
||||||
"~/.config/gosh/profiles".to_string()
|
"~/.config/naj/profiles".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use toml serialization to ensure string is escaped properly?
|
// 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"
|
// 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 escaped_profile_dir = profile_dir_str.replace("\\", "\\\\");
|
||||||
|
|
||||||
let generated_toml = format!(r#"# Gosh Configuration
|
let generated_toml = format!(r#"# Naj Configuration
|
||||||
|
|
||||||
profile_dir = "{}"
|
profile_dir = "{}"
|
||||||
|
|
||||||
|
|
@ -85,6 +85,6 @@ switch = "include" # Soft strategy
|
||||||
let expanded_profile_dir = expand_path(&profile_dir_str)?;
|
let expanded_profile_dir = expand_path(&profile_dir_str)?;
|
||||||
fs::create_dir_all(&expanded_profile_dir).context("Failed to create profiles directory")?;
|
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)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
69
src/git.rs
69
src/git.rs
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::config::GoshConfig;
|
use crate::config::NajConfig;
|
||||||
use crate::sanitizer;
|
use crate::sanitizer;
|
||||||
use crate::utils::expand_path;
|
use crate::utils::expand_path;
|
||||||
use anyhow::{Result, anyhow, Context};
|
use anyhow::{Result, anyhow, Context};
|
||||||
|
|
@ -11,7 +11,7 @@ enum Action {
|
||||||
Switch,
|
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
|
// Determine Action
|
||||||
let action = if args.is_empty() {
|
let action = if args.is_empty() {
|
||||||
Action::Switch
|
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 profile_dir = expand_path(&config.profile_dir)?;
|
||||||
let p = profile_dir.join(format!("{}.gitconfig", id));
|
let p = profile_dir.join(format!("{}.gitconfig", id));
|
||||||
if !p.exists() {
|
if !p.exists() {
|
||||||
|
|
@ -38,7 +38,7 @@ fn get_profile_path(config: &GoshConfig, id: &str) -> Result<PathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_mocking() -> bool {
|
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<()> {
|
fn run_command(cmd: &mut Command) -> Result<()> {
|
||||||
|
|
@ -70,7 +70,7 @@ fn run_command(cmd: &mut Command) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_profile_dir(config: &GoshConfig) -> Result<PathBuf> {
|
fn get_profile_dir(config: &NajConfig) -> Result<PathBuf> {
|
||||||
expand_path(&config.profile_dir)
|
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
|
// Check if the path belongs to our profile directory
|
||||||
// We use string containment as a heuristic since paths might be canonicalized differently
|
// We use string containment as a heuristic since paths might be canonicalized differently
|
||||||
// but typically we add absolute paths.
|
// 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"));
|
|| (val.contains("/profiles/") && val.ends_with(".gitconfig"));
|
||||||
|
|
||||||
if is_gosh_profile {
|
if is_naj_profile {
|
||||||
let mut cmd = Command::new("git");
|
let mut cmd = Command::new("git");
|
||||||
cmd.args(&["config", "--local", "--unset", "include.path", val]);
|
cmd.args(&["config", "--local", "--unset", "include.path", val]);
|
||||||
// We tolerate failure here (e.g. if key doesn't exist anymore for some reason)
|
// 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(())
|
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 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");
|
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)
|
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")
|
let status = Command::new("git")
|
||||||
.args(&["rev-parse", "--is-inside-work-tree"])
|
.args(&["rev-parse", "--is-inside-work-tree"])
|
||||||
.stdout(std::process::Stdio::null())
|
.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");
|
let mut cmd = Command::new("git");
|
||||||
cmd.args(&["config", "--remove-section", section]);
|
cmd.args(&["config", "--remove-section", section]);
|
||||||
// Ignore errors
|
// Ignore errors
|
||||||
let _ = cmd.output();
|
if is_mocking() {
|
||||||
|
eprintln!("[DRY-RUN] {:?}", cmd);
|
||||||
|
} else {
|
||||||
|
let _ = cmd.output();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unset keys
|
// Unset keys
|
||||||
|
|
@ -173,11 +181,15 @@ fn run_switch(config: &GoshConfig, profile_id: &str, force: bool) -> Result<()>
|
||||||
for key in keys {
|
for key in keys {
|
||||||
let mut cmd = Command::new("git");
|
let mut cmd = Command::new("git");
|
||||||
cmd.args(&["config", "--unset", key]);
|
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)?;
|
let profiles_dir = get_profile_dir(config)?;
|
||||||
clean_existing_profiles(&profiles_dir)?;
|
clean_existing_profiles(&profiles_dir)?;
|
||||||
|
|
||||||
|
|
@ -188,10 +200,12 @@ fn run_switch(config: &GoshConfig, profile_id: &str, force: bool) -> Result<()>
|
||||||
run_command(&mut cmd)?;
|
run_command(&mut cmd)?;
|
||||||
println!("Switched to profile '{}'", profile_id);
|
println!("Switched to profile '{}'", profile_id);
|
||||||
|
|
||||||
|
warn_if_dirty_config(profile_id)?;
|
||||||
|
|
||||||
Ok(())
|
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)
|
// 1. Pass raw args to git (no injection)
|
||||||
let mut cmd = Command::new("git");
|
let mut cmd = Command::new("git");
|
||||||
cmd.args(args);
|
cmd.args(args);
|
||||||
|
|
@ -272,3 +286,28 @@ fn extract_basename(url: &str) -> PathBuf {
|
||||||
let path = Path::new(s);
|
let path = Path::new(s);
|
||||||
path.file_name().map(|n| PathBuf::from(n)).unwrap_or_else(|| PathBuf::from("repo"))
|
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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,15 @@ use anyhow::{bail, Context, Result};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use crate::config::GoshConfig;
|
use crate::config::NajConfig;
|
||||||
use crate::utils::expand_path;
|
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)?;
|
let profile_dir = expand_path(&config.profile_dir)?;
|
||||||
Ok(profile_dir.join(format!("{}.gitconfig", id)))
|
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)?;
|
let file_path = get_profile_path(config, id)?;
|
||||||
|
|
||||||
if file_path.exists() {
|
if file_path.exists() {
|
||||||
|
|
@ -28,7 +28,7 @@ pub fn create_profile(config: &GoshConfig, name: &str, email: &str, id: &str) ->
|
||||||
Ok(())
|
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)?;
|
let file_path = get_profile_path(config, id)?;
|
||||||
|
|
||||||
if !file_path.exists() {
|
if !file_path.exists() {
|
||||||
|
|
@ -40,7 +40,7 @@ pub fn remove_profile(config: &GoshConfig, id: &str) -> Result<()> {
|
||||||
Ok(())
|
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)?;
|
let file_path = get_profile_path(config, id)?;
|
||||||
|
|
||||||
if !file_path.exists() {
|
if !file_path.exists() {
|
||||||
|
|
@ -60,7 +60,7 @@ pub fn edit_profile(config: &GoshConfig, id: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_profiles(config: &GoshConfig) -> Result<()> {
|
pub fn list_profiles(config: &NajConfig) -> Result<()> {
|
||||||
let profile_dir = expand_path(&config.profile_dir)?;
|
let profile_dir = expand_path(&config.profile_dir)?;
|
||||||
|
|
||||||
if !profile_dir.exists() {
|
if !profile_dir.exists() {
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,14 @@ pub const BLACKLIST_KEYS: &[&str] = &[
|
||||||
"http.cookieFile",
|
"http.cookieFile",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn get_blind_injections() -> Vec<(&'static str, &'static str)> {
|
pub const BLIND_INJECTIONS: &[(&str, &str)] = &[
|
||||||
vec![
|
("user.name", ""),
|
||||||
("user.name", ""),
|
("user.email", ""),
|
||||||
("user.email", ""),
|
("user.signingkey", ""),
|
||||||
("user.signingkey", ""),
|
("core.sshCommand", ""),
|
||||||
("core.sshCommand", ""),
|
("gpg.format", "openpgp"),
|
||||||
("gpg.format", "openpgp"),
|
("gpg.ssh.program", "ssh-keygen"),
|
||||||
("gpg.ssh.program", "ssh-keygen"),
|
("gpg.program", "gpg"),
|
||||||
("gpg.program", "gpg"),
|
("commit.gpgsign", "false"),
|
||||||
("commit.gpgsign", "false"),
|
("tag.gpgsign", "false"),
|
||||||
("tag.gpgsign", "false"),
|
];
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,15 @@ fn test_config_initialization() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let temp_dir = TempDir::new()?;
|
let temp_dir = TempDir::new()?;
|
||||||
let config_path = temp_dir.path().join("config");
|
let config_path = temp_dir.path().join("config");
|
||||||
|
|
||||||
// Run gosh with GOSH_CONFIG_PATH set to temp dir
|
// Run naj with NAJ_CONFIG_PATH set to temp dir
|
||||||
let mut cmd = Command::new(env!("CARGO_BIN_EXE_gosh"));
|
let mut cmd = Command::new(env!("CARGO_BIN_EXE_naj"));
|
||||||
cmd.env("GOSH_CONFIG_PATH", &config_path)
|
cmd.env("NAJ_CONFIG_PATH", &config_path)
|
||||||
.arg("-l") // Trigger config load using list flag (not positional "list" profile)
|
.arg("-l") // Trigger config load using list flag (not positional "list" profile)
|
||||||
.assert()
|
.assert()
|
||||||
.success();
|
.success();
|
||||||
|
|
||||||
// Verify config file exists
|
// Verify config file exists
|
||||||
assert!(config_path.join("gosh.toml").exists());
|
assert!(config_path.join("naj.toml").exists());
|
||||||
assert!(config_path.join("profiles").exists());
|
assert!(config_path.join("profiles").exists());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -26,8 +26,8 @@ fn test_profile_creation_and_listing() -> Result<(), Box<dyn std::error::Error>>
|
||||||
let temp_dir = TempDir::new()?;
|
let temp_dir = TempDir::new()?;
|
||||||
let config_path = temp_dir.path();
|
let config_path = temp_dir.path();
|
||||||
|
|
||||||
let mut cmd = Command::new(env!("CARGO_BIN_EXE_gosh"));
|
let mut cmd = Command::new(env!("CARGO_BIN_EXE_naj"));
|
||||||
cmd.env("GOSH_CONFIG_PATH", config_path)
|
cmd.env("NAJ_CONFIG_PATH", config_path)
|
||||||
.args(&["-c", "Test User", "test@example.com", "test_user"])
|
.args(&["-c", "Test User", "test@example.com", "test_user"])
|
||||||
.assert()
|
.assert()
|
||||||
.success();
|
.success();
|
||||||
|
|
@ -40,8 +40,8 @@ fn test_profile_creation_and_listing() -> Result<(), Box<dyn std::error::Error>>
|
||||||
assert!(content.contains("email = test@example.com"));
|
assert!(content.contains("email = test@example.com"));
|
||||||
|
|
||||||
// Verify list
|
// Verify list
|
||||||
let mut cmd_list = Command::new(env!("CARGO_BIN_EXE_gosh"));
|
let mut cmd_list = Command::new(env!("CARGO_BIN_EXE_naj"));
|
||||||
cmd_list.env("GOSH_CONFIG_PATH", config_path)
|
cmd_list.env("NAJ_CONFIG_PATH", config_path)
|
||||||
.arg("-l")
|
.arg("-l")
|
||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
|
|
@ -56,15 +56,15 @@ fn test_duplicate_creation_failure() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config_path = temp_dir.path();
|
let config_path = temp_dir.path();
|
||||||
|
|
||||||
// Create first
|
// Create first
|
||||||
Command::new(env!("CARGO_BIN_EXE_gosh"))
|
Command::new(env!("CARGO_BIN_EXE_naj"))
|
||||||
.env("GOSH_CONFIG_PATH", config_path)
|
.env("NAJ_CONFIG_PATH", config_path)
|
||||||
.args(&["-c", "User", "u@e.com", "dup_test"])
|
.args(&["-c", "User", "u@e.com", "dup_test"])
|
||||||
.assert()
|
.assert()
|
||||||
.success();
|
.success();
|
||||||
|
|
||||||
// Create duplicate
|
// Create duplicate
|
||||||
Command::new(env!("CARGO_BIN_EXE_gosh"))
|
Command::new(env!("CARGO_BIN_EXE_naj"))
|
||||||
.env("GOSH_CONFIG_PATH", config_path)
|
.env("NAJ_CONFIG_PATH", config_path)
|
||||||
.args(&["-c", "User2", "u2@e.com", "dup_test"])
|
.args(&["-c", "User2", "u2@e.com", "dup_test"])
|
||||||
.assert()
|
.assert()
|
||||||
.failure(); // Should fail
|
.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");
|
let profile_path = config_path.join("profiles").join("rem_test.gitconfig");
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
Command::new(env!("CARGO_BIN_EXE_gosh"))
|
Command::new(env!("CARGO_BIN_EXE_naj"))
|
||||||
.env("GOSH_CONFIG_PATH", config_path)
|
.env("NAJ_CONFIG_PATH", config_path)
|
||||||
.args(&["-c", "User", "u@e.com", "rem_test"])
|
.args(&["-c", "User", "u@e.com", "rem_test"])
|
||||||
.assert()
|
.assert()
|
||||||
.success();
|
.success();
|
||||||
assert!(profile_path.exists());
|
assert!(profile_path.exists());
|
||||||
|
|
||||||
// Remove
|
// Remove
|
||||||
Command::new(env!("CARGO_BIN_EXE_gosh"))
|
Command::new(env!("CARGO_BIN_EXE_naj"))
|
||||||
.env("GOSH_CONFIG_PATH", config_path)
|
.env("NAJ_CONFIG_PATH", config_path)
|
||||||
.args(&["-r", "rem_test"])
|
.args(&["-r", "rem_test"])
|
||||||
.assert()
|
.assert()
|
||||||
.success();
|
.success();
|
||||||
assert!(!profile_path.exists());
|
assert!(!profile_path.exists());
|
||||||
|
|
||||||
// Remove non-existent
|
// Remove non-existent
|
||||||
Command::new(env!("CARGO_BIN_EXE_gosh"))
|
Command::new(env!("CARGO_BIN_EXE_naj"))
|
||||||
.env("GOSH_CONFIG_PATH", config_path)
|
.env("NAJ_CONFIG_PATH", config_path)
|
||||||
.args(&["-r", "rem_test"])
|
.args(&["-r", "rem_test"])
|
||||||
.assert()
|
.assert()
|
||||||
.failure();
|
.failure();
|
||||||
|
|
@ -110,16 +110,16 @@ fn test_exec_dry_run_injection_strict() -> Result<(), Box<dyn std::error::Error>
|
||||||
let config_path = temp_dir.path();
|
let config_path = temp_dir.path();
|
||||||
|
|
||||||
// Create a profile first
|
// Create a profile first
|
||||||
Command::new(env!("CARGO_BIN_EXE_gosh"))
|
Command::new(env!("CARGO_BIN_EXE_naj"))
|
||||||
.env("GOSH_CONFIG_PATH", config_path)
|
.env("NAJ_CONFIG_PATH", config_path)
|
||||||
.args(&["-c", "Test", "test@e.com", "p1"])
|
.args(&["-c", "Test", "test@e.com", "p1"])
|
||||||
.assert()
|
.assert()
|
||||||
.success();
|
.success();
|
||||||
|
|
||||||
// Run exec with mocking
|
// Run exec with mocking
|
||||||
let mut cmd = Command::new(env!("CARGO_BIN_EXE_gosh"));
|
let mut cmd = Command::new(env!("CARGO_BIN_EXE_naj"));
|
||||||
cmd.env("GOSH_CONFIG_PATH", config_path)
|
cmd.env("NAJ_CONFIG_PATH", config_path)
|
||||||
.env("GOSH_MOCKING", "1")
|
.env("NAJ_MOCKING", "1")
|
||||||
.args(&["p1", "commit", "-m", "foo"])
|
.args(&["p1", "commit", "-m", "foo"])
|
||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
|
|
@ -150,15 +150,15 @@ fn test_switch_mode_persistent() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
// Create profile
|
// Create profile
|
||||||
Command::new(env!("CARGO_BIN_EXE_gosh"))
|
Command::new(env!("CARGO_BIN_EXE_naj"))
|
||||||
.env("GOSH_CONFIG_PATH", &config_path)
|
.env("NAJ_CONFIG_PATH", &config_path)
|
||||||
.args(&["-c", "Switch User", "s@e.com", "switch_test"])
|
.args(&["-c", "Switch User", "s@e.com", "switch_test"])
|
||||||
.assert()
|
.assert()
|
||||||
.success();
|
.success();
|
||||||
|
|
||||||
// Switch
|
// Switch
|
||||||
Command::new(env!("CARGO_BIN_EXE_gosh"))
|
Command::new(env!("CARGO_BIN_EXE_naj"))
|
||||||
.env("GOSH_CONFIG_PATH", &config_path)
|
.env("NAJ_CONFIG_PATH", &config_path)
|
||||||
.current_dir(&repo_dir)
|
.current_dir(&repo_dir)
|
||||||
.arg("switch_test")
|
.arg("switch_test")
|
||||||
.assert()
|
.assert()
|
||||||
|
|
@ -197,15 +197,15 @@ fn test_switch_force_mode_sanitization() -> Result<(), Box<dyn std::error::Error
|
||||||
fs::write(&config_file, current_config)?;
|
fs::write(&config_file, current_config)?;
|
||||||
|
|
||||||
// Create profile
|
// Create profile
|
||||||
Command::new(env!("CARGO_BIN_EXE_gosh"))
|
Command::new(env!("CARGO_BIN_EXE_naj"))
|
||||||
.env("GOSH_CONFIG_PATH", &config_path)
|
.env("NAJ_CONFIG_PATH", &config_path)
|
||||||
.args(&["-c", "Force User", "f@e.com", "force_test"])
|
.args(&["-c", "Force User", "f@e.com", "force_test"])
|
||||||
.assert()
|
.assert()
|
||||||
.success();
|
.success();
|
||||||
|
|
||||||
// Force Switch
|
// Force Switch
|
||||||
Command::new(env!("CARGO_BIN_EXE_gosh"))
|
Command::new(env!("CARGO_BIN_EXE_naj"))
|
||||||
.env("GOSH_CONFIG_PATH", &config_path)
|
.env("NAJ_CONFIG_PATH", &config_path)
|
||||||
.current_dir(&repo_dir)
|
.current_dir(&repo_dir)
|
||||||
.args(&["force_test", "-f"])
|
.args(&["force_test", "-f"])
|
||||||
.assert()
|
.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
|
// Git allows cloning an empty repo, so this is enough
|
||||||
|
|
||||||
// 2. Create Profile
|
// 2. Create Profile
|
||||||
Command::new(env!("CARGO_BIN_EXE_gosh"))
|
Command::new(env!("CARGO_BIN_EXE_naj"))
|
||||||
.env("GOSH_CONFIG_PATH", &config_path)
|
.env("NAJ_CONFIG_PATH", &config_path)
|
||||||
.args(&["-c", "CloneUser", "c@e.com", "clone_test"])
|
.args(&["-c", "CloneUser", "c@e.com", "clone_test"])
|
||||||
.assert()
|
.assert()
|
||||||
.success();
|
.success();
|
||||||
|
|
||||||
// 3. Run Gosh Clone (Setup Mode)
|
// 3. Run Naj Clone (Setup Mode)
|
||||||
// We clone from local source to a folder named "dest_repo"
|
// We clone from local source to a folder named "dest_repo"
|
||||||
// Gosh should:
|
// Naj should:
|
||||||
// a) Run git clone
|
// a) Run git clone
|
||||||
// b) Infer directory is "dest_repo"
|
// b) Infer directory is "dest_repo"
|
||||||
// c) Run switch logic on "dest_repo"
|
// c) Run switch logic on "dest_repo"
|
||||||
let dest_repo_name = "dest_repo";
|
let dest_repo_name = "dest_repo";
|
||||||
|
|
||||||
Command::new(env!("CARGO_BIN_EXE_gosh"))
|
Command::new(env!("CARGO_BIN_EXE_naj"))
|
||||||
.env("GOSH_CONFIG_PATH", &config_path)
|
.env("NAJ_CONFIG_PATH", &config_path)
|
||||||
.current_dir(temp_dir.path()) // Execute in temp root
|
.current_dir(temp_dir.path()) // Execute in temp root
|
||||||
.args(&["clone_test", "clone", source_repo.to_str().unwrap(), dest_repo_name])
|
.args(&["clone_test", "clone", source_repo.to_str().unwrap(), dest_repo_name])
|
||||||
.assert()
|
.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");
|
assert!(dest_git_config.exists(), "Cloned repo config should exist");
|
||||||
|
|
||||||
let content = fs::read_to_string(dest_git_config)?;
|
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("[include]"));
|
||||||
assert!(content.contains("clone_test.gitconfig"));
|
assert!(content.contains("clone_test.gitconfig"));
|
||||||
|
|
||||||
|
|
|
||||||
38
tests/test_force_mocking.rs
Normal file
38
tests/test_force_mocking.rs
Normal 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(())
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue