fix: replace SHA comparison with file size for LFS compatibility + add NoChangesModal

Git LFS stores pointer files whose SHA differs from the actual blob SHA,
causing false-positive diffs on every upload. Switching to file size
comparison resolves this for LFS-enabled repos.

Also replaces the inline error message with a dedicated NoChangesModal
when no differences are detected, offering cancel (reset) or modify
(close modal) actions.
This commit is contained in:
Tom Boullay
2026-04-14 14:39:19 +02:00
parent f9e15d5e1f
commit 91eaa5d186
5 changed files with 97 additions and 35 deletions
+24 -25
View File
@@ -13,22 +13,7 @@ import FolderDropzone from './upload/FolderDropzone'
import FolderCard from './upload/FolderCard'
import ActionButtons from './upload/ActionButtons'
import OverwriteConfirmModal from './upload/OverwriteConfirmModal'
// ---------------------------------------------------------------------------
// Client-side SHA computation (same as `git hash-object`)
// ---------------------------------------------------------------------------
async function computeGitBlobSha(file: File): Promise<string> {
const buffer = await file.arrayBuffer()
const content = new Uint8Array(buffer)
const header = new TextEncoder().encode(`blob ${content.length}\0`)
const store = new Uint8Array(header.length + content.length)
store.set(header)
store.set(content, header.length)
const hashBuffer = await crypto.subtle.digest('SHA-1', store)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
}
import NoChangesModal from './upload/NoChangesModal'
// ---------------------------------------------------------------------------
// API helpers
@@ -56,18 +41,19 @@ async function checkFolderDiffs(
return { exists: false, diffs: [] }
}
const remoteFiles: { name: string; sha: string }[] = data.files || []
const remoteMap = new Map(remoteFiles.map((f) => [f.name.toLowerCase(), f.sha]))
const remoteFiles: { name: string; size: number }[] = data.files || []
const remoteMap = new Map(remoteFiles.map((f) => [f.name.toLowerCase(), f.size]))
const localFiles: { name: string; sha: string }[] = []
// Build local file list with sizes
const localFiles: { name: string; size: number }[] = []
localFiles.push({
name: folder.modelFile.name,
sha: await computeGitBlobSha(folder.modelFile),
size: folder.modelFile.size,
})
for (const tex of folder.textures) {
localFiles.push({
name: tex.name,
sha: await computeGitBlobSha(tex.file),
size: tex.file.size,
})
}
@@ -77,10 +63,10 @@ async function checkFolderDiffs(
for (const local of localFiles) {
const key = local.name.toLowerCase()
localNames.add(key)
const remoteSha = remoteMap.get(key)
if (!remoteSha) {
const remoteSize = remoteMap.get(key)
if (remoteSize === undefined) {
diffs.push({ name: local.name, status: 'new' })
} else if (remoteSha !== local.sha) {
} else if (remoteSize !== local.size) {
diffs.push({ name: local.name, status: 'changed' })
}
}
@@ -177,6 +163,7 @@ export default function UploadZone() {
folderName: string
diffs: FileDiff[]
} | null>(null)
const [noChangesFolder, setNoChangesFolder] = useState<string | null>(null)
const abortRef = useRef<AbortController | null>(null)
// -- Handlers --
@@ -207,7 +194,7 @@ export default function UploadZone() {
const check = await checkFolderDiffs(folder, destination, secret, abortRef.current?.signal)
if (check.exists) {
if (check.diffs.length === 0) {
setGlobalError('Aucun fichier modifie — le dossier distant est identique.')
setNoChangesFolder(folder.folderName)
return
}
setOverwriteConfirm({ folderName: folder.folderName, diffs: check.diffs })
@@ -330,6 +317,18 @@ export default function UploadZone() {
onConfirm={proceedUpload}
/>
)}
{noChangesFolder && (
<NoChangesModal
destination={destination}
folderName={noChangesFolder}
onCancel={() => {
setNoChangesFolder(null)
handleReset()
}}
onModify={() => setNoChangesFolder(null)}
/>
)}
</div>
)
}
+63
View File
@@ -0,0 +1,63 @@
interface NoChangesModalProps {
destination: string
folderName: string
onCancel: () => void
onModify: () => void
}
export default function NoChangesModal({
destination,
folderName,
onCancel,
onModify,
}: NoChangesModalProps) {
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
role="dialog"
aria-modal="true"
aria-labelledby="no-changes-title"
>
<div className="bg-black-900 border border-white/20 rounded-2xl p-6 max-w-md w-full mx-4 space-y-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gray-700/50 flex items-center justify-center shrink-0">
<svg className="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<div>
<h3 id="no-changes-title" className="text-sm font-semibold text-gray-100">
Aucun changement detecte
</h3>
<p className="text-xs text-gray-400 mt-0.5">
Le dossier <span className="font-mono text-gray-300">{destination}/{folderName}</span> est identique au contenu distant.
</p>
</div>
</div>
<div className="flex gap-3">
<button
onClick={onCancel}
className="flex-1 bg-black-700 text-gray-300 font-medium text-sm
py-2.5 px-4 rounded-xl border border-white/10 transition-colors duration-150
hover:bg-black-600"
>
Annuler
</button>
<button
onClick={onModify}
className="flex-1 bg-white text-[#000000] font-medium text-sm
py-2.5 px-4 rounded-xl transition-colors duration-150
hover:bg-gray-200"
>
Modifier
</button>
</div>
</div>
</div>
)
}