Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create re-usable file service for reading git blobs #5884

Merged
merged 1 commit into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 11 additions & 19 deletions apps/desktop/src/lib/file/FileDiff.svelte
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
<script lang="ts">
import { invoke } from '$lib/backend/ipc';
import { Project } from '$lib/backend/projects';
import { FileService } from '$lib/files/fileService';
import HunkViewer from '$lib/hunk/HunkViewer.svelte';
import InfoMessage from '$lib/shared/InfoMessage.svelte';
import LargeDiffMessage from '$lib/shared/LargeDiffMessage.svelte';
import { computeAddedRemovedByHunk } from '$lib/utils/metrics';
import { getLocalCommits, getLocalAndRemoteCommits } from '$lib/vbranches/contexts';
import { getLockText } from '$lib/vbranches/tooltip';
import { getContext } from '@gitbutler/shared/context';
import type { FileInfo } from '$lib/files/file';
import type { HunkSection, ContentSection } from '$lib/utils/fileSections';
interface FileInfo {
content: string;
name?: string;
mimeType?: string;
size?: number;
}
interface Props {
filePath: string;
isBinary: boolean;
Expand All @@ -43,6 +37,7 @@
let alwaysShow = $state(false);
const project = getContext(Project);
const fileService = getContext(FileService);
const localCommits = isFileLocked ? getLocalCommits() : undefined;
const remoteCommits = isFileLocked ? getLocalAndRemoteCommits() : undefined;
Expand Down Expand Up @@ -80,18 +75,15 @@
});
async function fetchBlobInfo() {
if (!isBinary) {
return;
}
try {
const fetchedFileInfo: FileInfo = await invoke('get_blob_info', {
relativePath: filePath,
projectId: project.id,
commitId
});
fileInfo = fetchedFileInfo;
// If file.size > 5mb; don't render it
if (fileInfo.size && fileInfo.size > 5 * 1024 * 1024) {
isLarge = true;
}
const file = commitId
? await fileService.readFromCommit(filePath, project.id, commitId)
: await fileService.readFromWorkspace(filePath, project.id);
fileInfo = file.data;
isLarge = file.isLarge;
} catch (error) {
console.error(error);
}
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/lib/files/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type FileInfo = {
content: string;
name?: string;
mimeType?: string;
size?: number;
};
33 changes: 33 additions & 0 deletions apps/desktop/src/lib/files/fileService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Tauri } from '$lib/backend/tauri';
import type { FileInfo } from './file';

export class FileService {
constructor(private tauri: Tauri) {}

async readFromWorkspace(filePath: string, projectId: string) {
const data: FileInfo = await this.tauri.invoke('get_workspace_file', {
relativePath: filePath,
projectId: projectId
});
return {
data,
isLarge: isLarge(data.size)
};
}

async readFromCommit(filePath: string, projectId: string, commitId: string | undefined) {
const data: FileInfo = await this.tauri.invoke('get_commit_file', {
relativePath: filePath,
projectId: projectId,
commitId
});
return {
data,
isLarge: isLarge(data.size)
};
}
}

