@@ -342,7 +342,6 @@ function calculateMrSize(mergeRequestId: number, diffs: { stringifiedHunks: stri
342
342
. reduce ( ( a , b ) => a + b , 0 ) ;
343
343
}
344
344
345
-
346
345
return mrSize ;
347
346
}
348
347
@@ -361,15 +360,16 @@ type MergeRequestData = {
361
360
authorExternalId : extract . MergeRequest [ 'authorExternalId' ]
362
361
}
363
362
364
- type TimelineEventData = {
363
+ export type TimelineEventData = {
365
364
type : extract . TimelineEvents [ 'type' ] ;
366
365
timestamp : extract . TimelineEvents [ 'timestamp' ] ;
367
366
actorId : extract . TimelineEvents [ 'actorId' ] ;
368
367
data : extract . TimelineEvents [ 'data' ] ;
369
368
}
370
369
371
- type MergeRequestNoteData = {
372
- createdAt : extract . MergeRequestNote [ 'createdAt' ] ;
370
+ export type MergeRequestNoteData = {
371
+ type : 'note' ;
372
+ timestamp : extract . MergeRequestNote [ 'createdAt' ] ;
373
373
authorExternalId : extract . MergeRequestNote [ 'authorExternalId' ] ;
374
374
}
375
375
@@ -411,12 +411,12 @@ async function selectExtractData(db: ExtractDatabase, extractMergeRequestId: num
411
411
. all ( ) ;
412
412
413
413
const mergeRequestNotesData = await db . select ( {
414
- createdAt : mergeRequestNotes . createdAt ,
414
+ timestamp : mergeRequestNotes . createdAt ,
415
415
authorExternalId : mergeRequestNotes . authorExternalId ,
416
416
} )
417
417
. from ( mergeRequestNotes )
418
418
. where ( eq ( mergeRequestNotes . mergeRequestId , extractMergeRequestId ) )
419
- . all ( ) satisfies MergeRequestNoteData [ ] ;
419
+ . all ( ) satisfies Omit < MergeRequestNoteData , 'type' > [ ] ;
420
420
421
421
const timelineEventsData = await db . select ( {
422
422
type : timelineEvents . type ,
@@ -431,7 +431,7 @@ async function selectExtractData(db: ExtractDatabase, extractMergeRequestId: num
431
431
return {
432
432
diffs : mergerRequestDiffsData ,
433
433
...mergeRequestData || { mergeRequest : null } ,
434
- notes : mergeRequestNotesData ,
434
+ notes : mergeRequestNotesData . map ( note => ( { ... note , type : 'note' as const } ) ) ,
435
435
timelineEvents : timelineEventsData ,
436
436
...repositoryData || { repository : null } ,
437
437
} ;
@@ -442,114 +442,172 @@ export type RunContext = {
442
442
transformDatabase : TransformDatabase ;
443
443
} ;
444
444
445
- type TimelineMapKey = {
445
+ export type TimelineMapKey = {
446
446
type : extract . TimelineEvents [ 'type' ] | 'note' ,
447
447
timestamp : Date ,
448
- actorId : extract . TimelineEvents [ 'actorId' ] | extract . MergeRequestNote [ 'authorExternalId' ] | null ,
449
448
}
449
+
450
450
function setupTimeline ( timelineEvents : TimelineEventData [ ] , notes : MergeRequestNoteData [ ] ) {
451
451
const timeline = new Map < TimelineMapKey ,
452
452
TimelineEventData | MergeRequestNoteData
453
453
> ( ) ;
454
454
455
-
456
455
for ( const timelineEvent of timelineEvents ) {
457
456
timeline . set ( {
458
457
type : timelineEvent . type ,
459
458
timestamp : timelineEvent . timestamp ,
460
- actorId : timelineEvent . actorId ,
461
459
} , timelineEvent ) ;
462
460
}
463
461
464
462
for ( const note of notes ) {
465
463
timeline . set ( {
466
464
type : 'note' ,
467
- timestamp : note . createdAt ,
468
- actorId : note . authorExternalId ,
465
+ timestamp : note . timestamp ,
469
466
} , note ) ;
470
467
}
471
468
472
469
return timeline ;
473
470
474
471
}
475
472
476
- function runTimeline ( extractMergeRequest : MergeRequestData , timelineEvents : TimelineEventData [ ] , notes : MergeRequestNoteData [ ] ) {
473
+ type calcTimelineArgs = {
474
+ authorExternalId : extract . MergeRequest [ 'authorExternalId' ] ,
475
+ }
477
476
478
- const timelineMap = setupTimeline ( timelineEvents , notes ) ;
479
- const timelineMapKeys = [ ...timelineMap . keys ( ) ] ;
477
+ export function calculateTimeline ( timelineMapKeys : TimelineMapKey [ ] , timelineMap : Map < TimelineMapKey , MergeRequestNoteData | TimelineEventData > , { authorExternalId } : calcTimelineArgs ) {
478
+
479
+ const commitedEvents = timelineMapKeys . filter ( key => key . type === 'committed' ) ;
480
+ commitedEvents . sort ( ( a , b ) => a . timestamp . getTime ( ) - b . timestamp . getTime ( ) ) ;
481
+
482
+ const firstCommitEvent = commitedEvents [ 0 ] || null ;
483
+ const lastCommitEvent = commitedEvents [ commitedEvents . length - 1 ] || null ;
480
484
481
- //start coding at
485
+ const startedCodingAt = firstCommitEvent ? firstCommitEvent . timestamp : null ;
482
486
483
- const committedEvents = timelineMapKeys . filter ( ( { type } ) => type === 'committed' ) as ( TimelineMapKey & { type : 'committed' } ) [ ] ;
487
+ const mergedEvents = timelineMapKeys . filter ( key => key . type === 'merged' ) ;
488
+ mergedEvents . sort ( ( a , b ) => a . timestamp . getTime ( ) - b . timestamp . getTime ( ) ) ;
484
489
485
- let startedCodingAt : Date | null = null ;
490
+ const mergedAt = mergedEvents [ 0 ] ?. timestamp || null ;
486
491
487
- if ( committedEvents . length > 0 ) {
492
+ const readyForReviewEvents = timelineMapKeys . filter ( key => key . type === 'ready_for_review' || key . type === 'review_requested' ) ;
493
+ readyForReviewEvents . sort ( ( a , b ) => a . timestamp . getTime ( ) - b . timestamp . getTime ( ) ) ;
494
+ const lastReadyForReviewEvent = readyForReviewEvents [ readyForReviewEvents . length - 1 ] || null ;
488
495
489
- for ( const committedEvent of committedEvents ) {
490
- if ( ! startedCodingAt ) {
491
- startedCodingAt = committedEvent . timestamp ;
496
+ const startedPickupAt = ( ( ) => {
497
+ if ( lastCommitEvent === null && lastReadyForReviewEvent === null ) {
498
+ return null ;
499
+ }
500
+ if ( lastReadyForReviewEvent === null && lastCommitEvent ) {
501
+ // problematic code: everything below is problematic
502
+ const reviewedEventsBeforeLastCommitEvent = timelineMapKeys . filter ( key => key . type === 'reviewed' && key . timestamp < lastCommitEvent . timestamp ) ;
503
+ reviewedEventsBeforeLastCommitEvent . sort ( ( a , b ) => a . timestamp . getTime ( ) - b . timestamp . getTime ( ) ) ;
504
+ const firstReviewedEventBeforeLastCommitEvent = reviewedEventsBeforeLastCommitEvent [ 0 ] ;
505
+ if ( firstReviewedEventBeforeLastCommitEvent ) {
506
+ return [ ...commitedEvents ] . reverse ( ) . find ( event => event . timestamp < firstReviewedEventBeforeLastCommitEvent . timestamp ) ?. timestamp || null ;
492
507
}
493
- else if ( committedEvent . timestamp . getTime ( ) < startedCodingAt . getTime ( ) ) {
494
- startedCodingAt = committedEvent . timestamp ;
508
+
509
+ return lastCommitEvent . timestamp ;
510
+ }
511
+ if ( lastReadyForReviewEvent && lastCommitEvent ) {
512
+ // problematic code: there could be a commit between last commit and lastReadyForReviewEvent
513
+ const reviewedEventsAfterLastReadyForReviewEvent = timelineMapKeys . filter (
514
+ key =>
515
+ key . type === 'reviewed'
516
+ && key . timestamp > lastReadyForReviewEvent . timestamp
517
+ && key . timestamp < lastCommitEvent . timestamp
518
+ ) ;
519
+ reviewedEventsAfterLastReadyForReviewEvent . sort ( ( a , b ) => a . timestamp . getTime ( ) - b . timestamp . getTime ( ) ) ;
520
+ const firstReviewedEventAfterLastReadyForReviewEvent = reviewedEventsAfterLastReadyForReviewEvent [ 0 ]
521
+
522
+ if ( firstReviewedEventAfterLastReadyForReviewEvent ) {
523
+ const temp = [ ...commitedEvents ] . reverse ( ) . find (
524
+ event => event . timestamp > lastReadyForReviewEvent . timestamp
525
+ && event . timestamp < firstReviewedEventAfterLastReadyForReviewEvent . timestamp
526
+
527
+ ) ?. timestamp || null ;
528
+
529
+ if ( temp ) {
530
+ return temp ;
531
+ }
532
+ return lastReadyForReviewEvent . timestamp ;
495
533
}
534
+ return lastReadyForReviewEvent . timestamp > lastCommitEvent . timestamp ? lastReadyForReviewEvent . timestamp : lastCommitEvent . timestamp ;
496
535
}
497
536
498
- }
537
+ return null ;
538
+ } ) ( ) ;
499
539
500
- // start review at
540
+ let firstReviewedEvent = null ;
541
+ let reviewed = false ;
542
+ let reviewDepth = 0 ;
501
543
502
- const reviewEvents = timelineMapKeys . filter ( ( { type } ) => type === 'note' || type === 'reviewed' || type === 'commented' ) as ( TimelineMapKey & { type : 'note' | 'reviewed' | 'commented' } ) [ ] ;
503
- let startedReviewAt : Date | null = null ;
544
+ const noteEvents = timelineMapKeys . filter ( key => key . type === 'note' ) ;
545
+ for ( const noteEvent of noteEvents ) {
546
+ const eventData = timelineMap . get ( noteEvent ) as MergeRequestNoteData | undefined ;
547
+ if ( ! eventData ) {
548
+ console . error ( 'note event data not found' , noteEvent ) ;
549
+ continue ;
550
+ }
504
551
505
- if ( reviewEvents . length > 0 ) {
506
- for ( const reviewEvent of reviewEvents ) {
507
- if ( ! startedReviewAt && reviewEvent . actorId !== extractMergeRequest . authorExternalId ) {
508
- startedReviewAt = reviewEvent . timestamp ;
509
- }
510
- if ( startedReviewAt && reviewEvent . timestamp . getTime ( ) < startedReviewAt . getTime ( ) ) {
511
- startedReviewAt = reviewEvent . timestamp ;
512
- }
552
+ const afterStartedPickupAt = startedPickupAt ? noteEvent . timestamp > startedPickupAt : true ;
553
+ const beforeMergedEvent = mergedAt ? noteEvent . timestamp < mergedAt : true ;
554
+ const isAuthorReviewer = eventData . authorExternalId === authorExternalId ;
555
+ if ( afterStartedPickupAt && beforeMergedEvent && ! isAuthorReviewer ) {
556
+ reviewDepth ++ ;
513
557
}
514
558
}
515
559
516
- // start pickup at
517
-
518
- const convertToDraftEvents = timelineMapKeys . filter ( ( { type } ) => type === 'convert_to_draft' ) as ( TimelineMapKey & { type : 'convert_to_draft' } ) [ ] ;
519
- let lastConvertToDraftBeforeReview : Date | null = null ;
560
+ const reviewedEvents = timelineMapKeys . filter ( key => key . type === 'reviewed' && key . timestamp < ( mergedAt || new Date ( ) ) ) ;
561
+ for ( const reviewedEvent of reviewedEvents ) {
562
+ const eventData = timelineMap . get ( reviewedEvent ) ;
563
+ if ( ! eventData ) {
564
+ console . error ( 'reviewed event data not found' , reviewedEvent ) ;
565
+ continue ;
566
+ }
567
+ const res = extract . ReviewedEventSchema . safeParse ( ( eventData as TimelineEventData ) . data ) ;
568
+ if ( ! res . success ) {
569
+ console . error ( res . error ) ;
570
+ continue ;
571
+ }
572
+ const isValidState = res . data . state === 'approved' || res . data . state === 'changes_requested' || res . data . state === 'commented' ;
573
+ const afterStartedPickupAt = startedPickupAt ? reviewedEvent . timestamp > startedPickupAt : true ;
574
+ const beforeFirstReviewedEvent = firstReviewedEvent ? reviewedEvent . timestamp < firstReviewedEvent . timestamp : true ;
575
+ const beforeMergedEvent = mergedAt ? reviewedEvent . timestamp < mergedAt : true ;
576
+ const isAuthorReviewer = ( eventData as TimelineEventData ) . actorId === authorExternalId ;
577
+
578
+ if ( isValidState && afterStartedPickupAt && beforeMergedEvent && ! isAuthorReviewer ) {
579
+ reviewed = true ;
580
+ reviewDepth ++ ;
581
+ }
520
582
521
- for ( const convertToDraft of convertToDraftEvents ) {
522
- if (
523
- ( ! lastConvertToDraftBeforeReview || convertToDraft . timestamp . getTime ( ) > lastConvertToDraftBeforeReview . getTime ( ) )
524
- && ( ! startedReviewAt || convertToDraft . timestamp . getTime ( ) < startedReviewAt . getTime ( ) )
525
- ) {
526
- lastConvertToDraftBeforeReview = convertToDraft . timestamp ;
583
+ if ( isValidState && afterStartedPickupAt && beforeFirstReviewedEvent && ! isAuthorReviewer ) {
584
+ reviewed = true ;
585
+ firstReviewedEvent = reviewedEvent ;
527
586
}
528
587
}
529
588
530
- let startedPickupAt : Date | null = null ;
531
- const initialPickupEvents = timelineMapKeys . filter ( ( { type, timestamp } ) => type === 'ready_for_review' || type === 'review_requested'
532
- && ( ! lastConvertToDraftBeforeReview || timestamp . getTime ( ) > lastConvertToDraftBeforeReview . getTime ( ) )
533
- && ( ! startedReviewAt || timestamp . getTime ( ) < startedReviewAt . getTime ( ) ) ) as ( TimelineMapKey & { type : 'ready_for_review' | 'review_requested' } ) [ ] ;
589
+ return {
590
+ startedCodingAt,
591
+ startedPickupAt,
592
+ startedReviewAt : firstReviewedEvent ? firstReviewedEvent . timestamp : null ,
593
+ mergedAt,
594
+ reviewed,
595
+ reviewDepth,
596
+ } ;
597
+ }
534
598
535
- for ( const pickupEvent of initialPickupEvents ) {
536
- if ( ! startedPickupAt || pickupEvent . timestamp . getTime ( ) < startedPickupAt . getTime ( ) ) {
537
- startedPickupAt = pickupEvent . timestamp ;
538
- }
539
- }
540
599
541
- if ( startedReviewAt && ! startedPickupAt ) {
542
- for ( const committedEvent of committedEvents ) {
543
- if ( ! startedPickupAt && committedEvent . timestamp . getTime ( ) < startedReviewAt . getTime ( ) ) startedPickupAt = committedEvent . timestamp ;
544
- if ( startedPickupAt
545
- && committedEvent . timestamp . getTime ( ) > startedPickupAt . getTime ( )
546
- && committedEvent . timestamp . getTime ( ) < startedReviewAt . getTime ( ) ) startedPickupAt = committedEvent . timestamp ;
547
- }
548
- }
549
600
550
- if ( startedReviewAt && ! startedPickupAt ) {
551
- startedPickupAt = extractMergeRequest . openedAt ;
552
- }
601
+ function runTimeline ( mergeRequestData : MergeRequestData , timelineEvents : TimelineEventData [ ] , notes : MergeRequestNoteData [ ] ) {
602
+ const timelineMap = setupTimeline ( timelineEvents , notes ) ;
603
+ const timelineMapKeys = [ ...timelineMap . keys ( ) ] ;
604
+
605
+ const { startedCodingAt, startedReviewAt, startedPickupAt, reviewed, reviewDepth } = calculateTimeline (
606
+ timelineMapKeys ,
607
+ timelineMap ,
608
+ {
609
+ authorExternalId : mergeRequestData . authorExternalId ,
610
+ } ) ;
553
611
554
612
// TODO: can this be optimized with the map ?
555
613
const approved = timelineEvents . find ( ev => ev . type === 'reviewed' && ( JSON . parse ( ev . data as string ) as extract . ReviewedEvent ) . state === 'approved' ) !== undefined ;
@@ -558,13 +616,14 @@ function runTimeline(extractMergeRequest: MergeRequestData, timelineEvents: Time
558
616
startedCodingAt,
559
617
startedReviewAt,
560
618
startedPickupAt,
561
- reviewed : startedReviewAt !== null ,
619
+ reviewed,
620
+ reviewDepth,
562
621
approved,
563
- reviewDepth : reviewEvents . length ,
564
- } ;
622
+ }
565
623
}
566
624
567
625
626
+
568
627
export async function run ( extractMergeRequestId : number , ctx : RunContext ) {
569
628
const extractData = await selectExtractData ( ctx . extractDatabase , extractMergeRequestId ) ;
570
629
0 commit comments