feat(gitsync): synka wiki-repot mot remote över SSH

- ed25519-deploy-nyckel genereras av servern (config-volymen, 0600),
  GIT_SSH_COMMAND med egen known_hosts (accept-new)
- inställningar: remote-URL, live-gren, read-only, auto-synk-intervall
- bakgrundssynk: endast fast-forward, flaggar attention vid merge-behov
- pull/push, skapa/byt gren, merge-popup (ours/theirs per fil eller allt),
  abort, reset från remote med automatisk backup-gren
- headless-konfig via config.json eller updateGitSync-mutationen
- openssh-client tillagd i runtime-imagen; mutex kring alla git-operationer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 23:10:34 +02:00
parent 952ff5f951
commit e181e8e68e
11 changed files with 1506 additions and 1 deletions

View File

@@ -68,6 +68,15 @@ type Query {
# Effective permissions a subject has on each path (admin only, for the
# access-control tree). For a user this includes their group memberships.
subjectAccess(subjectType: String!, subjectId: Int!, paths: [String!]!): [PathAccess!]!
# Git sync settings incl. the public deploy key (admin only).
gitSyncSettings: GitSyncSettings!
# Live git sync state: branch, ahead/behind, conflicts (admin only).
gitSyncStatus: GitSyncStatus!
# Local and remote branches (admin only).
gitBranches: GitBranches!
}
type PathAccess {
@@ -135,6 +144,38 @@ type Mutation {
# Commit all uncommitted files in the storage path.
initCommit(message: String!): Boolean!
# ── Git sync (admin only) ────────────────────────────────────────────────────
# Update sync settings; the server (re)connects the remote and ticker.
updateGitSync(input: GitSyncInput!): Boolean!
# Merge origin/<current> into the current branch. result is OK,
# MERGE_CONFLICT (merge left open for gitResolveMerge/gitAbortMerge)
# or UNRELATED_HISTORIES (use gitResetFromRemote).
gitPull: GitPullResult!
# Push the current branch to origin. Fails when read-only.
gitPush: Boolean!
# Create a branch at HEAD and switch to it.
gitCreateBranch(name: String!): Boolean!
# Switch to a local branch, or track a remote-only one.
gitSwitchBranch(name: String!): Boolean!
# Settle conflicted paths with "ours" or "theirs" (empty paths = all),
# committing the merge when nothing remains.
gitResolveMerge(strategy: String!, paths: [String!]): GitResolveResult!
# Throw away an in-progress merge.
gitAbortMerge: Boolean!
# Force the current branch to match origin. Local-only state is kept
# in the returned backup branch.
gitResetFromRemote: GitResetResult!
# Replace the sync keypair (register the new public key as deploy key).
gitRegenerateKey: GitKeyResult!
# Delete an image file.
deleteImage(slug: String!, filename: String!): Boolean!
@@ -250,6 +291,64 @@ type RepoStatus {
hasUncommitted: Boolean!
}
type GitSyncSettings {
enabled: Boolean!
remoteUrl: String!
liveBranch: String!
readOnly: Boolean!
autoSyncMinutes: Int!
publicKey: String!
keySet: Boolean!
}
type GitSyncStatus {
enabled: Boolean!
currentBranch: String!
liveBranch: String!
readOnly: Boolean!
ahead: Int!
behind: Int!
hasUpstream: Boolean!
dirty: Boolean!
mergeInProgress: Boolean!
conflicts: [String!]!
lastSyncAt: String!
lastSyncError: String!
attention: Boolean!
}
type GitBranches {
current: String!
local: [String!]!
remote: [String!]!
}
type GitPullResult {
result: String!
conflicts: [String!]!
}
type GitResolveResult {
resolved: Boolean!
remaining: [String!]!
}
type GitResetResult {
backupBranch: String!
}
type GitKeyResult {
publicKey: String!
}
input GitSyncInput {
enabled: Boolean!
remoteUrl: String!
liveBranch: String!
readOnly: Boolean!
autoSyncMinutes: Int!
}
type ImageFile {
name: String!
url: String!