function isLarge(size: number | undefined) {
return size && size > 5 * 1024 * 1024 ? true : false;
}
9 changes: 7 additions & 2 deletions apps/desktop/src/routes/+layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PromptService } from '$lib/backend/prompt';
import { Tauri } from '$lib/backend/tauri';
import { UpdaterService } from '$lib/backend/updater';
import { loadAppSettings } from '$lib/config/appSettings';
import { FileService } from '$lib/files/fileService';
import { RemotesService } from '$lib/remotes/service';
import { RustSecretService } from '$lib/secrets/secretsService';
import { TokenMemoryService } from '$lib/stores/tokenMemoryService';
Expand Down Expand Up @@ -45,7 +46,8 @@ export const load: LayoutLoad = async () => {
const tokenMemoryService = new TokenMemoryService();
const httpClient = new HttpClient(window.fetch, PUBLIC_API_BASE_URL, tokenMemoryService.token);
const authService = new AuthService();
const updaterService = new UpdaterService(new Tauri(), posthog);
const tauri = new Tauri();
const updaterService = new UpdaterService(tauri, posthog);
const promptService = new PromptService();

const userService = new UserService(httpClient, tokenMemoryService, posthog);
Expand All @@ -59,6 +61,7 @@ export const load: LayoutLoad = async () => {
const aiPromptService = new AIPromptService();
const lineManagerFactory = new LineManagerFactory();
const stackingLineManagerFactory = new StackingLineManagerFactory();
const fileService = new FileService(tauri);

return {
commandService,
Expand All @@ -77,6 +80,8 @@ export const load: LayoutLoad = async () => {
lineManagerFactory,
stackingLineManagerFactory,
secretsService,
posthog
posthog,
tauri,
fileService
};
};
2 changes: 2 additions & 0 deletions apps/desktop/src/routes/[projectId]/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { showHistoryView } from '$lib/config/config';
import { cloudFunctionality } from '$lib/config/uiFeatureFlags';
import { StackingReorderDropzoneManagerFactory } from '$lib/dragging/stackingReorderDropzoneManager';
import { FileService } from '$lib/files/fileService';
import { DefaultForgeFactory } from '$lib/forge/forgeFactory';
import { octokitFromAccessToken } from '$lib/forge/github/octokit';
import { createForgeStore } from '$lib/forge/interface/forge';
Expand Down Expand Up @@ -93,6 +94,7 @@
setContext(SyncedSnapshotService, data.syncedSnapshotService);
setContext(CloudBranchesService, data.cloudBranchesService);
setContext(CloudBranchCreationService, data.cloudBranchCreationService);
setContext(FileService, data.fileService);
});

