Skip to content

Commit 96382f2

Browse files
committed
Create re-usable file service for reading git blobs
- decouples reading from workspace from reading from commit
1 parent f2a8c14 commit 96382f2

File tree

9 files changed

+115
-75
lines changed

9 files changed

+115
-75
lines changed

apps/desktop/src/lib/file/FileDiff.svelte

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
11
<script lang="ts">
2-
import { invoke } from '$lib/backend/ipc';
32
import { Project } from '$lib/backend/projects';
3+
import { FileService } from '$lib/files/fileService';
44
import HunkViewer from '$lib/hunk/HunkViewer.svelte';
55
import InfoMessage from '$lib/shared/InfoMessage.svelte';
66
import LargeDiffMessage from '$lib/shared/LargeDiffMessage.svelte';
77
import { computeAddedRemovedByHunk } from '$lib/utils/metrics';
88
import { getLocalCommits, getLocalAndRemoteCommits } from '$lib/vbranches/contexts';
99
import { getLockText } from '$lib/vbranches/tooltip';
1010
import { getContext } from '@gitbutler/shared/context';
11+
import type { FileInfo } from '$lib/files/file';
1112
import type { HunkSection, ContentSection } from '$lib/utils/fileSections';
1213
13-
interface FileInfo {
14-
content: string;
15-
name?: string;
16-
mimeType?: string;
17-
size?: number;
18-
}
19-
2014
interface Props {
2115
filePath: string;
2216
isBinary: boolean;
@@ -43,6 +37,7 @@
4337
4438
let alwaysShow = $state(false);
4539
const project = getContext(Project);
40+
const fileService = getContext(FileService);
4641
const localCommits = isFileLocked ? getLocalCommits() : undefined;
4742
const remoteCommits = isFileLocked ? getLocalAndRemoteCommits() : undefined;
4843
@@ -80,18 +75,15 @@
8075
});
8176
8277
async function fetchBlobInfo() {
78+
if (!isBinary) {
79+
return;
80+
}
8381
try {
84-
const fetchedFileInfo: FileInfo = await invoke('get_blob_info', {
85-
relativePath: filePath,
86-
projectId: project.id,
87-
commitId
88-
});
89-
fileInfo = fetchedFileInfo;
90-
91-
// If file.size > 5mb; don't render it
92-
if (fileInfo.size && fileInfo.size > 5 * 1024 * 1024) {
93-
isLarge = true;
94-
}
82+
const file = commitId
83+
? await fileService.readFromCommit(filePath, project.id, commitId)
84+
: await fileService.readFromWorkspace(filePath, project.id);
85+
fileInfo = file.data;
86+
isLarge = file.isLarge;
9587
} catch (error) {
9688
console.error(error);
9789
}

apps/desktop/src/lib/files/file.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type FileInfo = {
2+
content: string;
3+
name?: string;
4+
mimeType?: string;
5+
size?: number;
6+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Tauri } from '$lib/backend/tauri';
2+
import type { FileInfo } from './file';
3+
4+
export class FileService {
5+
constructor(private tauri: Tauri) {}
6+
7+
async readFromWorkspace(filePath: string, projectId: string) {
8+
const data: FileInfo = await this.tauri.invoke('get_workspace_file', {
9+
relativePath: filePath,
10+
projectId: projectId
11+
});
12+
return {
13+
data,
14+
isLarge: isLarge(data.size)
15+
};
16+
}
17+
18+
async readFromCommit(filePath: string, projectId: string, commitId: string | undefined) {
19+
const data: FileInfo = await this.tauri.invoke('get_commit_file', {
20+
relativePath: filePath,
21+
projectId: projectId,
22+
commitId
23+
});
24+
return {
25+
data,
26+
isLarge: isLarge(data.size)
27+
};
28+
}
29+
}
30+
31+
function isLarge(size: number | undefined) {
32+
return size && size > 5 * 1024 * 1024 ? true : false;
33+
}

apps/desktop/src/routes/+layout.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { PromptService } from '$lib/backend/prompt';
1010
import { Tauri } from '$lib/backend/tauri';
1111
import { UpdaterService } from '$lib/backend/updater';
1212
import { loadAppSettings } from '$lib/config/appSettings';
13+
import { FileService } from '$lib/files/fileService';
1314
import { RemotesService } from '$lib/remotes/service';
1415
import { RustSecretService } from '$lib/secrets/secretsService';
1516
import { TokenMemoryService } from '$lib/stores/tokenMemoryService';
@@ -45,7 +46,8 @@ export const load: LayoutLoad = async () => {
4546
const tokenMemoryService = new TokenMemoryService();
4647
const httpClient = new HttpClient(window.fetch, PUBLIC_API_BASE_URL, tokenMemoryService.token);
4748
const authService = new AuthService();
48-
const updaterService = new UpdaterService(new Tauri(), posthog);
49+
const tauri = new Tauri();
50+
const updaterService = new UpdaterService(tauri, posthog);
4951
const promptService = new PromptService();
5052

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

6366
return {
6467
commandService,
@@ -77,6 +80,8 @@ export const load: LayoutLoad = async () => {
7780
lineManagerFactory,
7881
stackingLineManagerFactory,
7982
secretsService,
80-
posthog
83+
posthog,
84+
tauri,
85+
fileService
8186
};
8287
};

apps/desktop/src/routes/[projectId]/+layout.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import { showHistoryView } from '$lib/config/config';
1919
import { cloudFunctionality } from '$lib/config/uiFeatureFlags';
2020
import { StackingReorderDropzoneManagerFactory } from '$lib/dragging/stackingReorderDropzoneManager';
21+
import { FileService } from '$lib/files/fileService';
2122
import { DefaultForgeFactory } from '$lib/forge/forgeFactory';
2223
import { octokitFromAccessToken } from '$lib/forge/github/octokit';
2324
import { createForgeStore } from '$lib/forge/interface/forge';
@@ -93,6 +94,7 @@
9394
setContext(SyncedSnapshotService, data.syncedSnapshotService);
9495
setContext(CloudBranchesService, data.cloudBranchesService);
9596
setContext(CloudBranchCreationService, data.cloudBranchCreationService);
97+
setContext(FileService, data.fileService);
9698
});
9799
98100
const routesService = getRoutesService();

