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
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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: Lint
run: yarn lint

- name: Build
run: yarn build
89 changes: 38 additions & 51 deletions src/dcc-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(f: (a: T) => Promise<void>) {
return (args: T & { dir?: string }) => {
if (args.dir) {
Expand Down Expand Up @@ -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()])
Expand All @@ -132,6 +113,18 @@ 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) {
if (!pr.lastCommit) {
throw new Error(`Failed to retreive information about the PR's latest commit`)
Expand Down Expand Up @@ -191,22 +184,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)
}

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
}`,
)
}
await catchUp('CHATTY')
}

async function push(args: { title?: string; submit?: boolean }) {
Expand Down Expand Up @@ -288,6 +266,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')
Expand Down Expand Up @@ -358,11 +345,11 @@ yargs(hideBin(process.argv))
.version(currentVersion)
.option('dir', {
alias: 'd',
describe: 'directroy to run at',
describe: 'directory to run at',
type: 'string',
})
.command(
[STATUS_COMMAND, '*'],
[STATUS_COMMAND, 's', '*'],
'Show the status of the current PR',
a => a,
async argv => {
Expand All @@ -375,7 +362,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', {
Expand All @@ -394,19 +381,7 @@ 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 PR from that GitHub user ID. If omiited shows from all users.',
type: 'string',
}),
launch(listClosed),
)
.command(['pending', 'p'], `Lists all changes 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'],
Expand All @@ -425,6 +400,18 @@ yargs(hideBin(process.argv))
}),
launch(createNew),
)
.command(
['restore <files..>', 'r <files..>'],
`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 <pr-number>', 'co <pr-number>'],
Expand Down
26 changes: 26 additions & 0 deletions src/git-ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,30 @@ export class GitOps {
async diff(commit: string, useDifftool: boolean): Promise<void> {
await execa('git', [useDifftool ? 'difftool' : 'diff', commit], { stdout: 'inherit' })
}

async restoreFiles(commit: string, files: string[]): Promise<void> {
for (const file of files) {
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])
}
}
}

async shortSha(commit: string): Promise<string> {
const out = await this.git.raw(['rev-parse', '--short', commit])
return out.trim()
}

async commitAll(message: string): Promise<void> {
await this.git.raw(['commit', '-m', message])
}
}
134 changes: 0 additions & 134 deletions src/github-ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: T | null | undefined): T {
if (t === null || t === undefined) {
throw new Error(`got a falsy value`)
}
return t
}

export type Check =
| {
tag: 'FAILING'
Expand Down Expand Up @@ -87,31 +44,6 @@ export class GithubOps {
return [...pending, ...passing, ...failing]
}

async getUser(): Promise<string> {
const d = await this.kit.users.getAuthenticated()
return d.data.login
}

async listPrs(): Promise<PrInfo[]> {
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<void> {
const r = await this.gitOps.getRepo()
await this.kit.pulls.merge({ owner: r.owner, repo: r.name, pull_number: prNumber, merge_method: 'squash' })
Expand All @@ -127,72 +59,6 @@ export class GithubOps {
})
}

async listChecks(): Promise<CheckInfo> {
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<string>(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<MergedPrInfo[]> {
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<void> {
const b = await this.gitOps.getRepo()
await this.kit.pulls.update({ owner: b.owner, repo: b.name, pull_number: prNumber, title: newTitle })
Expand Down