const routesService = getRoutesService();
Expand Down
84 changes: 39 additions & 45 deletions crates/gitbutler-repo/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,14 @@ pub trait RepoCommands {
fn get_local_config(&self, key: &str) -> Result<Option<String>>;
fn set_local_config(&self, key: &str, value: &str) -> Result<()>;
fn check_signing_settings(&self) -> Result<bool>;
/// Read `probably_relative_path` in the following order:

/// Read `path` from the tree of the given commit.
///
/// Bails when given an absolute path since that would suggest we are looking for a file in
/// the workspace. Returns `FileInfo::default()` if file could not be found.
fn read_file_from_commit(&self, commit_id: Oid, path: &Path) -> Result<FileInfo>;

/// Read `path` in the following order:
///
/// * worktree
/// * index
Expand All @@ -111,16 +118,8 @@ pub trait RepoCommands {
/// This order makes sense if you imagine that deleted files are shown, like in a `git status`,
/// so we want to know what's deleted.
///
/// If `probably_relative_path` is absolute, we will assure it's in the worktree.
/// If `treeish` is given, it will only be read from the given tree.
///
/// If nothing could be found at `probably_relative_path`, the returned structure indicates this,
/// but it's no error.
fn read_file_from_workspace(
&self,
treeish: Option<Oid>,
probably_relative_path: &Path,
) -> Result<FileInfo>;
/// Returns `FileInfo::default()` if file could not be found.
fn read_file_from_workspace(&self, path: &Path) -> Result<FileInfo>;
}

impl RepoCommands for Project {
Expand Down Expand Up @@ -182,23 +181,32 @@ impl RepoCommands for Project {
Ok(())
}

fn read_file_from_workspace(
&self,
treeish: Option<Oid>,
probably_relative_path: &Path,
) -> Result<FileInfo> {
fn read_file_from_commit(&self, commit_id: Oid, relative_path: &Path) -> Result<FileInfo> {
if !relative_path.is_relative() {
bail!(
"Refusing to read '{:?}' from commit {:?} as it's not relative to the worktree",
relative_path,
commit_id
);
}

let ctx = CommandContext::open(self)?;
let repo = ctx.repo();
let tree = repo.find_commit(commit_id)?.tree()?;

if let Some(treeish) = treeish {
if !probably_relative_path.is_relative() {
bail!(
"Refusing to read '{}' from tree as it's not relative to the worktree",
probably_relative_path.display(),
);
Ok(match tree.get_path(relative_path) {
Ok(entry) => {
let blob = repo.find_blob(entry.id())?;
FileInfo::from_content(relative_path, blob.content())
}
return read_file_from_tree(repo, Some(treeish), probably_relative_path);
}
Err(e) if e.code() == git2::ErrorCode::NotFound => FileInfo::deleted(),
Err(e) => return Err(e.into()),
})
}

fn read_file_from_workspace(&self, probably_relative_path: &Path) -> Result<FileInfo> {
let ctx = CommandContext::open(self)?;
let repo = ctx.repo();

let (path_in_worktree, relative_path) = if probably_relative_path.is_relative() {
(
Expand Down Expand Up @@ -234,34 +242,20 @@ impl RepoCommands for Project {
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
match repo.index()?.get_path(&relative_path, 0) {
// Read file that has been deleted and not staged for commit.
Some(entry) => {
let blob = repo.find_blob(entry.id)?;
FileInfo::from_content(&relative_path, blob.content())
}
None => read_file_from_tree(repo, None, &relative_path)?,
// Read file that has been deleted and staged for commit. Note that file not
// found returns FileInfo::default() rather than an error.
None => self.read_file_from_commit(
repo.head()?.peel_to_commit()?.id(),
&relative_path,
)?,
}
}
Err(err) => return Err(err.into()),
})
}
}

fn read_file_from_tree(
repo: &git2::Repository,
treeish: Option<Oid>,
relative_path: &Path,
) -> Result<FileInfo> {
let tree = if let Some(id) = treeish {
repo.find_object(id, None)?.peel_to_tree()?
} else {
repo.head()?.peel_to_tree()?
};
Ok(match tree.get_path(relative_path) {
Ok(entry) => {
let blob = repo.find_blob(entry.id())?;
FileInfo::from_content(relative_path, blob.content())
}
Err(e) if e.code() == git2::ErrorCode::NotFound => FileInfo::deleted(),
Err(e) => return Err(e.into()),
})
}
2 changes: 1 addition & 1 deletion crates/gitbutler-tauri/src/forge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub mod commands {
.into());
}
Ok(project
.read_file_from_workspace(None, relative_path)?
.read_file_from_workspace(relative_path)?
.content
.context("PR template was not valid UTF-8")?)
}
Expand Down
3 changes: 2 additions & 1 deletion crates/gitbutler-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ fn main() {
repo::commands::check_signing_settings,
repo::commands::git_clone_repository,
repo::commands::get_uncommited_files,
repo::commands::get_blob_info,
repo::commands::get_commit_file,
repo::commands::get_workspace_file,
virtual_branches::commands::list_virtual_branches,
virtual_branches::commands::create_virtual_branch,
virtual_branches::commands::delete_local_branch,
Expand Down
21 changes: 14 additions & 7 deletions crates/gitbutler-tauri/src/repo.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
pub mod commands {
use crate::error::{Error, UnmarkedError};
use anyhow::Result;
use git2::Oid;
use gitbutler_branch_actions::RemoteBranchFile;
use gitbutler_project as projects;
use gitbutler_project::ProjectId;
Expand Down Expand Up @@ -72,17 +71,25 @@ pub mod commands {

#[tauri::command(async)]
#[instrument(skip(projects))]
pub fn get_blob_info(
pub fn get_commit_file(
projects: State<'_, projects::Controller>,
project_id: ProjectId,
relative_path: &Path,
commit_id: Option<String>,
commit_id: String,
) -> Result<FileInfo, Error> {
let project = projects.get(project_id)?;
let commit_oid = commit_id
.map(|id| Oid::from_str(&id).map_err(|e| anyhow::anyhow!(e)))
.transpose()?;
let commit_oid = git2::Oid::from_str(commit_id.as_ref()).map_err(anyhow::Error::from)?;
Ok(project.read_file_from_commit(commit_oid, relative_path)?)
}

Ok(project.read_file_from_workspace(commit_oid, relative_path)?)
#[tauri::command(async)]
#[instrument(skip(projects))]
pub fn get_workspace_file(
projects: State<'_, projects::Controller>,
project_id: ProjectId,
relative_path: &Path,
) -> Result<FileInfo, Error> {
let project = projects.get(project_id)?;
Ok(project.read_file_from_workspace(relative_path)?)
}
}
Loading