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

@@ -8,11 +8,19 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
)
// Repo wraps a bare directory that is a Git repository.
type Repo struct {
root string
// mu serialises whole operations (a background sync racing a user
// save would otherwise corrupt index/merge state).
mu sync.Mutex
// sshCommand is exported to git as GIT_SSH_COMMAND when set, so
// remotes can be reached with a dedicated key and known_hosts file
// even though the server runs without a home directory.
sshCommand string
}
// Open returns a Repo for an existing directory, initialising Git if needed.
@@ -35,6 +43,8 @@ func (r *Repo) initIfNeeded() error {
// Commit writes data to path and creates a Git commit authored by author.
func (r *Repo) Commit(path, content, message, author, email string) error {
r.mu.Lock()
defer r.mu.Unlock()
full := filepath.Join(r.root, path)
if err := writeFile(full, content); err != nil {
return err
@@ -52,6 +62,8 @@ func (r *Repo) Commit(path, content, message, author, email string) error {
// Log returns the commit history for a file, including added/removed line counts.
func (r *Repo) Log(path string) ([]LogEntry, error) {
r.mu.Lock()
defer r.mu.Unlock()
out, err := r.run("log", "--format=COMMIT|%H|%an|%ae|%ai|%s", "--numstat", "--", path)
if err != nil {
return nil, err
@@ -92,24 +104,32 @@ func (r *Repo) Log(path string) ([]LogEntry, error) {
// Diff returns the unified diff between two commits for a file.
func (r *Repo) Diff(path, fromHash, toHash string) (string, error) {
r.mu.Lock()
defer r.mu.Unlock()
out, err := r.run("diff", fromHash, toHash, "--", path)
return out, err
}
// Show returns the file content at a specific commit.
func (r *Repo) Show(hash, path string) (string, error) {
r.mu.Lock()
defer r.mu.Unlock()
return r.run("show", fmt.Sprintf("%s:%s", hash, path))
}
// HasUncommitted returns true if the working tree contains untracked or
// modified files that have not yet been committed.
func (r *Repo) HasUncommitted() bool {
r.mu.Lock()
defer r.mu.Unlock()
out, err := r.run("status", "--porcelain")
return err == nil && strings.TrimSpace(out) != ""
}
// CommitAll stages every file in the repo root and creates a commit.
func (r *Repo) CommitAll(message, author, email string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, err := r.run("add", "-A"); err != nil {
return err
}
@@ -123,6 +143,8 @@ func (r *Repo) CommitAll(message, author, email string) error {
// Move renames a file from oldPath to newPath and creates a Git commit.
func (r *Repo) Move(oldPath, newPath, message, author, email string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, err := r.run("mv", oldPath, newPath); err != nil {
return err
}
@@ -153,6 +175,10 @@ func writeFile(path, content string) error {
func (r *Repo) run(args ...string) (string, error) {
cmd := exec.Command("git", append([]string{"-C", r.root}, args...)...)
cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
if r.sshCommand != "" {
cmd.Env = append(cmd.Env, "GIT_SSH_COMMAND="+r.sshCommand)
}
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr