1
+ import { prepareCommitMessage , sanitizeCommitMessage } from '@/utils/git' ;
1
2
import { type GitCommit } from '@onlook/git' ;
3
+ import stripAnsi from 'strip-ansi' ;
2
4
import type { EditorEngine } from '../engine' ;
3
5
4
6
export const ONLOOK_DISPLAY_NAME_NOTE_REF = 'refs/notes/onlook-display-name' ;
@@ -145,28 +147,54 @@ export class GitManager {
145
147
* Create a commit
146
148
*/
147
149
async commit ( message : string ) : Promise < GitCommandResult > {
148
- const escapedMessage = message . replace ( / \" / g, '\\"' ) ;
149
- return this . runCommand ( `git commit --allow-empty --no-verify -m "${ escapedMessage } "` ) ;
150
+ const sanitizedMessage = sanitizeCommitMessage ( message ) ;
151
+ const escapedMessage = prepareCommitMessage ( sanitizedMessage ) ;
152
+ return this . runCommand ( `git commit --allow-empty --no-verify -m ${ escapedMessage } ` ) ;
150
153
}
151
154
152
155
/**
153
156
* List commits with formatted output
154
157
*/
155
- async listCommits ( ) : Promise < GitCommit [ ] > {
156
- try {
157
- const result = await this . runCommand (
158
- 'git log --pretty=format:"%H|%an <%ae>|%ad|%s" --date=iso' ,
159
- ) ;
158
+ async listCommits ( maxRetries = 2 ) : Promise < GitCommit [ ] > {
159
+ let lastError : Error | null = null ;
160
+
161
+ for ( let attempt = 0 ; attempt <= maxRetries ; attempt ++ ) {
162
+ try {
163
+ // Use a more robust format with unique separators and handle multiline messages
164
+ const result = await this . runCommand (
165
+ 'git --no-pager log --pretty=format:"COMMIT_START%n%H%n%an <%ae>%n%ad%n%B%nCOMMIT_END" --date=iso' ,
166
+ ) ;
160
167
161
- if ( result . success && result . output ) {
162
- return this . parseGitLog ( result . output ) ;
163
- }
168
+ if ( result . success && result . output ) {
169
+ return this . parseGitLog ( result . output ) ;
170
+ }
164
171
165
- return [ ] ;
166
- } catch ( error ) {
167
- console . error ( 'Failed to list commits' , error ) ;
168
- return [ ] ;
172
+ // If git command failed but didn't throw, treat as error for retry logic
173
+ lastError = new Error ( `Git command failed: ${ result . error || 'Unknown error' } ` ) ;
174
+
175
+ if ( attempt < maxRetries ) {
176
+ // Wait before retry with exponential backoff
177
+ await new Promise ( resolve => setTimeout ( resolve , Math . pow ( 2 , attempt ) * 100 ) ) ;
178
+ continue ;
179
+ }
180
+
181
+ return [ ] ;
182
+ } catch ( error ) {
183
+ lastError = error instanceof Error ? error : new Error ( String ( error ) ) ;
184
+ console . warn ( `Attempt ${ attempt + 1 } failed to list commits:` , lastError . message ) ;
185
+
186
+ if ( attempt < maxRetries ) {
187
+ // Wait before retry with exponential backoff
188
+ await new Promise ( resolve => setTimeout ( resolve , Math . pow ( 2 , attempt ) * 100 ) ) ;
189
+ continue ;
190
+ }
191
+
192
+ console . error ( 'All attempts failed to list commits' , lastError ) ;
193
+ return [ ] ;
194
+ }
169
195
}
196
+
197
+ return [ ] ;
170
198
}
171
199
172
200
/**
@@ -180,9 +208,10 @@ export class GitManager {
180
208
* Add a display name note to a commit
181
209
*/
182
210
async addCommitNote ( commitOid : string , displayName : string ) : Promise < GitCommandResult > {
183
- const escapedDisplayName = displayName . replace ( / \" / g, '\\"' ) ;
211
+ const sanitizedDisplayName = sanitizeCommitMessage ( displayName ) ;
212
+ const escapedDisplayName = prepareCommitMessage ( sanitizedDisplayName ) ;
184
213
return this . runCommand (
185
- `git notes --ref=${ ONLOOK_DISPLAY_NAME_NOTE_REF } add -f -m " ${ escapedDisplayName } " ${ commitOid } ` ,
214
+ `git notes --ref=${ ONLOOK_DISPLAY_NAME_NOTE_REF } add -f -m ${ escapedDisplayName } ${ commitOid } ` ,
186
215
) ;
187
216
}
188
217
@@ -195,7 +224,11 @@ export class GitManager {
195
224
`git notes --ref=${ ONLOOK_DISPLAY_NAME_NOTE_REF } show ${ commitOid } ` ,
196
225
true ,
197
226
) ;
198
- return result . success ? this . formatGitLogOutput ( result . output ) : null ;
227
+ if ( result . success && result . output ) {
228
+ const cleanOutput = this . formatGitLogOutput ( result . output ) ;
229
+ return cleanOutput || null ;
230
+ }
231
+ return null ;
199
232
} catch ( error ) {
200
233
console . warn ( 'Failed to get commit note' , error ) ;
201
234
return null ;
@@ -220,71 +253,77 @@ export class GitManager {
220
253
}
221
254
222
255
const commits : GitCommit [ ] = [ ] ;
223
- const lines = cleanOutput . split ( '\n' ) . filter ( ( line ) => line . trim ( ) ) ;
224
256
225
- for ( const line of lines ) {
226
- if ( ! line . trim ( ) ) continue ;
257
+ // Split by COMMIT_START and COMMIT_END markers
258
+ const commitBlocks = cleanOutput . split ( 'COMMIT_START' ) . filter ( block => block . trim ( ) ) ;
227
259
228
- // Handle the new format: <hash>|<author>|<date>|<message>
229
- // The hash might have a prefix that we need to handle
230
- let cleanLine = line ;
260
+ for ( const block of commitBlocks ) {
261
+ // Remove COMMIT_END if present
262
+ const cleanBlock = block . replace ( / C O M M I T _ E N D \s * $ / , '' ) . trim ( ) ;
263
+ if ( ! cleanBlock ) continue ;
231
264
232
- // If line starts with escape sequences followed by =, extract everything after =
233
- const escapeMatch = cleanLine . match ( / ^ [ ^ \w ] * = ? ( .+ ) $ / ) ;
234
- if ( escapeMatch ) {
235
- cleanLine = escapeMatch [ 1 ] || '' ;
236
- }
265
+ // Split the block into lines
266
+ const lines = cleanBlock . split ( '\n' ) ;
267
+
268
+ if ( lines . length < 4 ) continue ; // Need at least hash, author, date, and message
269
+
270
+ const hash = lines [ 0 ] ?. trim ( ) ;
271
+ const authorLine = lines [ 1 ] ?. trim ( ) ;
272
+ const dateLine = lines [ 2 ] ?. trim ( ) ;
237
273
238
- const parts = cleanLine . split ( '|' ) ;
239
- if ( parts . length >= 4 ) {
240
- const hash = parts [ 0 ] ?. trim ( ) ;
241
- const authorLine = parts [ 1 ] ?. trim ( ) ;
242
- const dateLine = parts [ 2 ] ?. trim ( ) ;
243
- const message = parts . slice ( 3 ) . join ( '|' ) . trim ( ) ;
244
-
245
- if ( ! hash || ! authorLine || ! dateLine ) continue ;
246
-
247
- // Parse author name and email
248
- const authorMatch = authorLine . match ( / ^ ( .+ ?) \s * < ( .+ ?) > $ / ) ;
249
- const authorName = authorMatch ?. [ 1 ] ?. trim ( ) || authorLine ;
250
- const authorEmail = authorMatch ?. [ 2 ] ?. trim ( ) || '' ;
251
-
252
- // Parse date to timestamp
253
- const timestamp = Math . floor ( new Date ( dateLine ) . getTime ( ) / 1000 ) ;
254
-
255
- commits . push ( {
256
- oid : hash ,
257
- message : message || 'No message' ,
258
- author : {
259
- name : authorName ,
260
- email : authorEmail ,
261
- } ,
262
- timestamp : timestamp ,
263
- displayName : message || null ,
264
- } ) ;
274
+ // Everything from line 3 onwards is the commit message (including empty lines)
275
+ const messageLines = lines . slice ( 3 ) ;
276
+ // Join all message lines and trim only leading/trailing whitespace
277
+ const message = messageLines . join ( '\n' ) . trim ( ) ;
278
+
279
+ if ( ! hash || ! authorLine || ! dateLine ) continue ;
280
+
281
+ // Parse author name and email
282
+ const authorMatch = authorLine . match ( / ^ ( .+ ?) \s * < ( .+ ?) > $ / ) ;
283
+ const authorName = authorMatch ?. [ 1 ] ?. trim ( ) || authorLine ;
284
+ const authorEmail = authorMatch ?. [ 2 ] ?. trim ( ) || '' ;
285
+
286
+ // Parse date to timestamp
287
+ let timestamp : number ;
288
+ try {
289
+ timestamp = Math . floor ( new Date ( dateLine ) . getTime ( ) / 1000 ) ;
290
+ // Validate timestamp
291
+ if ( isNaN ( timestamp ) || timestamp < 0 ) {
292
+ timestamp = Math . floor ( Date . now ( ) / 1000 ) ;
293
+ }
294
+ } catch ( error ) {
295
+ console . warn ( 'Failed to parse commit date:' , dateLine , error ) ;
296
+ timestamp = Math . floor ( Date . now ( ) / 1000 ) ;
265
297
}
298
+
299
+ // Use the first line of the message as display name, or the full message if it's short
300
+ const displayMessage = message . split ( '\n' ) [ 0 ] || 'No message' ;
301
+
302
+ commits . push ( {
303
+ oid : hash ,
304
+ message : message || 'No message' ,
305
+ author : {
306
+ name : authorName ,
307
+ email : authorEmail ,
308
+ } ,
309
+ timestamp : timestamp ,
310
+ displayName : displayMessage ,
311
+ } ) ;
266
312
}
267
313
268
314
return commits ;
269
315
}
270
316
271
317
private formatGitLogOutput ( input : string ) : string {
272
- // Handle sequences with ESC characters anywhere within them
273
- // Pattern to match sequences like [?1h<ESC>= and [K<ESC>[?1l<ESC>>
274
- const ansiWithEscPattern = / \[ [ 0 - 9 ; ? a - z A - Z \x1b ] * [ a - z A - Z = > / ] * / g;
275
-
276
- // Handle standard ANSI escape sequences starting with ESC
277
- const ansiEscapePattern = / \x1b \[ [ 0 - 9 ; ? a - z A - Z ] * [ a - z A - Z = > / ] * / g;
318
+ // Use strip-ansi library for robust ANSI escape sequence removal
319
+ let cleanOutput = stripAnsi ( input ) ;
278
320
279
- // Handle control characters
280
- const controlChars = / [ \x00 - \x09 \x0B - \x1F \x7F ] / g;
321
+ // Remove any remaining control characters except newline and tab
322
+ cleanOutput = cleanOutput . replace ( / [ \x00 - \x08 \x0B \x0C \x0E - \x1F \x7F ] / g, '' ) ;
281
323
282
- const cleanOutput = input
283
- . replace ( ansiWithEscPattern , '' ) // Remove sequences with ESC chars in middle
284
- . replace ( ansiEscapePattern , '' ) // Remove standard ESC sequences
285
- . replace ( controlChars , '' ) // Remove control characters
286
- . trim ( ) ;
324
+ // Remove null bytes
325
+ cleanOutput = cleanOutput . replace ( / \0 / g, '' ) ;
287
326
288
- return cleanOutput ;
327
+ return cleanOutput . trim ( ) ;
289
328
}
290
329
}
0 commit comments