detect renames
This commit is contained in:
parent
4a2223d3d7
commit
bffa4236e7
|
|
@ -16,10 +16,16 @@ function notAppError(): string {
|
|||
return 'Not a toes app'
|
||||
}
|
||||
|
||||
interface Rename {
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
|
||||
interface ManifestDiff {
|
||||
changed: string[]
|
||||
localOnly: string[]
|
||||
remoteOnly: string[]
|
||||
renamed: Rename[]
|
||||
localManifest: Manifest
|
||||
remoteManifest: Manifest | null
|
||||
}
|
||||
|
|
@ -34,6 +40,7 @@ export async function historyApp(name?: string) {
|
|||
added: string[]
|
||||
modified: string[]
|
||||
deleted: string[]
|
||||
renamed: string[]
|
||||
}
|
||||
|
||||
type HistoryResponse = { history: HistoryEntry[] }
|
||||
|
|
@ -54,11 +61,14 @@ export async function historyApp(name?: string) {
|
|||
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
|
||||
const hasChanges = entry.added.length > 0 || entry.modified.length > 0 || entry.deleted.length > 0 || entry.renamed.length > 0
|
||||
if (!hasChanges) {
|
||||
console.log(color.gray(' No changes'))
|
||||
}
|
||||
|
||||
for (const rename of entry.renamed) {
|
||||
console.log(` ${color.cyan('→')} ${rename}`)
|
||||
}
|
||||
for (const file of entry.added) {
|
||||
console.log(` ${color.green('+')} ${file}`)
|
||||
}
|
||||
|
|
@ -155,6 +165,26 @@ export async function pushApp(options: { quiet?: boolean } = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
// Detect renames among upload/delete pairs (same hash, different path)
|
||||
const renames: Rename[] = []
|
||||
const remoteByHash = new Map<string, string>()
|
||||
for (const file of toDelete) {
|
||||
const hash = result.manifest!.files[file]!.hash
|
||||
remoteByHash.set(hash, file)
|
||||
}
|
||||
|
||||
const renamedUploads = new Set<string>()
|
||||
const renamedDeletes = new Set<string>()
|
||||
for (const file of toUpload) {
|
||||
const hash = localManifest.files[file]!.hash
|
||||
const remoteFile = remoteByHash.get(hash)
|
||||
if (remoteFile && !renamedDeletes.has(remoteFile)) {
|
||||
renames.push({ from: remoteFile, to: file })
|
||||
renamedUploads.add(file)
|
||||
renamedDeletes.add(remoteFile)
|
||||
}
|
||||
}
|
||||
|
||||
if (toUpload.length === 0 && toDelete.length === 0) {
|
||||
if (!options.quiet) console.log('Already up to date')
|
||||
return
|
||||
|
|
@ -174,11 +204,28 @@ export async function pushApp(options: { quiet?: boolean } = {}) {
|
|||
console.log(`Deploying version ${color.bold(version)}...`)
|
||||
|
||||
// 2. Upload changed files to new version
|
||||
if (toUpload.length > 0) {
|
||||
console.log(`Uploading ${toUpload.length} files...`)
|
||||
const actualUploads = toUpload.filter(f => !renamedUploads.has(f))
|
||||
const actualDeletes = toDelete.filter(f => !renamedDeletes.has(f))
|
||||
|
||||
if (renames.length > 0) {
|
||||
console.log(`Renaming ${renames.length} files...`)
|
||||
for (const { from, to } of renames) {
|
||||
const content = readFileSync(join(process.cwd(), to))
|
||||
const uploadOk = await put(`/api/sync/apps/${appName}/files/${to}?version=${version}`, content)
|
||||
const deleteOk = await del(`/api/sync/apps/${appName}/files/${from}?version=${version}`)
|
||||
if (uploadOk && deleteOk) {
|
||||
console.log(` ${color.cyan('→')} ${from} → ${to}`)
|
||||
} else {
|
||||
console.log(` ${color.red('✗')} ${from} → ${to} (failed)`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (actualUploads.length > 0) {
|
||||
console.log(`Uploading ${actualUploads.length} files...`)
|
||||
let failedUploads = 0
|
||||
|
||||
for (const file of toUpload) {
|
||||
for (const file of actualUploads) {
|
||||
const content = readFileSync(join(process.cwd(), file))
|
||||
const success = await put(`/api/sync/apps/${appName}/files/${file}?version=${version}`, content)
|
||||
if (success) {
|
||||
|
|
@ -197,9 +244,9 @@ export async function pushApp(options: { quiet?: boolean } = {}) {
|
|||
}
|
||||
|
||||
// 3. Delete files that no longer exist locally
|
||||
if (toDelete.length > 0) {
|
||||
console.log(`Deleting ${toDelete.length} files...`)
|
||||
for (const file of toDelete) {
|
||||
if (actualDeletes.length > 0) {
|
||||
console.log(`Deleting ${actualDeletes.length} files...`)
|
||||
for (const file of actualDeletes) {
|
||||
const success = await del(`/api/sync/apps/${appName}/files/${file}?version=${version}`)
|
||||
if (success) {
|
||||
console.log(` ${color.red('✗')} ${file}`)
|
||||
|
|
@ -308,13 +355,20 @@ export async function diffApp() {
|
|||
return
|
||||
}
|
||||
|
||||
const { changed, localOnly, remoteOnly } = diff
|
||||
const { changed, localOnly, remoteOnly, renamed } = diff
|
||||
|
||||
if (changed.length === 0 && localOnly.length === 0 && remoteOnly.length === 0) {
|
||||
if (changed.length === 0 && localOnly.length === 0 && remoteOnly.length === 0 && renamed.length === 0) {
|
||||
// console.log(color.green('✓ No differences'))
|
||||
return
|
||||
}
|
||||
|
||||
// Show renames
|
||||
for (const { from, to } of renamed) {
|
||||
console.log(color.cyan('\nRenamed'))
|
||||
console.log(color.bold(`${from} → ${to}`))
|
||||
console.log(color.gray('─'.repeat(60)))
|
||||
}
|
||||
|
||||
// Fetch all changed files in parallel
|
||||
const remoteContents = await Promise.all(
|
||||
changed.map(file => download(`/api/sync/apps/${appName}/files/${file}`))
|
||||
|
|
@ -395,13 +449,13 @@ export async function statusApp() {
|
|||
return
|
||||
}
|
||||
|
||||
const { changed, localOnly, remoteOnly, localManifest, remoteManifest } = diff
|
||||
const { changed, localOnly, remoteOnly, renamed, localManifest, remoteManifest } = diff
|
||||
|
||||
// toPush = changed + localOnly (new or modified locally)
|
||||
const toPush = [...changed, ...localOnly]
|
||||
|
||||
// Local changes block pull
|
||||
const hasLocalChanges = toPush.length > 0
|
||||
const hasLocalChanges = toPush.length > 0 || renamed.length > 0
|
||||
|
||||
// Display status
|
||||
// console.log(`Status for ${color.bold(appName)}:\n`)
|
||||
|
|
@ -414,8 +468,11 @@ export async function statusApp() {
|
|||
}
|
||||
|
||||
// Push status
|
||||
if (toPush.length > 0 || remoteOnly.length > 0) {
|
||||
if (toPush.length > 0 || remoteOnly.length > 0 || renamed.length > 0) {
|
||||
console.log(color.bold('Changes to push:'))
|
||||
for (const { from, to } of renamed) {
|
||||
console.log(` ${color.cyan('→')} ${from} → ${to}`)
|
||||
}
|
||||
for (const file of toPush) {
|
||||
console.log(` ${color.green('↑')} ${file}`)
|
||||
}
|
||||
|
|
@ -435,7 +492,7 @@ export async function statusApp() {
|
|||
}
|
||||
|
||||
// Summary
|
||||
if (toPush.length === 0 && remoteOnly.length === 0) {
|
||||
if (toPush.length === 0 && remoteOnly.length === 0 && renamed.length === 0) {
|
||||
console.log(color.green('✓ In sync with server'))
|
||||
}
|
||||
}
|
||||
|
|
@ -958,10 +1015,31 @@ async function getManifestDiff(appName: string): Promise<ManifestDiff | null> {
|
|||
}
|
||||
}
|
||||
|
||||
// Detect renames: localOnly + remoteOnly files with matching hashes
|
||||
const renamed: Rename[] = []
|
||||
const remoteByHash = new Map<string, string>()
|
||||
for (const file of remoteOnly) {
|
||||
const hash = result.manifest!.files[file]!.hash
|
||||
remoteByHash.set(hash, file)
|
||||
}
|
||||
|
||||
const matchedLocal = new Set<string>()
|
||||
const matchedRemote = new Set<string>()
|
||||
for (const file of localOnly) {
|
||||
const hash = localManifest.files[file]!.hash
|
||||
const remoteFile = remoteByHash.get(hash)
|
||||
if (remoteFile && !matchedRemote.has(remoteFile)) {
|
||||
renamed.push({ from: remoteFile, to: file })
|
||||
matchedLocal.add(file)
|
||||
matchedRemote.add(remoteFile)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
changed,
|
||||
localOnly,
|
||||
remoteOnly,
|
||||
localOnly: localOnly.filter(f => !matchedLocal.has(f)),
|
||||
remoteOnly: remoteOnly.filter(f => !matchedRemote.has(f)),
|
||||
renamed,
|
||||
localManifest,
|
||||
remoteManifest: result.manifest ?? null,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,32 @@ router.get('/apps/:app/history', c => {
|
|||
}
|
||||
}
|
||||
|
||||
return { version, current, added: added.sort(), modified: modified.sort(), deleted: deleted.sort() }
|
||||
// Detect renames: added + deleted files with matching hashes
|
||||
const renamed: string[] = []
|
||||
const deletedByHash = new Map<string, string>()
|
||||
for (const path of deleted) {
|
||||
deletedByHash.set(olderFiles[path]!.hash, path)
|
||||
}
|
||||
|
||||
const matchedAdded = new Set<string>()
|
||||
const matchedDeleted = new Set<string>()
|
||||
for (const path of added) {
|
||||
const oldPath = deletedByHash.get(files[path]!.hash)
|
||||
if (oldPath && !matchedDeleted.has(oldPath)) {
|
||||
renamed.push(`${oldPath} → ${path}`)
|
||||
matchedAdded.add(path)
|
||||
matchedDeleted.add(oldPath)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
current,
|
||||
added: added.filter(f => !matchedAdded.has(f)).sort(),
|
||||
modified: modified.sort(),
|
||||
deleted: deleted.filter(f => !matchedDeleted.has(f)).sort(),
|
||||
renamed: renamed.sort(),
|
||||
}
|
||||
})
|
||||
|
||||
return c.json({ history })
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user