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
45 changes: 42 additions & 3 deletions src/git/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -803,9 +803,15 @@ impl GitService {
Some(ignore) => Self::filter_ignored_files(&raw, ignore),
None => raw.clone(),
};
let content = Self::summarize_diff(&filtered);
let ignored = filtered != raw;
// パスと省略通知は一覧に載せるので、本文の予算を使わない。
let content = if ignored {
String::new()
} else {
Self::summarize_diff(&filtered)
};
let mut overview = Self::diff_block_overview(block);
if filtered != raw {
if ignored {
overview.push(' ');
overview.push_str(IGNORED_CONTENT_NOTICE);
} else if content != filtered {
Expand All @@ -825,12 +831,13 @@ impl GitService {
let mut sizes = excerpts
.iter()
.enumerate()
.filter(|(_, excerpt)| !excerpt.content.is_empty())
.map(|(index, excerpt)| (excerpt.content.chars().count(), index))
.collect::<Vec<_>>();
sizes.sort_unstable();
let mut allocations = vec![0; excerpts.len()];
// 本文を連結する改行も本文予算に含める。一覧と通知はこの予算の別枠。
let mut remaining = MAX_DIFF_CHARS.saturating_sub(excerpts.len().saturating_sub(1));
let mut remaining = MAX_DIFF_CHARS.saturating_sub(sizes.len().saturating_sub(1));
for (position, (size, index)) in sizes.iter().enumerate() {
let share = remaining / (sizes.len() - position);
allocations[*index] = (*size).min(share);
Expand Down Expand Up @@ -2588,6 +2595,38 @@ index 1234567..abcdefg 100644
assert!(!result.contains('\u{fffd}'));
}

#[rstest::rstest]
#[case(false)]
#[case(true)]
fn test_ignored_excerpts_do_not_spend_body_or_separator_budget(#[case] only_ignored: bool) {
let dir = tempfile::tempdir().unwrap();
let mut builder = GitignoreBuilder::new(dir.path());
builder.add_line(None, "data/").unwrap();
let ignore = builder.build().unwrap();
// 除外ファイルの区切りだけで予算を使い切る件数も扱う。
let count = MAX_DIFF_CHARS + 2;
let mut diff = (0..count)
.map(|index| {
format!(
"diff --git a/data/{index}.txt b/data/{index}.txt\n@@ -0,0 +1 @@\n+hidden\n"
)
})
.collect::<String>();
let visible = "diff --git a/visible.txt b/visible.txt\n@@ -0,0 +1 @@\n+visible change";
if !only_ignored {
diff.push_str(visible);
}
let filtered = GitService::filter_ignored_files(&diff, &ignore);
let result = GitService::truncate_diff_with_source(&filtered, &diff, Some(&ignore));
let (overview, body) = result
.split_once("body budget 10000 characters]\n\n")
.unwrap();
assert_eq!(overview.matches(IGNORED_CONTENT_NOTICE).count(), count);
assert!(!overview.contains("[body truncated]"));
assert!(!body.contains("hidden"));
assert_eq!(body, if only_ignored { "" } else { visible });
}

#[test]
fn test_truncate_diff_exactly_at_limit() {
// 10000文字ちょうどの場合は切り詰めない
Expand Down
75 changes: 75 additions & 0 deletions tests/cli_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,81 @@ macro_rules! git_sc {
};
}

/// 除外対象の数にかかわらず、残った本文だけで予算を分ける。
#[rstest::rstest]
#[case(false)]
#[case(true)]
#[cfg(unix)]
fn test_many_ignored_files_do_not_consume_prompt_body_budget(#[case] large_bodies: bool) {
let dir = setup_git_repo_with_commit();
let path = setup_fake_opencode_path(&dir);
std::fs::write(dir.path().join(".git-sc"), "providers = [\"opencode\"]\n").unwrap();
std::fs::write(dir.path().join(".git-sc-ignore"), "data/\n").unwrap();
std::fs::create_dir(dir.path().join("data")).unwrap();
for index in 0..254 {
std::fs::write(
dir.path()
.join(format!("data/record-{index:03}-{}.txt", "x".repeat(60))),
"hidden-data-value\n",
)
.unwrap();
}
let count = if large_bodies { 1500 } else { 40 };
for name in ["first", "second"] {
std::fs::write(
dir.path().join(format!("{name}.txt")),
format!("{name} visible body line\n").repeat(count),
)
.unwrap();
}
let staged = std::process::Command::new("git")
.args(["add", "data", "first.txt", "second.txt"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(staged.status.success());
let output = git_sc!()
.args(["--dry-run", "--debug", "-p", "opencode"])
.env("PATH", &path)
.env("HOME", dir.path())
.env("XDG_CONFIG_HOME", dir.path())
.current_dir(dir.path())
.assert()
.success()
.get_output()
.stdout
.clone();
let output = String::from_utf8(output).unwrap();
let changes = output.split_once("<changes>\n").unwrap().1;
let changes = changes.split_once("\n</changes>").unwrap().0;
let (_, body) = changes
.split_once("body budget 10000 characters]\n\n")
.unwrap();
assert_eq!(changes.matches("[File] ").count(), 256);
assert_eq!(
changes
.matches("[content omitted by .git-sc-ignore]")
.count(),
254
);
assert!(!changes.contains("hidden-data-value"));
assert!(!body.contains("data/record-"));
assert!(body.chars().count() <= 10000);
for name in ["first", "second"] {
let line = format!("+{name} visible body line");
let actual = body.lines().filter(|candidate| *candidate == line).count();
if large_bodies {
assert!(actual > 0 && actual < count, "{name}: {actual}");
} else {
assert_eq!(actual, count, "{name}");
}
}
assert_eq!(
changes.matches("[body truncated]").count(),
if large_bodies { 2 } else { 0 }
);
}

/// 大きな先頭ファイルが後続ファイルを隠さず、実際に送るプロンプトに全体像を残す。
#[test]
#[cfg(unix)]
Expand Down
Loading