mirror of
https://github.com/firstsun-dev/git-files-sync.git
synced 2026-07-22 06:54:27 +00:00
Refresh previously fetched each file's remote content via getFile() to
compare against local content — an O(n) network round-trip per file
even after the existing concurrency pool. Implements the two-phase
hybrid design from the issue discussion:
Phase 1 — bulk classify, no content:
- Extend GitTreeEntry with sha, populated from each service's tree
listing (GitHub/Gitea: item.sha, GitLab: item.id — GitLab's tree API
calls the blob SHA "id", not "sha").
- Add gitBlobSha(): sha1("blob " + byteLength + "\0" + contentBytes)
via crypto.subtle, matching `git hash-object` exactly (verified
against real git-produced SHAs in tests, including a multi-byte
UTF-8 case to check byte length vs char length).
- SyncStatusView.refreshFileStatus() now compares a locally-computed
blob SHA against the tree entry's SHA — no getFile call. Falls back
to the previous content-based comparison per-entry when a tree entry
has no SHA or isn't found in the tree at all (new local file).
- Symlink-aware hashing per the issue's follow-up note: a symlink's
blob content is its target path string, not the content it points
at. "real" mode hashes the raw local link target (readLocalSymlinkTarget)
instead of following it; "follow" mode always hashes the followed
content; "skip" entries are already excluded from the tree map.
Phase 2 — fetch content only when needed:
- Add getBlob(sha, path) to GitServiceInterface: GitHub/Gitea use
`git/blobs/{sha}` (base64 JSON, shared via a new
fetchGitHubStyleBlob() base helper); GitLab uses
`repository/blobs/{sha}/raw` (raw bytes, not JSON-wrapped).
- The diff panel no longer requires content prefetched during refresh.
FileListItem's Diff button always renders for a modified file and
fetches remote content on demand via a new onExpandDiff callback,
showing a loading placeholder while the request is in flight and
caching the result on the FileStatus so re-expanding doesn't refetch.
- A modified symlink shows "Symlink target changed" instead of running
a text diff.
Known limitation (pre-existing, not introduced here): comparing raw
file bytes has no CRLF/line-ending normalization, so a repo checked
out elsewhere with core.autocrlf could show a false "modified" status.
The previous content-based comparison (contentsEqual) had the same
exact-byte-equality behavior, so this isn't a regression.
Closes #36
34 lines
1.5 KiB
TypeScript
34 lines
1.5 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { gitBlobSha } from '../../src/utils/git-blob-sha';
|
|
|
|
// Test vectors verified against `git hash-object --stdin`.
|
|
describe('gitBlobSha', () => {
|
|
it('matches git for empty content', async () => {
|
|
expect(await gitBlobSha('')).toBe('e69de29bb2d1d6434b8b29ae775ad8c2e48c5391');
|
|
});
|
|
|
|
it('matches git for a string with no trailing newline', async () => {
|
|
expect(await gitBlobSha('hello world')).toBe('95d09f2b10159347eece71399a7e2e907ea3df4f');
|
|
});
|
|
|
|
it('matches git for a string with a trailing newline', async () => {
|
|
expect(await gitBlobSha('hello world\n')).toBe('3b18e512dba79e4c8300dd08aeb37f8e728b8dad');
|
|
});
|
|
|
|
it('matches git for multi-byte UTF-8 content', async () => {
|
|
// "café" is 5 bytes in UTF-8 (é is 2 bytes) — verifies byte length, not char length, is used.
|
|
expect(await gitBlobSha('café')).toBe('1c2e52cfe7542a64cdea57e5fec2fc1739846c03');
|
|
});
|
|
|
|
it('produces the same SHA for equivalent string and ArrayBuffer content', async () => {
|
|
const text = 'hello world';
|
|
const bytes = new TextEncoder().encode(text);
|
|
const arrayBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
|
|
expect(await gitBlobSha(arrayBuffer)).toBe(await gitBlobSha(text));
|
|
});
|
|
|
|
it('produces different SHAs for different content', async () => {
|
|
expect(await gitBlobSha('a')).not.toBe(await gitBlobSha('b'));
|
|
});
|
|
});
|