From c6289f13c159bbdb14ec59c2820f524b8b6800ce Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 7 Feb 2025 22:14:40 -0800 Subject: [PATCH] Handle external files --- src-tauri/yaak-git/bindings/gen_git.ts | 2 +- src-tauri/yaak-git/src/git.rs | 6 +- src-tauri/yaak-sync/src/models.rs | 23 +++-- src-tauri/yaak-sync/src/sync.rs | 2 +- src-web/components/GitCommitDialog.tsx | 93 +++++++++++++++---- .../components/HttpAuthenticationEditor.tsx | 2 +- src-web/init/sync.ts | 2 + 7 files changed, 97 insertions(+), 33 deletions(-) diff --git a/src-tauri/yaak-git/bindings/gen_git.ts b/src-tauri/yaak-git/bindings/gen_git.ts index 3e163d4d..c410c6ad 100644 --- a/src-tauri/yaak-git/bindings/gen_git.ts +++ b/src-tauri/yaak-git/bindings/gen_git.ts @@ -5,7 +5,7 @@ export type GitAuthor = { name: string | null, email: string | null, }; export type GitCommit = { author: GitAuthor, when: string, message: string | null, }; -export type GitStatus = "added" | "conflict" | "current" | "modified" | "removed" | "renamed" | "type_change"; +export type GitStatus = "untracked" | "conflict" | "current" | "modified" | "removed" | "renamed" | "type_change"; export type GitStatusEntry = { relaPath: string, status: GitStatus, staged: boolean, prev: SyncModel | null, next: SyncModel | null, }; diff --git a/src-tauri/yaak-git/src/git.rs b/src-tauri/yaak-git/src/git.rs index 69a6143e..0d76eb41 100644 --- a/src-tauri/yaak-git/src/git.rs +++ b/src-tauri/yaak-git/src/git.rs @@ -38,7 +38,7 @@ pub struct GitStatusEntry { #[serde(rename_all = "snake_case")] #[ts(export, export_to = "gen_git.ts")] pub enum GitStatus { - Added, + Untracked, Conflict, Current, Modified, @@ -217,7 +217,7 @@ pub fn git_status(dir: &Path) -> Result { let index_status = match status { // Note: order matters here, since we're checking a bitmap! s if s.contains(git2::Status::CONFLICTED) => GitStatus::Conflict, - s if s.contains(git2::Status::INDEX_NEW) => GitStatus::Added, + s if s.contains(git2::Status::INDEX_NEW) => GitStatus::Untracked, s if s.contains(git2::Status::INDEX_MODIFIED) => GitStatus::Modified, s if s.contains(git2::Status::INDEX_DELETED) => GitStatus::Removed, s if s.contains(git2::Status::INDEX_RENAMED) => GitStatus::Renamed, @@ -232,7 +232,7 @@ pub fn git_status(dir: &Path) -> Result { let worktree_status = match status { // Note: order matters here, since we're checking a bitmap! s if s.contains(git2::Status::CONFLICTED) => GitStatus::Conflict, - s if s.contains(git2::Status::WT_NEW) => GitStatus::Added, + s if s.contains(git2::Status::WT_NEW) => GitStatus::Untracked, s if s.contains(git2::Status::WT_MODIFIED) => GitStatus::Modified, s if s.contains(git2::Status::WT_DELETED) => GitStatus::Removed, s if s.contains(git2::Status::WT_RENAMED) => GitStatus::Renamed, diff --git a/src-tauri/yaak-sync/src/models.rs b/src-tauri/yaak-sync/src/models.rs index 1463d271..e1c20a12 100644 --- a/src-tauri/yaak-sync/src/models.rs +++ b/src-tauri/yaak-sync/src/models.rs @@ -1,4 +1,4 @@ -use crate::error::Error::{InvalidSyncFile, UnknownModel}; +use crate::error::Error::UnknownModel; use crate::error::Result; use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; @@ -23,26 +23,29 @@ pub enum SyncModel { } impl SyncModel { - pub fn from_bytes( - content: Vec, - file_path: &Path, - ) -> Result, String)>> { + pub fn from_bytes(content: Vec, file_path: &Path) -> Result> { let mut hasher = Sha1::new(); hasher.update(&content); let checksum = hex::encode(hasher.finalize()); + let content_str = String::from_utf8(content.clone()).unwrap_or_default(); + + // Check for some strings that will be in a model file for sure. If these strings + // don't exist, then it's probably not a Yaak file. + if !content_str.contains("model") || !content_str.contains("id") { + return Ok(None); + } let ext = file_path.extension().unwrap_or_default(); if ext == "yml" || ext == "yaml" { - Ok(Some((serde_yaml::from_slice(content.as_slice())?, content, checksum))) + Ok(Some((serde_yaml::from_str(&content_str)?, checksum))) } else if ext == "json" { - Ok(Some((serde_json::from_reader(content.as_slice())?, content, checksum))) + Ok(Some((serde_json::from_str(&content_str)?, checksum))) } else { - let p = file_path.to_str().unwrap().to_string(); - Err(InvalidSyncFile(format!("Unknown file extension {p}"))) + Ok(None) } } - pub fn from_file(file_path: &Path) -> Result, String)>> { + pub fn from_file(file_path: &Path) -> Result> { let content = match fs::read(file_path) { Ok(c) => c, Err(_) => return Ok(None), diff --git a/src-tauri/yaak-sync/src/sync.rs b/src-tauri/yaak-sync/src/sync.rs index ce127342..3d902273 100644 --- a/src-tauri/yaak-sync/src/sync.rs +++ b/src-tauri/yaak-sync/src/sync.rs @@ -163,7 +163,7 @@ pub(crate) async fn get_fs_candidates(dir: &Path) -> Result> { }; let path = dir_entry.path(); - let (model, _, checksum) = match SyncModel::from_file(&path) { + let (model, checksum) = match SyncModel::from_file(&path) { Ok(Some(m)) => m, Ok(None) => continue, Err(e) => { diff --git a/src-web/components/GitCommitDialog.tsx b/src-web/components/GitCommitDialog.tsx index 8475958c..9b6c2716 100644 --- a/src-web/components/GitCommitDialog.tsx +++ b/src-web/components/GitCommitDialog.tsx @@ -20,6 +20,7 @@ import { Checkbox } from './core/Checkbox'; import { Icon } from './core/Icon'; import { InlineCode } from './core/InlineCode'; import { Input } from './core/Input'; +import { Separator } from './core/Separator'; import { SplitLayout } from './core/SplitLayout'; import { HStack } from './core/Stacks'; import { EmptyStateText } from './EmptyStateText'; @@ -53,18 +54,27 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { onDone(); }; - const entries = status.data?.entries ?? null; + const { internalEntries, externalEntries, allEntries } = useMemo(() => { + const allEntries = []; + const yaakEntries = []; + const externalEntries = []; + for (const entry of status.data?.entries ?? []) { + allEntries.push(entry); + if (entry.next == null && entry.prev == null) { + externalEntries.push(entry); + } else { + yaakEntries.push(entry); + } + } + return { internalEntries: yaakEntries, externalEntries, allEntries }; + }, [status.data?.entries]); - const hasAddedAnything = entries?.find((s) => s.staged) != null; - const hasAnythingToAdd = entries?.find((s) => s.status !== 'current') != null; + const hasAddedAnything = allEntries.find((e) => e.staged) != null; + const hasAnythingToAdd = allEntries.find((e) => e.status !== 'current') != null; const tree: TreeNode | null = useMemo(() => { - if (entries == null) { - return null; - } - const next = (model: TreeNode['model'], ancestors: TreeNode[]): TreeNode | null => { - const statusEntry = entries?.find((s) => s.relaPath.includes(model.id)); + const statusEntry = internalEntries?.find((s) => s.relaPath.includes(model.id)); if (statusEntry == null) { return null; } @@ -76,9 +86,11 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { ancestors, }; - for (const entry of entries) { + for (const entry of internalEntries) { const childModel = entry.next ?? entry.prev; - if (childModel == null) return null; // TODO: Is this right? + + // Should never happen because we're iterating internalEntries + if (childModel == null) continue; // TODO: Figure out why not all of these show up if ('folderId' in childModel && childModel.folderId != null) { @@ -96,8 +108,9 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { return node; }; + return next(workspace, []); - }, [entries, workspace]); + }, [workspace, internalEntries]); if (tree == null) { return null; @@ -114,6 +127,11 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { // TODO: Also ensure parents are added properly }; + const checkEntry = (entry: GitStatusEntry) => { + if (entry.staged) unstage.mutate({ relaPaths: [entry.relaPath] }); + else add.mutate({ relaPaths: [entry.relaPath] }); + }; + return (
(
+ {externalEntries.length > 0 && ( + External file changes + )} + {externalEntries.map((entry) => ( + + ))}
)} secondSlot={({ style }) => ( @@ -211,17 +239,13 @@ function TreeNodeChildren({ ) : ( )} -
- {fallbackRequestName(node.model)} - {/*({node.model.model})*/} - {/*({node.status.staged ? 'Y' : 'N'})*/} -
+
{fallbackRequestName(node.model)}
{node.status.status !== 'current' && ( @@ -247,6 +271,41 @@ function TreeNodeChildren({ ); } +function ExternalTreeNode({ + entry, + onCheck, +}: { + entry: GitStatusEntry; + onCheck: (entry: GitStatusEntry) => void; +}) { + return ( + onCheck(entry)} + title={ +
+ +
{entry.relaPath}
+ {entry.status !== 'current' && ( + + {entry.status} + + )} +
+ } + /> + ); +} + function nodeCheckedStatus(root: TreeNode): CheckboxProps['checked'] { let numVisited = 0; let numChecked = 0; diff --git a/src-web/components/HttpAuthenticationEditor.tsx b/src-web/components/HttpAuthenticationEditor.tsx index 77850bd1..76789657 100644 --- a/src-web/components/HttpAuthenticationEditor.tsx +++ b/src-web/components/HttpAuthenticationEditor.tsx @@ -58,7 +58,7 @@ export function HttpAuthenticationEditor({ request }: Props) { onChange={(disabled) => handleChange({ ...request.authentication, disabled: !disabled })} title="Enabled" /> - {authConfig.data.actions && ( + {authConfig.data.actions && authConfig.data.actions.length > 0 && ( ({ diff --git a/src-web/init/sync.ts b/src-web/init/sync.ts index 09dda736..c3387381 100644 --- a/src-web/init/sync.ts +++ b/src-web/init/sync.ts @@ -10,6 +10,7 @@ import { jotaiStore } from '../lib/jotai'; export function initSync() { initModelListeners(); initFileChangeListeners(); + sync().catch(console.error); } export async function sync({ force }: { force?: boolean } = {}) { @@ -53,6 +54,7 @@ function initFileChangeListeners() { await unsub?.(); // Unsub to previous const workspaceMeta = jotaiStore.get(workspaceMetaAtom); if (workspaceMeta == null || workspaceMeta.settingSyncDir == null) return; + debouncedSync(); // Perform an initial sync when switching workspace unsub = watchWorkspaceFiles( workspaceMeta.workspaceId, workspaceMeta.settingSyncDir,