Merge pull request #2226 from gitbutlerapp/add-is-default-to-branch

GB-781 Add is default to branch
This commit is contained in:
Nikita Galaiko 2024-01-12 08:13:34 +01:00 committed by GitHub
commit d123832636
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 374 additions and 9 deletions

View File

@ -373,6 +373,23 @@ impl TryFrom<Content> for String {
}
}
impl TryFrom<Content> for i64 {
type Error = FromError;
fn try_from(content: Content) -> Result<Self, Self::Error> {
Self::try_from(&content)
}
}
impl TryFrom<&Content> for i64 {
type Error = FromError;
fn try_from(content: &Content) -> Result<Self, Self::Error> {
let text: String = content.try_into()?;
text.parse().map_err(FromError::ParseInt)
}
}
impl TryFrom<Content> for u64 {
type Error = FromError;

View File

@ -191,6 +191,7 @@ pub fn set_base_branch(
)?,
ownership,
order: 0,
selected_for_changes: None,
};
let branch_writer =

View File

@ -40,6 +40,9 @@ pub struct Branch {
pub ownership: Ownership,
// order is the number by which UI should sort branches
pub order: usize,
// is Some(timestamp), the branch is considered a default destination for new changes.
// if more than one branch is selected, the branch with the highest timestamp wins.
pub selected_for_changes: Option<i64>,
}
impl Branch {
@ -56,6 +59,7 @@ pub struct BranchUpdateRequest {
pub ownership: Option<Ownership>,
pub order: Option<usize>,
pub upstream: Option<String>, // just the branch name, so not refs/remotes/origin/branchA, just branchA
pub selected_for_changes: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
@ -63,6 +67,7 @@ pub struct BranchCreateRequest {
pub name: Option<String>,
pub ownership: Option<Ownership>,
pub order: Option<usize>,
pub selected_for_changes: Option<bool>,
}
impl TryFrom<&crate::reader::Reader<'_>> for Branch {
@ -82,6 +87,7 @@ impl TryFrom<&crate::reader::Reader<'_>> for Branch {
"meta/created_timestamp_ms",
"meta/updated_timestamp_ms",
"meta/ownership",
"meta/selected_for_changes",
])?;
let id: String = results[0].clone()?.try_into()?;
@ -162,6 +168,23 @@ impl TryFrom<&crate::reader::Reader<'_>> for Branch {
)
})?;
let selected_for_changes = match results[12].clone() {
Ok(raw_ts) => {
let ts = raw_ts.try_into().map_err(|e| {
crate::reader::Error::Io(
std::io::Error::new(
std::io::ErrorKind::Other,
format!("meta/selected_for_changes: {}", e),
)
.into(),
)
})?;
Ok(Some(ts))
}
Err(crate::reader::Error::NotFound) => Ok(None),
Err(e) => Err(e),
}?;
Ok(Self {
id,
name,
@ -185,6 +208,7 @@ impl TryFrom<&crate::reader::Reader<'_>> for Branch {
updated_timestamp_ms,
ownership,
order,
selected_for_changes,
})
}
}

View File

@ -79,6 +79,7 @@ mod tests {
.parse()
.unwrap()],
},
selected_for_changes: Some(1),
}
}

View File

@ -105,6 +105,18 @@ impl<'writer> BranchWriter<'writer> {
branch.ownership.to_string(),
));
if let Some(selected_for_changes) = branch.selected_for_changes {
batch.push(writer::BatchTask::Write(
format!("branches/{}/meta/selected_for_changes", branch.id),
selected_for_changes.to_string(),
));
} else {
batch.push(writer::BatchTask::Remove(format!(
"branches/{}/meta/selected_for_changes",
branch.id
)));
}
self.writer.batch(&batch)?;
Ok(())
@ -170,6 +182,7 @@ mod tests {
}],
},
order: TEST_INDEX.load(Ordering::Relaxed),
selected_for_changes: Some(1),
}
}

View File

@ -106,6 +106,7 @@ mod tests {
.unwrap(),
ownership: branch::Ownership::default(),
order: TEST_INDEX.load(Ordering::Relaxed),
selected_for_changes: Some(1),
}
}

View File

@ -85,6 +85,7 @@ mod tests {
}],
},
order: TEST_INDEX.load(Ordering::Relaxed),
selected_for_changes: None,
}
}

View File

