Skip to content

Commit d7f1ebe

Browse files
authored
Merge pull request #1148 from GitGuardian/kevinwestphal/handle-unmerged-files
fix(pre-commit): handle unmerged files
2 parents a4076b9 + 41d0958 commit d7f1ebe

File tree

6 files changed

+305
-0
lines changed

6 files changed

+305
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!--
2+
A new scriv changelog fragment.
3+
4+
Uncomment the section that is right (remove the HTML comment wrapper).
5+
For top level release notes, leave all the headers commented out.
6+
-->
7+
8+
<!--
9+
### Removed
10+
11+
- A bullet item for the Removed category.
12+
13+
-->
14+
<!--
15+
### Added
16+
17+
- A bullet item for the Added category.
18+
19+
-->
20+
<!--
21+
### Changed
22+
23+
- A bullet item for the Changed category.
24+
25+
-->
26+
<!--
27+
### Deprecated
28+
29+
- A bullet item for the Deprecated category.
30+
31+
-->
32+
33+
### Fixed
34+
35+
- Handle unmerged files in pre-commit scanning during an ongoing merge.
36+
<!--
37+
38+
### Security
39+
40+
- A bullet item for the Security category.
41+
42+
-->

ggshield/core/scan/commit_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ def from_string(line: str) -> "PatchFileInfo":
160160

161161
if "M" in status: # modify
162162
mode = Filemode.MODIFY
163+
elif "U" in status: # unmerged
164+
mode = Filemode.UNMERGED
163165
elif "C" in status: # copy
164166
mode = Filemode.NEW
165167
elif "A" in status: # add

ggshield/utils/git_shell.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class Filemode(Enum):
5656
NEW = "new file"
5757
RENAME = "renamed file"
5858
FILE = "file"
59+
UNMERGED = "unmerged file"
5960
UNKNOWN = "unknown"
6061

