From 54a78f47cda63582315620d320c3d30c5c0820a7 Mon Sep 17 00:00:00 2001 From: Daniel Wise Date: Tue, 10 Mar 2026 19:54:53 -0700 Subject: [PATCH] feat(info): add parent-relative diff output --- .beads/dolt-monitor.pid.lock | 0 README.md | 5 ++- apps/docs/content/docs/commands/log.mdx | 3 ++ packages/cli/src/commands/branch.test.ts | 46 +++++++++++++++++++++++- packages/cli/src/commands/branch.ts | 44 ++++++++++++++++++++++- packages/cli/src/index.ts | 23 +++++++----- 6 files changed, 109 insertions(+), 12 deletions(-) delete mode 100644 .beads/dolt-monitor.pid.lock diff --git a/.beads/dolt-monitor.pid.lock b/.beads/dolt-monitor.pid.lock deleted file mode 100644 index e69de29..0000000 diff --git a/README.md b/README.md index 02f9eca..2211363 100644 --- a/README.md +++ b/README.md @@ -280,12 +280,15 @@ dub bottom ### `dub info` and `dub branch info` -Show tracked metadata for a branch. +Show tracked metadata for a branch, optionally including the parent-relative diff. ```bash # current branch dub info +# current branch with parent-relative diff +dub info --diff + # explicit branch dub info feat/auth-login diff --git a/apps/docs/content/docs/commands/log.mdx b/apps/docs/content/docs/commands/log.mdx index d9d2d22..248124b 100644 --- a/apps/docs/content/docs/commands/log.mdx +++ b/apps/docs/content/docs/commands/log.mdx @@ -49,6 +49,9 @@ For detailed metadata about a specific branch: # Current branch dub info +# Current branch with parent-relative diff +dub info --diff + # Explicit branch dub info feat/auth-login ``` diff --git a/packages/cli/src/commands/branch.test.ts b/packages/cli/src/commands/branch.test.ts index ef3c78e..51139fa 100644 --- a/packages/cli/src/commands/branch.test.ts +++ b/packages/cli/src/commands/branch.test.ts @@ -1,6 +1,7 @@ +import * as fs from 'node:fs'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { createTestRepo, gitInRepo } from '../../test/helpers'; -import { branchInfo, formatBranchInfo } from './branch'; +import { branchInfo, branchInfoOutput, formatBranchInfo } from './branch'; import { create } from './create'; import { init } from './init'; @@ -100,4 +101,47 @@ describe('branch info', () => { expect(untracked).toContain('Tracked: no'); expect(untracked).toContain('not tracked by DubStack'); }); + + it('includes a parent-relative diff when requested', async () => { + await create('feat/a', dir); + await create('feat/b', dir); + await gitInRepo(dir, ['checkout', 'feat/b']); + + await fs.promises.writeFile(`${dir}/feature.txt`, 'hello from feat/b\n'); + await gitInRepo(dir, ['add', 'feature.txt']); + await gitInRepo(dir, ['commit', '-m', 'feat: add branch diff fixture']); + + const output = await branchInfoOutput(dir, undefined, { diff: true }); + expect(output).toContain('Branch: feat/b'); + expect(output).toContain('Diff vs feat/a:'); + expect(output).toContain('diff --git a/feature.txt b/feature.txt'); + }); + + it('shows an explicit marker when there are no changes relative to the parent', async () => { + await create('feat/a', dir); + await create('feat/b', dir); + + const output = await branchInfoOutput(dir, undefined, { diff: true }); + expect(output).toContain('Diff vs feat/a:'); + expect(output).toContain('(no changes)'); + }); + + it('explains that root branches do not have a parent-relative diff', async () => { + await create('feat/a', dir); + await gitInRepo(dir, ['checkout', 'main']); + + const output = await branchInfoOutput(dir, 'main', { diff: true }); + expect(output).toContain('Branch: main'); + expect(output).toContain('Diff: unavailable for stack root branches.'); + }); + + it('explains that untracked branches cannot show a dubstack diff', async () => { + await gitInRepo(dir, ['checkout', '-b', 'rogue']); + + const output = await branchInfoOutput(dir, undefined, { diff: true }); + expect(output).toContain('Branch: rogue'); + expect(output).toContain( + 'Diff: unavailable because this branch is not tracked by DubStack.', + ); + }); }); diff --git a/packages/cli/src/commands/branch.ts b/packages/cli/src/commands/branch.ts index 1654bf5..b40db4a 100644 --- a/packages/cli/src/commands/branch.ts +++ b/packages/cli/src/commands/branch.ts @@ -1,4 +1,4 @@ -import { getCurrentBranch } from '../lib/git'; +import { getCurrentBranch, getDiffBetween } from '../lib/git'; import { findStackForBranch, readState, type Stack } from '../lib/state'; export interface BranchInfoResult { @@ -80,3 +80,45 @@ export function formatBranchInfo(info: BranchInfoResult): string { `Children: ${childrenLabel}`, ].join('\n'); } + +export interface BranchInfoOutputOptions { + diff?: boolean; +} + +/** + * Formats branch info and optionally appends a parent-relative git diff. + */ +export async function branchInfoOutput( + cwd: string, + branchName?: string, + options: BranchInfoOutputOptions = {}, +): Promise { + const info = await branchInfo(cwd, branchName); + const summary = formatBranchInfo(info); + + if (!options.diff) { + return summary; + } + + if (!info.tracked) { + return [ + summary, + '', + 'Diff: unavailable because this branch is not tracked by DubStack.', + ].join('\n'); + } + + if (!info.parent) { + return [summary, '', 'Diff: unavailable for stack root branches.'].join( + '\n', + ); + } + + const diff = await getDiffBetween(info.parent, info.currentBranch, cwd); + return [ + summary, + '', + `Diff vs ${info.parent}:`, + diff.trim().length > 0 ? diff.trimEnd() : '(no changes)', + ].join('\n'); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 7a5bb48..edd63f4 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -22,7 +22,7 @@ import { createRequire } from 'node:module'; import chalk from 'chalk'; import { Command } from 'commander'; import { abortCommand } from './commands/abort'; -import { branchInfo, formatBranchInfo } from './commands/branch'; +import { branchInfoOutput } from './commands/branch'; import { checkout, interactiveCheckout, @@ -76,6 +76,15 @@ const { version } = require('../package.json') as { version: string }; const program = new Command(); +async function showInfo( + branch: string | undefined, + options: { diff?: boolean }, +): Promise { + console.log( + await branchInfoOutput(process.cwd(), branch, { diff: options.diff }), + ); +} + program .name('dub') .description('Manage stacked diffs (dependent git branches) with ease') @@ -329,20 +338,16 @@ program new Command('info') .description('Show tracked stack info for the current branch') .argument('[branch]', 'Branch to inspect (defaults to current branch)') - .action(async (branch?: string) => { - const info = await branchInfo(process.cwd(), branch); - console.log(formatBranchInfo(info)); - }), + .option('-d, --diff', 'Show the parent-relative git diff for the branch') + .action(showInfo), ); program .command('info') .argument('[branch]', 'Branch to inspect (defaults to current branch)') + .option('-d, --diff', 'Show the parent-relative git diff for the branch') .description('Show tracked stack info for a branch') - .action(async (branch?: string) => { - const info = await branchInfo(process.cwd(), branch); - console.log(formatBranchInfo(info)); - }); + .action(showInfo); program .command('track')