Skip to content

[Feat] Bidirectional GitHub/GitLab folder sync (documents ↔ repo) - #1732

Merged
CREDO23 merged 43 commits into
MODSetter:devfrom
CREDO23:feat/kb-bidirectional-folder-sync
Sep 1, 2026
Merged

[Feat] Bidirectional GitHub/GitLab folder sync (documents ↔ repo)#1732
CREDO23 merged 43 commits into
MODSetter:devfrom
CREDO23:feat/kb-bidirectional-folder-sync

Conversation

@CREDO23

@CREDO23 CREDO23 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Connect one GitHub/GitLab repo folder as a documents/{Forge}/{owner}/{repo}/{sourcepath} tree; text docs (.md/.markdown/.mdx/.rst/.txt) round-trip both ways, binaries untouched.
  • Sync runs from the worker as a pure 3-way plan on a shadow clone (fast-forward push, never force); conflicts and both-sides-have-content fail closed to explicit "keep local / keep remote" resolves, never an auto-merge.
  • Connect via GitHub user OAuth (lists installations, repos, branches, folders); the settings card plus a Sources-panel badge surface sync state and "needs attention".

Test plan

  • Backend unit + integration suites (knowledge-store remote) green
  • Connect a repo at root and at a nested folder — pull lands under the mount and indexes into the sidebar
  • Edit locally → pushed; edit on the forge → pulled; conflict and both-sides-content → resolve keeps the chosen side

High-level PR Summary

This PR introduces bidirectional GitHub/GitLab folder synchronization that mounts one repository folder (e.g., docs/) as documents/{GitHub|GitLab}/{owner}/{repo}/{sourcepath}/ in the workspace. Text documents (.md, .markdown, .mdx, .rst, .txt) round-trip bidirectionally while binaries remain untouched. Sync runs via a worker using a 3-way merge planner on a shadow clone with fast-forward-only pushes. Conflicts and scenarios where both sides have content fail closed and require explicit user resolution (keep local or keep remote). The feature includes GitHub user OAuth for listing installations, repos, branches, and folders, plus UI for displaying sync state and handling conflicts. The implementation replaces the previous push-only export product (Phase 12) with a folder-based bidirectional sync model (Phase 13).

⏱️ Estimated Review Time: 3+ hours

💡 Review Order Suggestion
Order File Path
1 plans/git-native-kb/13-bidirectional-folder-sync.md
2 plans/git-native-kb/00-umbrella-plan.md
3 plans/git-native-kb/12-connect-own-remote.md
4 plans/git-native-kb/11-one-write-path-design.md
5 surfsense_backend/alembic/versions/191_git_remote_folder_sync.py
6 surfsense_backend/app/config/__init__.py
7 surfsense_backend/.env.example
8 surfsense_backend/app/knowledge_store/remote/schemas/spec.py
9 surfsense_backend/app/knowledge_store/remote/persistence/models.py
10 surfsense_backend/app/knowledge_store/remote/persistence/repository.py
11 surfsense_backend/app/knowledge_store/remote/schemas/status.py
12 surfsense_backend/app/knowledge_store/remote/api/schemas.py
13 surfsense_backend/app/knowledge_store/remote/paths.py
14 surfsense_backend/app/knowledge_store/remote/planner.py
15 surfsense_backend/app/knowledge_store/remote/guards.py
16 surfsense_backend/app/knowledge_store/remote/shadow.py
17 surfsense_backend/app/knowledge_store/remote/sync.py
18 surfsense_backend/app/knowledge_store/remote/facade.py
19 surfsense_backend/app/knowledge_store/remote/forges/github.py
20 surfsense_backend/app/knowledge_store/remote/api/routes.py
21 surfsense_backend/app/knowledge_store/remote/exceptions.py
22 surfsense_backend/app/knowledge_store/remote/queue.py
23 surfsense_backend/app/knowledge_store/service.py
24 surfsense_backend/app/observability/domains/knowledge_store.py
25 surfsense_backend/app/tasks/celery_tasks/knowledge_store/push_task.py
26 surfsense_backend/app/celery_app.py
27 surfsense_backend/tests/unit/knowledge_store/remote/test_planner.py
28 surfsense_backend/tests/unit/knowledge_store/remote/test_paths.py
29 surfsense_backend/tests/unit/knowledge_store/remote/test_guards.py
30 surfsense_backend/tests/unit/knowledge_store/remote/test_shadow.py
31 surfsense_backend/tests/unit/knowledge_store/remote/test_forges.py
32 surfsense_backend/tests/unit/knowledge_store/remote/test_routes_github.py
33 surfsense_backend/tests/unit/knowledge_store/remote/test_after_record.py
34 surfsense_backend/tests/unit/knowledge_store/remote/test_queue.py
35 surfsense_backend/tests/unit/observability/test_domains.py
36 surfsense_backend/tests/integration/knowledge_store/remote/test_connect.py
37 surfsense_backend/tests/integration/knowledge_store/remote/test_sync.py
38 surfsense_web/contracts/types/git-remote.types.ts
39 surfsense_web/lib/apis/git-remotes-api.service.ts
40 surfsense_web/lib/query-client/cache-keys.ts
41 surfsense_web/hooks/use-git-remote-status.ts
42 surfsense_web/components/settings/git-remote-settings.tsx
43 surfsense_web/components/documents/DocumentsFilters.tsx
44 surfsense_web/components/layout/ui/right-panel/DocumentRightPanel.tsx
45 surfsense_web/app/dashboard/[workspace_id]/workspace-settings/git-remote/page.tsx
46 surfsense_web/messages/en.json
47 surfsense_web/messages/es.json
48 surfsense_web/messages/hi.json
49 surfsense_web/messages/ko.json
50 surfsense_web/messages/pt.json
51 surfsense_web/messages/zh.json
52 plans/account-deletion/account-deletion-and-welcome-credit.md
⚠️ Inconsistent Changes Detected
File Path Warning
plans/account-deletion/account-deletion-and-welcome-credit.md This file about account deletion and welcome credit control is completely unrelated to the GitHub/GitLab bidirectional folder sync feature.

