Files
Kigi-CLI/crates/codegen/kigi-tools/src/gitignore.rs
T
ZacharyZhang-NY d6c20fc13f M0: compilable skeleton — Kigi 0.1.0 fork surgery
Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
2026-07-17 05:31:01 -04:00

122 lines
4.4 KiB
Rust

//! Shared gitignore matching utility.
//!
//! Single source of truth for checking whether a path is ignored by
//! `.gitignore` rules. Used by both the initial AGENTS.md discovery
//! (`kigi-agent::prompt::ignore`) and the runtime tracker
//! (`AgentsMdTracker`).
use ignore::gitignore::Gitignore;
use std::path::Path;
/// Check if a path is ignored by the given gitignore rules.
///
/// Strips `git_root` prefix before matching — gitignore patterns are
/// repo-relative, so `/repo/build/out.o` becomes `build/out.o` when
/// `git_root` is `/repo`.
///
/// This is a pure function — no filesystem access, just `Gitignore::matched()`.
pub fn is_ignored(gitignore: &Gitignore, path: &Path, git_root: Option<&Path>) -> bool {
let check_path = match git_root {
Some(root) => match path.strip_prefix(root) {
Ok(relative) => relative,
// Outside the repo (e.g. ~/.kigi/Agents.md) → not ignored.
Err(_) => return false,
},
None => {
// Absolute path + no git root → can't strip to repo-relative;
// the `ignore` crate panics on absolute paths not under root.
if path.is_absolute() {
return false;
}
path
}
};
// matched_path_or_any_parents checks parent dirs too, so
// `build/AGENTS.md` correctly matches a `build/` pattern.
gitignore
.matched_path_or_any_parents(check_path, check_path.is_dir())
.is_ignore()
}
#[cfg(test)]
mod tests {
use super::*;
use ignore::gitignore::GitignoreBuilder;
fn build_gitignore(root: &Path, patterns: &[&str]) -> Gitignore {
let mut builder = GitignoreBuilder::new(root);
for pattern in patterns {
builder.add_line(None, pattern).unwrap();
}
builder.build().unwrap()
}
#[test]
fn is_ignored_matches_gitignored_paths() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
// Canonicalize to handle macOS /tmp → /private/tmp
let root = &dunce::canonicalize(root).unwrap();
let gi = build_gitignore(root, &["build/"]);
assert!(is_ignored(&gi, &root.join("build/out.o"), Some(root)));
assert!(is_ignored(&gi, &root.join("build/sub/file.rs"), Some(root)));
}
#[test]
fn is_ignored_does_not_match_non_gitignored_paths() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let root = &dunce::canonicalize(root).unwrap();
let gi = build_gitignore(root, &["build/"]);
assert!(!is_ignored(&gi, &root.join("src/main.rs"), Some(root)));
assert!(!is_ignored(&gi, &root.join("AGENTS.md"), Some(root)));
}
#[test]
fn is_ignored_strips_git_root_prefix() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let root = &dunce::canonicalize(root).unwrap();
let gi = build_gitignore(root, &["build/"]);
// With root: strips prefix, matches build/out.o
assert!(is_ignored(&gi, &root.join("build/out.o"), Some(root)));
// Without root: relative path still matches
assert!(is_ignored(
&gi,
&std::path::PathBuf::from("build/out.o"),
None
));
}
#[test]
fn is_ignored_returns_false_for_path_outside_git_root() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let root = &dunce::canonicalize(root).unwrap();
let gi = build_gitignore(root, &["build/", "*.md"]);
// A path completely outside the git root should not be checked
// against the repo's .gitignore (e.g., ~/.kigi/Agents.md).
let outside_path = std::path::PathBuf::from("/some/other/path/Agents.md");
assert!(!is_ignored(&gi, &outside_path, Some(root)));
}
/// Regression: running outside a git repo panicked with
/// "path is expected to be under the root" (ignore crate assert).
#[test]
fn regression_no_panic_on_absolute_path_without_git_root() {
let gi = build_gitignore(Path::new("."), &["node_modules/", "*.log"]);
let abs_path = Path::new("/Users/someone/home/AGENTS.md");
// Proves the raw crate panics with these inputs.
assert!(
std::panic::catch_unwind(|| {
gi.matched_path_or_any_parents(abs_path, false);
})
.is_err()
);
// Our wrapper guards against it.
assert!(!is_ignored(&gi, abs_path, None));
}
}