crates/gitbutler-repo/src/commands.rs

Lines changed: 39 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,14 @@ pub trait RepoCommands {
102102
fn get_local_config(&self, key: &str) -> Result<Option<String>>;
103103
fn set_local_config(&self, key: &str, value: &str) -> Result<()>;
104104
fn check_signing_settings(&self) -> Result<bool>;
105-
/// Read `probably_relative_path` in the following order:
105+
106+
/// Read `path` from the tree of the given commit.
107+
///
108+
/// Bails when given an absolute path since that would suggest we are looking for a file in
109+
/// the workspace. Returns `FileInfo::default()` if file could not be found.
110+
fn read_file_from_commit(&self, commit_id: Oid, path: &Path) -> Result<FileInfo>;
111+
112+
/// Read `path` in the following order:
106113
///
107114
/// * worktree
108115
/// * index
@@ -111,16 +118,8 @@ pub trait RepoCommands {
111118
/// This order makes sense if you imagine that deleted files are shown, like in a `git status`,
112119
/// so we want to know what's deleted.
113120
///
114-
/// If `probably_relative_path` is absolute, we will assure it's in the worktree.
115-
/// If `treeish` is given, it will only be read from the given tree.
116-
///
117-
/// If nothing could be found at `probably_relative_path`, the returned structure indicates this,
118-
/// but it's no error.
119-
fn read_file_from_workspace(
120-
&self,
121-
treeish: Option<Oid>,
122-
probably_relative_path: &Path,
123-
) -> Result<FileInfo>;
121+
/// Returns `FileInfo::default()` if file could not be found.
122+
fn read_file_from_workspace(&self, path: &Path) -> Result<FileInfo>;
124123
}
125124

126125
impl RepoCommands for Project {
@@ -182,23 +181,32 @@ impl RepoCommands for Project {
182181
Ok(())
183182
}
184183

185-
fn read_file_from_workspace(
186-
&self,
187-
treeish: Option<Oid>,
188-
probably_relative_path: &Path,
189-
) -> Result<FileInfo> {
184+
fn read_file_from_commit(&self, commit_id: Oid, relative_path: &Path) -> Result<FileInfo> {
185+
if !relative_path.is_relative() {
186+
bail!(
187+
"Refusing to read '{:?}' from commit {:?} as it's not relative to the worktree",
188+
relative_path,
189+
commit_id
190+
);
191+
}
192+
190193
let ctx = CommandContext::open(self)?;
191194
let repo = ctx.repo();
195+
let tree = repo.find_commit(commit_id)?.tree()?;
192196

193-
if let Some(treeish) = treeish {
194-
if !probably_relative_path.is_relative() {
195-
bail!(
196-
"Refusing to read '{}' from tree as it's not relative to the worktree",
197-
probably_relative_path.display(),
198-
);
197+
Ok(match tree.get_path(relative_path) {
198+
Ok(entry) => {
199+
let blob = repo.find_blob(entry.id())?;
200+
FileInfo::from_content(relative_path, blob.content())
199201
}
200-
return read_file_from_tree(repo, Some(treeish), probably_relative_path);
201-
}
202+
Err(e) if e.code() == git2::ErrorCode::NotFound => FileInfo::deleted(),
203+
Err(e) => return Err(e.into()),
204+
})
205+
}
206+
207+
fn read_file_from_workspace(&self, probably_relative_path: &Path) -> Result<FileInfo> {
208+
let ctx = CommandContext::open(self)?;
209+
let repo = ctx.repo();
202210

203211
let (path_in_worktree, relative_path) = if probably_relative_path.is_relative() {
204212
(
@@ -234,34 +242,20 @@ impl RepoCommands for Project {
234242
}
235243
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
236244
match repo.index()?.get_path(&relative_path, 0) {
245+
// Read file that has been deleted and not staged for commit.
237246
Some(entry) => {
238247
let blob = repo.find_blob(entry.id)?;
239248
FileInfo::from_content(&relative_path, blob.content())
240249
}
241-
None => read_file_from_tree(repo, None, &relative_path)?,
250+
// Read file that has been deleted and staged for commit. Note that file not
251+
// found returns FileInfo::default() rather than an error.
252+
None => self.read_file_from_commit(
253+
repo.head()?.peel_to_commit()?.id(),
254+
&relative_path,
255+
)?,
242256
}
243257
}
244258
Err(err) => return Err(err.into()),
245259
})
246260
}
247261
}
248-
249-
fn read_file_from_tree(
250-
repo: &git2::Repository,
251-
treeish: Option<Oid>,
252-
relative_path: &Path,
253-
) -> Result<FileInfo> {
254-
let tree = if let Some(id) = treeish {
255-
repo.find_object(id, None)?.peel_to_tree()?
256-
} else {
257-
repo.head()?.peel_to_tree()?
258-
};
259-
Ok(match tree.get_path(relative_path) {
260-
Ok(entry) => {
261-
let blob = repo.find_blob(entry.id())?;
262-
FileInfo::from_content(relative_path, blob.content())
263-
}
264-
Err(e) if e.code() == git2::ErrorCode::NotFound => FileInfo::deleted(),
265-
Err(e) => return Err(e.into()),
266-
})
267-
}

crates/gitbutler-tauri/src/forge.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub mod commands {
4949
.into());
5050
}
5151
Ok(project
52-
.read_file_from_workspace(None, relative_path)?
52+
.read_file_from_workspace(relative_path)?
5353
.content
5454
.context("PR template was not valid UTF-8")?)
5555
}

