Skip to content

Commit

Permalink
Handle external files
Browse files Browse the repository at this point in the history
  • Loading branch information
gschier committed Feb 8, 2025
1 parent 266892d commit c6289f1
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 33 deletions.
2 changes: 1 addition & 1 deletion src-tauri/yaak-git/bindings/gen_git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, };

Expand Down
6 changes: 3 additions & 3 deletions src-tauri/yaak-git/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -217,7 +217,7 @@ pub fn git_status(dir: &Path) -> Result<GitStatusSummary> {
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,
Expand All @@ -232,7 +232,7 @@ pub fn git_status(dir: &Path) -> Result<GitStatusSummary> {
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,
Expand Down
23 changes: 13 additions & 10 deletions src-tauri/yaak-sync/src/models.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -23,26 +23,29 @@ pub enum SyncModel {
}

impl SyncModel {
pub fn from_bytes(
content: Vec<u8>,
file_path: &Path,
) -> Result<Option<(SyncModel, Vec<u8>, String)>> {
pub fn from_bytes(content: Vec<u8>, file_path: &Path) -> Result<Option<(SyncModel, String)>> {
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<Option<(SyncModel, Vec<u8>, String)>> {
pub fn from_file(file_path: &Path) -> Result<Option<(SyncModel, String)>> {
let content = match fs::read(file_path) {
Ok(c) => c,
Err(_) => return Ok(None),
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/yaak-sync/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ pub(crate) async fn get_fs_candidates(dir: &Path) -> Result<Vec<FsCandidate>> {
};

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) => {
Expand Down
93 changes: 76 additions & 17 deletions src-web/components/GitCommitDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -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 (
<div className="grid grid-rows-1 h-full">
<SplitLayout
Expand All @@ -123,6 +141,16 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
firstSlot={({ style }) => (
<div style={style} className="h-full overflow-y-auto -ml-1 pb-3">
<TreeNodeChildren node={tree} depth={0} onCheck={checkNode} />
{externalEntries.length > 0 && (
<Separator className="mt-3 mb-1">External file changes</Separator>
)}
{externalEntries.map((entry) => (
<ExternalTreeNode
key={entry.relaPath + entry.status}
entry={entry}
onCheck={checkEntry}
/>
))}
</div>
)}
secondSlot={({ style }) => (
Expand Down Expand Up @@ -211,17 +239,13 @@ function TreeNodeChildren({
) : (
<span aria-hidden />
)}
<div className="truncate">
{fallbackRequestName(node.model)}
{/*({node.model.model})*/}
{/*({node.status.staged ? 'Y' : 'N'})*/}
</div>
<div className="truncate">{fallbackRequestName(node.model)}</div>
{node.status.status !== 'current' && (
<InlineCode
className={classNames(
'py-0 ml-auto bg-transparent w-[6rem] text-center',
node.status.status === 'modified' && 'text-info',
node.status.status === 'added' && 'text-success',
node.status.status === 'untracked' && 'text-success',
node.status.status === 'removed' && 'text-danger',
)}
>
Expand All @@ -247,6 +271,41 @@ function TreeNodeChildren({
);
}

function ExternalTreeNode({
entry,
onCheck,
}: {
entry: GitStatusEntry;
onCheck: (entry: GitStatusEntry) => void;
}) {
return (
<Checkbox
fullWidth
className="h-xs w-full hover:bg-surface-highlight rounded px-1 group"
checked={entry.staged}
onChange={() => onCheck(entry)}
title={
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] gap-1 w-full items-center">
<Icon color="secondary" icon="file_code" />
<div className="truncate">{entry.relaPath}</div>
{entry.status !== 'current' && (
<InlineCode
className={classNames(
'py-0 ml-auto bg-transparent w-[6rem] text-center',
entry.status === 'modified' && 'text-info',
entry.status === 'untracked' && 'text-success',
entry.status === 'removed' && 'text-danger',
)}
>
{entry.status}
</InlineCode>
)}
</div>
}
/>
);
}

function nodeCheckedStatus(root: TreeNode): CheckboxProps['checked'] {
let numVisited = 0;
let numChecked = 0;
Expand Down
2 changes: 1 addition & 1 deletion src-web/components/HttpAuthenticationEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 && (
<Dropdown
items={authConfig.data.actions.map(
(a): DropdownItem => ({
Expand Down
2 changes: 2 additions & 0 deletions src-web/init/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {}) {
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit c6289f1

Please sign in to comment.