feat: initial import — SVN↔Git bi-directional mirror for Gitea
Server-side sidecar daemon. Uses own SQLite mapping table (not git svn) so Git commit hashes never change. Pure Python, zero framework deps. Includes: - SVN→Git sync engine with nested-directory tree reconstruction - Git→SVN push with author mapping and working-copy management - Branch/tag sync (svn copy → git branch/tag and reverse) - Gitea integration (on-disk push/fetch, webhook receiver) - Reconciliation tool for divergence recovery (assess + auto-fix) - Full end-to-end regression test (bash, 34 checks) - systemd unit and deployment docs Only external dependency: PyYAML (config parsing).
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""SQLite mapping database for SVN↔Git revision mapping."""
|
||||
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class MappingDB:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
self.conn = sqlite3.connect(str(path))
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
self.conn.execute("PRAGMA journal_mode=WAL")
|
||||
self.conn.execute("PRAGMA synchronous=NORMAL")
|
||||
self._init_schema()
|
||||
|
||||
def _init_schema(self):
|
||||
self.conn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER PRIMARY KEY
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS svn_to_git (
|
||||
svn_revision INTEGER NOT NULL,
|
||||
git_commit_hash TEXT NOT NULL,
|
||||
svn_branch TEXT NOT NULL,
|
||||
git_ref TEXT NOT NULL,
|
||||
synced_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||
source TEXT NOT NULL DEFAULT 'svn'
|
||||
CHECK (source IN ('svn', 'git')),
|
||||
UNIQUE(svn_revision, svn_branch)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_svn_to_git_hash
|
||||
ON svn_to_git(git_commit_hash);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_svn_to_git_svn
|
||||
ON svn_to_git(svn_revision);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sync_state (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO schema_version (version) VALUES (1);
|
||||
""")
|
||||
self.conn.commit()
|
||||
|
||||
def close(self):
|
||||
self.conn.close()
|
||||
|
||||
# --- Mapping CRUD ---
|
||||
|
||||
def record_mapping(
|
||||
self,
|
||||
svn_revision: int,
|
||||
git_commit_hash: str,
|
||||
svn_branch: str,
|
||||
git_ref: str,
|
||||
source: str = "svn",
|
||||
):
|
||||
self.conn.execute(
|
||||
"""INSERT OR REPLACE INTO svn_to_git
|
||||
(svn_revision, git_commit_hash, svn_branch, git_ref, source)
|
||||
VALUES (?, ?, ?, ?, ?)""",
|
||||
(svn_revision, git_commit_hash, svn_branch, git_ref, source),
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def get_git_hash(self, svn_revision: int, svn_branch: str) -> Optional[str]:
|
||||
row = self.conn.execute(
|
||||
"SELECT git_commit_hash FROM svn_to_git WHERE svn_revision=? AND svn_branch=?",
|
||||
(svn_revision, svn_branch),
|
||||
).fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
def get_svn_revision(self, git_commit_hash: str) -> Optional[int]:
|
||||
row = self.conn.execute(
|
||||
"SELECT svn_revision FROM svn_to_git WHERE git_commit_hash=?",
|
||||
(git_commit_hash,),
|
||||
).fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
def get_last_svn_revision(self, svn_branch: str = "trunk") -> Optional[int]:
|
||||
row = self.conn.execute(
|
||||
"SELECT MAX(svn_revision) FROM svn_to_git WHERE svn_branch=?",
|
||||
(svn_branch,),
|
||||
).fetchone()
|
||||
return row[0] if row and row[0] is not None else None
|
||||
|
||||
def mapping_exists(self, svn_revision: int, svn_branch: str) -> bool:
|
||||
row = self.conn.execute(
|
||||
"SELECT 1 FROM svn_to_git WHERE svn_revision=? AND svn_branch=?",
|
||||
(svn_revision, svn_branch),
|
||||
).fetchone()
|
||||
return row is not None
|
||||
|
||||
def get_all_mappings(self):
|
||||
return self.conn.execute(
|
||||
"SELECT svn_revision, git_commit_hash, svn_branch, git_ref, source "
|
||||
"FROM svn_to_git ORDER BY svn_revision"
|
||||
).fetchall()
|
||||
|
||||
def ref_exists(self, git_ref: str) -> bool:
|
||||
row = self.conn.execute(
|
||||
"SELECT 1 FROM svn_to_git WHERE git_ref=?", (git_ref,)
|
||||
).fetchone()
|
||||
return row is not None
|
||||
|
||||
def get_mapping_by_git_hash(self, git_hash: str) -> Optional[dict]:
|
||||
row = self.conn.execute(
|
||||
"SELECT svn_revision, svn_branch, git_ref, source "
|
||||
"FROM svn_to_git WHERE git_commit_hash=? LIMIT 1",
|
||||
(git_hash,),
|
||||
).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def get_max_revision_across_all_branches(self) -> Optional[int]:
|
||||
row = self.conn.execute(
|
||||
"SELECT MAX(svn_revision) FROM svn_to_git",
|
||||
).fetchone()
|
||||
return row[0] if row and row[0] is not None else None
|
||||
|
||||
def mapping_count(self) -> int:
|
||||
row = self.conn.execute("SELECT COUNT(*) FROM svn_to_git").fetchone()
|
||||
return row[0] if row else 0
|
||||
|
||||
# --- Sync State ---
|
||||
|
||||
def set_state(self, key: str, value: str):
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO sync_state (key, value) VALUES (?, ?)",
|
||||
(key, value),
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def get_state(self, key: str) -> Optional[str]:
|
||||
row = self.conn.execute(
|
||||
"SELECT value FROM sync_state WHERE key=?", (key,)
|
||||
).fetchone()
|
||||
return row[0] if row else None
|
||||
Reference in New Issue
Block a user