diff options
| author | soryu <soryu@soryu.co> | 2026-01-15 03:37:44 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-15 03:37:44 +0000 |
| commit | 764bd28d08ceaef03cd4050f9568a62d77bbcfca (patch) | |
| tree | dbd83ea7d213902f2b8021acc98798b6f3545946 /makima/src/db/repository.rs | |
| parent | eeafe072bc6bb81459f7d087b48fc921afe9cc11 (diff) | |
| download | soryu-764bd28d08ceaef03cd4050f9568a62d77bbcfca.tar.gz soryu-764bd28d08ceaef03cd4050f9568a62d77bbcfca.zip | |
Add repository history feature to store and suggest previously used repositories (#18)
- Add repository_history table migration with repo_type, repo_path, use_count, last_used_at
- Add RepositoryHistoryEntry model and CRUD database functions
- Create API endpoints: GET/POST/DELETE /api/v1/repository-history, GET /api/v1/repository-history/suggestions
- Update add_remote_repository and add_local_repository handlers to automatically track history
- Update frontend API with repository history types and functions
- Add Repository History section to Settings page with list of entries and delete functionality
- Add suggestions dropdown to RepositoryPanel when entering new repository URL/path
- Suggestions filter by repo type (remote vs local) and match on user input
Test plan:
- Add a remote repository to a contract - verify it appears in Settings history
- Add a local repository to a contract - verify it appears in Settings history
- Add same repository again - verify use_count increments, not duplicate
- When adding new repository, verify suggestions appear based on history
- Delete a history entry from Settings - verify it's removed
- Verify suggestions only show matching type (remote for remote, local for local)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/src/db/repository.rs')
| -rw-r--r-- | makima/src/db/repository.rs | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 7933f1e..2f28c1a 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -3005,3 +3005,169 @@ pub async fn get_daemon_task_assignment( .fetch_optional(pool) .await } + +// ============================================================================ +// Repository History Functions +// ============================================================================ + +use super::models::RepositoryHistoryEntry; + +/// List all repository history entries for an owner, ordered by use_count DESC, last_used_at DESC. +pub async fn list_repository_history_for_owner( + pool: &PgPool, + owner_id: Uuid, +) -> Result<Vec<RepositoryHistoryEntry>, sqlx::Error> { + sqlx::query_as::<_, RepositoryHistoryEntry>( + r#" + SELECT id, owner_id, name, repository_url, local_path, source_type, use_count, last_used_at, created_at + FROM repository_history + WHERE owner_id = $1 + ORDER BY use_count DESC, last_used_at DESC + "#, + ) + .bind(owner_id) + .fetch_all(pool) + .await +} + +/// Get repository suggestions for an owner, optionally filtered by source type and query. +pub async fn get_repository_suggestions( + pool: &PgPool, + owner_id: Uuid, + source_type: Option<&str>, + query: Option<&str>, + limit: i32, +) -> Result<Vec<RepositoryHistoryEntry>, sqlx::Error> { + // Build query dynamically based on filters + let mut sql = String::from( + r#" + SELECT id, owner_id, name, repository_url, local_path, source_type, use_count, last_used_at, created_at + FROM repository_history + WHERE owner_id = $1 + "#, + ); + + let mut param_idx = 2; + + if source_type.is_some() { + sql.push_str(&format!(" AND source_type = ${}", param_idx)); + param_idx += 1; + } + + if query.is_some() { + sql.push_str(&format!( + " AND (LOWER(name) LIKE ${} OR LOWER(COALESCE(repository_url, '')) LIKE ${} OR LOWER(COALESCE(local_path, '')) LIKE ${})", + param_idx, param_idx, param_idx + )); + param_idx += 1; + } + + sql.push_str(&format!( + " ORDER BY use_count DESC, last_used_at DESC LIMIT ${}", + param_idx + )); + + // Build and execute query with the appropriate bindings + let mut query_builder = sqlx::query_as::<_, RepositoryHistoryEntry>(&sql).bind(owner_id); + + if let Some(st) = source_type { + query_builder = query_builder.bind(st); + } + + if let Some(q) = query { + let search_pattern = format!("%{}%", q.to_lowercase()); + query_builder = query_builder.bind(search_pattern); + } + + query_builder = query_builder.bind(limit); + + query_builder.fetch_all(pool).await +} + +/// Add or update a repository history entry. +/// If an entry with the same URL (for remote) or path (for local) already exists, +/// increment use_count and update last_used_at and name. +/// Otherwise, create a new entry. +pub async fn add_or_update_repository_history( + pool: &PgPool, + owner_id: Uuid, + name: &str, + repository_url: Option<&str>, + local_path: Option<&str>, + source_type: &str, +) -> Result<RepositoryHistoryEntry, sqlx::Error> { + // Use UPSERT (INSERT ... ON CONFLICT) + if source_type == "remote" { + let url = repository_url.ok_or_else(|| { + sqlx::Error::Protocol("repository_url required for remote type".to_string()) + })?; + + sqlx::query_as::<_, RepositoryHistoryEntry>( + r#" + INSERT INTO repository_history (owner_id, name, repository_url, local_path, source_type, use_count, last_used_at) + VALUES ($1, $2, $3, NULL, $4, 1, NOW()) + ON CONFLICT (owner_id, repository_url) WHERE source_type = 'remote' AND repository_url IS NOT NULL + DO UPDATE SET + name = EXCLUDED.name, + use_count = repository_history.use_count + 1, + last_used_at = NOW() + RETURNING id, owner_id, name, repository_url, local_path, source_type, use_count, last_used_at, created_at + "#, + ) + .bind(owner_id) + .bind(name) + .bind(url) + .bind(source_type) + .fetch_one(pool) + .await + } else if source_type == "local" { + let path = local_path.ok_or_else(|| { + sqlx::Error::Protocol("local_path required for local type".to_string()) + })?; + + sqlx::query_as::<_, RepositoryHistoryEntry>( + r#" + INSERT INTO repository_history (owner_id, name, repository_url, local_path, source_type, use_count, last_used_at) + VALUES ($1, $2, NULL, $3, $4, 1, NOW()) + ON CONFLICT (owner_id, local_path) WHERE source_type = 'local' AND local_path IS NOT NULL + DO UPDATE SET + name = EXCLUDED.name, + use_count = repository_history.use_count + 1, + last_used_at = NOW() + RETURNING id, owner_id, name, repository_url, local_path, source_type, use_count, last_used_at, created_at + "#, + ) + .bind(owner_id) + .bind(name) + .bind(path) + .bind(source_type) + .fetch_one(pool) + .await + } else { + Err(sqlx::Error::Protocol(format!( + "Invalid source_type: {}", + source_type + ))) + } +} + +/// Delete a repository history entry. +/// Returns true if an entry was deleted, false if not found. +pub async fn delete_repository_history( + pool: &PgPool, + id: Uuid, + owner_id: Uuid, +) -> Result<bool, sqlx::Error> { + let result = sqlx::query( + r#" + DELETE FROM repository_history + WHERE id = $1 AND owner_id = $2 + "#, + ) + .bind(id) + .bind(owner_id) + .execute(pool) + .await?; + + Ok(result.rows_affected() > 0) +} |