Need help? Join our Discord

CREDO23 added 30 commits August 31, 2026 19:08
Phase 13: one connected repo mounts under documents/GitHub|GitLab/{full_name}/{sourcepath}, md bijection, 3-way fail-closed. Phase 12 remains the shipped push-only export.
The work is done; the spec is no longer needed on this branch.
Spell out env, volume, and migrate-once steps from the live compose/entrypoint so the fleet flip is not a lean sketch.
One function decides take-remote / keep-local / conflict so later ticks
share the same rules without git or Postgres.
The tree path is computed from provider + repo + sourcepath so it cannot
clash with a user's notes folder.
Pathspec mistakes must not wipe pngs or src/ on their branch.
A commit is a full snapshot of their tree; our documents/ layout cannot
be that snapshot without dumping the KB or deleting src/.
last_local_revision is the 3-way base after a successful agree.
SQLAlchemy needs the new columns so the worker can read the base SHA.
Connect and the UI need the folder plus conflict/need_direction without
leaking the PAT.
Status and the next plan() tick both read these from Postgres, not git.
Pull is just another store writer so index/Zero stay one-way from our git.
Direction is only for first sync and conflict, when 3-way has no winner.
A save should mirror the mount, not push our store HEAD onto their branch.
Phase 12's engine.push of our repo is the product this phase supersedes.
Retry is POST /sync; resolve takes from_remote|from_local. Drop /push.
Real store plus a second git repo; no fakes of either side of the bijection.
…tings

Stop empty-repo/push-only copy; Use GitHub vs Use SurfSense when 3-way cannot infer.
Retry in settings is the recovery until drops show up in logs.
It only re-drove lost enqueues; we have no evidence of drop rate yet.
Nothing schedules it after the beat entry is gone.
The leftover checkout is not source of truth and should not sit on disk.
We only need their current tree to make a pathspec commit, not history.
Conflicts and blocked ticks were collapsing into remote.push noop, so
alerts could not tell a stuck bijection from a workspace with nothing
to do.
HTTP and the worker share these methods, so one set of spans and
counters covers both callers without double-counting a tick.
Broker failures were log-only, and the worker stamped every None
return as idle, hiding conflict and worktree_busy behind a noop.
Replace the fragile App Setup-URL callback with a user-to-server OAuth
flow: the callback exchanges the code for a user token, lists the user's
installations, and routes to a repo picker, an installation chooser, or
an install prompt. Add endpoints to list a repo's branches and its
recursive folder tree so the settings UI can offer pickers instead of
free-text, and carry each repo's default branch through the API.
Pulled and applied remote changes went into the store but never enqueued
indexing, so they never reached the sidebar; enqueue an index after those
transactions. Stamp last_pushed_* on an initial pull and a mirror so the
card reads "Synced" instead of "Not synced yet". Commit the resolve path
like add/remove so a cleared conflict is not rolled back by the request
lifecycle while its git push already went out.
…kers

