Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"just-git": patch
---

Support `--summary` in `git show`, `git log` and `git diff`. It prints git's create, delete, rename and mode-change lines, after `--stat`, `--shortstat` or `--numstat` output and before a `-p` patch. On its own it replaces the patch. The summary that `commit` and `merge` print now uses the same lines, so a mode change prints `mode change 100644 => 100755 <path>` instead of a delete and a create, a rename that changes the mode prints the extra mode line, and unusual paths are C-quoted as in git.

`git show` on a merge commit now prints `--stat`, `--shortstat`, `--numstat` and `--summary` against the first parent, as git does, instead of printing nothing. `git log --graph --format=<fmt>` now prints the blank line between each commit and its diff output.
3 changes: 3 additions & 0 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ Options:
--name-status Show names and status of changed files
--shortstat Show only the shortstat summary line
--numstat Machine-readable insertions/deletions per file
--summary Show creations, deletions, renames and mode changes
-U, --unified <number> Generate diffs with <n> lines of context
-M, --find-renames Detect renames (enabled by default)
-C, --find-copies Detect copies (accepted for compatibility)
Expand Down Expand Up @@ -609,6 +610,7 @@ Options:
--name-only Show only names of changed files
--shortstat Show only the shortstat summary line
--numstat Machine-readable insertions/deletions per file
--summary Show creations, deletions, renames and mode changes
-q, --quiet suppress diff output
--graph Draw text-based graph of the commit history
--first-parent Follow only the first parent of merge commits
Expand Down Expand Up @@ -1015,6 +1017,7 @@ Options:
--name-status Show names and status of changed files
--shortstat Show only the shortstat summary line
--numstat Machine-readable insertions/deletions per file
--summary Show creations, deletions, renames and mode changes
--format <string> Pretty-print format string
--pretty <string> Pretty-print format or preset name
```
Expand Down
3 changes: 2 additions & 1 deletion docs/FILE_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ Exported symbols from `src/lib/`. Generated by `bun scripts/gen-lib-reference.ts

### lib/commit-summary.ts

- `formatShortstatParts`, `computeDiffStats`, `formatCommitSummary`, `renderStatLines`, `formatDiffStat`
- `formatShortstatParts`, `computeDiffStats`, `formatSummaryLines`, `withSummary`, `formatCommitSummary`, `renderStatLines`, `formatDiffStat`
- **interfaces**: `FileStat`
- **types**: `SummaryRename`

### lib/commit-walk.ts

Expand Down
32 changes: 30 additions & 2 deletions src/commands/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import {
requireGitContext,
requireWorkTree,
} from "../lib/command-utils.ts";
import { type FileStat, formatShortstatParts, renderStatLines } from "../lib/commit-summary.ts";
import {
type FileStat,
formatShortstatParts,
formatSummaryLines,
renderStatLines,
type SummaryRename,
} from "../lib/commit-summary.ts";
import { formatUnifiedDiff, myersDiff, splitLinesWithNL } from "../lib/diff-algorithm.ts";
import { getStage0Entries, readIndex } from "../lib/index.ts";
import { findAllMergeBases } from "../lib/merge.ts";
Expand Down Expand Up @@ -119,6 +125,7 @@ export function registerDiffCommand(parent: Command, ext?: GitExtensions) {
nameStatus: f().describe("Show names and status of changed files"),
shortstat: f().describe("Show only the shortstat summary line"),
numstat: f().describe("Machine-readable insertions/deletions per file"),
summary: f().describe("Show creations, deletions, renames and mode changes"),
unified: o.number().alias("U").describe("Generate diffs with <n> lines of context"),
findRenames: f().alias("M").describe("Detect renames (enabled by default)"),
findCopies: f().alias("C").describe("Detect copies (accepted for compatibility)"),
Expand Down Expand Up @@ -185,7 +192,14 @@ export function registerDiffCommand(parent: Command, ext?: GitExtensions) {

if (isError(result)) return result;
const contextLines = args.unified;
const output = await formatOutput(gitCtx, result.items, format, contextLines);
// Git drops --summary under the name formats, and --summary on its
// own replaces the patch.
const summary = args.summary && format !== "name-only" && format !== "name-status";
const output =
summary && format === "unified"
? { stdout: "", stderr: "", exitCode: 0 }
: await formatOutput(gitCtx, result.items, format, contextLines);
if (summary) output.stdout += formatAsSummary(result.items);
if (result.stderr) output.stderr = result.stderr;
return output;
},
Expand Down Expand Up @@ -864,6 +878,20 @@ async function formatAsNumstat(gitCtx: GitContext, items: DiffFileResult[]): Pro
return output;
}