crates/gitbutler-tauri/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ fn main() {
172172
repo::commands::check_signing_settings,
173173
repo::commands::git_clone_repository,
174174
repo::commands::get_uncommited_files,
175-
repo::commands::get_blob_info,
175+
repo::commands::get_commit_file,
176+
repo::commands::get_workspace_file,
176177
virtual_branches::commands::list_virtual_branches,
177178
virtual_branches::commands::create_virtual_branch,
178179
virtual_branches::commands::delete_local_branch,

crates/gitbutler-tauri/src/repo.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
pub mod commands {
22
use crate::error::{Error, UnmarkedError};
33
use anyhow::Result;
4-
use git2::Oid;
54
use gitbutler_branch_actions::RemoteBranchFile;
65
use gitbutler_project as projects;
76
use gitbutler_project::ProjectId;
@@ -72,17 +71,25 @@ pub mod commands {
7271

7372
#[tauri::command(async)]
7473
#[instrument(skip(projects))]
75-
pub fn get_blob_info(
74+
pub fn get_commit_file(
7675
projects: State<'_, projects::Controller>,
7776
project_id: ProjectId,
7877
relative_path: &Path,
79-
commit_id: Option<String>,
78+
commit_id: String,
8079
) -> Result<FileInfo, Error> {
8180
let project = projects.get(project_id)?;
82-
let commit_oid = commit_id
83-
.map(|id| Oid::from_str(&id).map_err(|e| anyhow::anyhow!(e)))
84-
.transpose()?;
81+
let commit_oid = git2::Oid::from_str(commit_id.as_ref()).map_err(anyhow::Error::from)?;
82+
Ok(project.read_file_from_commit(commit_oid, relative_path)?)
83+
}
8584

86-
Ok(project.read_file_from_workspace(commit_oid, relative_path)?)
85+
#[tauri::command(async)]
86+
#[instrument(skip(projects))]
87+
pub fn get_workspace_file(
88+
projects: State<'_, projects::Controller>,
89+
project_id: ProjectId,
90+
relative_path: &Path,
91+
) -> Result<FileInfo, Error> {
92+
let project = projects.get(project_id)?;
93+
Ok(project.read_file_from_workspace(relative_path)?)
8794
}
8895
}

0 commit comments

Comments
 (0)