Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,41 @@ env:
RUST_BACKTRACE: 1

jobs:
version:
name: Check version is not already tagged
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- name: Reject an existing version tag
run: |
python3 - <<'PY'
import subprocess
import tomllib

with open("Cargo.toml", "rb") as manifest:
version = tomllib.load(manifest)["package"]["version"]
tag = f"v{version}"
tags = subprocess.check_output(["git", "tag", "--list"], text=True).splitlines()
if tag in tags:
print(
f"::error file=Cargo.toml::Tag {tag} already exists on GitHub. "
"Bump package.version in Cargo.toml to a new version and update Cargo.lock."
)
raise SystemExit(1)
print(f"Version {version} is available: tag {tag} does not exist.")
PY

format:
name: Rust and TOML formatting
needs: version
# Tag-triggered release checks intentionally skip the PR-only version job.
if: >-
${{ !cancelled() && (needs.version.result == 'success' ||
(github.event_name != 'pull_request' && needs.version.result == 'skipped')) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand All @@ -36,6 +69,9 @@ jobs:

check:
name: Check, Clippy, tests, and binary (${{ matrix.os }})
needs: format
# Allow the skipped version ancestor on releases, but require formatting to pass.
if: ${{ !cancelled() && needs.format.result == 'success' }}
strategy:
fail-fast: false
matrix:
Expand Down
9 changes: 8 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ one executable for macOS and Linux. Read README.md before changing its behavior.
examples, and section coverage equivalent. Preserve their language-switch links.
- Use the exact toolchain in rust-toolchain.toml. Keep Cargo.lock checked in.
- Building requires a C compiler for vendored libgit2 and SQLite. The distributed executable
does not require a separate Rust or Git installation.
needs no separate Rust installation, and its built-in commands need no system Git.
Only the explicit `filetrail git` passthrough and its integration tests require Git on PATH.
- Run `cargo fmt --all`, `cargo clippy --locked --all-targets -- -D warnings`,
`cargo test --locked --all-targets`, and `cargo test --locked --doc`.
- Completion integration tests require Bash, Zsh, and Fish on PATH.
Expand Down Expand Up @@ -114,4 +115,10 @@ Only run `filetrail commit` when a commit is within the user's requested scope.
Omit `-m` to use the generated message. Do not run `resolve --use-source`, enable
deletion, or install a service merely to make a test or diagnostic pass.

`filetrail cd` jumps to the repository root when the Bash, Zsh, or Fish integration
is loaded; `command filetrail cd` prints its path. `filetrail git <args...>` runs
system Git in that root under the operation lock. Put `--data-dir` before `git`.
This explicit passthrough follows normal Git behavior, including unmanaged files;
only run mutations such as commits or pushes when the user requests them.

Report actual test outcomes and distinguish local checks from GitHub-hosted CI.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ edition = "2024"
license = "MIT"
name = "filetrail"
repository = "https://github.com/RinChanNOWWW/FileTrail"
version = "0.1.0"
version = "0.2.0"

[dependencies]
anyhow = "1.0"
Expand Down
40 changes: 36 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ lets you review and commit them on your terms.

Use it for dotfiles, scripts, notes, or other files spread across your machine.
Keep separate macOS and Linux configurations in the same repository. Everything
runs from a single executable, with no separate Git installation required.
runs from a single executable. Built-in synchronization and version-control commands
need no separate Git installation; the optional `filetrail git` passthrough uses system Git.

## Install

Expand All @@ -30,7 +31,7 @@ with `./install.sh zsh`. It installs with Cargo, then configures that shell's
completion. Open a new shell afterward. The installation root defaults to
`${CARGO_HOME:-$HOME/.cargo}`; set `CARGO_INSTALL_ROOT` to override it.

## Tab completion
## Tab completion and shell integration

If you installed FileTrail with `cargo install`, enable completion with:

Expand All @@ -49,11 +50,12 @@ filetrail completions fish --install
Run the command for the shell you use, then open a new shell. Tab completes
subcommands (including `daemon` and `service` actions), options, and file paths.
For example, try `filetrail da<Tab>`, `filetrail daemon st<Tab>`, or
`filetrail add --f<Tab>`.
`filetrail add --f<Tab>`. This also enables `filetrail cd` to change the current
shell's directory to the target repository.

Installation preserves existing shell configuration and is safe to repeat. It
uses `.zshrc` (respecting `ZDOTDIR`), `.bashrc` and Bash's active login profile,
or Fish's completion directory (respecting `XDG_CONFIG_HOME`). Home paths use
or Fish's completion and function directories (respecting `XDG_CONFIG_HOME`). Home paths use
`$HOME` in the installed hooks and command output, so your username is not embedded.
Paths outside Home retain their absolute location. Completion stays
in sync when you upgrade the executable at the same location. Run installation
Expand Down Expand Up @@ -228,6 +230,36 @@ filetrail resume
commands still copy files while automatic synchronization is paused. Paths passed
to `diff`, `commit`, and `resolve` are relative to the repository root.

## Jump to the repository and run Git

With Bash, Zsh, or Fish integration installed, jump to the target repository:

```sh
filetrail completions --install # Also run once when upgrading to enable directory jumping
# Open a new shell, then:
filetrail cd
```

This changes the current shell's directory to the repository root, even when a
`--subdir` is configured. Without shell integration, the executable prints the path;
in Bash or Zsh you can use `cd "$(command filetrail cd)"`. Use `command filetrail cd`
to print the path when integration is loaded, or add `--print0` for NUL-terminated output.

Run any system Git command in the target repository without changing directories:

```sh
filetrail git status
filetrail git log --oneline -10
filetrail git push origin master
filetrail --data-dir ~/filetrail-work git push origin master
```

This requires `git` on PATH and uses its normal configuration, credentials, and hooks.
Put FileTrail's `--data-dir` before `git`; Git arguments, input/output, and exit codes
are passed through. Synchronization waits while the Git command runs. No automatic
sync, staging, or commit is added. `filetrail git commit` follows normal Git staging
and can include any staged file; `filetrail commit` remains limited to managed changes.

## Resolve conflicts

FileTrail reports a conflict if a destination differs from an existing source on
Expand Down
38 changes: 35 additions & 3 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ FileTrail 是一个支持 Git 版本管理的文件同步工具。它监听你
将变化同步到本地 Git 仓库,让你查看差异并决定何时提交。

你可以用它管理分散在电脑上的 dotfiles、脚本、笔记等文件,也可以在同一仓库中
分别保存 macOS 和 Linux 的配置。所有功能由一个可执行文件完成,无需额外安装 Git。
分别保存 macOS 和 Linux 的配置。所有功能由一个可执行文件提供。内置同步和版本管理命令
无需额外安装 Git;可选的 `filetrail git` 透传命令使用系统 Git。

## 安装

Expand All @@ -27,7 +28,7 @@ filetrail --help
它先通过 Cargo 安装,再配置所选 shell 的补全,完成后重新打开 shell 即可。
安装根目录默认为 `${CARGO_HOME:-$HOME/.cargo}`,可通过 `CARGO_INSTALL_ROOT` 覆盖。

## Tab 补全
## Tab 补全与 shell 集成

如果使用 `cargo install` 安装 FileTrail,执行以下命令启用补全:

Expand All @@ -46,9 +47,10 @@ filetrail completions fish --install
执行你所用 shell 对应的命令,然后重新打开 shell。Tab 可以补全子命令
(包括 `daemon` 和 `service` 的操作)、选项及文件路径。例如:
`filetrail da<Tab>`、`filetrail daemon st<Tab>`、`filetrail add --f<Tab>`。
这也会启用 `filetrail cd`,用于将当前 shell 的工作目录切换到目标仓库。

安装会保留已有 shell 配置,重复执行不会添加重复配置。配置位置为 `.zshrc`
(遵循 `ZDOTDIR`)、`.bashrc` 和 Bash 当前使用的登录配置文件,或 Fish 的补全目录
(遵循 `ZDOTDIR`)、`.bashrc` 和 Bash 当前使用的登录配置文件,或 Fish 的补全与函数目录
(遵循 `XDG_CONFIG_HOME`)。安装的补全配置和命令输出使用 `$HOME` 表示 Home 路径,
不写入用户名;Home 以外的路径保留绝对位置。在相同位置升级可执行文件后,补全会同步更新;
移动可执行文件后需重新安装补全。若要移除补全,删除安装命令所列配置文件中
Expand Down Expand Up @@ -210,6 +212,36 @@ filetrail resume
`resume` 会补齐暂停期间的变化。显式执行 `sync` 和 `add` 时,即使自动同步已暂停,
仍会复制文件。`diff`、`commit` 和 `resolve` 接收的路径都相对于仓库根目录。

## 跳转到仓库与执行 Git

安装 Bash、Zsh 或 Fish 集成后,可以直接跳转到目标仓库:

```sh
filetrail completions --install # 从旧版升级时也执行一次,以启用目录跳转
# 重新打开 shell 后:
filetrail cd
```

这会将当前 shell 的工作目录切换到仓库根目录,即使配置了 `--subdir` 也是如此。
未加载 shell 集成时,可执行文件只输出路径;在 Bash 或 Zsh 中可以使用
`cd "$(command filetrail cd)"`。加载集成后,使用 `command filetrail cd` 仍可输出路径,
或加上 `--print0` 输出以 NUL 结尾的路径。

无需切换目录,也可以直接在目标仓库执行任意系统 Git 命令:

```sh
filetrail git status
filetrail git log --oneline -10
filetrail git push origin master
filetrail --data-dir ~/filetrail-work git push origin master
```

此功能要求 PATH 中存在 `git`,并使用 Git 原有的配置、凭据和 hooks。
FileTrail 的 `--data-dir` 应放在 `git` 之前;Git 参数、输入输出和退出码会透传。
Git 命令执行期间,同步操作会等待。该命令不会额外执行同步、暂存或提交。
`filetrail git commit` 按 Git 的正常暂存规则工作,可以包含任意已暂存文件;
`filetrail commit` 仍然只提交受管理的修改。

## 处理冲突

首次同步时目标与已有来源内容不同,或者你在 FileTrail 之外修改了目标文件,
Expand Down
23 changes: 19 additions & 4 deletions src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,13 @@ pub fn install(shell: Shell, binary: &Path) -> Result<Vec<PathBuf>> {
vec![home.join(".bashrc"), login]
}
Shell::Zsh => vec![environment_directory("ZDOTDIR", &home).join(".zshrc")],
Shell::Fish => vec![
environment_directory("XDG_CONFIG_HOME", &home.join(".config"))
.join("fish/completions/filetrail.fish"),
],
Shell::Fish => {
let fish = environment_directory("XDG_CONFIG_HOME", &home.join(".config")).join("fish");
vec![
fish.join("completions/filetrail.fish"),
fish.join("functions/filetrail.fish"),
]
}
_ => bail!("automatic installation supports bash, zsh, and fish only"),
};

Expand Down Expand Up @@ -126,6 +129,18 @@ fn executable_expression(shell: Shell, binary: &Path, home: &Path) -> Result<Str
})
}

