Compare commits
2 Commits
891b08ecd8
...
79a0471383
| Author | SHA1 | Date | |
|---|---|---|---|
| 79a0471383 | |||
| 115d3199e8 |
|
|
@ -13,4 +13,4 @@ export {
|
||||||
stopApp,
|
stopApp,
|
||||||
} from './manage'
|
} from './manage'
|
||||||
export { statsApp } from './stats'
|
export { statsApp } from './stats'
|
||||||
export { cleanApp, diffApp, getApp, pullApp, pushApp, rollbackApp, stashApp, stashListApp, stashPopApp, statusApp, syncApp, versionsApp } from './sync'
|
export { cleanApp, diffApp, getApp, historyApp, pullApp, pushApp, rollbackApp, stashApp, stashListApp, stashPopApp, statusApp, syncApp, versionsApp } from './sync'
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,55 @@ interface ManifestDiff {
|
||||||
remoteManifest: Manifest | null
|
remoteManifest: Manifest | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function historyApp(name?: string) {
|
||||||
|
const appName = resolveAppName(name)
|
||||||
|
if (!appName) return
|
||||||
|
|
||||||
|
type HistoryEntry = {
|
||||||
|
version: string
|
||||||
|
current: boolean
|
||||||
|
added: string[]
|
||||||
|
modified: string[]
|
||||||
|
deleted: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type HistoryResponse = { history: HistoryEntry[] }
|
||||||
|
|
||||||
|
const result = await get<HistoryResponse>(`/api/sync/apps/${appName}/history`)
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
if (result.history.length === 0) {
|
||||||
|
console.log(`No versions found for ${color.bold(appName)}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`History for ${color.bold(appName)}:\n`)
|
||||||
|
|
||||||
|
for (const entry of result.history) {
|
||||||
|
const date = formatVersion(entry.version)
|
||||||
|
const label = entry.current ? ` ${color.green('→')} ${color.bold(entry.version)}` : ` ${entry.version}`
|
||||||
|
const suffix = entry.current ? ` ${color.green('(current)')}` : ''
|
||||||
|
console.log(`${label} ${color.gray(date)}${suffix}`)
|
||||||
|
|
||||||
|
const hasChanges = entry.added.length > 0 || entry.modified.length > 0 || entry.deleted.length > 0
|
||||||
|
if (!hasChanges) {
|
||||||
|
console.log(color.gray(' No changes'))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of entry.added) {
|
||||||
|
console.log(` ${color.green('+')} ${file}`)
|
||||||
|
}
|
||||||
|
for (const file of entry.modified) {
|
||||||
|
console.log(` ${color.yellow('~')} ${file}`)
|
||||||
|
}
|
||||||
|
for (const file of entry.deleted) {
|
||||||
|
console.log(` ${color.red('-')} ${file}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getApp(name: string) {
|
export async function getApp(name: string) {
|
||||||
console.log(`Fetching ${color.bold(name)} from server...`)
|
console.log(`Fetching ${color.bold(name)} from server...`)
|
||||||
|
|
||||||
|
|
@ -886,12 +935,28 @@ function showDiff(remote: string, local: string) {
|
||||||
let lineCount = 0
|
let lineCount = 0
|
||||||
const maxLines = 50
|
const maxLines = 50
|
||||||
const contextLines = 3
|
const contextLines = 3
|
||||||
|
let remoteLine = 1
|
||||||
|
let localLine = 1
|
||||||
|
let needsHeader = true
|
||||||
|
|
||||||
|
let hunkCount = 0
|
||||||
|
|
||||||
|
const printHeader = (_rStart: number, lStart: number) => {
|
||||||
|
if (hunkCount > 0) console.log()
|
||||||
|
if (lStart > 1) {
|
||||||
|
console.log(color.cyan(`Line ${lStart}:`))
|
||||||
|
lineCount++
|
||||||
|
}
|
||||||
|
needsHeader = false
|
||||||
|
hunkCount++
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < changes.length; i++) {
|
for (let i = 0; i < changes.length; i++) {
|
||||||
const part = changes[i]!
|
const part = changes[i]!
|
||||||
const lines = part.value.replace(/\n$/, '').split('\n')
|
const lines = part.value.replace(/\n$/, '').split('\n')
|
||||||
|
|
||||||
if (part.added) {
|
if (part.added) {
|
||||||
|
if (needsHeader) printHeader(remoteLine, localLine)
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (lineCount >= maxLines) {
|
if (lineCount >= maxLines) {
|
||||||
console.log(color.gray('... diff truncated'))
|
console.log(color.gray('... diff truncated'))
|
||||||
|
|
@ -900,7 +965,9 @@ function showDiff(remote: string, local: string) {
|
||||||
console.log(color.green(`+ ${line}`))
|
console.log(color.green(`+ ${line}`))
|
||||||
lineCount++
|
lineCount++
|
||||||
}
|
}
|
||||||
|
localLine += lines.length
|
||||||
} else if (part.removed) {
|
} else if (part.removed) {
|
||||||
|
if (needsHeader) printHeader(remoteLine, localLine)
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (lineCount >= maxLines) {
|
if (lineCount >= maxLines) {
|
||||||
console.log(color.gray('... diff truncated'))
|
console.log(color.gray('... diff truncated'))
|
||||||
|
|
@ -909,6 +976,7 @@ function showDiff(remote: string, local: string) {
|
||||||
console.log(color.red(`- ${line}`))
|
console.log(color.red(`- ${line}`))
|
||||||
lineCount++
|
lineCount++
|
||||||
}
|
}
|
||||||
|
remoteLine += lines.length
|
||||||
} else {
|
} else {
|
||||||
// Context: show lines near changes
|
// Context: show lines near changes
|
||||||
const prevHasChange = i > 0 && (changes[i - 1]!.added || changes[i - 1]!.removed)
|
const prevHasChange = i > 0 && (changes[i - 1]!.added || changes[i - 1]!.removed)
|
||||||
|
|
@ -929,9 +997,11 @@ function showDiff(remote: string, local: string) {
|
||||||
if (nextHasChange) {
|
if (nextHasChange) {
|
||||||
const start = Math.max(0, lines.length - contextLines)
|
const start = Math.max(0, lines.length - contextLines)
|
||||||
if (start > 0) {
|
if (start > 0) {
|
||||||
console.log(color.gray(' ...'))
|
needsHeader = true
|
||||||
lineCount++
|
|
||||||
}
|
}
|
||||||
|
const headerLine = remoteLine + start
|
||||||
|
const headerLocalLine = localLine + start
|
||||||
|
if (needsHeader) printHeader(headerLine, headerLocalLine)
|
||||||
for (let j = start; j < lines.length; j++) {
|
for (let j = start; j < lines.length; j++) {
|
||||||
if (lineCount >= maxLines) {
|
if (lineCount >= maxLines) {
|
||||||
console.log(color.gray('... diff truncated'))
|
console.log(color.gray('... diff truncated'))
|
||||||
|
|
@ -942,7 +1012,7 @@ function showDiff(remote: string, local: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Show context after previous change
|
// Show context after previous change
|
||||||
if (prevHasChange) {
|
if (prevHasChange && !nextHasChange) {
|
||||||
const end = Math.min(lines.length, contextLines)
|
const end = Math.min(lines.length, contextLines)
|
||||||
for (let j = 0; j < end; j++) {
|
for (let j = 0; j < end; j++) {
|
||||||
if (lineCount >= maxLines) {
|
if (lineCount >= maxLines) {
|
||||||
|
|
@ -952,11 +1022,13 @@ function showDiff(remote: string, local: string) {
|
||||||
console.log(color.gray(` ${lines[j]}`))
|
console.log(color.gray(` ${lines[j]}`))
|
||||||
lineCount++
|
lineCount++
|
||||||
}
|
}
|
||||||
if (end < lines.length && !nextHasChange) {
|
if (end < lines.length) {
|
||||||
console.log(color.gray(' ...'))
|
needsHeader = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
remoteLine += lines.length
|
||||||
|
localLine += lines.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
envRm,
|
envRm,
|
||||||
envSet,
|
envSet,
|
||||||
getApp,
|
getApp,
|
||||||
|
historyApp,
|
||||||
infoApp,
|
infoApp,
|
||||||
listApps,
|
listApps,
|
||||||
logApp,
|
logApp,
|
||||||
|
|
@ -258,6 +259,13 @@ program
|
||||||
.argument('[name]', 'app name (uses current directory if omitted)')
|
.argument('[name]', 'app name (uses current directory if omitted)')
|
||||||
.action(versionsApp)
|
.action(versionsApp)
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('history')
|
||||||
|
.helpGroup('Config:')
|
||||||
|
.description('Show file changes between versions')
|
||||||
|
.argument('[name]', 'app name (uses current directory if omitted)')
|
||||||
|
.action(historyApp)
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('rollback')
|
.command('rollback')
|
||||||
.helpGroup('Config:')
|
.helpGroup('Config:')
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,68 @@ router.get('/apps/:app/versions', c => {
|
||||||
return c.json({ versions, current: currentVersion })
|
return c.json({ versions, current: currentVersion })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.get('/apps/:app/history', c => {
|
||||||
|
const appName = c.req.param('app')
|
||||||
|
if (!appName) return c.json({ error: 'App not found' }, 404)
|
||||||
|
|
||||||
|
const appDir = safePath(APPS_DIR, appName)
|
||||||
|
if (!appDir) return c.json({ error: 'Invalid path' }, 400)
|
||||||
|
if (!existsSync(appDir)) return c.json({ error: 'App not found' }, 404)
|
||||||
|
|
||||||
|
const currentLink = join(appDir, 'current')
|
||||||
|
const currentVersion = existsSync(currentLink)
|
||||||
|
? realpathSync(currentLink).split('/').pop()
|
||||||
|
: null
|
||||||
|
|
||||||
|
const entries = readdirSync(appDir, { withFileTypes: true })
|
||||||
|
const versions = entries
|
||||||
|
.filter(e => e.isDirectory() && /^\d{8}-\d{6}$/.test(e.name))
|
||||||
|
.map(e => e.name)
|
||||||
|
.sort()
|
||||||
|
.reverse() // Newest first
|
||||||
|
|
||||||
|
// Generate manifests for each version
|
||||||
|
const manifests = new Map<string, Record<string, { hash: string }>>()
|
||||||
|
for (const version of versions) {
|
||||||
|
const versionPath = join(appDir, version)
|
||||||
|
const manifest = generateManifest(versionPath, appName)
|
||||||
|
manifests.set(version, manifest.files)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diff consecutive pairs
|
||||||
|
const history = versions.map((version, i) => {
|
||||||
|
const current = version === currentVersion
|
||||||
|
const files = manifests.get(version)!
|
||||||
|
const olderVersion = versions[i + 1]
|
||||||
|
const olderFiles = olderVersion ? manifests.get(olderVersion)! : {}
|
||||||
|
|
||||||
|
const added: string[] = []
|
||||||
|
const modified: string[] = []
|
||||||
|
const deleted: string[] = []
|
||||||
|
|
||||||
|
// Files in this version
|
||||||
|
for (const [path, info] of Object.entries(files)) {
|
||||||
|
const older = olderFiles[path]
|
||||||
|
if (!older) {
|
||||||
|
added.push(path)
|
||||||
|
} else if (older.hash !== info.hash) {
|
||||||
|
modified.push(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files removed in this version
|
||||||
|
for (const path of Object.keys(olderFiles)) {
|
||||||
|
if (!files[path]) {
|
||||||
|
deleted.push(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { version, current, added: added.sort(), modified: modified.sort(), deleted: deleted.sort() }
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.json({ history })
|
||||||
|
})
|
||||||
|
|
||||||
router.get('/apps/:app/manifest', c => {
|
router.get('/apps/:app/manifest', c => {
|
||||||
const appName = c.req.param('app')
|
const appName = c.req.param('app')
|
||||||
if (!appName) return c.json({ error: 'App not found' }, 404)
|
if (!appName) return c.json({ error: 'App not found' }, 404)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user