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
54 lines
2.1 KiB
TypeScript
54 lines
2.1 KiB
TypeScript
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
import { renderDiffPanel } from '../../src/ui/components/DiffPanel';
|
|
import { setupObsidianDOM, createContainer } from './setup-dom';
|
|
|
|
beforeAll(() => { setupObsidianDOM(); });
|
|
|
|
describe('renderDiffPanel', () => {
|
|
let container: HTMLElement;
|
|
|
|
beforeEach(() => {
|
|
container = createContainer();
|
|
});
|
|
|
|
it('renders the diff grid into the given container', () => {
|
|
renderDiffPanel(container, '', '');
|
|
expect(container.querySelector('.ssv-diff-grid')).not.toBeNull();
|
|
});
|
|
|
|
it('renders Remote and Local column headers', () => {
|
|
renderDiffPanel(container, 'a', 'b');
|
|
const headers = container.querySelectorAll('.ssv-diff-hd');
|
|
expect(headers[0]?.textContent).toBe('Remote');
|
|
expect(headers[1]?.textContent).toBe('Local');
|
|
});
|
|
|
|
it('renders unchanged lines with unchanged class in both columns', () => {
|
|
renderDiffPanel(container, 'same', 'same');
|
|
const cells = container.querySelectorAll('.ssv-diff-cell.unchanged');
|
|
expect(cells.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
it('renders added line in unified view', () => {
|
|
renderDiffPanel(container, '', 'new line');
|
|
const added = container.querySelector('.ssv-u-line.added');
|
|
expect(added?.textContent).toContain('new line');
|
|
});
|
|
|
|
it('renders removed line in unified view', () => {
|
|
renderDiffPanel(container, 'old line', '');
|
|
const removed = container.querySelector('.ssv-u-line.removed');
|
|
expect(removed?.textContent).toContain('old line');
|
|
});
|
|
|
|
it('renders both removed and added lines for a replacement', () => {
|
|
renderDiffPanel(container, 'before', 'after');
|
|
expect(container.querySelector('.ssv-u-line.removed')).not.toBeNull();
|
|
expect(container.querySelector('.ssv-u-line.added')).not.toBeNull();
|
|
});
|
|
|
|
it('renders unchanged lines in unified view for identical content', () => {
|
|
renderDiffPanel(container, 'context', 'context');
|
|
expect(container.querySelector('.ssv-u-line.unchanged')).not.toBeNull();
|
|
});
|
|
});
|