mirror of
https://github.com/KohakuBlueleaf/KohakuHub.git
synced 2026-07-15 18:25:09 -05:00
495 lines
10 KiB
Markdown
495 lines
10 KiB
Markdown
---
|
|
title: Branches & Tags API
|
|
description: Branch/tag management, merge, revert, reset operations
|
|
icon: i-carbon-branch
|
|
---
|
|
|
|
# Branches & Tags API
|
|
|
|
Manage branches, tags, and advanced Git operations (merge, revert, reset).
|
|
|
|
---
|
|
|
|
## Branches
|
|
|
|
### Create Branch
|
|
|
|
**Pattern:** `POST /api/{repo_type}s/{namespace}/{name}/branch`
|
|
|
|
**Authentication:** Required (delete permission)
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"branch": "feature-branch",
|
|
"revision": "main"
|
|
}
|
|
```
|
|
|
|
**Fields:**
|
|
- `branch`: New branch name (required)
|
|
- `revision`: Source revision (branch/commit, default: "main")
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Branch 'feature-branch' created"
|
|
}
|
|
```
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - Branch created
|
|
- `404 Not Found` - Repository or source revision not found
|
|
- `409 Conflict` - Branch already exists
|
|
|
|
---
|
|
|
|
### Delete Branch
|
|
|
|
**Pattern:** `DELETE /api/{repo_type}s/{namespace}/{name}/branch/{branch}`
|
|
|
|
**Authentication:** Required (delete permission)
|
|
|
|
**Protection:** Cannot delete "main" branch
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Branch 'feature-branch' deleted"
|
|
}
|
|
```
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - Branch deleted
|
|
- `400 Bad Request` - Attempting to delete main branch
|
|
- `404 Not Found` - Repository or branch not found
|
|
|
|
---
|
|
|
|
### Merge Branches
|
|
|
|
**Pattern:** `POST /api/{repo_type}s/{namespace}/{name}/merge/{source_ref}/into/{destination_branch}`
|
|
|
|
**Authentication:** Required (write permission)
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"message": "Merge feature into main",
|
|
"metadata": {
|
|
"author": "alice"
|
|
},
|
|
"strategy": "dest-wins",
|
|
"force": false,
|
|
"allow_empty": false,
|
|
"squash_merge": false
|
|
}
|
|
```
|
|
|
|
**Fields (all optional):**
|
|
- `message`: Commit message
|
|
- `metadata`: Additional metadata
|
|
- `strategy`: Conflict resolution
|
|
- `"dest-wins"`: Destination wins on conflict
|
|
- `"source-wins"`: Source wins on conflict
|
|
- `null`: Fail on conflict (default)
|
|
- `force`: Force merge even with conflicts
|
|
- `allow_empty`: Allow merge with no changes
|
|
- `squash_merge`: Squash all commits into one
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Successfully merged feature into main",
|
|
"result": {
|
|
"reference": "commit_hash",
|
|
"summary": {
|
|
"added": 5,
|
|
"removed": 2,
|
|
"changed": 3
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - Merged successfully
|
|
- `409 Conflict` - Merge conflict (use strategy to resolve)
|
|
- `500 Internal Server Error` - Merge failed
|
|
|
|
**Auto-tracking:**
|
|
- LFS objects tracked automatically
|
|
- Commit recorded in database
|
|
- User attribution preserved
|
|
|
|
---
|
|
|
|
### Revert Commit
|
|
|
|
**Pattern:** `POST /api/{repo_type}s/{namespace}/{name}/branch/{branch}/revert`
|
|
|
|
**Authentication:** Required (write permission)
|
|
|
|
**Purpose:** Create new commit that undoes changes from a specific commit
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"ref": "commit_id_to_revert",
|
|
"parent_number": 1,
|
|
"message": "Revert: broken feature",
|
|
"metadata": {
|
|
"reason": "broke production"
|
|
},
|
|
"force": false,
|
|
"allow_empty": false
|
|
}
|
|
```
|
|
|
|
**Fields:**
|
|
- `ref`: Commit ID or ref to revert (required)
|
|
- `parent_number`: For merge commits (default: 1)
|
|
- `message`: Commit message (optional)
|
|
- `metadata`: Additional metadata (optional)
|
|
- `force`: Force revert even with conflicts
|
|
- `allow_empty`: Allow empty revert
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Successfully reverted commit abc123de on branch 'main'",
|
|
"new_commit_id": "new_commit_hash"
|
|
}
|
|
```
|
|
|
|
**Important Notes:**
|
|
- Creates NEW commit (doesn't delete history)
|
|
- LFS files automatically handled (no recoverability check needed)
|
|
- If conflicts: returns 409 error
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - Reverted successfully
|
|
- `404 Not Found` - Commit not found
|
|
- `409 Conflict` - Revert caused conflicts
|
|
|
|
---
|
|
|
|
### Reset Branch
|
|
|
|
**Pattern:** `POST /api/{repo_type}s/{namespace}/{name}/branch/{branch}/reset`
|
|
|
|
**Authentication:** Required (write permission)
|
|
|
|
**Purpose:** Reset branch to a specific commit (like `git reset --hard`)
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"ref": "commit_id_to_reset_to",
|
|
"message": "Reset to stable version",
|
|
"force": false
|
|
}
|
|
```
|
|
|
|
**Fields:**
|
|
- `ref`: Commit ID or ref to reset to (required)
|
|
- `message`: Commit message (optional)
|
|
- `force`: Skip LFS recoverability check
|
|
|
|
**Safety Checks:**
|
|
1. Main branch requires `force: true`
|
|
2. LFS file availability checked (unless `force: true`)
|
|
3. Creates new commit (preserves history)
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Successfully reset branch 'main' to commit abc123de (new commit created)",
|
|
"commit_id": "new_commit_hash"
|
|
}
|
|
```
|
|
|
|
**LFS Recoverability Check:**
|
|
- Validates all LFS files from target commit still exist
|
|
- Checks all commits between target and HEAD
|
|
- If files garbage-collected: returns 400 error
|
|
|
|
**Error if LFS unavailable:**
|
|
```json
|
|
{
|
|
"error": "Cannot reset to commit abc123: 5 LFS file(s) across 3 commit(s) have been garbage collected",
|
|
"missing_files": ["model.safetensors", "data.bin"],
|
|
"affected_commits": ["commit1", "commit2", "commit3"],
|
|
"recoverable": false
|
|
}
|
|
```
|
|
|
|
**Use `force: true` to bypass check (may break LFS refs)**
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - Reset successful
|
|
- `400 Bad Request` - LFS files not recoverable or main branch without force
|
|
- `404 Not Found` - Commit not found
|
|
|
|
**Implementation Details:**
|
|
- Uses diff-based approach (not destructive)
|
|
- Restores files from target commit
|
|
- Syncs File table after reset
|
|
- Records reset commit in database
|
|
|
|
---
|
|
|
|
## Tags
|
|
|
|
### Create Tag
|
|
|
|
**Pattern:** `POST /api/{repo_type}s/{namespace}/{name}/tag`
|
|
|
|
**Authentication:** Required (delete permission)
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"tag": "v1.0.0",
|
|
"revision": "main",
|
|
"message": "Release version 1.0"
|
|
}
|
|
```
|
|
|
|
**Fields:**
|
|
- `tag`: Tag name (required)
|
|
- `revision`: Source revision (default: "main")
|
|
- `message`: Tag message (optional)
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Tag 'v1.0.0' created"
|
|
}
|
|
```
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - Tag created
|
|
- `404 Not Found` - Repository or revision not found
|
|
- `500 Internal Server Error` - Failed to create tag
|
|
|
|
---
|
|
|
|
### Delete Tag
|
|
|
|
**Pattern:** `DELETE /api/{repo_type}s/{namespace}/{name}/tag/{tag}`
|
|
|
|
**Authentication:** Required (delete permission)
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Tag 'v1.0.0' deleted"
|
|
}
|
|
```
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - Tag deleted
|
|
- `404 Not Found` - Repository or tag not found
|
|
- `500 Internal Server Error` - Failed to delete tag
|
|
|
|
---
|
|
|
|
## Usage Examples
|
|
|
|
### Merge Feature Branch
|
|
|
|
```python
|
|
import requests
|
|
|
|
API_BASE = "http://localhost:28080/api"
|
|
TOKEN = "your_token"
|
|
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
|
|
|
|
# Merge feature into main
|
|
response = requests.post(
|
|
f"{API_BASE}/models/username/repo/merge/feature-branch/into/main",
|
|
json={
|
|
"message": "Merge feature: Add new model",
|
|
"strategy": "dest-wins" # Main wins on conflicts
|
|
},
|
|
headers=HEADERS
|
|
)
|
|
|
|
result = response.json()
|
|
print(f"Merged: {result['message']}")
|
|
print(f"Commit: {result['result']['reference']}")
|
|
```
|
|
|
|
---
|
|
|
|
### Revert Bad Commit
|
|
|
|
```python
|
|
# Find the bad commit
|
|
commits_resp = requests.get(
|
|
f"{API_BASE}/models/username/repo/commits/main?limit=10",
|
|
headers=HEADERS
|
|
)
|
|
commits = commits_resp.json()["commits"]
|
|
bad_commit = commits[0]["id"] # Latest commit
|
|
|
|
# Revert it
|
|
revert_resp = requests.post(
|
|
f"{API_BASE}/models/username/repo/branch/main/revert",
|
|
json={
|
|
"ref": bad_commit,
|
|
"message": "Revert: broken model weights"
|
|
},
|
|
headers=HEADERS
|
|
)
|
|
|
|
result = revert_resp.json()
|
|
print(f"Reverted: {result['new_commit_id']}")
|
|
```
|
|
|
|
---
|
|
|
|
### Reset to Previous Version
|
|
|
|
```python
|
|
# Reset to 3 commits ago
|
|
commits_resp = requests.get(
|
|
f"{API_BASE}/models/username/repo/commits/main?limit=5",
|
|
headers=HEADERS
|
|
)
|
|
commits = commits_resp.json()["commits"]
|
|
target_commit = commits[3]["id"] # 3 commits back
|
|
|
|
# Reset (with LFS check)
|
|
reset_resp = requests.post(
|
|
f"{API_BASE}/models/username/repo/branch/main/reset",
|
|
json={
|
|
"ref": target_commit,
|
|
"message": "Reset to stable version",
|
|
"force": False # Check LFS availability
|
|
},
|
|
headers=HEADERS
|
|
)
|
|
|
|
if reset_resp.status_code == 400:
|
|
# LFS files not available
|
|
error = reset_resp.json()
|
|
print(f"Cannot reset: {error['error']}")
|
|
print(f"Missing files: {error['missing_files']}")
|
|
|
|
# Force reset anyway (may break)
|
|
reset_resp = requests.post(
|
|
f"{API_BASE}/models/username/repo/branch/main/reset",
|
|
json={"ref": target_commit, "force": True},
|
|
headers=HEADERS
|
|
)
|
|
|
|
result = reset_resp.json()
|
|
print(f"Reset to: {result['commit_id']}")
|
|
```
|
|
|
|
---
|
|
|
|
### Create Release Tag
|
|
|
|
```python
|
|
# Tag current main branch
|
|
tag_resp = requests.post(
|
|
f"{API_BASE}/models/username/repo/tag",
|
|
json={
|
|
"tag": "v1.0.0",
|
|
"revision": "main",
|
|
"message": "Release version 1.0 - Production ready"
|
|
},
|
|
headers=HEADERS
|
|
)
|
|
|
|
result = tag_resp.json()
|
|
print(result["message"])
|
|
|
|
# Download specific version
|
|
file_url = f"{API_BASE}/models/username/repo/resolve/v1.0.0/model.safetensors"
|
|
```
|
|
|
|
---
|
|
|
|
## Git Command Equivalents
|
|
|
|
### Branches
|
|
|
|
```bash
|
|
# Create branch
|
|
POST /branch {"branch": "feature", "revision": "main"}
|
|
# git checkout -b feature main
|
|
|
|
# Delete branch
|
|
DELETE /branch/feature
|
|
# git branch -d feature
|
|
|
|
# Merge
|
|
POST /merge/feature/into/main {"strategy": "dest-wins"}
|
|
# git checkout main && git merge feature -X theirs
|
|
```
|
|
|
|
### Tags
|
|
|
|
```bash
|
|
# Create tag
|
|
POST /tag {"tag": "v1.0", "revision": "main"}
|
|
# git tag v1.0 main
|
|
|
|
# Delete tag
|
|
DELETE /tag/v1.0
|
|
# git tag -d v1.0
|
|
```
|
|
|
|
### Advanced Operations
|
|
|
|
```bash
|
|
# Revert
|
|
POST /branch/main/revert {"ref": "abc123"}
|
|
# git revert abc123
|
|
|
|
# Reset
|
|
POST /branch/main/reset {"ref": "abc123"}
|
|
# git reset --hard abc123 (but creates new commit)
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
**Branching Strategy:**
|
|
- Use `main` for production
|
|
- Create feature branches for development
|
|
- Merge with `dest-wins` to protect main
|
|
- Tag releases for versioning
|
|
|
|
**LFS Considerations:**
|
|
- Reset checks LFS availability (prevent broken refs)
|
|
- Use `force: true` only if you understand the risk
|
|
- Keep `lfs_keep_versions` high enough for your workflow
|
|
- Monitor storage after resets/reverts
|
|
|
|
**Safety:**
|
|
- Always use `force: false` first (see errors)
|
|
- Test merges on feature branches first
|
|
- Tag before risky operations (easy rollback)
|
|
- Document reset/revert reasons in commit message
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
- [Commits API](./commits.md) - View commit history
|
|
- [File Upload API](./file-upload.md) - Commit files
|
|
- [Repositories API](./repositories.md) - Repo management
|