6162

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
interactions:
2+
- request:
3+
body: null
4+
headers:
5+
Accept:
6+
- '*/*'
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Connection:
10+
- keep-alive
11+
User-Agent:
12+
- pygitguardian/1.26.0 (Darwin;py3.11.13) ggshield
13+
method: GET
14+
uri: https://api.gitguardian.com/v1/metadata
15+
response:
16+
body:
17+
string:
18+
'{"version":"v2.331.0","preferences":{"marketplaces__aws_product_url":"http://aws.amazon.com/marketplace/pp/prodview-mrmulzykamba6","on_premise__restrict_signup":true,"on_premise__is_email_server_configured":true,"on_premise__default_sso_config_api_id":null,"on_premise__default_sso_config_force_sso":null,"onboarding__segmentation_v1_enabled":true,"onboarding__detectors_for_revocation":["github_fine_grained_pat","github_personal_access_token_v2","github_token","openai_admin_apikey","openai_apikey","openai_project_apikey","openai_project_apikey_v2"],"general__maximum_payload_size":26214400,"general__mutual_tls_mode":"disabled","general__signup_enabled":true},"secret_scan_preferences":{"maximum_documents_per_scan":20,"maximum_document_size":1048576},"remediation_messages":{"pre_commit":">
19+
How to remediate\n\n Since the secret was detected before the commit was
20+
made:\n 1. replace the secret with its reference (e.g. environment variable).\n 2.
21+
commit again.\n\n> [To apply with caution] If you want to bypass ggshield
22+
(false positive or other reason), run:\n - if you use the pre-commit framework:\n\n SKIP=ggshield
23+
git commit -m \"<your message\"","pre_push":"> How to remediate\n\n Since
24+
the secret was detected before the push BUT after the commit, you need to:\n 1.
25+
rewrite the git history making sure to replace the secret with its reference
26+
(e.g. environment variable).\n 2. push again.\n\n To prevent having to rewrite
27+
git history in the future, setup ggshield as a pre-commit hook:\n https://docs.gitguardian.com/ggshield-docs/integrations/git-hooks/pre-commit\n\n>
28+
[Apply with caution] If you want to bypass ggshield (false positive or other
29+
reason), run:\n - if you use the pre-commit framework:\n\n SKIP=ggshield-push
30+
git push","pre_receive":"> How to remediate\n\n A pre-receive hook set server
31+
side prevented you from pushing secrets.\n\n Since the secret was detected
32+
during the push BUT after the commit, you need to:\n 1. rewrite the git history
33+
making sure to replace the secret with its reference (e.g. environment variable).\n 2.
34+
push again.\n\n To prevent having to rewrite git history in the future, setup
35+
ggshield as a pre-commit hook:\n https://docs.gitguardian.com/ggshield-docs/integrations/git-hooks/pre-commit\n\n>
36+
[Apply with caution] If you want to bypass ggshield (false positive or other
37+
reason), run:\n\n git push -o breakglass"}}'
38+
headers:
39+
access-control-expose-headers:
40+
- X-App-Version
41+
allow:
42+
- GET, HEAD, OPTIONS
43+
content-length:
44+
- '2399'
45+
content-security-policy:
46+
- frame-ancestors 'none'
47+
content-type:
48+
- application/json
49+
cross-origin-opener-policy:
50+
- same-origin
51+
date:
52+
- Thu, 13 Nov 2025 10:54:01 GMT
53+
referrer-policy:
54+
- strict-origin-when-cross-origin
55+
server:
56+
- istio-envoy
57+
strict-transport-security:
58+
- max-age=31536000; includeSubDomains
59+
vary:
60+
- Cookie
61+
x-app-version:
62+
- v2.331.0
63+
x-content-type-options:
64+
- nosniff
65+
x-envoy-upstream-service-time:
66+
- '34'
67+
x-frame-options:
68+
- DENY
69+
x-secrets-engine-version:
70+
- 2.150.0
71+
x-xss-protection:
72+
- 1; mode=block
73+
status:
74+
code: 200
75+
message: OK
76+
- request:
77+
body: null
78+
headers:
79+
Accept:
80+
- '*/*'
81+
Accept-Encoding:
82+
- gzip, deflate
83+
Connection:
84+
- keep-alive
85+
User-Agent:
86+
- pygitguardian/1.26.0 (Darwin;py3.11.13) ggshield
87+
method: GET
88+
uri: https://api.gitguardian.com/v1/api_tokens/self
89+
response:
90+
body:
91+
string: '{"id":"775758eb-b33b-43e9-8ff7-4ce5f5cc73df","name":"ggshield-dev-token","type":"personal_access_token","scopes":["scan"],"member_id":1094429,"workspace_id":8,"created_at":"2025-11-13T08:27:47.394379Z","last_used_at":"2025-11-13T10:54:00Z","expire_at":"2026-05-12T08:27:47.366197Z","revoked_at":null,"status":"active","creator_id":1094429}'
92+
headers:
93+
access-control-expose-headers:
94+
- X-App-Version
95+
allow:
96+
- GET, DELETE, HEAD, OPTIONS
97+
content-length:
98+
- '339'
99+
content-security-policy:
100+
- frame-ancestors 'none'
101+
content-type:
102+
- application/json
103+
cross-origin-opener-policy:
104+
- same-origin
105+
date:
106+
- Thu, 13 Nov 2025 10:54:03 GMT
107+
referrer-policy:
108+
- strict-origin-when-cross-origin
109+
server:
110+
- istio-envoy
111+
strict-transport-security:
112+
- max-age=31536000; includeSubDomains
113+
vary:
114+
- Cookie
115+
x-app-version:
116+
- v2.331.0
117+
x-content-type-options:
118+
- nosniff
119+
x-envoy-upstream-service-time:
120+
- '24'
121+
x-frame-options:
122+
- DENY
123+
x-secrets-engine-version:
124+
- 2.150.0
125+
x-xss-protection:
126+
- 1; mode=block
127+
status:
128+
code: 200
129+
message: OK
130+
- request:
131+
body:
132+
'[{"filename": "commit://staged/conflict.txt", "document": "@@ -1 +1 @@\n-Version
133+
from feature\n\\ No newline at end of file\n+Resolved version\n\\ No newline
134+
at end of file"}]'
135+
headers:
136+
Accept:
137+
- '*/*'
138+
Accept-Encoding:
139+
- gzip, deflate
140+
Connection:
141+
- keep-alive
142+
Content-Length:
143+
- '175'
144+
Content-Type:
145+
- application/json
146+
GGShield-Command-Id:
147+
- 12964ae8-7cb5-430e-b403-0df6d90d0f8c
148+
GGShield-Command-Path:
149+
- cli secret scan pre-commit
150+
GGShield-OS-Name:
151+
- darwin
152+
GGShield-OS-Version:
153+
- 'Darwin Kernel Version 24.2.0: Fri Dec 6 18:56:34 PST 2024; root:xnu-11215.61.5~2/RELEASE_ARM64_T6020'
154+
GGShield-Python-Version:
155+
- 3.11.13
156+
GGShield-Version:
157+
- 1.44.1
158+
User-Agent:
159+
- pygitguardian/1.26.0 (Darwin;py3.11.13) ggshield
160+
mode:
161+
- pre_commit
162+
scan_options:
163+
- '{"show_secrets": false, "ignored_detectors_count": 0, "ignored_matches_count":
164+
0, "ignored_paths_count": 14, "ignore_known_secrets": false, "with_incident_details":
165+
false, "has_prereceive_remediation_message": false, "all_secrets": false,
166+
"source_uuid": null}'
167+
method: POST
168+
uri: https://api.gitguardian.com/v1/multiscan?all_secrets=True
169+
response:
170+
body:
171+
string: '[{"policy_break_count":0,"policies":["Secrets detection"],"policy_breaks":[],"is_diff":true}]'
172+
headers:
173+
access-control-expose-headers:
174+
- X-App-Version
175+
allow:
176+
- POST, OPTIONS
177+
content-length:
178+
- '93'
179+
content-security-policy:
180+
- frame-ancestors 'none'
181+
content-type:
182+
- application/json
183+
cross-origin-opener-policy:
184+
- same-origin
185+
date:
186+
- Thu, 13 Nov 2025 10:54:04 GMT
187+
referrer-policy:
188+
- strict-origin-when-cross-origin
189+
server:
190+
- istio-envoy
191+
strict-transport-security:
192+
- max-age=31536000; includeSubDomains
193+
vary:
194+
- Cookie
195+
x-app-version:
196+
- v2.331.0
197+
x-content-type-options:
198+
- nosniff
199+
x-envoy-upstream-service-time:
200+
- '125'
201+
x-frame-options:
202+
- DENY
203+
x-secrets-engine-version:
204+
- 2.150.0
205+
x-xss-protection:
206+
- 1; mode=block
207+
status:
208+
code: 200
209+
message: OK
210+
version: 1

