Files
profilarr/docs/todo/2.manual-pull-handling.md
Sam Chau 47ba9dd7e9 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.
2026-01-17 15:25:24 +10:30

7.9 KiB

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

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:

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

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

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

<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