Skip to content

Commit 0f01651

Browse files
committed
Migrate Project::path to be git_dir.
1 parent a7c1211 commit 0f01651

File tree

10 files changed

+95
-43
lines changed

10 files changed

+95
-43
lines changed

Cargo.lock

Lines changed: 0 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src/lib/project/project.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type Project = {
1616
title: string;
1717
description?: string;
1818
path: string;
19+
git_dir?: string,
1920
api?: CloudProject & {
2021
sync: boolean;
2122
sync_code: boolean | undefined;

crates/but-worktrees/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ doctest = false
1010

1111
[dependencies]
1212
anyhow.workspace = true
13-
but-db.workspace = true
1413
but-workspace.workspace = true
1514
but-graph.workspace = true
1615
but-status.workspace = true
@@ -24,7 +23,6 @@ gitbutler-oxidize.workspace = true
2423
gitbutler-workspace.workspace = true
2524
gitbutler-branch-actions.workspace = true
2625
serde.workspace = true
27-
serde_json.workspace = true
2826
uuid.workspace = true
2927
bstr.workspace = true
3028
tracing.workspace = true

crates/but/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ rmcp.workspace = true
3131
command-group = { version = "5.0.1", features = ["with-tokio"] }
3232
sysinfo = "0.37.1"
3333
gitbutler-project.workspace = true
34-
gitbutler-repo.workspace = true
3534
gix.workspace = true
3635
but-core.workspace = true
3736
but-api.workspace = true

crates/but/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,11 +274,11 @@ fn get_or_init_project(
274274
) -> anyhow::Result<gitbutler_project::Project> {
275275
let repo = gix::discover(current_dir)?;
276276
if let Some(path) = repo.workdir() {
277-
let project = match gitbutler_project::Project::find_by_path(path) {
277+
let project = match gitbutler_project::Project::find_by_worktree_dir(path) {
278278
Ok(p) => Ok(p),
279279
Err(_e) => {
280280
crate::init::repo(path, false, false)?;
281-
gitbutler_project::Project::find_by_path(path)
281+
gitbutler_project::Project::find_by_worktree_dir(path)
282282
}
283283
}?;
284284
Ok(project)

crates/but/src/rub/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ fn get_all_files_in_display_order(ctx: &mut CommandContext) -> anyhow::Result<Ve
265265
use bstr::BString;
266266
use but_hunk_assignment::HunkAssignment;
267267

268-
let project = gitbutler_project::Project::find_by_path(&ctx.project().worktree_dir())?;
268+
let project = gitbutler_project::Project::find_by_worktree_dir(&ctx.project().worktree_dir())?;
269269
let changes =
270270
but_core::diff::ui::worktree_changes_by_worktree_dir(project.worktree_dir())?.changes;
271271
let (assignments, _) =

crates/gitbutler-project/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ edition = "2024"
55
authors = ["GitButler <[email protected]>"]
66
publish = false
77
rust-version = "1.89"
8+
89
[dependencies]
910
anyhow = "1.0.100"
1011
parking_lot = { workspace = true, features = ["arc_lock"] }
@@ -18,7 +19,6 @@ gitbutler-storage.workspace = true
1819
but-core.workspace = true
1920
git2.workspace = true
2021
gix = { workspace = true, features = ["dirwalk", "credentials", "parallel"] }
21-
uuid.workspace = true
2222
strum = { version = "0.27", features = ["derive"] }
2323
tracing.workspace = true
2424
resolve-path = "0.1.0"
@@ -29,4 +29,3 @@ fslock = "0.2.1"
2929
[dev-dependencies]
3030
gitbutler-testsupport.workspace = true
3131
tempfile.workspace = true
32-
tokio = { workspace = true, features = ["rt-multi-thread", "rt", "macros"] }

crates/gitbutler-project/src/controller.rs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,34 +60,37 @@ impl Controller {
6060
}
6161
}
6262

63-
pub(crate) fn add<P: AsRef<Path>>(&self, path: P) -> Result<AddProjectOutcome> {
64-
let path = path.as_ref();
63+
pub(crate) fn add(&self, worktree_dir: impl AsRef<Path>) -> Result<AddProjectOutcome> {
64+
let worktree_dir = worktree_dir.as_ref();
6565
let all_projects = self
6666
.projects_storage
6767
.list()
6868
.context("failed to list projects from storage")?;
6969
if let Some(existing_project) = all_projects
7070
.iter()
71-
.find(|project| project.worktree_dir() == path)
71+
.find(|project| project.worktree_dir() == worktree_dir)
7272
{
7373
return Ok(AddProjectOutcome::AlreadyExists(
7474
existing_project.to_owned(),
7575
));
7676
}
77-
if !path.exists() {
77+
if !worktree_dir.exists() {
7878
return Ok(AddProjectOutcome::PathNotFound);
7979
}
80-
if !path.is_dir() {
80+
if !worktree_dir.is_dir() {
8181
return Ok(AddProjectOutcome::NotADirectory);
8282
}
83-
match gix::open_opts(path, gix::open::Options::isolated()) {
83+
let resolved_path = gix::path::realpath(worktree_dir)?;
84+
// Make sure the repo is opened from the resolved path - it must be absolute for persistence.
85+
let repo = match gix::open_opts(&resolved_path, gix::open::Options::isolated()) {
8486
Ok(repo) if repo.is_bare() => {
8587
return Ok(AddProjectOutcome::BareRepository);
8688
}
8789
Ok(repo) if repo.worktree().is_some_and(|wt| !wt.is_main()) => {
88-
if path.join(".git").is_file() {
90+
if worktree_dir.join(".git").is_file() {
8991
return Ok(AddProjectOutcome::NonMainWorktree);
9092
};
93+
repo
9194
}
9295
Ok(repo) => match repo.workdir() {
9396
None => {
@@ -97,25 +100,25 @@ impl Controller {
97100
if !wd.join(".git").is_dir() {
98101
return Ok(AddProjectOutcome::NoDotGitDirectory);
99102
}
103+
repo
100104
}
101105
},
102106
Err(err) => {
103107
return Ok(AddProjectOutcome::NotAGitRepository(err.to_string()));
104108
}
105-
}
109+
};
106110

107111
let id = ProjectId::generate();
108112

109113
// Resolve the path first to get the actual directory name
110-
let resolved_path = gix::path::realpath(path)?;
111-
let title_is_not_normal_component = path
114+
let title_is_not_normal_component = worktree_dir
112115
.components()
113116
.next_back()
114117
.is_none_or(|c| !matches!(c, Component::Normal(_)));
115118
let path_for_title = if title_is_not_normal_component {
116119
&resolved_path
117120
} else {
118-
path
121+
worktree_dir
119122
};
120123

121124
let title = path_for_title.file_name().map_or_else(
@@ -126,8 +129,10 @@ impl Controller {
126129
let project = Project {
127130
id,
128131
title,
132+
// TODO(1.0): make this always `None`, until the field can be removed for good.
129133
worktree_dir: resolved_path,
130134
api: None,
135+
git_dir: repo.git_dir().to_owned(),
131136
..Default::default()
132137
};
133138

@@ -201,6 +206,7 @@ impl Controller {
201206
fn get_inner(&self, id: ProjectId, validate: bool) -> Result<Project> {
202207
#[cfg_attr(not(windows), allow(unused_mut))]
203208
let mut project = self.projects_storage.get(id)?;
209+
// BACKWARD-COMPATIBLE MIGRATION
204210
if validate {
205211
let worktree_dir = project.worktree_dir();
206212
if gix::open_opts(&worktree_dir, gix::open::Options::isolated()).is_err() {
@@ -217,13 +223,14 @@ impl Controller {
217223
}
218224
}
219225

226+
project.migrate()?;
220227
if !project.gb_dir().exists()
221228
&& let Err(error) = std::fs::create_dir_all(project.gb_dir())
222229
{
223230
tracing::error!(project_id = %project.id, ?error, "failed to create \"{}\" on project get", project.gb_dir().display());
224231
}
225232
// Clean up old virtual_branches.toml that was never used
226-
let old_virtual_branches_path = project.git_dir()?.join("virtual_branches.toml");
233+
let old_virtual_branches_path = project.git_dir().join("virtual_branches.toml");
227234
if old_virtual_branches_path.exists()
228235
&& let Err(error) = std::fs::remove_file(old_virtual_branches_path)
229236
{

crates/gitbutler-project/src/project.rs

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{
2-
path::{self, Path, PathBuf},
2+
path::{Path, PathBuf},
33
time,
44
};
55

@@ -14,7 +14,7 @@ use crate::default_true::DefaultTrue;
1414
pub enum AuthKey {
1515
GitCredentialsHelper,
1616
Local {
17-
private_key_path: path::PathBuf,
17+
private_key_path: PathBuf,
1818
},
1919
// There used to be more auth option variants that we are deprecating and replacing with this
2020
#[serde(other)]
@@ -78,8 +78,20 @@ pub struct Project {
7878
pub title: String,
7979
pub description: Option<String>,
8080
/// The worktree directory of the project's repository.
81+
// TODO: Make it optional for bare repo support!
82+
// TODO: Do not actually store it, but obtain it on the fly by using a repository!
8183
#[serde(rename = "path")]
82-
pub(crate) worktree_dir: path::PathBuf,
84+
// TODO(1.0): enable the line below to clear the value from storage - we only want the git dir,
85+
// but need to remain compatible. The frontend shouldn't care, so we may need a specific type for that
86+
// which already exists, but… needs cleanup.
87+
// #[serde(skip_serializing_if = "Option::is_none")]
88+
pub(crate) worktree_dir: PathBuf,
89+
/// The storage location of the Git repository itself.
90+
/// This is the only value we need to access everything related to the Git repository.
91+
///
92+
// TODO(1.0): remove the `default` which is just needed while there is project files without it.
93+
#[serde(default)]
94+
pub(crate) git_dir: PathBuf,
8395
#[serde(default)]
8496
pub preferred_key: AuthKey,
8597
/// if ok_with_force_push is true, we'll not try to avoid force pushing
@@ -117,6 +129,8 @@ impl Project {
117129
worktree_dir,
118130
..Default::default()
119131
}
132+
.migrated()
133+
.unwrap()
120134
}
121135

122136
/// A special constructor needed as `worktree_dir` isn't accessible anymore.
@@ -126,6 +140,28 @@ impl Project {
126140
preferred_key,
127141
..Default::default()
128142
}
143+
.migrated()
144+
.unwrap()
145+
}
146+
}
147+
148+
impl Project {
149+
pub(crate) fn migrated(mut self) -> anyhow::Result<Self> {
150+
self.migrate()?;
151+
Ok(self)
152+
}
153+
154+
pub(crate) fn migrate(&mut self) -> anyhow::Result<()> {
155+
if !self.git_dir.as_os_str().is_empty() {
156+
return Ok(());
157+
}
158+
let repo = gix::open_opts(&self.worktree_dir, gix::open::Options::isolated())
159+
.context("BUG: worktree is supposed to be valid here for migration")?;
160+
self.git_dir = repo.git_dir().to_owned();
161+
// NOTE: we set the worktree so the frontend is happier until this usage can be reviewed,
162+
// probably for supporting bare repositories.
163+
self.worktree_dir = repo.workdir().context("BUG: we currently only support non-bare repos, yet this one didn't have a worktree dir")?.to_owned();
164+
Ok(())
129165
}
130166
}
131167

@@ -143,7 +179,8 @@ impl Project {
143179
})
144180
}
145181
/// Finds an existing project by its path. Errors out if not found.
146-
pub fn find_by_path(path: &Path) -> anyhow::Result<Project> {
182+
// TODO: find by git-dir instead!
183+
pub fn find_by_worktree_dir(worktree_dir: &Path) -> anyhow::Result<Project> {
147184
let mut projects = crate::list()?;
148185
// Sort projects by longest pathname to shortest.
149186
// We need to do this because users might have one gitbutler project
@@ -157,10 +194,12 @@ impl Project {
157194
// longest first
158195
.reverse()
159196
});
160-
let resolved_path = if path.is_relative() {
161-
path.canonicalize().context("Failed to canonicalize path")?
197+
let resolved_path = if worktree_dir.is_relative() {
198+
worktree_dir
199+
.canonicalize()
200+
.context("Failed to canonicalize path")?
162201
} else {
163-
path.to_path_buf()
202+
worktree_dir.to_path_buf()
164203
};
165204
let project = projects
166205
.into_iter()
@@ -176,6 +215,20 @@ impl Project {
176215
}
177216
}
178217

218+
/// Repository helpers.
219+
impl Project {
220+
/// Open an isolated repository, one that didn't read options beyond `.git/config` and
221+
/// knows no environment variables.
222+
///
223+
/// Use it for fastest-possible access, when incomplete configuration is acceptable.
224+
pub fn open_isolated(&self) -> anyhow::Result<gix::Repository> {
225+
Ok(gix::open_opts(
226+
&self.git_dir,
227+
gix::open::Options::isolated(),
228+
)?)
229+
}
230+
}
231+
179232
impl Project {
180233
/// Determines if the project Operations log will be synched with the GitButHub
181234
pub fn oplog_sync_enabled(&self) -> bool {
@@ -207,34 +260,34 @@ impl Project {
207260
///
208261
/// Normally this is `.git/gitbutler` in the project's repository.
209262
pub fn gb_dir(&self) -> PathBuf {
210-
// TODO(ST): store the gitdir instead. This needs a migration to switch existing `worktree_dir` fields over.
211-
self.worktree_dir.join(".git").join("gitbutler")
263+
self.git_dir.join("gitbutler")
212264
}
213265

214266
pub fn snapshot_lines_threshold(&self) -> usize {
215267
self.snapshot_lines_threshold.unwrap_or(20)
216268
}
217269

218-
// TODO(ST): for bare repo support, make this optional, but store the gitdir instead.
270+
// TODO(ST): Actually remove this - people should use the `gix::Repository` for worktree handling (which makes it optional, too)
219271
pub fn worktree_dir(&self) -> PathBuf {
220272
self.worktree_dir.clone()
221273
}
222274

223275
/// Set the worktree directory to `worktree_dir`.
224-
pub fn set_worktree_dir(&mut self, worktree_dir: path::PathBuf) {
276+
pub fn set_worktree_dir(&mut self, worktree_dir: PathBuf) -> anyhow::Result<()> {
277+
let repo = gix::open_opts(&worktree_dir, gix::open::Options::isolated())?;
225278
self.worktree_dir = worktree_dir;
279+
self.git_dir = repo.git_dir().to_owned();
280+
Ok(())
226281
}
227282

228283
/// Return the path to the directory that holds the repository data and that is associated with the current worktree.
229-
// TODO(ST): store this directory in future, as everything else can be obtained from it: worktree_dir, common_dir.
230-
pub fn git_dir(&self) -> anyhow::Result<path::PathBuf> {
231-
let repo = gix::open_opts(&self.worktree_dir, gix::open::Options::isolated())?;
232-
Ok(repo.git_dir().to_owned())
284+
pub fn git_dir(&self) -> &Path {
285+
&self.git_dir
233286
}
234287

235288
/// Return the path to the Git directory of the 'prime' repository, the one that holds all worktrees.
236-
pub fn common_git_dir(&self) -> anyhow::Result<path::PathBuf> {
237-
let repo = gix::open_opts(&self.worktree_dir, gix::open::Options::isolated())?;
289+
pub fn common_git_dir(&self) -> anyhow::Result<PathBuf> {
290+
let repo = self.open_isolated()?;
238291
Ok(repo.common_dir().to_owned())
239292
}
240293
}

crates/gitbutler-project/src/storage.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ impl Storage {
9797
}
9898

9999
if let Some(path) = &update_request.path {
100-
project.set_worktree_dir(path.clone());
100+
project.set_worktree_dir(path.clone())?;
101101
}
102102

103103
if let Some(api) = &update_request.api {

0 commit comments

Comments
 (0)