tests/unit/cmd/scan/test_precommit.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,52 @@ def test_precommit_with_emoji_filename(tmp_path, cli_fs_runner):
128128
result = cli_fs_runner.invoke(cli, ["secret", "scan", "pre-commit"])
129129
# Verify the command executed successfully
130130
assert_invoke_ok(result)
131+
132+
133+
@my_vcr.use_cassette("test_precommit_with_unmerged_files")
134+
def test_precommit_with_unmerged_files(tmp_path, cli_fs_runner):
135+
"""
136+
GIVEN a repository with a merge conflict containing unmerged files
137+
WHEN the precommit command is run
138+
THEN it executes successfully and scans the conflicted files
139+
"""
140+
# Create repository with initial commit
141+
repo = Repository.create(tmp_path, initial_branch="master")
142+
repo.create_commit("Initial commit on master")
143+
144+
# Create feature branch and add a file
145+
repo.create_branch("feature_branch")
146+
repo.checkout("master")
147+
conflict_file = tmp_path / "conflict.txt"
148+
conflict_file.write_text("Version from master")
149+
non_conflict_file = tmp_path / "no_conflict.txt"
150+
non_conflict_file.write_text("This file won't conflict")
151+
repo.add(".")
152+
repo.create_commit("Commit on master")
153+
154+
# Switch to feature branch and create conflicting change
155+
repo.checkout("feature_branch")
156+
conflict_file.write_text("Version from feature")
157+
another_file = tmp_path / "feature.txt"
158+
another_file.write_text("New file from feature")
159+
repo.add(".")
160+
repo.create_commit("Commit on feature_branch")
161+
162+
# Attempt merge which will create conflict
163+
with pytest.raises(subprocess.CalledProcessError) as exc:
164+
repo.git("merge", "master")
165+
166+
# Verify we have a conflict
167+
stdout = exc.value.stdout.decode()
168+
assert "CONFLICT" in stdout
169+
170+
# Resolve conflict and stage the resolution
171+
conflict_file.write_text("Resolved version")
172+
repo.add(conflict_file)
173+
174+
# Run pre-commit scan on the merge with unmerged files
175+
with cd(repo.path):
176+
result = cli_fs_runner.invoke(cli, ["secret", "scan", "pre-commit"])
177+
178+
# Verify the command executed successfully
179+
assert_invoke_ok(result)

tests/unit/core/scan/test_commit.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
+ ":000000 100644 0000000 19465ef A\0tests/test_scannable.py\0"
2121
+ ":100644 100755 b4d3aef b4d3aef M\0bin/shutdown.sh\0"
2222
+ ":000000 100644 0000000 12356ef A\0.env\0"
23+
+ ":000000 100644 0000000 12323ef U\0unmerged.txt\0"
2324
+ ":100644 100644 ac204ec ac204ec R100\0ggshield/tests/test_config.py\0tests/test_config.py\0"
2425
+ ":100644 100644 6546aef b41653f M\0data/utils/email_sender.py\0"
2526
+ """\0diff --git a/ggshield/tests/cassettes/test_files_yes.yaml b/ggshield/tests/cassettes/test_files_yes.yaml

0 commit comments

Comments
 (0)