Skip to content

Commit 6d8df30

Browse files
authored
Handle combined diffs (#51)
This adds support for "combined diffs", which is the default format in which git displays merge commits. There wasn't one obvious approach to display them in split and unified formats, so I've chosen what looked reasonable to me. Github itself appears to show the output of git diff --first-parent instead.
1 parent cc24481 commit 6d8df30

File tree

9 files changed

+1532
-219
lines changed

9 files changed

+1532
-219
lines changed

src/__snapshots__/index.test.ts.snap

Lines changed: 1045 additions & 0 deletions
Large diffs are not rendered by default.

src/context.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ export type Context = Config & {
1111
CHALK: ChalkInstance;
1212
SPLIT_DIFFS: boolean;
1313
SCREEN_WIDTH: number;
14-
LINE_WIDTH: number;
15-
BLANK_LINE: string;
1614
HORIZONTAL_SEPARATOR: FormattedString;
1715
HIGHLIGHTER?: shikiji.Highlighter;
1816
};
@@ -27,14 +25,6 @@ export async function getContextForConfig(
2725
// Only split diffs if there's enough room
2826
const SPLIT_DIFFS = SCREEN_WIDTH >= config.MIN_LINE_WIDTH * 2;
2927

30-
let LINE_WIDTH: number;
31-
if (SPLIT_DIFFS) {
32-
LINE_WIDTH = Math.floor(SCREEN_WIDTH / 2);
33-
} else {
34-
LINE_WIDTH = SCREEN_WIDTH;
35-
}
36-
37-
const BLANK_LINE = ''.padStart(LINE_WIDTH);
3828
const HORIZONTAL_SEPARATOR = T()
3929
.fillWidth(SCREEN_WIDTH, '─')
4030
.addSpan(0, SCREEN_WIDTH, config.BORDER_COLOR);
@@ -51,8 +41,6 @@ export async function getContextForConfig(
5141
CHALK: chalk,
5242
SCREEN_WIDTH,
5343
SPLIT_DIFFS,
54-
LINE_WIDTH,
55-
BLANK_LINE,
5644
HORIZONTAL_SEPARATOR,
5745
HIGHLIGHTER,
5846
};

src/formatAndFitHunkLine.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,13 @@ const LINE_NUMBER_WIDTH = 5;
1111

1212
export async function* formatAndFitHunkLine(
1313
context: Context,
14+
lineWidth: number,
1415
fileName: string,
1516
lineNo: number,
1617
line: string | null,
1718
changes: Change[] | null
1819
): AsyncIterable<FormattedString> {
1920
const {
20-
BLANK_LINE,
21-
LINE_WIDTH,
2221
MISSING_LINE_COLOR,
2322
DELETED_LINE_COLOR,
2423
DELETED_LINE_NO_COLOR,
@@ -28,10 +27,12 @@ export async function* formatAndFitHunkLine(
2827
UNMODIFIED_LINE_NO_COLOR,
2928
} = context;
3029

30+
const blankLine = ''.padStart(lineWidth);
31+
3132
// A line number of 0 happens when we read the "No newline at end of file"
3233
// message as a line at the end of a deleted/inserted file.
3334
if (line === null || lineNo === 0) {
34-
yield T().appendString(BLANK_LINE, MISSING_LINE_COLOR);
35+
yield T().appendString(blankLine, MISSING_LINE_COLOR);
3536
return;
3637
}
3738

@@ -59,9 +60,9 @@ export async function* formatAndFitHunkLine(
5960
Each line is rendered as follows:
6061
<lineNo> <linePrefix> <lineText>
6162
62-
So (LINE_NUMBER_WIDTH + 2 + 1 + 1 + lineTextWidth) * 2 = LINE_WIDTH
63+
So (LINE_NUMBER_WIDTH + 2 + 1 + 1 + lineTextWidth) * 2 = lineWidth
6364
*/
64-
const lineTextWidth = LINE_WIDTH - 2 - 1 - 1 - LINE_NUMBER_WIDTH;
65+
const lineTextWidth = lineWidth - 2 - 1 - 1 - LINE_NUMBER_WIDTH;
6566

6667
let isFirstLine = true;
6768
const formattedLine = T().appendString(lineText);

src/index.test.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,5 +494,124 @@ index 095ee29..439621e 100644
494494
This is file2`)
495495
).toMatchSnapshot();
496496
});
497+
498+
test('merge commit with 2 parents', async function () {
499+
// Source: the TypeScript repo
500+
expect(
501+
await transform(`
502+
commit 3f504f4fbc1caf9c10814d48d8897a34f8a34dec
503+
Merge: 2439767601 fbcdb8cf4f
504+
Author: Gabriela Araujo Britto <gabrielaa@microsoft.com>
505+
Date: Thu Dec 21 17:57:42 2023 -0800
506+
507+
Merge branch 'main' into gabritto/d2
508+
509+
diff --cc src/compiler/binder.ts
510+
index b2f0d9f384,6ea9b82695..c638984e3b
511+
--- a/src/compiler/binder.ts
512+
+++ b/src/compiler/binder.ts
513+
@@@ -137,9 -136,9 +137,10 @@@ import
514+
isBlock,
515+
isBlockOrCatchScoped,
516+
IsBlockScopedContainer,
517+
+ isBooleanLiteral,
518+
isCallExpression,
519+
isClassStaticBlockDeclaration,
520+
+ isConditionalExpression,
521+
isConditionalTypeNode,
522+
IsContainer,
523+
isDeclaration,
524+
diff --cc src/compiler/types.ts
525+
index 2e204671f7,e56bba5ab4..ab6229d1b6
526+
--- a/src/compiler/types.ts
527+
+++ b/src/compiler/types.ts
528+
@@@ -5985,8 -6063,7 +6063,8 @@@ export interface NodeLinks
529+
decoratorSignature?: Signature; // Signature for decorator as if invoked by the runtime.
530+
spreadIndices?: { first: number | undefined, last: number | undefined }; // Indices of first and last spread elements in array literal
531+
parameterInitializerContainsUndefined?: boolean; // True if this is a parameter declaration whose type annotation contains "undefined".
532+
- fakeScopeForSignatureDeclaration?: boolean; // True if this is a fake scope injected into an enclosing declaration chain.
533+
+ contextualReturnType?: Type; // If the node is a return statement's expression, then this is the contextual return type.
534+
+ fakeScopeForSignatureDeclaration?: "params" | "typeParams"; // If present, this is a fake scope injected into an enclosing declaration chain.
535+
assertionExpressionType?: Type; // Cached type of the expression of a type assertion
536+
}`)
537+
).toMatchSnapshot();
538+
});
539+
540+
test('merge commit with 3 parents', async function () {
541+
// Source: the TypeScript repo
542+
expect(
543+
await transform(`
544+
commit d6d6a4aedfa78794c1b611c13d2ed1d3a66e1798
545+
Merge: 0dc976df1e 5f16a48236 3eadbf6c96
546+
Author: Andy Hanson <anhans@microsoft.com>
547+
Date: Thu Sep 1 12:52:42 2016 -0700
548+
549+
Merge branch 'goto_definition_super', remote-tracking branch 'origin' into constructor_references
550+
551+
diff --cc src/services/services.ts
552+
index b95feb9207,c19eb487d7,83a2192659..7e9a356e73
553+
--- a/src/services/services.ts
554+
+++ b/src/services/services.ts
555+
@@@@ -2788,26 -2792,18 -2788,34 +2792,42 @@@@ namespace ts
556+
return node && node.parent && node.parent.kind === SyntaxKind.PropertyAccessExpression && (<PropertyAccessExpression>node.parent).name === node;
557+
}
558+
559+
+ function climbPastPropertyAccess(node: Node) {
560+
+ return isRightSideOfPropertyAccess(node) ? node.parent : node;
561+
+ }
562+
+
563+
- function climbPastManyPropertyAccesses(node: Node): Node {
564+
- return isRightSideOfPropertyAccess(node) ? climbPastManyPropertyAccesses(node.parent) : node;
565+
+++ /** Get \`C\` given \`N\` if \`N\` is in the position \`class C extends N\` or \`class C extends foo.N\` where \`N\` is an identifier. */
566+
+++ function tryGetClassExtendingIdentifier(node: Node): ClassLikeDeclaration | undefined {
567+
+++ return tryGetClassExtendingExpressionWithTypeArguments(climbPastPropertyAccess(node).parent);
568+
++ }
569+
++
570+
function isCallExpressionTarget(node: Node): boolean {
571+
- if (isRightSideOfPropertyAccess(node)) {
572+
- node = node.parent;
573+
- }
574+
- node = climbPastPropertyAccess(node);
575+
-- return node && node.parent && node.parent.kind === SyntaxKind.CallExpression && (<CallExpression>node.parent).expression === node;
576+
++ return isCallOrNewExpressionTarget(node, SyntaxKind.CallExpression);
577+
}
578+
579+
function isNewExpressionTarget(node: Node): boolean {
580+
- if (isRightSideOfPropertyAccess(node)) {
581+
- node = node.parent;
582+
- }
583+
- node = climbPastPropertyAccess(node);
584+
-- return node && node.parent && node.parent.kind === SyntaxKind.NewExpression && (<CallExpression>node.parent).expression === node;
585+
++ return isCallOrNewExpressionTarget(node, SyntaxKind.NewExpression);
586+
++ }
587+
++
588+
++ function isCallOrNewExpressionTarget(node: Node, kind: SyntaxKind) {
589+
++ const target = climbPastPropertyAccess(node);
590+
++ return target && target.parent && target.parent.kind === kind && (<CallExpression>target.parent).expression === target;
591+
++ }
592+
++
593+
- /** Get \`C\` given \`N\` if \`N\` is in the position \`class C extends N\` or \`class C extends foo.N\` where \`N\` is an identifier. */
594+
- function tryGetClassExtendingIdentifier(node: Node): ClassLikeDeclaration | undefined {
595+
- return tryGetClassExtendingExpressionWithTypeArguments(climbPastPropertyAccess(node).parent);
596+
+++ function climbPastManyPropertyAccesses(node: Node): Node {
597+
+++ return isRightSideOfPropertyAccess(node) ? climbPastManyPropertyAccesses(node.parent) : node;
598+
++ }
599+
++
600+
++ /** Returns a CallLikeExpression where \`node\` is the target being invoked. */
601+
++ function getAncestorCallLikeExpression(node: Node): CallLikeExpression | undefined {
602+
++ const target = climbPastManyPropertyAccesses(node);
603+
++ const callLike = target.parent;
604+
++ return callLike && isCallLikeExpression(callLike) && getInvokedExpression(callLike) === target && callLike;
605+
++ }
606+
++
607+
++ function tryGetSignatureDeclaration(typeChecker: TypeChecker, node: Node): SignatureDeclaration | undefined {
608+
++ const callLike = getAncestorCallLikeExpression(node);
609+
++ return callLike && typeChecker.getResolvedSignature(callLike).declaration;
610+
}
611+
612+
function isNameOfModuleDeclaration(node: Node) {
613+
`)
614+
).toMatchSnapshot();
615+
});
497616
});
498617
}

0 commit comments

Comments
 (0)