function formatAsSummary(items: DiffFileResult[]): string {
const diffs: TreeDiffEntry[] = [];
const renames: SummaryRename[] = [];
for (const { path, status, oldPath, oldMode, newMode, similarity } of items) {
if (status === "R" && oldPath) {
renames.push({ oldPath, newPath: path, similarity: similarity ?? 100, oldMode, newMode });
} else if (status === "A" || status === "D" || status === "M") {
const treeStatus = status === "A" ? "added" : status === "D" ? "deleted" : "modified";
diffs.push({ path, status: treeStatus, oldMode, newMode });
}
}
return formatSummaryLines(diffs, renames);
}

async function formatAsStat(gitCtx: GitContext, items: DiffFileResult[]): Promise<string> {
const fileStats = await buildFileStats(gitCtx, items);
return renderStatLines(fileStats);
Expand Down
79 changes: 59 additions & 20 deletions src/commands/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import {
requireGitContext,
requireHead,
} from "../lib/command-utils.ts";
import { computeDiffStats, formatShortstatParts, renderStatLines } from "../lib/commit-summary.ts";
import {
computeDiffStats,
formatShortstatParts,
formatSummaryLines,
renderStatLines,
withSummary,
} from "../lib/commit-summary.ts";
import { CommitHeap, walkCommits } from "../lib/commit-walk.ts";
import { parseDate } from "../lib/date.ts";
import { formatUnifiedDiff, myersDiff, splitLinesWithNL } from "../lib/diff-algorithm.ts";
Expand Down Expand Up @@ -39,6 +45,12 @@ import type { Commit, GitRepo, ObjectId, TreeDiffEntry } from "../lib/types.ts";
import { worktreeHeadCommits } from "../lib/worktree-admin.ts";
import { a, type Command, f, o } from "../parse/index.ts";

/** The diff each commit is shown with: a main format and/or the --summary lines. */
interface LogDiffOutput {
format: LogDiffFormat;
summary: boolean;
}

type LogDiffFormat =
| "name-status"
| "name-only"
Expand Down Expand Up @@ -73,6 +85,7 @@ export function registerLogCommand(parent: Command, ext?: GitExtensions) {
nameOnly: f().describe("Show only names of changed files"),
shortstat: f().describe("Show only the shortstat summary line"),
numstat: f().describe("Machine-readable insertions/deletions per file"),
summary: f().describe("Show creations, deletions, renames and mode changes"),
quiet: quietFlag("suppress diff output"),
graph: f().describe("Draw text-based graph of the commit history"),
firstParent: f().describe("Follow only the first parent of merge commits"),
Expand Down Expand Up @@ -241,6 +254,10 @@ export function registerLogCommand(parent: Command, ext?: GitExtensions) {
: args.numstat
? "numstat"
: null;
// Git drops --summary when a name format is in effect.
const summary = args.summary && diffFormat !== "name-only" && diffFormat !== "name-status";
const diffOutput: LogDiffOutput | null =
diffFormat || summary ? { format: diffFormat, summary } : null;

// ── Graph / reverse conflict ────────────────────────────
const useGraph = args.graph;
Expand Down Expand Up @@ -326,7 +343,7 @@ export function registerLogCommand(parent: Command, ext?: GitExtensions) {
customFormat,
presetName,
abbrevCommit,
diffFormat,
diffOutput,
decoFn,
decoRawFn,
renameState,
Expand All @@ -350,9 +367,9 @@ export function registerLogCommand(parent: Command, ext?: GitExtensions) {
]),
};
let line = expandFormat(customFormat, fctx);
const diffText = await formatCommitDiff(gitCtx, entry.commit, diffFormat, renameState);
if (diffText) {
line += `\n\n${diffText.replace(/\n$/, "")}`;
const diffText = await formatCommitDiff(gitCtx, entry.commit, diffOutput, renameState);
if (diffText !== null) {
line += `\n\n${diffText}`.replace(/\n$/, "");
}
lines.push(line);
}
Expand Down Expand Up @@ -381,10 +398,10 @@ export function registerLogCommand(parent: Command, ext?: GitExtensions) {
]),
};
let line = formatPreset(effectivePreset, fctx, idx === 0, abbrevCommit);
const diffText = await formatCommitDiff(gitCtx, entry.commit, diffFormat, renameState);
if (diffText) {
const diffText = await formatCommitDiff(gitCtx, entry.commit, diffOutput, renameState);
if (diffText !== null) {
const sep = isOneline ? "\n" : "\n\n";
line += `${sep}${diffText.replace(/\n$/, "")}`;
line += `${sep}${diffText}`.replace(/\n$/, "");
}
lines.push(line);
}
Expand Down Expand Up @@ -606,7 +623,7 @@ async function formatWithGraph(
customFormat: string | null,
presetName: string | null,
abbrevCommit: boolean,
diffFormat: LogDiffFormat,
diffOutput: LogDiffOutput | null,
decoFn: ((h: ObjectId) => string) | undefined,
decoRawFn: ((h: ObjectId) => string) | undefined,
renameState: RenameWarningState,
Expand Down Expand Up @@ -679,20 +696,20 @@ async function formatWithGraph(
const diffText = await formatCommitDiff(
gitCtx,
entry.commit,
diffFormat,
diffOutput,
renameState,
statWidth,
);

// Diff/stat output gets padding prefixes (columns already settled)
if (diffText) {
const diffLines = diffText.replace(/\n$/, "").split("\n");
if (isOneline || customFormat !== null) {
if (diffText !== null) {
const diffLines = diffText ? diffText.replace(/\n$/, "").split("\n") : [];
if (isOneline) {
for (const dl of diffLines) {
output.push(graph.paddingPrefix() + dl);
}
} else {
// Medium/full format: blank separator before diff
// Blank separator before the diff, as for non-graph output
output.push(graph.paddingPrefix());
for (const dl of diffLines) {
output.push(graph.paddingPrefix() + dl);
Expand All @@ -710,15 +727,22 @@ async function formatWithGraph(

// ── Diff output for log ─────────────────────────────────────────────

/**
* The diff text shown under a commit, or null when nothing is shown at
* all: no diff output requested, a merge commit, or an empty diff. An
* empty string means the commit has changes but the requested output is
* empty (e.g. --summary on a content-only change); git still prints the
* separator line for it.
*/
async function formatCommitDiff(
ctx: GitRepo,
commit: Commit,
format: LogDiffFormat,
output: LogDiffOutput | null,
renameState: RenameWarningState,
statWidth?: number,
): Promise<string> {
if (!format) return "";
if (commit.parents.length >= 2) return "";
): Promise<string | null> {
if (!output) return null;
if (commit.parents.length >= 2) return null;

const parentTree =
commit.parents.length === 1
Expand All @@ -730,7 +754,20 @@ async function formatCommitDiff(
limit: renameState.limit,
});
renameState.needed = Math.max(renameState.needed ?? 0, neededRenameLimit ?? 0) || undefined;
if (remaining.length === 0 && renames.length === 0) return null;

const main = await formatLogDiffFormat(ctx, remaining, renames, output.format, statWidth);
if (!output.summary) return main;
return withSummary(main, formatSummaryLines(remaining, renames), output.format === "patch");
}

async function formatLogDiffFormat(
ctx: GitRepo,
remaining: TreeDiffEntry[],
renames: RenamePair[],
format: LogDiffFormat,
statWidth?: number,
): Promise<string> {
switch (format) {
case "name-only":
return logNameOnly(remaining, renames);
Expand All @@ -744,6 +781,8 @@ async function formatCommitDiff(
return logNumstat(ctx, remaining, renames);
case "patch":
return logPatch(ctx, remaining, renames);
case null:
return "";
}
}

Expand Down Expand Up @@ -775,7 +814,7 @@ async function logStat(
renames: RenamePair[],
statWidth?: number,
): Promise<string> {
const { fileStats } = await computeDiffStats(ctx, remaining, renames);
const fileStats = await computeDiffStats(ctx, remaining, renames);
fileStats.sort((a, b) => (a.sortKey < b.sortKey ? -1 : a.sortKey > b.sortKey ? 1 : 0));
return renderStatLines(fileStats, statWidth);
}
Expand All @@ -785,7 +824,7 @@ async function logShortstat(
remaining: TreeDiffEntry[],
renames: RenamePair[],
): Promise<string> {
const { fileStats } = await computeDiffStats(ctx, remaining, renames);
const fileStats = await computeDiffStats(ctx, remaining, renames);
let totalIns = 0;
let totalDel = 0;
for (const s of fileStats) {
Expand Down
Loading
Loading