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
99 changes: 99 additions & 0 deletions cmd/kai/init_no_history_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package main

import (
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/kaicontext/kai-engine/graph"
)

// initTestRepo builds a throwaway git repo with n commits and chdirs into it.
func initTestRepo(t *testing.T, commits int) string {
t.Helper()
dir := t.TempDir()

run := func(args ...string) {
t.Helper()
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = dir
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("%v: %v\n%s", args, err, out)
}
}
run("git", "init", "--quiet")
run("git", "config", "user.email", "test@example.com")
run("git", "config", "user.name", "Test")
for i := 0; i < commits; i++ {
name := filepath.Join(dir, "f"+string(rune('a'+i))+".go")
if err := os.WriteFile(name, []byte("package main\n\nfunc F"+string(rune('a'+i))+"() {}\n"), 0o644); err != nil {
t.Fatalf("write: %v", err)
}
run("git", "add", "-A")
run("git", "commit", "--quiet", "-m", "commit")
}

wd, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
if err := os.Chdir(dir); err != nil {
t.Fatalf("chdir: %v", err)
}
t.Cleanup(func() { os.Chdir(wd) })
return dir
}

// snapshotCount reports how many Snapshot nodes the repo's graph holds.
func snapshotCount(t *testing.T, dir string) int {
t.Helper()
db, err := graph.Open(filepath.Join(dir, kaiDir, dbFile), filepath.Join(dir, kaiDir, objectsDir))
if err != nil {
t.Fatalf("open graph: %v", err)
}
defer db.Close()
nodes, err := db.GetNodesByKind(graph.KindSnapshot)
if err != nil {
t.Fatalf("nodes by kind: %v", err)
}
return len(nodes)
}

// runInitInRepo runs a local-only init in the current directory.
func runInitInRepo(t *testing.T, noHistory bool) {
t.Helper()
oldKaiDir, oldRemote, oldYes, oldHist := kaiDir, initNoRemote, initAssumeYes, initNoHistory
kaiDir = ".kai"
initNoRemote, initAssumeYes, initNoHistory = true, true, noHistory
t.Cleanup(func() {
kaiDir, initNoRemote, initAssumeYes, initNoHistory = oldKaiDir, oldRemote, oldYes, oldHist
})
if err := runInit(nil, nil); err != nil {
t.Fatalf("runInit(noHistory=%v): %v", noHistory, err)
}
}

// The history import is nearly all of init's wall clock and a review pod never
// reads what it produces, so --no-history must actually skip it — and must
// still leave the working tree's own graph behind, which is the thing the
// review does read.
func TestInitNoHistorySkipsTheImport(t *testing.T) {
dir := initTestRepo(t, 3)
runInitInRepo(t, true)

if got := snapshotCount(t, dir); got != 1 {
t.Fatalf("--no-history should leave only the working-tree capture, got %d snapshots", got)
}
}

// The default is unchanged: a person running `kai init` still gets the history
// that makes log, blame and bisect worth having.
func TestInitImportsHistoryByDefault(t *testing.T) {
dir := initTestRepo(t, 3)
runInitInRepo(t, false)

if got := snapshotCount(t, dir); got <= 1 {
t.Fatalf("default init should import history, got %d snapshots", got)
}
}
25 changes: 20 additions & 5 deletions cmd/kai/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4525,6 +4525,7 @@ func init() {
initCmd.Flags().StringVar(&initOrg, "org", "", "Org slug to initialize under (default: your personal org; also via KAI_ORG)")
initCmd.Flags().StringVar(&initEmail, "email", "", "Email to sign up / log in with non-interactively (also via KAI_INIT_EMAIL)")
initCmd.Flags().BoolVar(&initNoRemote, "no-remote", false, "Build the local semantic graph only: skip kaicontext.com signup/login and the automatic push")
initCmd.Flags().BoolVar(&initNoHistory, "no-history", false, "Skip the git-history import: build the graph for the working tree only. For one-shot checkouts (CI review pods, ephemeral workspaces) that read the head graph and never query past snapshots")
rootCmd.AddCommand(initCmd)
rootCmd.AddCommand(captureCmd)

Expand Down Expand Up @@ -4891,6 +4892,13 @@ var initEmail string
// 2026-08-23).
var initNoRemote bool

// initNoHistory: skip replaying git history as snapshots. The import is the
// bulk of `kai init` on any real repo — 50 commits, each a full snapshot — and
// a one-shot checkout never reads them: the review's blast walk uses the
// freshly-captured single-snapshot graph, and base...head comes from git.
// Measured on kai-server at ba7e4c2: 83.7s with the import, 4.1s without.
var initNoHistory bool

// initOrgOverride returns an explicit org slug from --org or KAI_ORG, else "".
func initOrgOverride() string {
if strings.TrimSpace(initOrg) != "" {
Expand Down Expand Up @@ -5852,11 +5860,18 @@ CREATE INDEX IF NOT EXISTS authorship_file ON authorship_ranges(snapshot_id, fil

// Auto-import git history. runGitImport caps at importMaxCommits (default 50)
// so large repos only import the most recent slice.
if countOut, err := exec.Command("git", "rev-list", "--count", "HEAD").Output(); err == nil {
if count, _ := strconv.Atoi(strings.TrimSpace(string(countOut))); count > 0 {
stop := spinner("Importing git history")
importErr := runGitImport(db)
stop(importErr)
//
// --no-history skips it outright. Those snapshots are what make `kai
// log`, blame and bisect useful to a person, and they cost nearly all of
// init's wall clock; a checkout that is deleted at the end of one review
// pays that price for history nothing will ever read.
if !initNoHistory {
if countOut, err := exec.Command("git", "rev-list", "--count", "HEAD").Output(); err == nil {
if count, _ := strconv.Atoi(strings.TrimSpace(string(countOut))); count > 0 {
stop := spinner("Importing git history")
importErr := runGitImport(db)
stop(importErr)
}
}
}

Expand Down