Skip to content

Commit e16db03

Browse files
dhruv-15-03Copilot
andcommitted
fix(git-extension): exclude --message-file transport file from staging
The temp file passed via --message-file / -MessageFile was read but left in the worktree. If written inside the project (as an agent's file-editing tool would naturally do), git add . staged it into the commit, and its mere presence as an untracked file could also defeat the no-changes short-circuit, causing a spurious commit containing only that file. Remove the file immediately after its content is captured, before the change-detection check and before staging. Add bash + pwsh regression tests covering both scenarios. Assisted-by: GitHub Copilot (model: claude-sonnet-5, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 4d3ae00 commit e16db03

3 files changed

Lines changed: 141 additions & 2 deletions

File tree

extensions/git/scripts/bash/auto-commit.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ while [ $# -gt 0 ]; do
3939
exit 1
4040
fi
4141
GENERATED_MESSAGE="$(cat "$_message_file")"
42+
# The message file is a transport-only artifact: its content is
43+
# now captured above, so remove it immediately. Otherwise, if it
44+
# was written inside the worktree, it would be picked up as an
45+
# untracked change by both the "any changes?" check below and by
46+
# `git add .`, polluting the commit or defeating the no-changes
47+
# short-circuit even when nothing else changed.
48+
rm -f "$_message_file"
4249
shift 2
4350
;;
4451
*)

extensions/git/scripts/powershell/auto-commit.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ if ($MessageFile) {
3535
if ($null -ne $GeneratedMessage) {
3636
$GeneratedMessage = $GeneratedMessage.TrimEnd("`r", "`n")
3737
}
38+
# The message file is a transport-only artifact: its content is now
39+
# captured above, so remove it immediately. Otherwise, if it was written
40+
# inside the worktree, it would be picked up as an untracked change by
41+
# both the "any changes?" check below and by `git add .`, polluting the
42+
# commit or defeating the no-changes short-circuit even when nothing
43+
# else changed.
44+
Remove-Item -Path $MessageFile -Force -ErrorAction SilentlyContinue
3845
}
3946

4047
function Find-ProjectRoot {

tests/extensions/git/test_git_extension.py

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,7 +1151,10 @@ def test_conventional_message_file_used(self, tmp_path: Path):
11511151
' message: "[Spec Kit] Add specification"\n'
11521152
))
11531153
(project / "new-file.txt").write_text("content")
1154-
msg_file = tmp_path / "commit-msg.txt"
1154+
# Write the message file inside the worktree (as an agent invoking
1155+
# this from a working directory tool naturally would) to exercise
1156+
# the exclusion-from-staging behavior below.
1157+
msg_file = project / "commit-msg.txt"
11551158
msg_file.write_text("feat: add $(dangerous) `injection` test\n")
11561159
result = _run_bash(
11571160
"auto-commit.sh", project, "after_specify", "--message-file", str(msg_file)
@@ -1163,6 +1166,67 @@ def test_conventional_message_file_used(self, tmp_path: Path):
11631166
)
11641167
assert "feat: add $(dangerous) `injection` test" in log.stdout
11651168

1169+
def test_message_file_not_staged_or_left_behind(self, tmp_path: Path):
1170+
"""--message-file written inside the worktree must never be staged or
1171+
committed itself, and must be removed once its content is consumed."""
1172+
project = _setup_project(tmp_path)
1173+
_write_config(project, (
1174+
"commit_style: conventional\n"
1175+
"auto_commit:\n"
1176+
" default: false\n"
1177+
" after_specify:\n"
1178+
" enabled: true\n"
1179+
))
1180+
(project / "new-file.txt").write_text("content")
1181+
msg_file = project / "commit-msg.txt"
1182+
msg_file.write_text("feat: real change\n")
1183+
result = _run_bash(
1184+
"auto-commit.sh", project, "after_specify", "--message-file", str(msg_file)
1185+
)
1186+
assert result.returncode == 0
1187+
assert not msg_file.exists()
1188+
show = subprocess.run(
1189+
["git", "show", "--stat", "--oneline", "HEAD"],
1190+
cwd=project, capture_output=True, text=True,
1191+
)
1192+
assert "new-file.txt" in show.stdout
1193+
assert "commit-msg.txt" not in show.stdout
1194+
1195+
def test_message_file_alone_does_not_defeat_no_changes_shortcircuit(self, tmp_path: Path):
1196+
"""If the message file is the only 'change' in the worktree (no real
1197+
edits), auto-commit must still report no changes rather than
1198+
committing the transport file by itself."""
1199+
project = _setup_project(tmp_path)
1200+
_write_config(project, (
1201+
"commit_style: conventional\n"
1202+
"auto_commit:\n"
1203+
" default: false\n"
1204+
" after_specify:\n"
1205+
" enabled: true\n"
1206+
))
1207+
# Baseline-commit the scaffolding (and config) so the tree is
1208+
# genuinely clean before introducing the message file — otherwise
1209+
# the untracked scaffold files would mask whether the message file
1210+
# alone is enough to (incorrectly) trigger a commit.
1211+
subprocess.run(["git", "add", "-A"], cwd=project, check=True, capture_output=True)
1212+
subprocess.run(
1213+
["git", "commit", "-q", "-m", "baseline"],
1214+
cwd=project, check=True, capture_output=True, env={**os.environ, **_GIT_ENV},
1215+
)
1216+
msg_file = project / "commit-msg.txt"
1217+
msg_file.write_text("feat: no real changes\n")
1218+
result = _run_bash(
1219+
"auto-commit.sh", project, "after_specify", "--message-file", str(msg_file)
1220+
)
1221+
assert result.returncode == 0
1222+
assert "No changes to commit" in result.stderr
1223+
assert not msg_file.exists()
1224+
log = subprocess.run(
1225+
["git", "log", "--oneline", "-1"],
1226+
cwd=project, capture_output=True, text=True,
1227+
)
1228+
assert "baseline" in log.stdout
1229+
11661230
def test_message_file_missing_fails(self, tmp_path: Path):
11671231
"""--message-file pointing at a nonexistent file fails clearly."""
11681232
project = _setup_project(tmp_path)
@@ -1436,7 +1500,7 @@ def test_conventional_message_file_used(self, tmp_path: Path):
14361500
' message: "[Spec Kit] Add specification"\n'
14371501
))
14381502
(project / "new-file.txt").write_text("content")
1439-
msg_file = tmp_path / "commit-msg.txt"
1503+
msg_file = project / "commit-msg.txt"
14401504
msg_file.write_text("feat: add $(dangerous) `injection` test\n")
14411505
result = _run_pwsh(
14421506
"auto-commit.ps1", project, "after_specify", "-MessageFile", str(msg_file)
@@ -1448,6 +1512,67 @@ def test_conventional_message_file_used(self, tmp_path: Path):
14481512
)
14491513
assert "feat: add $(dangerous) `injection` test" in log.stdout
14501514