/// A child process cannot change its parent's directory. These shell functions
/// intercept `cd`, while all other commands keep their original arguments.
pub fn shell_integration(shell: Shell, binary: &Path) -> Result<String> {
let template = match shell {
Shell::Bash | Shell::Zsh => include_str!("shell_integration.sh"),
Shell::Fish => include_str!("shell_integration.fish"),
_ => return Ok(String::new()),
};
let home = dirs::home_dir().context("cannot determine home directory")?;
Ok(template.replace("@FILETRAIL@", &executable_expression(shell, binary, &home)?))
}

fn hook(shell: Shell, binary: &Path, home: &Path) -> Result<String> {
let quoted = executable_expression(shell, binary, home)?;
let body = match shell {
Expand Down
16 changes: 16 additions & 0 deletions src/git.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::ffi::OsString;
use std::path::Path;
use std::process::Command;
use std::process::ExitStatus;

use anyhow::Context;
use anyhow::Result;
Expand All @@ -22,6 +25,19 @@ pub fn open(config: &Config) -> Result<Repository> {
Ok(repository)
}

/// Explicit passthrough only; built-in Git operations continue to use libgit2.
pub fn run(store: &Store, args: &[OsString]) -> Result<ExitStatus> {
// Keep sync and retarget from changing the working tree while Git is running.
let _lock = store.lock()?;
let config = store.config()?;
open(&config)?;
Command::new("git")
.args(args)
.current_dir(&config.repository)
.status()
.context("cannot run system Git; install git and make sure it is on PATH")
}

pub fn ensure_idle(repository: &Repository) -> Result<()> {
if repository.state() != RepositoryState::Clean || repository.index()?.has_conflicts() {
bail!("repository has an ongoing Git operation or unresolved conflicts");
Expand Down
Loading
Loading