feat: manual incoming changes handling

- Enhanced Git class to include method for fetching incoming changes from remote repository.
- Implemented logic to retrieve and display incoming commits in the changes page.
- Updated API routes to handle incoming changes and pull requests.
- Modified UI components to show incoming changes and allow users to pull updates.
- Improved actions bar to disable commit actions when there are incoming changes.
- Added sync button to refresh repository status and check for updates.
This commit is contained in:
Sam Chau
2026-01-17 15:25:24 +10:30
parent b13ec91e32
commit 47ba9dd7e9
10 changed files with 832 additions and 224 deletions

View File

@@ -0,0 +1,302 @@
# Manual Pull Handling for Databases
**Status: Complete**
## Summary
When `auto_pull = 0`, users receive notifications that updates are available but
have no way to review or pull them. This document plans extending the existing
`/databases/[id]/changes` page to support incoming changes (pull) alongside
outgoing changes (push).
---
## Current State
### The `/databases/[id]/changes` Page
Currently this page:
- Only accessible to developers (requires `personal_access_token`)
- Shows **outgoing changes** (uncommitted local ops)
- Allows: select files, write commit message, push to remote
- Allows: discard local changes
- Allows: switch branches
### The Gap
When `auto_pull = 0` and updates are found:
1. User receives notification "Updates available for X"
2. User has no way to see what those updates contain
3. User has no way to pull them from the UI
---
## Proposed Solution
Extend the changes page to show both directions:
| Section | Who Sees It | Description |
| ----------------- | ----------- | ---------------------------------- |
| Incoming Changes | Everyone | Commits available to pull |
| Outgoing Changes | Developers | Uncommitted local ops to push |
### User Flow
**For regular users (no PAT):**
1. Navigate to `/databases/[id]/changes`
2. See "Incoming Changes" section with commits behind
3. Review the changes (files modified in each commit)
4. Click "Pull Updates" to sync
**For developers (with PAT):**
1. Same as above, plus...
2. See "Outgoing Changes" section with uncommitted ops
3. Full commit/push/discard functionality
---
## Implementation Plan
### 1. Remove PAT Requirement for Page Access
**File:** `src/routes/databases/[id]/changes/+page.server.ts`
```typescript
export const load: PageServerLoad = async ({ parent }) => {
const { database } = await parent();
// Remove the PAT check - page is now accessible to everyone
// PAT only needed for push actions
return {
isDeveloper: !!database.personal_access_token
};
};
```
### 2. Update API to Return Incoming Changes
**File:** `src/routes/api/databases/[id]/changes/+server.ts`
Remove PAT requirement for GET. Add incoming changes data:
```typescript
export const GET: RequestHandler = async ({ params }) => {
const database = databaseInstancesQueries.getById(id);
const git = new Git(database.local_path);
// Fetch for everyone
const [status, incomingChanges, branches, repoInfo] = await Promise.all([
git.status(),
git.getIncomingChanges(),
git.getBranches(),
getRepoInfo(database.repository_url, database.personal_access_token)
]);
// Only fetch outgoing changes for developers
let uncommittedOps = null;
if (database.personal_access_token) {
uncommittedOps = await git.getUncommittedOps();
}
return json({
status,
incomingChanges,
branches,
repoInfo,
uncommittedOps
});
};
```
### 3. Add Git Function for Incoming Changes
**File:** `src/lib/server/utils/git/status.ts`
```typescript
export interface IncomingCommit {
hash: string;
shortHash: string;
message: string;
author: string;
date: string;
files: string[];
}
export interface IncomingChanges {
hasUpdates: boolean;
commitsBehind: number;
commits: IncomingCommit[];
}
export async function getIncomingChanges(repoPath: string): Promise<IncomingChanges> {
// Fetch latest from remote
await execGitSafe(['fetch'], repoPath);
const branch = await getBranch(repoPath);
const remoteBranch = `origin/${branch}`;
// Count commits behind
const countOutput = await execGitSafe(
['rev-list', '--count', `HEAD..${remoteBranch}`],
repoPath
);
const commitsBehind = parseInt(countOutput || '0') || 0;
if (commitsBehind === 0) {
return { hasUpdates: false, commitsBehind: 0, commits: [] };
}
// Get commit details for incoming commits
const logOutput = await execGitSafe(
['log', '--format=%H|%h|%s|%an|%aI', `HEAD..${remoteBranch}`],
repoPath
);
const commits: IncomingCommit[] = [];
for (const line of logOutput.split('\n').filter(Boolean)) {
const [hash, shortHash, message, author, date] = line.split('|');
// Get files changed in this commit
const filesOutput = await execGitSafe(
['diff-tree', '--no-commit-id', '--name-only', '-r', hash],
repoPath
);
const files = filesOutput.split('\n').filter(Boolean);
commits.push({ hash, shortHash, message, author, date, files });
}
return { hasUpdates: true, commitsBehind, commits };
}
```
### 4. Add Pull Action
**File:** `src/routes/databases/[id]/changes/+page.server.ts`
```typescript
export const actions: Actions = {
// ... existing actions ...
pull: async ({ params }) => {
const id = parseInt(params.id || '', 10);
const database = databaseInstancesQueries.getById(id);
if (!database) {
return { success: false, error: 'Database not found' };
}
try {
const result = await pcdManager.sync(id);
return result;
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : 'Failed to pull'
};
}
}
};
```
### 5. Update Page UI
**File:** `src/routes/databases/[id]/changes/+page.svelte`
```svelte
<script lang="ts">
export let data: PageData;
let incomingChanges: IncomingChanges | null = null;
// ... existing state ...
$: isDeveloper = data.isDeveloper;
</script>
<!-- Incoming Changes Section (visible to everyone) -->
<section>
<h2>Incoming Changes</h2>
{#if incomingChanges?.hasUpdates}
<p>{incomingChanges.commitsBehind} commits available</p>
<!-- Expandable table showing commits -->
<ExpandableTable data={incomingChanges.commits} ...>
<!-- Show commit message, author, date -->
<!-- Expanded: show files changed -->
</ExpandableTable>
<Button on:click={handlePull}>Pull Updates</Button>
{:else}
<p>Up to date</p>
{/if}
</section>
<!-- Outgoing Changes Section (developers only) -->
{#if isDeveloper}
<section>
<h2>Outgoing Changes</h2>
<!-- Existing uncommitted ops table -->
<!-- Existing commit message + push UI -->
</section>
{/if}
```
---
## Sync Job Integration
When the sync job runs with `auto_pull = 0`:
1. `checkForUpdates()` already fetches and counts commits behind
2. This data is already available via `git.status()` (the `behind` field)
3. The `/api/databases/[id]/changes` endpoint will show this when user visits
No additional "store" needed - the git state IS the store. Each time the user
visits the changes page, we fetch fresh data from git.
---
## Files to Create/Modify
### Modified Files
- `src/routes/databases/[id]/changes/+page.server.ts` - Remove PAT requirement
for load, add pull action
- `src/routes/databases/[id]/changes/+page.svelte` - Add incoming changes UI
- `src/routes/api/databases/[id]/changes/+server.ts` - Remove PAT requirement
for GET, add incoming changes data
- `src/lib/server/utils/git/status.ts` - Add `getIncomingChanges()` function
- `src/lib/server/utils/git/types.ts` - Add `IncomingChanges` types
- `src/lib/server/utils/git/Git.ts` - Expose `getIncomingChanges()`
### New Components (optional)
- `src/routes/databases/[id]/changes/components/IncomingChangesTable.svelte`
- `src/routes/databases/[id]/changes/components/OutgoingChangesTable.svelte`
Could extract existing table into `OutgoingChangesTable` and create matching
`IncomingChangesTable` for consistency.
---
## Edge Cases
1. **No incoming changes**: Show "Up to date" message
2. **Pull fails**: Show error, allow retry
3. **Conflicts**: Shouldn't happen since user_ops are gitignored, but handle
gracefully if it does
4. **Large number of commits**: Paginate or limit to recent N commits
---
## UI Considerations
- StatusCard (repo info, branch switcher) visible for everyone
- Use consistent table styling between incoming/outgoing
- Incoming table is read-only (no checkboxes)
- Clear visual separation between sections
- Consider showing incoming changes count in the tab/nav if updates available