FossilRepo

Fix fossil http proxy: use raw HTTP request on stdin instead of CGI env vars

ragelink 2026-04-07 22:00 trunk
Commit 7d099f36f18de78311efe0fbaacef8ede8d6d58e57facb815bf8e30102e98ff5
--- fossil/__pycache__/cli.cpython-314.pyc
+++ fossil/__pycache__/cli.cpython-314.pyc
cannot compute difference between binary files
11
--- fossil/__pycache__/cli.cpython-314.pyc
+++ fossil/__pycache__/cli.cpython-314.pyc
0 annot compute difference between binary files
1
--- fossil/__pycache__/cli.cpython-314.pyc
+++ fossil/__pycache__/cli.cpython-314.pyc
0 annot compute difference between binary files
1
+18 -16
--- fossil/cli.py
+++ fossil/cli.py
@@ -389,44 +389,46 @@
389389
except Exception as e:
390390
return {"success": False, "public_key": "", "fingerprint": "", "error": str(e)}
391391
return {"success": False, "public_key": "", "fingerprint": ""}
392392
393393
def http_proxy(self, repo_path: Path, request_body: bytes, content_type: str = "", localauth: bool = True) -> tuple[bytes, str]:
394
- """Proxy a single Fossil HTTP sync request via CGI mode.
394
+ """Proxy a single Fossil HTTP sync request.
395395
396
- Runs ``fossil http <repo_path>`` with the request piped to stdin.
397
- Fossil writes a full HTTP response (headers + body) to stdout;
398
- we split the two apart and return (response_body, response_content_type).
396
+ Runs ``fossil http <repo_path>`` with a full HTTP request on stdin.
397
+ Fossil reads the HTTP method line + headers + body from stdin and
398
+ writes a full HTTP response (headers + body) to stdout.
399399
400400
When *localauth* is True, ``--localauth`` grants full push permissions.
401401
When False, only anonymous pull/clone is allowed (for public repos).
402402
"""
403403
import os
404404
405405
env = {
406406
**os.environ,
407407
**{k: v for k, v in self._env.items() if k not in os.environ or k == "USER"},
408
- "REQUEST_METHOD": "POST",
409
- "CONTENT_TYPE": content_type,
410
- "CONTENT_LENGTH": str(len(request_body)),
411
- "PATH_INFO": "/xfer",
412
- "SCRIPT_NAME": "",
413
- "HTTP_HOST": "localhost",
414
- "SERVER_PROTOCOL": "HTTP/1.1",
415
- }
416
- # Do NOT set GATEWAY_INTERFACE — it causes fossil to auto-enter CGI
417
- # mode, treating the "http" subcommand as a repository path.
418
- env.pop("GATEWAY_INTERFACE", None)
408
+ }
409
+ # Ensure GATEWAY_INTERFACE is NOT set — it triggers CGI auto-detect
410
+ # which conflicts with the explicit "http" subcommand.
411
+ env.pop("GATEWAY_INTERFACE", None)
412
+
413
+ # Build a raw HTTP request for fossil http's stdin
414
+ http_request = (
415
+ f"POST /xfer HTTP/1.1\r\n"
416
+ f"Host: localhost\r\n"
417
+ f"Content-Type: {content_type or 'application/x-fossil'}\r\n"
418
+ f"Content-Length: {len(request_body)}\r\n"
419
+ f"\r\n"
420
+ ).encode() + request_body
419421
420422
cmd = [self.binary, "http", str(repo_path)]
421423
if localauth:
422424
cmd.append("--localauth")
423425
424426
try:
425427
result = subprocess.run(
426428
cmd,
427
- input=request_body,
429
+ input=http_request,
428430
capture_output=True,
429431
timeout=120,
430432
env=env,
431433
)
432434
except subprocess.TimeoutExpired:
433435
--- fossil/cli.py
+++ fossil/cli.py
@@ -389,44 +389,46 @@
389 except Exception as e:
390 return {"success": False, "public_key": "", "fingerprint": "", "error": str(e)}
391 return {"success": False, "public_key": "", "fingerprint": ""}
392
393 def http_proxy(self, repo_path: Path, request_body: bytes, content_type: str = "", localauth: bool = True) -> tuple[bytes, str]:
394 """Proxy a single Fossil HTTP sync request via CGI mode.
395
396 Runs ``fossil http <repo_path>`` with the request piped to stdin.
397 Fossil writes a full HTTP response (headers + body) to stdout;
398 we split the two apart and return (response_body, response_content_type).
399
400 When *localauth* is True, ``--localauth`` grants full push permissions.
401 When False, only anonymous pull/clone is allowed (for public repos).
402 """
403 import os
404
405 env = {
406 **os.environ,
407 **{k: v for k, v in self._env.items() if k not in os.environ or k == "USER"},
408 "REQUEST_METHOD": "POST",
409 "CONTENT_TYPE": content_type,
410 "CONTENT_LENGTH": str(len(request_body)),
411 "PATH_INFO": "/xfer",
412 "SCRIPT_NAME": "",
413 "HTTP_HOST": "localhost",
414 "SERVER_PROTOCOL": "HTTP/1.1",
415 }
416 # Do NOT set GATEWAY_INTERFACE — it causes fossil to auto-enter CGI
417 # mode, treating the "http" subcommand as a repository path.
418 env.pop("GATEWAY_INTERFACE", None)
 
 
419
420 cmd = [self.binary, "http", str(repo_path)]
421 if localauth:
422 cmd.append("--localauth")
423
424 try:
425 result = subprocess.run(
426 cmd,
427 input=request_body,
428 capture_output=True,
429 timeout=120,
430 env=env,
431 )
432 except subprocess.TimeoutExpired:
433
--- fossil/cli.py
+++ fossil/cli.py
@@ -389,44 +389,46 @@
389 except Exception as e:
390 return {"success": False, "public_key": "", "fingerprint": "", "error": str(e)}
391 return {"success": False, "public_key": "", "fingerprint": ""}
392
393 def http_proxy(self, repo_path: Path, request_body: bytes, content_type: str = "", localauth: bool = True) -> tuple[bytes, str]:
394 """Proxy a single Fossil HTTP sync request.
395
396 Runs ``fossil http <repo_path>`` with a full HTTP request on stdin.
397 Fossil reads the HTTP method line + headers + body from stdin and
398 writes a full HTTP response (headers + body) to stdout.
399
400 When *localauth* is True, ``--localauth`` grants full push permissions.
401 When False, only anonymous pull/clone is allowed (for public repos).
402 """
403 import os
404
405 env = {
406 **os.environ,
407 **{k: v for k, v in self._env.items() if k not in os.environ or k == "USER"},
408 }
409 # Ensure GATEWAY_INTERFACE is NOT set — it triggers CGI auto-detect
410 # which conflicts with the explicit "http" subcommand.
411 env.pop("GATEWAY_INTERFACE", None)
412
413 # Build a raw HTTP request for fossil http's stdin
414 http_request = (
415 f"POST /xfer HTTP/1.1\r\n"
416 f"Host: localhost\r\n"
417 f"Content-Type: {content_type or 'application/x-fossil'}\r\n"
418 f"Content-Length: {len(request_body)}\r\n"
419 f"\r\n"
420 ).encode() + request_body
421
422 cmd = [self.binary, "http", str(repo_path)]
423 if localauth:
424 cmd.append("--localauth")
425
426 try:
427 result = subprocess.run(
428 cmd,
429 input=http_request,
430 capture_output=True,
431 timeout=120,
432 env=env,
433 )
434 except subprocess.TimeoutExpired:
435
--- templates/fossil/branch_list.html
+++ templates/fossil/branch_list.html
@@ -25,11 +25,11 @@
2525
</span>
2626
</div>
2727
</div>
2828
2929
<div id="branch-content">
30
-<div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm overflow-x-auto">
30
+<div class="overflow-x-auto rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
3131
<table class="min-w-full divide-y divide-gray-700">
3232
<thead class="bg-gray-900">
3333
<tr>
3434
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Branch</th>
3535
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Last Checkin</th>
3636
--- templates/fossil/branch_list.html
+++ templates/fossil/branch_list.html
@@ -25,11 +25,11 @@
25 </span>
26 </div>
27 </div>
28
29 <div id="branch-content">
30 <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm overflow-x-auto">
31 <table class="min-w-full divide-y divide-gray-700">
32 <thead class="bg-gray-900">
33 <tr>
34 <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Branch</th>
35 <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Last Checkin</th>
36
--- templates/fossil/branch_list.html
+++ templates/fossil/branch_list.html
@@ -25,11 +25,11 @@
25 </span>
26 </div>
27 </div>
28
29 <div id="branch-content">
30 <div class="overflow-x-auto rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
31 <table class="min-w-full divide-y divide-gray-700">
32 <thead class="bg-gray-900">
33 <tr>
34 <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Branch</th>
35 <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Last Checkin</th>
36
--- templates/organization/role_detail.html
+++ templates/organization/role_detail.html
@@ -76,11 +76,11 @@
7676
{% endif %}
7777
</div>
7878
7979
<div class="mt-8">
8080
<h2 class="text-lg font-semibold text-gray-100 mb-4">Members with this Role</h2>
81
- <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
81
+ <div class="overflow-x-auto rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
8282
<table class="min-w-full divide-y divide-gray-700">
8383
<thead class="bg-gray-900">
8484
<tr>
8585
<th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Username</th>
8686
<th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Email</th>
8787
--- templates/organization/role_detail.html
+++ templates/organization/role_detail.html
@@ -76,11 +76,11 @@
76 {% endif %}
77 </div>
78
79 <div class="mt-8">
80 <h2 class="text-lg font-semibold text-gray-100 mb-4">Members with this Role</h2>
81 <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
82 <table class="min-w-full divide-y divide-gray-700">
83 <thead class="bg-gray-900">
84 <tr>
85 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Username</th>
86 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Email</th>
87
--- templates/organization/role_detail.html
+++ templates/organization/role_detail.html
@@ -76,11 +76,11 @@
76 {% endif %}
77 </div>
78
79 <div class="mt-8">
80 <h2 class="text-lg font-semibold text-gray-100 mb-4">Members with this Role</h2>
81 <div class="overflow-x-auto rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
82 <table class="min-w-full divide-y divide-gray-700">
83 <thead class="bg-gray-900">
84 <tr>
85 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Username</th>
86 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Email</th>
87
--- templates/organization/user_detail.html
+++ templates/organization/user_detail.html
@@ -86,11 +86,11 @@
8686
</div>
8787
</div>
8888
8989
<div class="mt-8">
9090
<h2 class="text-lg font-semibold text-gray-100 mb-4">Team Memberships</h2>
91
- <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
91
+ <div class="overflow-x-auto rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
9292
<table class="min-w-full divide-y divide-gray-700">
9393
<thead class="bg-gray-900">
9494
<tr>
9595
<th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Team</th>
9696
<th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Description</th>
@@ -114,11 +114,11 @@
114114
</div>
115115
</div>
116116
117117
<div class="mt-8">
118118
<h2 class="text-lg font-semibold text-gray-100 mb-4">SSH Keys</h2>
119
- <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
119
+ <div class="overflow-x-auto rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
120120
<table class="min-w-full divide-y divide-gray-700">
121121
<thead class="bg-gray-900">
122122
<tr>
123123
<th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Title</th>
124124
<th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Type</th>
125125
--- templates/organization/user_detail.html
+++ templates/organization/user_detail.html
@@ -86,11 +86,11 @@
86 </div>
87 </div>
88
89 <div class="mt-8">
90 <h2 class="text-lg font-semibold text-gray-100 mb-4">Team Memberships</h2>
91 <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
92 <table class="min-w-full divide-y divide-gray-700">
93 <thead class="bg-gray-900">
94 <tr>
95 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Team</th>
96 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Description</th>
@@ -114,11 +114,11 @@
114 </div>
115 </div>
116
117 <div class="mt-8">
118 <h2 class="text-lg font-semibold text-gray-100 mb-4">SSH Keys</h2>
119 <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
120 <table class="min-w-full divide-y divide-gray-700">
121 <thead class="bg-gray-900">
122 <tr>
123 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Title</th>
124 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Type</th>
125
--- templates/organization/user_detail.html
+++ templates/organization/user_detail.html
@@ -86,11 +86,11 @@
86 </div>
87 </div>
88
89 <div class="mt-8">
90 <h2 class="text-lg font-semibold text-gray-100 mb-4">Team Memberships</h2>
91 <div class="overflow-x-auto rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
92 <table class="min-w-full divide-y divide-gray-700">
93 <thead class="bg-gray-900">
94 <tr>
95 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Team</th>
96 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Description</th>
@@ -114,11 +114,11 @@
114 </div>
115 </div>
116
117 <div class="mt-8">
118 <h2 class="text-lg font-semibold text-gray-100 mb-4">SSH Keys</h2>
119 <div class="overflow-x-auto rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
120 <table class="min-w-full divide-y divide-gray-700">
121 <thead class="bg-gray-900">
122 <tr>
123 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Title</th>
124 <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-400">Type</th>
125
--- templates/projects/project_detail.html
+++ templates/projects/project_detail.html
@@ -143,12 +143,12 @@
143143
144144
<!-- Clone instructions -->
145145
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4" x-data="{ copied: false }">
146146
<h3 class="text-sm font-medium text-gray-300 mb-2">Clone</h3>
147147
<div class="flex items-center gap-2">
148
- <code class="flex-1 text-xs font-mono text-gray-400 bg-gray-900 rounded px-3 py-2 truncate">fossil clone /path/to/{{ project.slug }}.fossil</code>
149
- <button @click="navigator.clipboard.writeText('fossil clone /path/to/{{ project.slug }}.fossil'); copied = true; setTimeout(() => copied = false, 1500)"
148
+ <code class="flex-1 text-xs font-mono text-gray-400 bg-gray-900 rounded px-3 py-2 truncate">fossil clone {{ request.scheme }}://{{ request.get_host }}/projects/{{ project.slug }}/fossil/xfer {{ project.slug }}.fossil</code>
149
+ <button @click="navigator.clipboard.writeText('fossil clone {{ request.scheme }}://{{ request.get_host }}/projects/{{ project.slug }}/fossil/xfer {{ project.slug }}.fossil'); copied = true; setTimeout(() => copied = false, 1500)"
150150
class="flex-shrink-0 rounded px-2 py-2 text-gray-500 hover:text-brand-light hover:bg-gray-700">
151151
<svg x-show="!copied" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /></svg>
152152
<svg x-show="copied" class="h-4 w-4 text-green-400" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="display:none"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>
153153
</button>
154154
</div>
155155
--- templates/projects/project_detail.html
+++ templates/projects/project_detail.html
@@ -143,12 +143,12 @@
143
144 <!-- Clone instructions -->
145 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4" x-data="{ copied: false }">
146 <h3 class="text-sm font-medium text-gray-300 mb-2">Clone</h3>
147 <div class="flex items-center gap-2">
148 <code class="flex-1 text-xs font-mono text-gray-400 bg-gray-900 rounded px-3 py-2 truncate">fossil clone /path/to/{{ project.slug }}.fossil</code>
149 <button @click="navigator.clipboard.writeText('fossil clone /path/to/{{ project.slug }}.fossil'); copied = true; setTimeout(() => copied = false, 1500)"
150 class="flex-shrink-0 rounded px-2 py-2 text-gray-500 hover:text-brand-light hover:bg-gray-700">
151 <svg x-show="!copied" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /></svg>
152 <svg x-show="copied" class="h-4 w-4 text-green-400" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="display:none"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>
153 </button>
154 </div>
155
--- templates/projects/project_detail.html
+++ templates/projects/project_detail.html
@@ -143,12 +143,12 @@
143
144 <!-- Clone instructions -->
145 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4" x-data="{ copied: false }">
146 <h3 class="text-sm font-medium text-gray-300 mb-2">Clone</h3>
147 <div class="flex items-center gap-2">
148 <code class="flex-1 text-xs font-mono text-gray-400 bg-gray-900 rounded px-3 py-2 truncate">fossil clone {{ request.scheme }}://{{ request.get_host }}/projects/{{ project.slug }}/fossil/xfer {{ project.slug }}.fossil</code>
149 <button @click="navigator.clipboard.writeText('fossil clone {{ request.scheme }}://{{ request.get_host }}/projects/{{ project.slug }}/fossil/xfer {{ project.slug }}.fossil'); copied = true; setTimeout(() => copied = false, 1500)"
150 class="flex-shrink-0 rounded px-2 py-2 text-gray-500 hover:text-brand-light hover:bg-gray-700">
151 <svg x-show="!copied" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /></svg>
152 <svg x-show="copied" class="h-4 w-4 text-green-400" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="display:none"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>
153 </button>
154 </div>
155
--- tests/__pycache__/test_security.cpython-314-pytest-9.0.2.pyc
+++ tests/__pycache__/test_security.cpython-314-pytest-9.0.2.pyc
cannot compute difference between binary files
11
--- tests/__pycache__/test_security.cpython-314-pytest-9.0.2.pyc
+++ tests/__pycache__/test_security.cpython-314-pytest-9.0.2.pyc
0 annot compute difference between binary files
1
--- tests/__pycache__/test_security.cpython-314-pytest-9.0.2.pyc
+++ tests/__pycache__/test_security.cpython-314-pytest-9.0.2.pyc
0 annot compute difference between binary files
1
--- tests/test_security.py
+++ tests/test_security.py
@@ -205,12 +205,14 @@
205205
cmd_str = " ".join(captured_cmd)
206206
assert "ghp_s3cretTOKEN123" not in cmd_str
207207
# URL should not have token embedded
208208
assert "ghp_s3cretTOKEN123@" not in cmd_str
209209
210
- def test_token_passed_via_env(self):
211
- """When auth_token is provided, git credential helper is configured via env."""
210
+ def test_token_passed_via_askpass(self):
211
+ """When auth_token is provided, GIT_ASKPASS is configured and token is in a separate file."""
212
+ import os
213
+
212214
from fossil.cli import FossilCLI
213215
214216
cli = FossilCLI(binary="/usr/bin/false")
215217
captured_env = {}
216218
@@ -225,13 +227,17 @@
225227
autopush_url="https://github.com/user/repo.git",
226228
auth_token="ghp_s3cretTOKEN123",
227229
)
228230
229231
assert captured_env.get("GIT_TERMINAL_PROMPT") == "0"
230
- assert captured_env.get("GIT_CONFIG_COUNT") == "1"
231
- assert captured_env.get("GIT_CONFIG_KEY_0") == "credential.helper"
232
- assert "ghp_s3cretTOKEN123" in captured_env.get("GIT_CONFIG_VALUE_0", "")
232
+ # Uses GIT_ASKPASS instead of shell credential helper
233
+ askpass_path = captured_env.get("GIT_ASKPASS")
234
+ assert askpass_path is not None
235
+ # Temp files are cleaned up after git_export returns
236
+ assert not os.path.exists(askpass_path)
237
+ # No shell credential helper should be set
238
+ assert "GIT_CONFIG_COUNT" not in captured_env
233239
234240
def test_token_redacted_from_output(self):
235241
"""If the token somehow leaks into Fossil/Git stdout, it is scrubbed."""
236242
from fossil.cli import FossilCLI
237243
238244
--- tests/test_security.py
+++ tests/test_security.py
@@ -205,12 +205,14 @@
205 cmd_str = " ".join(captured_cmd)
206 assert "ghp_s3cretTOKEN123" not in cmd_str
207 # URL should not have token embedded
208 assert "ghp_s3cretTOKEN123@" not in cmd_str
209
210 def test_token_passed_via_env(self):
211 """When auth_token is provided, git credential helper is configured via env."""
 
 
212 from fossil.cli import FossilCLI
213
214 cli = FossilCLI(binary="/usr/bin/false")
215 captured_env = {}
216
@@ -225,13 +227,17 @@
225 autopush_url="https://github.com/user/repo.git",
226 auth_token="ghp_s3cretTOKEN123",
227 )
228
229 assert captured_env.get("GIT_TERMINAL_PROMPT") == "0"
230 assert captured_env.get("GIT_CONFIG_COUNT") == "1"
231 assert captured_env.get("GIT_CONFIG_KEY_0") == "credential.helper"
232 assert "ghp_s3cretTOKEN123" in captured_env.get("GIT_CONFIG_VALUE_0", "")
 
 
 
 
233
234 def test_token_redacted_from_output(self):
235 """If the token somehow leaks into Fossil/Git stdout, it is scrubbed."""
236 from fossil.cli import FossilCLI
237
238
--- tests/test_security.py
+++ tests/test_security.py
@@ -205,12 +205,14 @@
205 cmd_str = " ".join(captured_cmd)
206 assert "ghp_s3cretTOKEN123" not in cmd_str
207 # URL should not have token embedded
208 assert "ghp_s3cretTOKEN123@" not in cmd_str
209
210 def test_token_passed_via_askpass(self):
211 """When auth_token is provided, GIT_ASKPASS is configured and token is in a separate file."""
212 import os
213
214 from fossil.cli import FossilCLI
215
216 cli = FossilCLI(binary="/usr/bin/false")
217 captured_env = {}
218
@@ -225,13 +227,17 @@
227 autopush_url="https://github.com/user/repo.git",
228 auth_token="ghp_s3cretTOKEN123",
229 )
230
231 assert captured_env.get("GIT_TERMINAL_PROMPT") == "0"
232 # Uses GIT_ASKPASS instead of shell credential helper
233 askpass_path = captured_env.get("GIT_ASKPASS")
234 assert askpass_path is not None
235 # Temp files are cleaned up after git_export returns
236 assert not os.path.exists(askpass_path)
237 # No shell credential helper should be set
238 assert "GIT_CONFIG_COUNT" not in captured_env
239
240 def test_token_redacted_from_output(self):
241 """If the token somehow leaks into Fossil/Git stdout, it is scrubbed."""
242 from fossil.cli import FossilCLI
243
244

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button