@ -146,6 +146,7 @@ mod tests {
}],
},
order: TEST_INDEX.load(Ordering::Relaxed),
selected_for_changes: None,
}
}

View File

@ -59,6 +59,7 @@ pub struct VirtualBranch {
pub base_current: bool, // is this vbranch based on the current base branch? if false, this needs to be manually merged with conflicts
pub ownership: Ownership,
pub updated_at: u128,
pub selected_for_changes: bool,
}
// this is the struct that maps to the view `Commit` type in Typescript
@ -600,6 +601,7 @@ pub fn unapply_branch(
// this means we can just reset to the default target tree.
{
target_branch.applied = false;
target_branch.selected_for_changes = None;
branch_writer.write(&mut target_branch)?;
}
@ -642,6 +644,7 @@ pub fn unapply_branch(
target_branch.tree = write_tree(project_repository, &default_target, files)?;
target_branch.applied = false;
target_branch.selected_for_changes = None;
branch_writer.write(&mut target_branch)?;
}
@ -725,6 +728,11 @@ pub fn list_virtual_branches(
})?;
let statuses = get_status_by_branch(gb_repository, project_repository)?;
let max_selected_for_changes = statuses
.iter()
.filter_map(|(branch, _)| branch.selected_for_changes)
.max()
.unwrap_or(-1);
for (branch, files) in &statuses {
let file_diffs = files
.iter()
@ -866,6 +874,7 @@ pub fn list_virtual_branches(
base_current,
ownership: branch.ownership.clone(),
updated_at: branch.updated_timestamp_ms,
selected_for_changes: branch.selected_for_changes == Some(max_selected_for_changes),
};
branches.push(branch);
}
@ -1074,6 +1083,27 @@ pub fn create_virtual_branch(
let branch_writer = branch::Writer::new(gb_repository).context("failed to create writer")?;
let selected_for_changes = if let Some(selected_for_changes) = create.selected_for_changes {
if selected_for_changes {
for mut other_branch in Iterator::new(&current_session_reader)
.context("failed to create branch iterator")?
.collect::<Result<Vec<branch::Branch>, reader::Error>>()
.context("failed to read virtual branches")?
{
other_branch.selected_for_changes = None;
branch_writer.write(&mut other_branch)?;
}
Some(chrono::Utc::now().timestamp_millis())
} else {
None
}
} else {
(!all_virtual_branches
.iter()
.any(|b| b.selected_for_changes.is_some()))
.then_some(chrono::Utc::now().timestamp_millis())
};
// make space for the new branch
for (i, branch) in all_virtual_branches.iter().enumerate() {
let mut branch = branch.clone();
@ -1115,6 +1145,7 @@ pub fn create_virtual_branch(
updated_timestamp_ms: now,
ownership: Ownership::default(),
order,
selected_for_changes,
};
if let Some(ownership) = &create.ownership {
@ -1380,6 +1411,24 @@ pub fn update_branch(
branch.order = order;
};
if let Some(selected_for_changes) = branch_update.selected_for_changes {
branch.selected_for_changes = if selected_for_changes {
for mut other_branch in Iterator::new(&current_session_reader)
.context("failed to create branch iterator")?
.collect::<Result<Vec<branch::Branch>, reader::Error>>()
.context("failed to read virtual branches")?
.into_iter()
.filter(|b| b.id != branch.id)
{
other_branch.selected_for_changes = None;
branch_writer.write(&mut other_branch)?;
}
Some(chrono::Utc::now().timestamp_millis())
} else {
None
};
};
branch_writer
.write(&mut branch)
.context("failed to write target branch")?;
@ -1747,17 +1796,29 @@ fn get_applied_status(
.for_each(|file_ownership| branch.ownership.put(file_ownership));
}
let max_selected_for_changes = virtual_branches
.iter()
.filter_map(|b| b.selected_for_changes)
.max()
.unwrap_or(-1);
let default_vbranch_pos = virtual_branches
.iter()
.position(|b| b.selected_for_changes == Some(max_selected_for_changes))
.unwrap_or(0);
// put the remaining hunks into the default (first) branch
for (filepath, hunks) in diff {
for hunk in hunks {
virtual_branches[0].ownership.put(&FileOwnership {
file_path: filepath.clone(),
hunks: vec![Hunk::from(&hunk)
.with_timestamp(get_mtime(&mut mtimes, &filepath))
.with_hash(diff_hash(hunk.diff.as_str()).as_str())],
});
virtual_branches[default_vbranch_pos]
.ownership
.put(&FileOwnership {
file_path: filepath.clone(),
hunks: vec![Hunk::from(&hunk)
.with_timestamp(get_mtime(&mut mtimes, &filepath))
.with_hash(diff_hash(hunk.diff.as_str()).as_str())],
});
hunks_by_branch_id
.entry(virtual_branches[0].id)
.entry(virtual_branches[default_vbranch_pos].id)
.or_default()
.entry(filepath.clone())
.or_default()
@ -3280,11 +3341,19 @@ pub fn create_virtual_branch_from_branch(
.context("failed to peel to commit")?;
let head_commit_tree = head_commit.tree().context("failed to find tree")?;
let order = Iterator::new(&current_session_reader)
let all_virtual_branches = Iterator::new(&current_session_reader)
.context("failed to create branch iterator")?
.collect::<Result<Vec<branch::Branch>, reader::Error>>()
.context("failed to read virtual branches")?
.len();
.into_iter()
.collect::<Vec<branch::Branch>>();
let order = all_virtual_branches.len();
let selected_for_changes = (!all_virtual_branches
.iter()
.any(|b| b.selected_for_changes.is_some()))
.then_some(chrono::Utc::now().timestamp_millis());
let now = time::UNIX_EPOCH
.elapsed()
@ -3356,6 +3425,7 @@ pub fn create_virtual_branch_from_branch(
updated_timestamp_ms: now,
ownership,
order,
selected_for_changes,
};
let writer = branch::Writer::new(gb_repository).context("failed to create writer")?;

View File

@ -243,6 +243,7 @@ mod test {
.unwrap(),
ownership: branch::Ownership::default(),
order: TEST_INDEX.load(Ordering::Relaxed),
selected_for_changes: None,
}
}

View File

@ -5559,3 +5559,238 @@ mod create_virtual_branch_from_branch {
assert_eq!(branches[0].commits[0].description, "branch commit");
}
}
mod selected_for_changes {
use super::*;
#[tokio::test]
async fn create_virtual_branch_should_set_selected_for_changes() {
let Test {
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
// first branch should be created as default
let b_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b_id)
.unwrap();
assert!(branch.selected_for_changes);
// if default branch exists, new branch should not be created as default
let b_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b_id)
.unwrap();
assert!(!branch.selected_for_changes);
// explicitly don't make this one default
let b_id = controller
.create_virtual_branch(
&project_id,
&branch::BranchCreateRequest {
selected_for_changes: Some(false),
..Default::default()
},
)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b_id)
.unwrap();
assert!(!branch.selected_for_changes);
// explicitly make this one default
let b_id = controller
.create_virtual_branch(
&project_id,
&branch::BranchCreateRequest {
selected_for_changes: Some(true),
..Default::default()
},
)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b_id)
.unwrap();
assert!(branch.selected_for_changes);
}
#[tokio::test]
async fn update_virtual_branch_should_reset_selected_for_changes() {
let Test {
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let b1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let b1 = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b1_id)
.unwrap();
assert!(b1.selected_for_changes);
let b2_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let b2 = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b2_id)
.unwrap();
assert!(!b2.selected_for_changes);
controller
.update_virtual_branch(
&project_id,
branch::BranchUpdateRequest {
id: b2_id,
selected_for_changes: Some(true),
..Default::default()
},
)
.await
.unwrap();
let b1 = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b1_id)
.unwrap();
assert!(!b1.selected_for_changes);
let b2 = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b2_id)
.unwrap();
assert!(b2.selected_for_changes);
}
#[tokio::test]
async fn unapply_virtual_branch_should_reset_selected_for_changes() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let b1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
let b1 = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b1_id)
.unwrap();
assert!(b1.selected_for_changes);
controller
.unapply_virtual_branch(&project_id, &b1_id)
.await
.unwrap();
let b1 = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b1_id)
.unwrap();
assert!(!b1.selected_for_changes);
}
#[tokio::test]
async fn hunks_distribution() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches[0].files.len(), 1);
controller
.create_virtual_branch(
&project_id,
&branch::BranchCreateRequest {
selected_for_changes: Some(true),
..Default::default()
},
)
.await
.unwrap();
std::fs::write(repository.path().join("another_file.txt"), "content").unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[1].files.len(), 1);
}
}