@@ -70,6 +70,54 @@ const STATUS_UPSTREAM_REFRESH_ENV = Object.freeze({
7070} satisfies NodeJS . ProcessEnv ) ;
7171const DEFAULT_BASE_BRANCH_CANDIDATES = [ "main" , "master" ] as const ;
7272const GIT_LIST_BRANCHES_DEFAULT_LIMIT = 100 ;
73+
74+ const COMMIT_SIGNING_FAILURE_PATTERNS = [
75+ / g p g (?: 2 ) ? (?: \. e x e ) ? : .* f a i l e d t o s i g n / i,
76+ / g p g f a i l e d t o s i g n t h e d a t a / i,
77+ / s i g n i n g f a i l e d : / i,
78+ / f a i l e d t o s i g n t h e d a t a / i,
79+ / p i n e n t r y .* (?: f a i l e d | e r r o r | n o t f o u n d | n o s u c h f i l e | c a n c e l l ? e d ) / i,
80+ / (?: f a i l e d | e r r o r | n o s u c h f i l e | c a n c e l l ? e d ) .* p i n e n t r y / i,
81+ / i n a p p r o p r i a t e i o c t l f o r d e v i c e / i,
82+ / c a n n o t o p e n \/ d e v \/ t t y / i,
83+ / n o s e c r e t k e y / i,
84+ / s e c r e t k e y n o t a v a i l a b l e / i,
85+ / s s h - k e y g e n (?: \. e x e ) ? : ? .* (?: f a i l e d | e r r o r | c o u l d n [ ' ’ ] t ) .* s i g n / i,
86+ / c o u l d n [ ' ’ ] t s i g n (?: m e s s a g e | d a t a ) / i,
87+ / c o u l d n [ ' ’ ] t l o a d p u b l i c k e y / i,
88+ / n o p r i v a t e k e y f o u n d f o r p u b l i c k e y / i,
89+ / l o a d k e y .* : (?: i n v a l i d f o r m a t | n o s u c h f i l e o r d i r e c t o r y | p e r m i s s i o n d e n i e d ) / i,
90+ / a g e n t r e f u s e d o p e r a t i o n / i,
91+ ] as const ;
92+
93+ export function isCommitSigningFailureStderr ( stderr : string ) : boolean {
94+ return COMMIT_SIGNING_FAILURE_PATTERNS . some ( ( pattern ) => pattern . test ( stderr ) ) ;
95+ }
96+
97+ /** Longer than any real git error line, short enough to keep logs readable. */
98+ const GIT_STDERR_LOG_LIMIT = 2000 ;
99+
100+ /**
101+ * Strip credentials from git output so it can be logged.
102+ *
103+ * git echoes the remote URL it used, and those URLs routinely carry secrets
104+ * (`https://x-access-token:TOKEN@github.com/...`), so raw stderr must never
105+ * reach a log. Redacts the userinfo component of any URL plus bare tokens that
106+ * commonly appear on their own.
107+ */
108+ export function redactGitOutput ( stderr : string ) : string {
109+ return (
110+ stderr
111+ . slice ( 0 , GIT_STDERR_LOG_LIMIT )
112+ . replace ( / ( [ a - z A - Z ] [ \w + . - ] * : \/ \/ ) [ ^ / @ \s ] * @ / g, "$1<redacted>@" )
113+ . replace ( / \b ( g h [ p o u s r ] _ | g i t h u b _ p a t _ | g l p a t - ) [ A - Z a - z 0 - 9 _ - ] + / g, "$1<redacted>" )
114+ // Take the whole value, not just the scheme word: `Authorization: Bearer X`
115+ // must not redact `Bearer` and leave `X` behind.
116+ . replace ( / \b ( A u t h o r i z a t i o n ) \s * [: = ] \s * .* / gi, "$1: <redacted>" )
117+ . replace ( / \b ( B e a r e r | t o k e n ) \s * [: = ] ? \s + \S + / gi, "$1 <redacted>" )
118+ ) ;
119+ }
120+
73121const NON_REPOSITORY_STATUS_DETAILS = Object . freeze < GitVcsDriver . GitStatusDetails > ( {
74122 isRepo : false ,
75123 hasOriginRemote : false ,
@@ -379,6 +427,7 @@ function gitCommandContext(
379427 command : "git" ,
380428 cwd : input . cwd ,
381429 argumentCount : input . args . length ,
430+ failureKind : "unknown" as const ,
382431 } as const ;
383432}
384433
@@ -1737,25 +1786,52 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
17371786 body ,
17381787 options ?: GitVcsDriver . GitCommitOptions ,
17391788 ) {
1740- const args = [ "commit" , "-m" , subject ] ;
1789+ const args = [ "commit" ] ;
1790+ if ( options ?. disableSigning ) {
1791+ args . push ( "--no-gpg-sign" ) ;
1792+ }
1793+ args . push ( "-m" , subject ) ;
17411794 const trimmedBody = body . trim ( ) ;
17421795 if ( trimmedBody . length > 0 ) {
17431796 args . push ( "-m" , trimmedBody ) ;
17441797 }
1745- const progress =
1746- options ?. progress ?. onOutputLine === undefined
1747- ? options ?. progress
1748- : {
1749- ...options . progress ,
1798+ let hookFailed = false ;
1799+ const progress : GitVcsDriver . ExecuteGitProgress = {
1800+ ...( options ?. progress ?. onOutputLine
1801+ ? {
17501802 onStdoutLine : ( line : string ) =>
17511803 options . progress ?. onOutputLine ?.( { stream : "stdout" , text : line } ) ?? Effect . void ,
17521804 onStderrLine : ( line : string ) =>
17531805 options . progress ?. onOutputLine ?.( { stream : "stderr" , text : line } ) ?? Effect . void ,
1754- } ;
1755- yield * executeGit ( "GitVcsDriver.commit.commit" , cwd , args , {
1806+ }
1807+ : { } ) ,
1808+ ...( options ?. progress ?. onHookStarted
1809+ ? { onHookStarted : options . progress . onHookStarted }
1810+ : { } ) ,
1811+ onHookFinished : ( input ) => {
1812+ if ( input . exitCode !== null && input . exitCode !== 0 ) {
1813+ hookFailed = true ;
1814+ }
1815+ return options ?. progress ?. onHookFinished ?.( input ) ?? Effect . void ;
1816+ } ,
1817+ } ;
1818+ const result = yield * executeGitWithStableDiagnostics ( "GitVcsDriver.commit.commit" , cwd , args , {
1819+ allowNonZeroExit : true ,
17561820 ...( options ?. timeoutMs !== undefined ? { timeoutMs : options . timeoutMs } : { } ) ,
1757- ...( progress ? { progress } : { } ) ,
1758- } ) . pipe ( Effect . asVoid ) ;
1821+ progress,
1822+ } ) ;
1823+ if ( result . exitCode !== 0 ) {
1824+ return yield * new GitCommandError ( {
1825+ ...gitCommandContext ( { operation : "GitVcsDriver.commit.commit" , cwd, args } ) ,
1826+ detail : "Git command exited with a non-zero status." ,
1827+ ...( result . exitCode === null ? { } : { exitCode : result . exitCode } ) ,
1828+ stdoutLength : result . stdout . length ,
1829+ stderrLength : result . stderr . length ,
1830+ ...( ! options ?. disableSigning && ! hookFailed && isCommitSigningFailureStderr ( result . stderr )
1831+ ? { failureKind : "commit_signing_failed" as const }
1832+ : { } ) ,
1833+ } ) ;
1834+ }
17591835 const commitSha = yield * runGitStdout ( "GitVcsDriver.commit.revParseHead" , cwd , [
17601836 "rev-parse" ,
17611837 "HEAD" ,
@@ -2107,6 +2183,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
21072183 operation : "GitVcsDriver.getReviewDiffPreview.hash" ,
21082184 command : "crypto.digest SHA-256" ,
21092185 cwd : input . cwd ,
2186+ failureKind : "unknown" ,
21102187 detail : "Failed to hash review diff." ,
21112188 cause,
21122189 } ) ,
0 commit comments