Restructure the connected-repo card into a label/value grid with a
provider badge and a status pill instead of run-on text. Start the
connect flow through GitHub user OAuth, add an installation chooser and
an install prompt, and replace the free-text branch and source-path
inputs with pickers backed by the new branches and folders endpoints.
Add the matching API service methods, zod types, cache keys, and locale
strings.
The folder bijection hardcoded ".md", so a .txt/.rst/.mdx doc under a
connected folder was silently dropped. Introduce a single allowlist
(SYNCED_SUFFIXES) and is_syncable(), and widen the path guard to accept any
tracked text document. Binary formats (PDF, images) stay excluded and are
left untouched on the remote for the ingestion connectors to handle.
check_staged refused a staged set that was not markdown; broaden it to the
text-document allowlist so mirroring a .txt/.rst change no longer trips the
"would delete foreign" guard.
Rename md_under_mount -> text_under_mount, select by is_syncable, and update
the facade call sites and local names (local_docs/remote_docs) plus the
need-direction message to speak in terms of documents rather than markdown.
list_md/replace_md walked only *.md; rename to list_text/replace_text and
select files by is_syncable so the shadow round-trips the full text-document
set while leaving binaries (png, pdf) in place on the remote.
Rename the shadow helpers in the sync integration tests and add a case that
pulls .txt/.rst into the mount, skips a .pdf, and pushes a locally authored
.mdx while the pdf survives on the remote.
Connecting when both the folder and the remote already hold documents no
longer errors and strands the user: the connect succeeds, mirrors nothing,
and flags the row `need_direction` so the card asks which side to keep.
Resolving stamps the synced marker so the card reads "Synced" afterward.

Drop the legacy `reconnect_required` path — it only ever fired on
pre-`sourcepath` rows, which do not exist in prod. `sourcepath` is now
NOT NULL (normalized to "" = repo root at save), so the guard is dead.
Add a shared git-remote status hook and flag "Sync needs attention" as a
small warning-triangle badge in the corner of the connect button, so a
conflict or failed push is visible from the Sources panel without opening
settings. Rename the button to the provider-agnostic "Connect a repository".

On the settings page, offer searchable folder and branch pickers and show
an info toast (not a success) when a connect lands in the need-direction
state, pointing the user at the resolve buttons.
@vercel

vercel Bot commented Sep 1, 2026

Copy link
Copy Markdown

@CREDO23 is attempting to deploy a commit to the Rohan Verma's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 5052ae2a-3992-4538-ad00-610928b1452a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Add resolve_folder_id_by_parts, a read-only twin of the hierarchy
builder, so callers can look up an existing folder id from a sanitized
name chain without creating rows.
Resolve the folder each connected repo mounts onto and return it on the
status DTO, giving the frontend a reliable id instead of matching folder
names. It stays null until the indexer creates the folder.
Add mount_folder_id to the git remote type and helpers that classify a
folder against it: whether it is synced content, whether deleting it
would touch the sync, and the repo label for display.
FolderTreeView and FolderNode take an optional set of git-synced folder
ids and render a small green dot on those folders so a connected repo is
visible at a glance.
Swap the "Connect a repository" label for "Repository settings" when a
remote is already connected, with the manage_repo string translated
across locales.
The indexer creates the mount folder a moment after connect, so poll the
status every 8s while mount_folder_id is null and stop once it resolves.
The synced badge then appears without a manual reload.
Wire the mount folder id into the panel: badge the synced folder and
confirm before deletions that reach the sync. The heads-up wording
distinguishes a folder that is synced from one that only contains synced
files, and bulk delete flags when the selection touches the sync.
@CREDO23
CREDO23 merged commit 1a77cff into MODSetter:dev Sep 1, 2026
5 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant