cinchdb 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cinchdb/utils/name_validator.py +6 -6
- {cinchdb-0.1.1.dist-info → cinchdb-0.1.3.dist-info}/METADATA +16 -18
- {cinchdb-0.1.1.dist-info → cinchdb-0.1.3.dist-info}/RECORD +6 -22
- {cinchdb-0.1.1.dist-info → cinchdb-0.1.3.dist-info}/entry_points.txt +0 -1
- cinchdb/api/__init__.py +0 -5
- cinchdb/api/app.py +0 -76
- cinchdb/api/auth.py +0 -290
- cinchdb/api/main.py +0 -137
- cinchdb/api/routers/__init__.py +0 -25
- cinchdb/api/routers/auth.py +0 -135
- cinchdb/api/routers/branches.py +0 -368
- cinchdb/api/routers/codegen.py +0 -164
- cinchdb/api/routers/columns.py +0 -290
- cinchdb/api/routers/data.py +0 -479
- cinchdb/api/routers/databases.py +0 -184
- cinchdb/api/routers/projects.py +0 -133
- cinchdb/api/routers/query.py +0 -156
- cinchdb/api/routers/tables.py +0 -349
- cinchdb/api/routers/tenants.py +0 -216
- cinchdb/api/routers/views.py +0 -219
- {cinchdb-0.1.1.dist-info → cinchdb-0.1.3.dist-info}/WHEEL +0 -0
- {cinchdb-0.1.1.dist-info → cinchdb-0.1.3.dist-info}/licenses/LICENSE +0 -0
cinchdb/api/main.py
DELETED
@@ -1,137 +0,0 @@
|
|
1
|
-
"""Main entry point for CinchDB API server."""
|
2
|
-
|
3
|
-
import uvicorn
|
4
|
-
from pathlib import Path
|
5
|
-
from typing import Optional
|
6
|
-
|
7
|
-
import typer
|
8
|
-
from rich.console import Console
|
9
|
-
|
10
|
-
from cinchdb.api.auth import APIKeyManager
|
11
|
-
from cinchdb.core.path_utils import get_project_root
|
12
|
-
|
13
|
-
|
14
|
-
cli = typer.Typer(
|
15
|
-
name="cinch-server",
|
16
|
-
help="CinchDB API server",
|
17
|
-
add_completion=False,
|
18
|
-
)
|
19
|
-
console = Console()
|
20
|
-
|
21
|
-
|
22
|
-
@cli.command()
|
23
|
-
def serve(
|
24
|
-
host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
|
25
|
-
port: int = typer.Option(8000, "--port", "-p", help="Port to bind to"),
|
26
|
-
reload: bool = typer.Option(False, "--reload", "-r", help="Enable auto-reload"),
|
27
|
-
project_dir: Optional[Path] = typer.Option(
|
28
|
-
None, "--project-dir", "-d", help="Project directory"
|
29
|
-
),
|
30
|
-
create_key: bool = typer.Option(
|
31
|
-
False, "--create-key", help="Create an API key on startup"
|
32
|
-
),
|
33
|
-
):
|
34
|
-
"""Start the CinchDB API server."""
|
35
|
-
# Find project directory
|
36
|
-
if project_dir:
|
37
|
-
project_path = Path(project_dir)
|
38
|
-
else:
|
39
|
-
project_path = get_project_root(Path.cwd())
|
40
|
-
|
41
|
-
if not project_path or not (project_path / ".cinchdb").exists():
|
42
|
-
console.print("[red]❌ No CinchDB project found[/red]")
|
43
|
-
console.print("[yellow]Run 'cinch init' to create a project[/yellow]")
|
44
|
-
raise typer.Exit(1)
|
45
|
-
|
46
|
-
console.print("[green]Starting CinchDB API server[/green]")
|
47
|
-
console.print(f"Project: {project_path}")
|
48
|
-
console.print(f"Host: {host}:{port}")
|
49
|
-
|
50
|
-
# Create API key if requested
|
51
|
-
if create_key:
|
52
|
-
manager = APIKeyManager(project_path)
|
53
|
-
api_key = manager.create_key("Initial API Key", permissions="write")
|
54
|
-
console.print("\n[bold green]Created API key:[/bold green]")
|
55
|
-
console.print(f"[yellow]Key: {api_key.key}[/yellow]")
|
56
|
-
console.print(f"[yellow]Permissions: {api_key.permissions}[/yellow]")
|
57
|
-
console.print("\n[bold]Save this key - it won't be shown again![/bold]\n")
|
58
|
-
|
59
|
-
# Start server
|
60
|
-
uvicorn.run(
|
61
|
-
"cinchdb.api.app:app", host=host, port=port, reload=reload, log_level="info"
|
62
|
-
)
|
63
|
-
|
64
|
-
|
65
|
-
@cli.command()
|
66
|
-
def create_key(
|
67
|
-
name: str = typer.Argument(..., help="Name for the API key"),
|
68
|
-
permissions: str = typer.Option(
|
69
|
-
"read", "--permissions", "-p", help="Permissions (read/write)"
|
70
|
-
),
|
71
|
-
project_dir: Optional[Path] = typer.Option(
|
72
|
-
None, "--project-dir", "-d", help="Project directory"
|
73
|
-
),
|
74
|
-
):
|
75
|
-
"""Create a new API key."""
|
76
|
-
# Find project directory
|
77
|
-
if project_dir:
|
78
|
-
project_path = Path(project_dir)
|
79
|
-
else:
|
80
|
-
project_path = get_project_root(Path.cwd())
|
81
|
-
|
82
|
-
if not project_path or not (project_path / ".cinchdb").exists():
|
83
|
-
console.print("[red]❌ No CinchDB project found[/red]")
|
84
|
-
raise typer.Exit(1)
|
85
|
-
|
86
|
-
if permissions not in ["read", "write"]:
|
87
|
-
console.print("[red]❌ Invalid permissions. Must be 'read' or 'write'[/red]")
|
88
|
-
raise typer.Exit(1)
|
89
|
-
|
90
|
-
manager = APIKeyManager(project_path)
|
91
|
-
api_key = manager.create_key(name, permissions=permissions)
|
92
|
-
|
93
|
-
console.print(f"[green]✅ Created API key '{name}'[/green]")
|
94
|
-
console.print(f"[yellow]Key: {api_key.key}[/yellow]")
|
95
|
-
console.print(f"[yellow]Permissions: {api_key.permissions}[/yellow]")
|
96
|
-
console.print("\n[bold]Save this key - it won't be shown again![/bold]")
|
97
|
-
|
98
|
-
|
99
|
-
@cli.command()
|
100
|
-
def list_keys(
|
101
|
-
project_dir: Optional[Path] = typer.Option(
|
102
|
-
None, "--project-dir", "-d", help="Project directory"
|
103
|
-
),
|
104
|
-
):
|
105
|
-
"""List all API keys."""
|
106
|
-
# Find project directory
|
107
|
-
if project_dir:
|
108
|
-
project_path = Path(project_dir)
|
109
|
-
else:
|
110
|
-
project_path = get_project_root(Path.cwd())
|
111
|
-
|
112
|
-
if not project_path or not (project_path / ".cinchdb").exists():
|
113
|
-
console.print("[red]❌ No CinchDB project found[/red]")
|
114
|
-
raise typer.Exit(1)
|
115
|
-
|
116
|
-
manager = APIKeyManager(project_path)
|
117
|
-
keys = manager.list_keys()
|
118
|
-
|
119
|
-
if not keys:
|
120
|
-
console.print("[yellow]No API keys found[/yellow]")
|
121
|
-
return
|
122
|
-
|
123
|
-
console.print("\n[bold]API Keys:[/bold]")
|
124
|
-
for key in keys:
|
125
|
-
status = "[green]Active[/green]" if key.active else "[red]Revoked[/red]"
|
126
|
-
branches = (
|
127
|
-
f"Branches: {', '.join(key.branches)}" if key.branches else "All branches"
|
128
|
-
)
|
129
|
-
console.print(f"\n{key.name} - {status}")
|
130
|
-
console.print(f" Key: {key.key}")
|
131
|
-
console.print(f" Permissions: {key.permissions}")
|
132
|
-
console.print(f" {branches}")
|
133
|
-
console.print(f" Created: {key.created_at}")
|
134
|
-
|
135
|
-
|
136
|
-
if __name__ == "__main__":
|
137
|
-
cli()
|
cinchdb/api/routers/__init__.py
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
"""API routers package."""
|
2
|
-
|
3
|
-
from cinchdb.api.routers import (
|
4
|
-
auth,
|
5
|
-
projects,
|
6
|
-
databases,
|
7
|
-
branches,
|
8
|
-
tenants,
|
9
|
-
tables,
|
10
|
-
columns,
|
11
|
-
views,
|
12
|
-
query,
|
13
|
-
)
|
14
|
-
|
15
|
-
__all__ = [
|
16
|
-
"auth",
|
17
|
-
"projects",
|
18
|
-
"databases",
|
19
|
-
"branches",
|
20
|
-
"tenants",
|
21
|
-
"tables",
|
22
|
-
"columns",
|
23
|
-
"views",
|
24
|
-
"query",
|
25
|
-
]
|
cinchdb/api/routers/auth.py
DELETED
@@ -1,135 +0,0 @@
|
|
1
|
-
"""Authentication router for CinchDB API."""
|
2
|
-
|
3
|
-
from typing import List
|
4
|
-
from fastapi import APIRouter, Depends, HTTPException
|
5
|
-
from pydantic import BaseModel
|
6
|
-
|
7
|
-
from cinchdb.api.auth import (
|
8
|
-
APIKeyManager,
|
9
|
-
AuthContext,
|
10
|
-
verify_api_key,
|
11
|
-
get_current_project,
|
12
|
-
)
|
13
|
-
from pathlib import Path
|
14
|
-
|
15
|
-
|
16
|
-
router = APIRouter()
|
17
|
-
|
18
|
-
|
19
|
-
class CreateAPIKeyRequest(BaseModel):
|
20
|
-
"""Request model for creating API key."""
|
21
|
-
|
22
|
-
name: str
|
23
|
-
permissions: str = "read" # "read" or "write"
|
24
|
-
branches: List[str] = None
|
25
|
-
|
26
|
-
|
27
|
-
class APIKeyResponse(BaseModel):
|
28
|
-
"""Response model for API key."""
|
29
|
-
|
30
|
-
key: str
|
31
|
-
name: str
|
32
|
-
created_at: str
|
33
|
-
permissions: str
|
34
|
-
branches: List[str] = None
|
35
|
-
active: bool
|
36
|
-
|
37
|
-
|
38
|
-
@router.post("/keys", response_model=APIKeyResponse)
|
39
|
-
async def create_api_key(
|
40
|
-
request: CreateAPIKeyRequest,
|
41
|
-
project_dir: Path = Depends(get_current_project),
|
42
|
-
auth: AuthContext = Depends(verify_api_key),
|
43
|
-
):
|
44
|
-
"""Create a new API key (requires existing API key with write permission)."""
|
45
|
-
# Only write permission can create new keys
|
46
|
-
if auth.api_key.permissions != "write":
|
47
|
-
raise HTTPException(
|
48
|
-
status_code=403, detail="Write permission required to create API keys"
|
49
|
-
)
|
50
|
-
|
51
|
-
manager = APIKeyManager(project_dir)
|
52
|
-
|
53
|
-
# Validate permissions
|
54
|
-
if request.permissions not in ["read", "write"]:
|
55
|
-
raise HTTPException(
|
56
|
-
status_code=400, detail="Invalid permissions. Must be 'read' or 'write'"
|
57
|
-
)
|
58
|
-
|
59
|
-
api_key = manager.create_key(
|
60
|
-
name=request.name, permissions=request.permissions, branches=request.branches
|
61
|
-
)
|
62
|
-
|
63
|
-
return APIKeyResponse(
|
64
|
-
key=api_key.key,
|
65
|
-
name=api_key.name,
|
66
|
-
created_at=api_key.created_at.isoformat(),
|
67
|
-
permissions=api_key.permissions,
|
68
|
-
branches=api_key.branches,
|
69
|
-
active=api_key.active,
|
70
|
-
)
|
71
|
-
|
72
|
-
|
73
|
-
@router.get("/keys", response_model=List[APIKeyResponse])
|
74
|
-
async def list_api_keys(
|
75
|
-
project_dir: Path = Depends(get_current_project),
|
76
|
-
auth: AuthContext = Depends(verify_api_key),
|
77
|
-
):
|
78
|
-
"""List all API keys (requires write permission)."""
|
79
|
-
# Only write permission can list keys
|
80
|
-
if auth.api_key.permissions != "write":
|
81
|
-
raise HTTPException(
|
82
|
-
status_code=403, detail="Write permission required to list API keys"
|
83
|
-
)
|
84
|
-
|
85
|
-
manager = APIKeyManager(project_dir)
|
86
|
-
keys = manager.list_keys()
|
87
|
-
|
88
|
-
return [
|
89
|
-
APIKeyResponse(
|
90
|
-
key=k.key,
|
91
|
-
name=k.name,
|
92
|
-
created_at=k.created_at.isoformat(),
|
93
|
-
permissions=k.permissions,
|
94
|
-
branches=k.branches,
|
95
|
-
active=k.active,
|
96
|
-
)
|
97
|
-
for k in keys
|
98
|
-
]
|
99
|
-
|
100
|
-
|
101
|
-
@router.delete("/keys/{key}")
|
102
|
-
async def revoke_api_key(
|
103
|
-
key: str,
|
104
|
-
project_dir: Path = Depends(get_current_project),
|
105
|
-
auth: AuthContext = Depends(verify_api_key),
|
106
|
-
):
|
107
|
-
"""Revoke an API key (requires write permission)."""
|
108
|
-
# Only write permission can revoke keys
|
109
|
-
if auth.api_key.permissions != "write":
|
110
|
-
raise HTTPException(
|
111
|
-
status_code=403, detail="Write permission required to revoke API keys"
|
112
|
-
)
|
113
|
-
|
114
|
-
# Cannot revoke your own key
|
115
|
-
if auth.api_key.key == key:
|
116
|
-
raise HTTPException(status_code=400, detail="Cannot revoke your own API key")
|
117
|
-
|
118
|
-
manager = APIKeyManager(project_dir)
|
119
|
-
if not manager.revoke_key(key):
|
120
|
-
raise HTTPException(status_code=404, detail="API key not found")
|
121
|
-
|
122
|
-
return {"message": "API key revoked"}
|
123
|
-
|
124
|
-
|
125
|
-
@router.get("/me")
|
126
|
-
async def get_current_key_info(auth: AuthContext = Depends(verify_api_key)):
|
127
|
-
"""Get information about the current API key."""
|
128
|
-
return APIKeyResponse(
|
129
|
-
key=auth.api_key.key,
|
130
|
-
name=auth.api_key.name,
|
131
|
-
created_at=auth.api_key.created_at.isoformat(),
|
132
|
-
permissions=auth.api_key.permissions,
|
133
|
-
branches=auth.api_key.branches,
|
134
|
-
active=auth.api_key.active,
|
135
|
-
)
|
cinchdb/api/routers/branches.py
DELETED
@@ -1,368 +0,0 @@
|
|
1
|
-
"""Branches router for CinchDB API."""
|
2
|
-
|
3
|
-
from typing import List, Optional, Dict, Any
|
4
|
-
from fastapi import APIRouter, Depends, HTTPException, Query
|
5
|
-
from pydantic import BaseModel
|
6
|
-
from datetime import datetime
|
7
|
-
|
8
|
-
from cinchdb.config import Config
|
9
|
-
from cinchdb.core.database import CinchDB
|
10
|
-
from cinchdb.managers.merge_manager import MergeError
|
11
|
-
from cinchdb.managers.change_comparator import ChangeComparator
|
12
|
-
from cinchdb.models import Change
|
13
|
-
from cinchdb.api.auth import (
|
14
|
-
AuthContext,
|
15
|
-
require_write_permission,
|
16
|
-
require_read_permission,
|
17
|
-
)
|
18
|
-
|
19
|
-
|
20
|
-
router = APIRouter()
|
21
|
-
|
22
|
-
|
23
|
-
class BranchInfo(BaseModel):
|
24
|
-
"""Branch information."""
|
25
|
-
|
26
|
-
name: str
|
27
|
-
parent: Optional[str]
|
28
|
-
created_at: datetime
|
29
|
-
is_active: bool
|
30
|
-
tenant_count: int
|
31
|
-
|
32
|
-
|
33
|
-
class CreateBranchRequest(BaseModel):
|
34
|
-
"""Request to create a branch."""
|
35
|
-
|
36
|
-
name: str
|
37
|
-
source: str = "main"
|
38
|
-
|
39
|
-
|
40
|
-
class MergeBranchRequest(BaseModel):
|
41
|
-
"""Request to merge branches."""
|
42
|
-
|
43
|
-
source: str
|
44
|
-
target: str
|
45
|
-
force: bool = False
|
46
|
-
|
47
|
-
|
48
|
-
class BranchComparisonResult(BaseModel):
|
49
|
-
"""Result of branch comparison."""
|
50
|
-
|
51
|
-
source_branch: str
|
52
|
-
target_branch: str
|
53
|
-
source_only_changes: int
|
54
|
-
target_only_changes: int
|
55
|
-
common_ancestor: Optional[str]
|
56
|
-
can_fast_forward: bool
|
57
|
-
|
58
|
-
|
59
|
-
class MergeCheckResult(BaseModel):
|
60
|
-
"""Result of merge feasibility check."""
|
61
|
-
|
62
|
-
can_merge: bool
|
63
|
-
reason: Optional[str] = None
|
64
|
-
merge_type: Optional[str] = None
|
65
|
-
changes_to_merge: Optional[int] = None
|
66
|
-
target_changes: Optional[int] = None
|
67
|
-
conflicts: Optional[List[Dict[str, Any]]] = None
|
68
|
-
|
69
|
-
|
70
|
-
@router.get("/", response_model=List[BranchInfo])
|
71
|
-
async def list_branches(
|
72
|
-
database: str = Query(..., description="Database name"),
|
73
|
-
auth: AuthContext = Depends(require_read_permission),
|
74
|
-
):
|
75
|
-
"""List all branches in a database."""
|
76
|
-
config = Config(auth.project_dir)
|
77
|
-
config_data = config.load()
|
78
|
-
|
79
|
-
db_name = database
|
80
|
-
|
81
|
-
try:
|
82
|
-
db = CinchDB(
|
83
|
-
database=db_name, branch="main", tenant="main", project_dir=auth.project_dir
|
84
|
-
)
|
85
|
-
branches = db.branches.list_branches()
|
86
|
-
|
87
|
-
result = []
|
88
|
-
for branch in branches:
|
89
|
-
# Count tenants
|
90
|
-
tenants_dir = (
|
91
|
-
auth.project_dir
|
92
|
-
/ ".cinchdb"
|
93
|
-
/ "databases"
|
94
|
-
/ db_name
|
95
|
-
/ "branches"
|
96
|
-
/ branch.name
|
97
|
-
/ "tenants"
|
98
|
-
)
|
99
|
-
tenant_count = (
|
100
|
-
len(list(tenants_dir.glob("*.db"))) if tenants_dir.exists() else 0
|
101
|
-
)
|
102
|
-
|
103
|
-
result.append(
|
104
|
-
BranchInfo(
|
105
|
-
name=branch.name,
|
106
|
-
parent=branch.parent_branch,
|
107
|
-
created_at=branch.metadata.get("created_at", "Unknown"),
|
108
|
-
is_active=branch.name == config_data.active_branch
|
109
|
-
and db_name == config_data.active_database,
|
110
|
-
tenant_count=tenant_count,
|
111
|
-
)
|
112
|
-
)
|
113
|
-
|
114
|
-
return result
|
115
|
-
|
116
|
-
except ValueError as e:
|
117
|
-
raise HTTPException(status_code=404, detail=str(e))
|
118
|
-
|
119
|
-
|
120
|
-
@router.post("/")
|
121
|
-
async def create_branch(
|
122
|
-
request: CreateBranchRequest,
|
123
|
-
database: str = Query(..., description="Database name"),
|
124
|
-
auth: AuthContext = Depends(require_write_permission),
|
125
|
-
):
|
126
|
-
"""Create a new branch."""
|
127
|
-
db_name = database
|
128
|
-
|
129
|
-
# Check branch permissions
|
130
|
-
if auth.api_key.branches and request.source not in auth.api_key.branches:
|
131
|
-
raise HTTPException(
|
132
|
-
status_code=403,
|
133
|
-
detail=f"Access denied for source branch '{request.source}'",
|
134
|
-
)
|
135
|
-
|
136
|
-
if auth.api_key.branches and request.name not in auth.api_key.branches:
|
137
|
-
raise HTTPException(
|
138
|
-
status_code=403,
|
139
|
-
detail=f"Cannot create branch '{request.name}' - not in allowed branches",
|
140
|
-
)
|
141
|
-
|
142
|
-
try:
|
143
|
-
db = CinchDB(
|
144
|
-
database=db_name, branch="main", tenant="main", project_dir=auth.project_dir
|
145
|
-
)
|
146
|
-
branch = db.branches.create_branch(request.name, request.source)
|
147
|
-
|
148
|
-
return {
|
149
|
-
"message": f"Created branch '{request.name}' from '{request.source}'",
|
150
|
-
"branch": {
|
151
|
-
"name": branch.name,
|
152
|
-
"parent": branch.parent_branch,
|
153
|
-
"created_at": branch.metadata.get("created_at","Unknown"),
|
154
|
-
},
|
155
|
-
}
|
156
|
-
|
157
|
-
except ValueError as e:
|
158
|
-
raise HTTPException(status_code=400, detail=str(e))
|
159
|
-
|
160
|
-
|
161
|
-
@router.delete("/{name}")
|
162
|
-
async def delete_branch(
|
163
|
-
name: str,
|
164
|
-
database: str = Query(..., description="Database name"),
|
165
|
-
auth: AuthContext = Depends(require_write_permission),
|
166
|
-
):
|
167
|
-
"""Delete a branch."""
|
168
|
-
if name == "main":
|
169
|
-
raise HTTPException(status_code=400, detail="Cannot delete the main branch")
|
170
|
-
|
171
|
-
config = Config(auth.project_dir)
|
172
|
-
config_data = config.load()
|
173
|
-
|
174
|
-
db_name = database
|
175
|
-
|
176
|
-
# Check branch permissions
|
177
|
-
if auth.api_key.branches and name not in auth.api_key.branches:
|
178
|
-
raise HTTPException(
|
179
|
-
status_code=403, detail=f"Access denied for branch '{name}'"
|
180
|
-
)
|
181
|
-
|
182
|
-
try:
|
183
|
-
db = CinchDB(
|
184
|
-
database=db_name, branch="main", tenant="main", project_dir=auth.project_dir
|
185
|
-
)
|
186
|
-
db.branches.delete_branch(name)
|
187
|
-
|
188
|
-
# If this was the active branch, switch to main
|
189
|
-
if config_data.active_branch == name and config_data.active_database == db_name:
|
190
|
-
config_data.active_branch = "main"
|
191
|
-
config.save(config_data)
|
192
|
-
|
193
|
-
return {"message": f"Deleted branch '{name}'"}
|
194
|
-
|
195
|
-
except ValueError as e:
|
196
|
-
raise HTTPException(status_code=404, detail=str(e))
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
@router.get("/{source}/compare/{target}", response_model=BranchComparisonResult)
|
201
|
-
async def compare_branches(
|
202
|
-
source: str,
|
203
|
-
target: str,
|
204
|
-
database: str = Query(..., description="Database name"),
|
205
|
-
auth: AuthContext = Depends(require_read_permission),
|
206
|
-
):
|
207
|
-
"""Compare two branches to see their differences."""
|
208
|
-
db_name = database
|
209
|
-
|
210
|
-
# Check branch permissions
|
211
|
-
await require_read_permission(auth, source)
|
212
|
-
await require_read_permission(auth, target)
|
213
|
-
|
214
|
-
try:
|
215
|
-
comparator = ChangeComparator(auth.project_dir, db_name)
|
216
|
-
|
217
|
-
# Get divergent changes
|
218
|
-
source_only, target_only = comparator.get_divergent_changes(source, target)
|
219
|
-
|
220
|
-
# Find common ancestor
|
221
|
-
common_ancestor = comparator.find_common_ancestor(source, target)
|
222
|
-
|
223
|
-
# Check if fast-forward merge is possible
|
224
|
-
can_fast_forward = comparator.can_fast_forward_merge(source, target)
|
225
|
-
|
226
|
-
return BranchComparisonResult(
|
227
|
-
source_branch=source,
|
228
|
-
target_branch=target,
|
229
|
-
source_only_changes=len(source_only),
|
230
|
-
target_only_changes=len(target_only),
|
231
|
-
common_ancestor=common_ancestor,
|
232
|
-
can_fast_forward=can_fast_forward,
|
233
|
-
)
|
234
|
-
|
235
|
-
except ValueError as e:
|
236
|
-
raise HTTPException(status_code=404, detail=str(e))
|
237
|
-
|
238
|
-
|
239
|
-
@router.get("/{source}/can-merge/{target}", response_model=MergeCheckResult)
|
240
|
-
async def check_merge_feasibility(
|
241
|
-
source: str,
|
242
|
-
target: str,
|
243
|
-
database: str = Query(..., description="Database name"),
|
244
|
-
auth: AuthContext = Depends(require_read_permission),
|
245
|
-
):
|
246
|
-
"""Check if source branch can be merged into target branch."""
|
247
|
-
db_name = database
|
248
|
-
|
249
|
-
# Check branch permissions
|
250
|
-
await require_read_permission(auth, source)
|
251
|
-
await require_read_permission(auth, target)
|
252
|
-
|
253
|
-
try:
|
254
|
-
db = CinchDB(
|
255
|
-
database=db_name, branch="main", tenant="main", project_dir=auth.project_dir
|
256
|
-
)
|
257
|
-
result = db.merge.can_merge(source, target)
|
258
|
-
|
259
|
-
return MergeCheckResult(**result)
|
260
|
-
|
261
|
-
except ValueError as e:
|
262
|
-
raise HTTPException(status_code=404, detail=str(e))
|
263
|
-
|
264
|
-
|
265
|
-
@router.post("/{source}/merge/{target}")
|
266
|
-
async def merge_branches(
|
267
|
-
source: str,
|
268
|
-
target: str,
|
269
|
-
database: str = Query(..., description="Database name"),
|
270
|
-
force: bool = Query(False, description="Force merge even with conflicts"),
|
271
|
-
dry_run: bool = Query(False, description="Preview merge without executing"),
|
272
|
-
auth: AuthContext = Depends(require_write_permission),
|
273
|
-
):
|
274
|
-
"""Merge source branch into target branch."""
|
275
|
-
db_name = database
|
276
|
-
|
277
|
-
# Check branch permissions for both branches
|
278
|
-
await require_write_permission(auth, source)
|
279
|
-
await require_write_permission(auth, target)
|
280
|
-
|
281
|
-
try:
|
282
|
-
db = CinchDB(
|
283
|
-
database=db_name, branch="main", tenant="main", project_dir=auth.project_dir
|
284
|
-
)
|
285
|
-
|
286
|
-
if target == "main":
|
287
|
-
# Use the merge_into_main method for main branch protection
|
288
|
-
result = db.merge.merge_into_main(source, force=force, dry_run=dry_run)
|
289
|
-
else:
|
290
|
-
# Use internal merge method for non-main branches
|
291
|
-
result = db.merge._merge_branches_internal(
|
292
|
-
source, target, force=force, dry_run=dry_run
|
293
|
-
)
|
294
|
-
|
295
|
-
return result
|
296
|
-
|
297
|
-
except MergeError as e:
|
298
|
-
raise HTTPException(status_code=409, detail=str(e))
|
299
|
-
except ValueError as e:
|
300
|
-
raise HTTPException(status_code=400, detail=str(e))
|
301
|
-
|
302
|
-
|
303
|
-
@router.post("/{source}/merge-into-main")
|
304
|
-
async def merge_into_main_branch(
|
305
|
-
source: str,
|
306
|
-
database: str = Query(..., description="Database name"),
|
307
|
-
force: bool = Query(False, description="Force merge even with conflicts"),
|
308
|
-
dry_run: bool = Query(False, description="Preview merge without executing"),
|
309
|
-
auth: AuthContext = Depends(require_write_permission),
|
310
|
-
):
|
311
|
-
"""Merge source branch into the main branch with additional protections."""
|
312
|
-
db_name = database
|
313
|
-
|
314
|
-
# Check branch permissions
|
315
|
-
await require_write_permission(auth, source)
|
316
|
-
await require_write_permission(auth, "main")
|
317
|
-
|
318
|
-
try:
|
319
|
-
db = CinchDB(
|
320
|
-
database=db_name, branch="main", tenant="main", project_dir=auth.project_dir
|
321
|
-
)
|
322
|
-
result = db.merge.merge_into_main(source, force=force, dry_run=dry_run)
|
323
|
-
|
324
|
-
return result
|
325
|
-
|
326
|
-
except MergeError as e:
|
327
|
-
raise HTTPException(status_code=409, detail=str(e))
|
328
|
-
except ValueError as e:
|
329
|
-
raise HTTPException(status_code=400, detail=str(e))
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
@router.get("/{branch}/changes", response_model=Dict[str, Any])
|
334
|
-
async def list_branch_changes(
|
335
|
-
branch: str,
|
336
|
-
database: str = Query(..., description="Database name"),
|
337
|
-
auth: AuthContext = Depends(require_read_permission),
|
338
|
-
):
|
339
|
-
"""List all changes in a branch."""
|
340
|
-
db_name = database
|
341
|
-
|
342
|
-
# Check branch permissions
|
343
|
-
if auth.api_key.branches and branch not in auth.api_key.branches:
|
344
|
-
raise HTTPException(
|
345
|
-
status_code=403, detail=f"Access denied for branch '{branch}'"
|
346
|
-
)
|
347
|
-
|
348
|
-
try:
|
349
|
-
from cinchdb.managers.change_tracker import ChangeTracker
|
350
|
-
|
351
|
-
tracker = ChangeTracker(auth.project_dir, db_name, branch)
|
352
|
-
changes = tracker.get_changes()
|
353
|
-
|
354
|
-
# Convert Change objects to dicts for JSON serialization
|
355
|
-
changes_data = []
|
356
|
-
for change in changes:
|
357
|
-
change_dict = change.model_dump()
|
358
|
-
# Convert datetime to string for JSON serialization
|
359
|
-
if change_dict.get("created_at") and hasattr(change_dict["created_at"], "isoformat"):
|
360
|
-
change_dict["created_at"] = change_dict["created_at"].isoformat()
|
361
|
-
if change_dict.get("updated_at") and hasattr(change_dict.get("updated_at"), "isoformat"):
|
362
|
-
change_dict["updated_at"] = change_dict["updated_at"].isoformat()
|
363
|
-
changes_data.append(change_dict)
|
364
|
-
|
365
|
-
return {"changes": changes_data}
|
366
|
-
|
367
|
-
except ValueError as e:
|
368
|
-
raise HTTPException(status_code=404, detail=str(e))
|