1515+
def test_message_file_not_staged_or_left_behind(self, tmp_path: Path):
1516+
"""-MessageFile written inside the worktree must never be staged or
1517+
committed itself, and must be removed once its content is consumed."""
1518+
project = _setup_project(tmp_path)
1519+
_write_config(project, (
1520+
"commit_style: conventional\n"
1521+
"auto_commit:\n"
1522+
" default: false\n"
1523+
" after_specify:\n"
1524+
" enabled: true\n"
1525+
))
1526+
(project / "new-file.txt").write_text("content")
1527+
msg_file = project / "commit-msg.txt"
1528+
msg_file.write_text("feat: real change\n")
1529+
result = _run_pwsh(
1530+
"auto-commit.ps1", project, "after_specify", "-MessageFile", str(msg_file)
1531+
)
1532+
assert result.returncode == 0
1533+
assert not msg_file.exists()
1534+
show = subprocess.run(
1535+
["git", "show", "--stat", "--oneline", "HEAD"],
1536+
cwd=project, capture_output=True, text=True,
1537+
)
1538+
assert "new-file.txt" in show.stdout
1539+
assert "commit-msg.txt" not in show.stdout
1540+
1541+
def test_message_file_alone_does_not_defeat_no_changes_shortcircuit(self, tmp_path: Path):
1542+
"""If the message file is the only 'change' in the worktree (no real
1543+
edits), auto-commit must still report no changes rather than
1544+
committing the transport file by itself."""
1545+
project = _setup_project(tmp_path)
1546+
_write_config(project, (
1547+
"commit_style: conventional\n"
1548+
"auto_commit:\n"
1549+
" default: false\n"
1550+
" after_specify:\n"
1551+
" enabled: true\n"
1552+
))
1553+
# Baseline-commit the scaffolding (and config) so the tree is
1554+
# genuinely clean before introducing the message file — otherwise
1555+
# the untracked scaffold files would mask whether the message file
1556+
# alone is enough to (incorrectly) trigger a commit.
1557+
subprocess.run(["git", "add", "-A"], cwd=project, check=True, capture_output=True)
1558+
subprocess.run(
1559+
["git", "commit", "-q", "-m", "baseline"],
1560+
cwd=project, check=True, capture_output=True, env={**os.environ, **_GIT_ENV},
1561+
)
1562+
msg_file = project / "commit-msg.txt"
1563+
msg_file.write_text("feat: no real changes\n")
1564+
result = _run_pwsh(
1565+
"auto-commit.ps1", project, "after_specify", "-MessageFile", str(msg_file)
1566+
)
1567+
assert result.returncode == 0
1568+
assert "No changes to commit" in (result.stdout + result.stderr)
1569+
assert not msg_file.exists()
1570+
log = subprocess.run(
1571+
["git", "log", "--oneline", "-1"],
1572+
cwd=project, capture_output=True, text=True,
1573+
)
1574+
assert "baseline" in log.stdout
1575+
14511576
def test_message_file_missing_fails(self, tmp_path: Path):
14521577
"""-MessageFile pointing at a nonexistent file fails clearly."""
14531578
project = _setup_project(tmp_path)

0 commit comments

Comments
 (0)