summaryrefslogtreecommitdiff
path: root/makima/src/db/repository.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-15 03:37:44 +0000
committerGitHub <noreply@github.com>2026-01-15 03:37:44 +0000
commit764bd28d08ceaef03cd4050f9568a62d77bbcfca (patch)
treedbd83ea7d213902f2b8021acc98798b6f3545946 /makima/src/db/repository.rs
parenteeafe072bc6bb81459f7d087b48fc921afe9cc11 (diff)
downloadsoryu-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.rs166
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)
+}