From c98dba38a14ea328936c45c097b24ee0cc0164bf Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:54:31 +0200 Subject: [PATCH 01/14] open in main --- src/dcc-cli.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/dcc-cli.ts b/src/dcc-cli.ts index 905dd57..456a242 100644 --- a/src/dcc-cli.ts +++ b/src/dcc-cli.ts @@ -288,6 +288,15 @@ async function status() { } async function openPr() { + const [branch, mainBranch] = await Promise.all([gitOps.getBranch(), gitOps.mainBranch()]) + if (branch.name === mainBranch) { + const repo = await gitOps.getRepo() + const url = `https://github.com/${repo.owner}/${repo.name}/commits/${mainBranch}` + print(`🌐 Opening ${url}`) + await open(url) + return + } + const pr = await graphqlOps.getCurrentPr() if (!pr) { print('🚫 No PR found for this branch') From de681cf2d41acab21dfc9a29fdb13d8660b3f4b6 Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:01:31 +0200 Subject: [PATCH 02/14] stayonthebranchwhenmerging --- src/dcc-cli.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/dcc-cli.ts b/src/dcc-cli.ts index 456a242..2fd6a8f 100644 --- a/src/dcc-cli.ts +++ b/src/dcc-cli.ts @@ -191,10 +191,7 @@ async function submit() { await githubOps.merge(pr.number) print('merged') - const mainBranch = await catchUp('SILENT') - // Order is important here: merge will work only if we have switched to the main branch. - await gitOps.switchToMainBranch() - await gitOps.merge('origin', mainBranch) + await catchUp('CHATTY') } async function listClosed(args: { user?: string }) { From 025487050b68ccce36ca1c9579946035f2b2eaa1 Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:08:40 +0200 Subject: [PATCH 03/14] ci --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4fa06e2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: ['**'] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: yarn + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Build + run: yarn build From c65ad2c69ebef0fa90c206938c5ca5b69de5b4c6 Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:09:29 +0200 Subject: [PATCH 04/14] lint --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fa06e2..2ab4370 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,5 +23,8 @@ jobs: - name: Install dependencies run: yarn install --frozen-lockfile + - name: Lint + run: yarn lint + - name: Build run: yarn build From 65e892ee98a8960b318ecd1e670cb2a966cdc4ff Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:15:56 +0200 Subject: [PATCH 05/14] dcc pending -> dcc files --- src/dcc-cli.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dcc-cli.ts b/src/dcc-cli.ts index 2fd6a8f..a985f54 100644 --- a/src/dcc-cli.ts +++ b/src/dcc-cli.ts @@ -368,7 +368,7 @@ yargs(hideBin(process.argv)) type: 'string', }) .command( - [STATUS_COMMAND, '*'], + [STATUS_COMMAND, 's', '*'], 'Show the status of the current PR', a => a, async argv => { @@ -381,7 +381,7 @@ yargs(hideBin(process.argv)) }, ) .command( - 'push [title..]', + ['push [title..]', 'p [title..]'], 'Push your changes to GitHub (creates a PR, if a title is specified)', yargs => yargs.positional('title', { @@ -412,7 +412,7 @@ yargs(hideBin(process.argv)) }), launch(listClosed), ) - .command(['pending', 'p'], `Lists all changes files (compared to branch's baseline commit)`, a => a, launch(pending)) + .command(['files', 'f'], `Lists all changed files (compared to branch's baseline commit)`, a => a, launch(pending)) .command(['diff', 'd'], `Diffs against the branch's baseline commit`, a => a, launch(diff)) .command( ['difftool', 'dt'], From b12460c09d4e3c85fb748e5ea221f7a79017d07b Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:17:23 +0200 Subject: [PATCH 06/14] typos --- src/dcc-cli.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dcc-cli.ts b/src/dcc-cli.ts index a985f54..1b47103 100644 --- a/src/dcc-cli.ts +++ b/src/dcc-cli.ts @@ -364,7 +364,7 @@ yargs(hideBin(process.argv)) .version(currentVersion) .option('dir', { alias: 'd', - describe: 'directroy to run at', + describe: 'directory to run at', type: 'string', }) .command( @@ -407,12 +407,12 @@ yargs(hideBin(process.argv)) yargs => yargs.option('user', { alias: 'u', - describe: 'Shows only PR from that GitHub user ID. If omiited shows from all users.', + describe: 'Shows only PRs from that GitHub user ID. If omitted, shows from all users.', type: 'string', }), launch(listClosed), ) - .command(['files', 'f'], `Lists all changed files (compared to branch's baseline commit)`, a => a, launch(pending)) + .command(['files', 'f'], `List all changed files (compared to branch's baseline commit)`, a => a, launch(pending)) .command(['diff', 'd'], `Diffs against the branch's baseline commit`, a => a, launch(diff)) .command( ['difftool', 'dt'], From 49a287c98900a7e64dd6e08ea366a004810d19bf Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:19:37 +0200 Subject: [PATCH 07/14] drop list-ongoing, list-closed --- src/dcc-cli.ts | 43 --------------- src/github-ops.ts | 134 ---------------------------------------------- 2 files changed, 177 deletions(-) diff --git a/src/dcc-cli.ts b/src/dcc-cli.ts index 1b47103..8013cf0 100644 --- a/src/dcc-cli.ts +++ b/src/dcc-cli.ts @@ -38,14 +38,6 @@ function print(...args: string[]) { logger.info(args.join(' ')) } -function format(s: string, n: number) { - if (s.length > n) { - return s.substr(0, n) - } - - return s.padEnd(n) -} - function launch(f: (a: T) => Promise) { return (args: T & { dir?: string }) => { if (args.dir) { @@ -99,17 +91,6 @@ async function catchUp(mode: 'SILENT' | 'CHATTY') { return mainBranch } -async function listOngoing() { - const d = await githubOps.listPrs() - for (const curr of d) { - print( - `${curr.updatedAt} ${('#' + curr.number).padStart(6)} ${format(curr.user, 10)} ${format(curr.title, 60)} ${ - curr.url - }`, - ) - } -} - async function createNew(a: { branch: string }) { await gitOps.noUncommittedChanges() const [currBranch, mainBranch] = await Promise.all([gitOps.getBranch(), gitOps.mainBranch()]) @@ -194,18 +175,6 @@ async function submit() { await catchUp('CHATTY') } -async function listClosed(args: { user?: string }) { - const d = await githubOps.listMerged(args.user) - - for (const curr of d) { - print( - `${curr.mergedAt} ${('#' + curr.number).padStart(6)} ${format(curr.user, 10)} ${format(curr.title, 60)} ${ - curr.url - }`, - ) - } -} - async function push(args: { title?: string; submit?: boolean }) { logger.silly(`args=`, args) await gitOps.notOnMainBranch() @@ -400,18 +369,6 @@ yargs(hideBin(process.argv)) await catchUp('CHATTY') }), ) - .command('list-ongoing', 'List currently open PRs', a => a, launch(listOngoing)) - .command( - 'list-closed', - 'List recently merged PRs', - yargs => - yargs.option('user', { - alias: 'u', - describe: 'Shows only PRs from that GitHub user ID. If omitted, shows from all users.', - type: 'string', - }), - launch(listClosed), - ) .command(['files', 'f'], `List all changed files (compared to branch's baseline commit)`, a => a, launch(pending)) .command(['diff', 'd'], `Diffs against the branch's baseline commit`, a => a, launch(diff)) .command( diff --git a/src/github-ops.ts b/src/github-ops.ts index 6457874..06ff3bf 100644 --- a/src/github-ops.ts +++ b/src/github-ops.ts @@ -2,49 +2,6 @@ import { GitOps } from './git-ops.js' import { Octokit } from '@octokit/rest' import { logger } from './logger.js' -interface PrInfo { - url: string - updatedAt: string - number: number - user: string - title: string -} - -interface CheckStatusInfo { - context: string - required: boolean - state: string - createdAt: string - updatedAt: string -} - -interface CheckCommitInfo { - ordinal: number - data: { hash: string; message: string } -} - -interface CheckInfo { - statuses: CheckStatusInfo[] - state: string - sha: string - commit: CheckCommitInfo -} - -interface MergedPrInfo { - mergedAt: string | null - title: string - number: number - url: string - user: string -} - -function reify(t: T | null | undefined): T { - if (t === null || t === undefined) { - throw new Error(`got a falsy value`) - } - return t -} - export type Check = | { tag: 'FAILING' @@ -87,31 +44,6 @@ export class GithubOps { return [...pending, ...passing, ...failing] } - async getUser(): Promise { - const d = await this.kit.users.getAuthenticated() - return d.data.login - } - - async listPrs(): Promise { - const [repo, user] = await Promise.all([this.gitOps.getRepo(), this.getUser()]) - - const respB = await this.kit.search.issuesAndPullRequests({ - q: `type:pr author:${user} state:open repo:${repo.owner}/${repo.name} sort:updated-desc`, - }) - const prs = respB.data.items.map(curr => ({ - user: reify(curr.user?.login), - title: curr.title, - url: `https://github.com/${repo.owner}/${repo.name}/pull/${curr.number}`, - body: curr.body, - updatedAt: curr.updated_at, - createdAt: curr.created_at, - number: curr.number, - state: curr.state, - })) - - return prs.filter(curr => curr.user === user) - } - async merge(prNumber: number): Promise { const r = await this.gitOps.getRepo() await this.kit.pulls.merge({ owner: r.owner, repo: r.name, pull_number: prNumber, merge_method: 'squash' }) @@ -127,72 +59,6 @@ export class GithubOps { }) } - async listChecks(): Promise { - const r = await this.gitOps.getRepo() - const b = await this.gitOps.getBranch() - const statusPromise = this.kit.repos.getCombinedStatusForRef({ - owner: r.owner, - repo: r.name, - ref: b.name, - }) - - const branchPromise = await this.kit.repos.getBranch({ - owner: r.owner, - repo: r.name, - branch: await this.gitOps.mainBranch(), - }) - - const [status, branch] = await Promise.all([statusPromise, branchPromise]) - const required = new Set(reify(branch.data.protection.required_status_checks?.contexts)) - const statuses = status.data.statuses.map(s => ({ - context: s.context, - required: required.has(s.context), - state: s.state, - createdAt: s.created_at, - updatedAt: s.updated_at, - })) - - const d = await this.gitOps.describeCommit(status.data.sha) - if (!d) { - throw new Error(`Could not find sha ${status.data.sha} in git log`) - } - - return { statuses, state: status.data.state, sha: status.data.sha, commit: d } - } - - async listMerged(user?: string): Promise { - const r = await this.gitOps.getRepo() - - const pageSize = user ? 100 : 40 - const resp = await this.kit.pulls.list({ - owner: r.owner, - repo: r.name, - state: 'closed', - sort: 'updated', - direction: 'desc', - per_page: pageSize, - }) - let prs = resp.data.map(curr => ({ - user: reify(curr.user?.login), - title: curr.title, - url: `https://github.com/${r.owner}/${r.name}/pull/${curr.number}`, - body: curr.body, - branch: curr.head.ref, - updatedAt: curr.updated_at, - createdAt: curr.created_at, - mergedAt: curr.merged_at, - number: curr.number, - state: curr.state, - })) - - prs = prs.filter(curr => Boolean(curr.mergedAt)) - - if (user) { - prs = prs.filter(curr => curr.user === user) - } - return prs - } - async updatePrTitle(prNumber: number, newTitle: string): Promise { const b = await this.gitOps.getRepo() await this.kit.pulls.update({ owner: b.owner, repo: b.name, pull_number: prNumber, title: newTitle }) From ef7bce41609d9dc0759031628253b93efe269128 Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:27:24 +0200 Subject: [PATCH 08/14] restore --- src/dcc-cli.ts | 18 ++++++++++++++++++ src/git-ops.ts | 11 +++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/dcc-cli.ts b/src/dcc-cli.ts index 8013cf0..2f1b7ab 100644 --- a/src/dcc-cli.ts +++ b/src/dcc-cli.ts @@ -113,6 +113,12 @@ async function pending() { } } +async function restore(a: { files: string[] }) { + const mainBranch = await gitOps.mainBranch() + const baselineCommit = await gitOps.findBaselineCommit(`origin/${mainBranch}`) + await gitOps.restoreFiles(baselineCommit, a.files) +} + function prIsUpToDate(pr: CurrentPrInfo) { if (!pr.lastCommit) { throw new Error(`Failed to retreive information about the PR's latest commit`) @@ -388,6 +394,18 @@ yargs(hideBin(process.argv)) }), launch(createNew), ) + .command( + ['restore ', 'r '], + `Restore files to their state at the branch's baseline commit`, + yargs => + yargs.positional('files', { + type: 'string', + array: true, + describe: 'Files to restore', + demandOption: true, + }), + launch((a: { files: string[] }) => restore(a)), + ) .command(['open', 'o'], 'Open the current PR files page in your browser', a => a, launch(openPr)) .command( ['checkout ', 'co '], diff --git a/src/git-ops.ts b/src/git-ops.ts index 7afab3a..e44a950 100644 --- a/src/git-ops.ts +++ b/src/git-ops.ts @@ -192,4 +192,15 @@ export class GitOps { async diff(commit: string, useDifftool: boolean): Promise { await execa('git', [useDifftool ? 'difftool' : 'diff', commit], { stdout: 'inherit' }) } + + async restoreFiles(commit: string, files: string[]): Promise { + for (const file of files) { + const result = await execa('git', ['cat-file', '-e', `${commit}:${file}`], { reject: false }) + if (result.exitCode === 0) { + await execa('git', ['checkout', commit, '--', file]) + } else { + await this.git.rm([file]) + } + } + } } From 5033d699e88dfa8bb2bd2232a310cb8d8cb29f49 Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:29:23 +0200 Subject: [PATCH 09/14] streamline --- src/git-ops.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/git-ops.ts b/src/git-ops.ts index e44a950..0563335 100644 --- a/src/git-ops.ts +++ b/src/git-ops.ts @@ -195,9 +195,15 @@ export class GitOps { async restoreFiles(commit: string, files: string[]): Promise { for (const file of files) { - const result = await execa('git', ['cat-file', '-e', `${commit}:${file}`], { reject: false }) - if (result.exitCode === 0) { - await execa('git', ['checkout', commit, '--', file]) + let existsInBaseline = true + try { + await this.git.raw(['cat-file', '-e', `${commit}:${file}`]) + } catch { + existsInBaseline = false + } + + if (existsInBaseline) { + await this.git.checkout([commit, '--', file]) } else { await this.git.rm([file]) } From 9e010e138e0279619b51556ae8dbe5a02cafa12f Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:31:11 +0200 Subject: [PATCH 10/14] TBD --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2af5856..7073b40 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dev-control-center", "version": "0.2.9", - "description": "", + "description": "TBD", "type": "module", "main": "lib/dcc-cli.js", "bin": { From e4cfea7972497d0131702306cb3c4bc99985d908 Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:38:02 +0200 Subject: [PATCH 11/14] unchange --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7073b40..2af5856 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dev-control-center", "version": "0.2.9", - "description": "TBD", + "description": "", "type": "module", "main": "lib/dcc-cli.js", "bin": { From 00fff6ee5263d082fa50520c2476d8228cc359f6 Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:38:12 +0200 Subject: [PATCH 12/14] restore commits --- src/dcc-cli.ts | 6 ++++++ src/git-ops.ts | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/dcc-cli.ts b/src/dcc-cli.ts index 2f1b7ab..e789604 100644 --- a/src/dcc-cli.ts +++ b/src/dcc-cli.ts @@ -114,9 +114,15 @@ async function pending() { } async function restore(a: { files: string[] }) { + await gitOps.noUncommittedChanges() const mainBranch = await gitOps.mainBranch() const baselineCommit = await gitOps.findBaselineCommit(`origin/${mainBranch}`) await gitOps.restoreFiles(baselineCommit, a.files) + const shortSha = await gitOps.shortSha(baselineCommit) + const fileList = a.files.join(' ') + const longMessage = `restore from ${shortSha}: ${fileList}` + const message = longMessage.length <= 100 ? longMessage : `restore ${a.files.length} files from ${shortSha}` + await gitOps.commitAll(message) } function prIsUpToDate(pr: CurrentPrInfo) { diff --git a/src/git-ops.ts b/src/git-ops.ts index 0563335..29d9f39 100644 --- a/src/git-ops.ts +++ b/src/git-ops.ts @@ -209,4 +209,13 @@ export class GitOps { } } } + + async shortSha(commit: string): Promise { + const out = await this.git.raw(['rev-parse', '--short', commit]) + return out.trim() + } + + async commitAll(message: string): Promise { + await this.git.raw(['commit', '-m', message]) + } } From 7ae652b5e1b5a1dbad89b764f57560a0bfa29570 Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:38:30 +0200 Subject: [PATCH 13/14] tbd --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2af5856..7073b40 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dev-control-center", "version": "0.2.9", - "description": "", + "description": "TBD", "type": "module", "main": "lib/dcc-cli.js", "bin": { From b048045827c8350a3a2adc81afa679888173b928 Mon Sep 17 00:00:00 2001 From: Itay Maman <94941+imaman@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:38:44 +0200 Subject: [PATCH 14/14] restore from 1dded3c: package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7073b40..2af5856 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dev-control-center", "version": "0.2.9", - "description": "TBD", + "description": "", "type": "module", "main": "lib/dcc-cli.js", "bin": {