Virtual Commits (#507)

* add virtual commits to vbranches, return and render them
* remove file changes from status
This commit is contained in:
Scott Chacon 2023-06-24 05:07:16 -07:00 committed by GitHub
parent 6651a605c1
commit e46f3be1a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 274 additions and 89 deletions

View File

@ -4,10 +4,12 @@ mod writer;
pub use reader::BranchReader as Reader; pub use reader::BranchReader as Reader;
pub use writer::BranchWriter as Writer; pub use writer::BranchWriter as Writer;
use std::{fmt, ops, path}; use std::{fmt, ops, path, vec};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use super::VirtualBranchCommit;
#[derive(Debug, PartialEq, Eq, Hash, Clone)] #[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Ownership { pub struct Ownership {
pub file_path: path::PathBuf, pub file_path: path::PathBuf,

View File

@ -26,6 +26,18 @@ pub struct VirtualBranch {
pub name: String, pub name: String,
pub active: bool, pub active: bool,
pub files: Vec<VirtualBranchFile>, pub files: Vec<VirtualBranchFile>,
pub commits: Vec<VirtualBranchCommit>,
}
#[derive(Debug, PartialEq, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VirtualBranchCommit {
pub id: String,
pub description: String,
pub created_at: u128,
pub author_name: String,
pub author_email: String,
pub is_remote: bool,
} }
#[derive(Debug, PartialEq, Clone, Serialize)] #[derive(Debug, PartialEq, Clone, Serialize)]
@ -193,23 +205,93 @@ pub fn remote_branches(
Ok(branches) Ok(branches)
} }
// just for debugging for now
fn print_diff(diff: git2::Diff) -> Result<()> {
diff.print(git2::DiffFormat::Patch, |delta, hunk, line| {
println!(
"delta: {:?} {:?}",
line.origin(),
std::str::from_utf8(line.content()).unwrap()
);
true
})?;
Ok(())
}
pub fn list_virtual_branches( pub fn list_virtual_branches(
gb_repository: &gb_repository::Repository, gb_repository: &gb_repository::Repository,
project_repository: &project_repository::Repository, project_repository: &project_repository::Repository,
) -> Result<Vec<VirtualBranch>> { ) -> Result<Vec<VirtualBranch>> {
let mut branches: Vec<VirtualBranch> = Vec::new(); let mut branches: Vec<VirtualBranch> = Vec::new();
let default_target = get_default_target(gb_repository)?;
let default_sha = default_target.sha.clone();
let statuses = get_status_by_branch(gb_repository, project_repository)?; let statuses = get_status_by_branch(gb_repository, project_repository)?;
for (branch, files) in &statuses { for (branch, files) in &statuses {
let mut vfiles = vec![]; let mut vfiles = vec![];
for file in files {
vfiles.push(file.clone()); // check if head tree does not match target tree
// if so, we diff the head tree and the new write_tree output to see what is new and filter the hunks to just those
if default_sha != branch.head {
let vtree = write_tree(gb_repository, project_repository, &files)?;
let repo = &project_repository.git_repository;
// get the trees
let commit_old = repo.find_commit(branch.head)?;
let tree_old = commit_old.tree()?;
let vtree_tree = repo.find_tree(vtree)?;
// do a diff between branch.head and the tree we _would_ commit
let diff = repo.diff_tree_to_tree(Some(&tree_old), Some(&vtree_tree), None)?;
let hunks_by_filepath = diff_to_hunks_by_filepath(diff, project_repository)?;
vfiles = hunks_by_filepath
.iter()
.map(|(file_path, hunks)| VirtualBranchFile {
id: file_path.clone(),
path: file_path.to_string(),
hunks: hunks.clone(),
})
.collect::<Vec<_>>();
} else {
for file in files {
vfiles.push(file.clone());
}
} }
let mut commits = vec![];
// find all commits on head that are not on target.sha
let repo = &project_repository.git_repository;
let mut revwalk = repo.revwalk()?;
revwalk.set_sorting(git2::Sort::TOPOLOGICAL)?;
revwalk.push(branch.head)?;
revwalk.hide(default_target.sha)?;
for oid in revwalk {
let oid = oid?;
let commit = repo.find_commit(oid)?;
let timestamp = commit.time().seconds() as u128;
let signature = commit.author();
let name = signature.name().unwrap().to_string();
let email = signature.email().unwrap().to_string();
let message = commit.message().unwrap().to_string();
let sha = oid.to_string();
let commit = VirtualBranchCommit {
id: sha,
created_at: timestamp * 1000,
author_name: name,
author_email: email,
description: message,
is_remote: false,
};
commits.push(commit);
}
let branch = VirtualBranch { let branch = VirtualBranch {
id: branch.id.to_string(), id: branch.id.to_string(),
name: branch.name.to_string(), name: branch.name.to_string(),
active: branch.applied, active: branch.applied,
files: vfiles, files: vfiles,
commits,
}; };
branches.push(branch); branches.push(branch);
} }
@ -363,35 +445,10 @@ fn find_owner(stack: &[branch::Branch], needle: &branch::Ownership) -> Option<br
explicitly_owned_by.or(implicitly_owned_by).cloned() explicitly_owned_by.or(implicitly_owned_by).cloned()
} }
// list the virtual branches and their file statuses (statusi?) fn diff_to_hunks_by_filepath(
pub fn get_status_by_branch( diff: git2::Diff,
gb_repository: &gb_repository::Repository, project_repository: &project_repository::Repository,
project_repository: &project_repository::Repository<'_>, ) -> Result<HashMap<String, Vec<VirtualBranchHunk>>> {
) -> Result<Vec<(branch::Branch, Vec<VirtualBranchFile>)>> {
let current_session = gb_repository
.get_or_create_current_session()
.context("failed to get or create currnt session")?;
let current_session_reader = sessions::Reader::open(gb_repository, &current_session)
.context("failed to open current session")?;
let target_reader = target::Reader::new(&current_session_reader);
let default_target = match target_reader.read_default() {
Ok(target) => Ok(target),
Err(reader::Error::NotFound) => {
println!(" no base sha set, run butler setup");
return Ok(vec![]);
}
Err(e) => Err(e),
}
.context("failed to read default target")?;
let diff = project_repository
.workdir_diff(&default_target.sha)
.context(format!(
"failed to get diff workdir with {}",
default_target.sha
))?;
// find all the hunks // find all the hunks
let mut hunks_by_filepath: HashMap<String, Vec<VirtualBranchHunk>> = HashMap::new(); let mut hunks_by_filepath: HashMap<String, Vec<VirtualBranchHunk>> = HashMap::new();
let mut current_diff = String::new(); let mut current_diff = String::new();
@ -428,11 +485,13 @@ pub fn get_status_by_branch(
.workdir() .workdir()
.unwrap() .unwrap()
.join(file_path); .join(file_path);
let metadata = file_path.metadata().unwrap(); let mtime = 0;
let mtime = FileTime::from_last_modification_time(&metadata); if let Ok(metadata) = file_path.metadata() {
// convert seconds and nanoseconds to milliseconds let mtime = FileTime::from_last_modification_time(&metadata);
let mtime = mtime.seconds() as u128 * 1000; // convert seconds and nanoseconds to milliseconds
mtimes.insert(file_path, mtime); let mtime = mtime.seconds() as u128 * 1000;
mtimes.insert(file_path, mtime);
}
mtime mtime
} }
}; };
@ -513,6 +572,39 @@ pub fn get_status_by_branch(
file_path, file_path,
}); });
} }
Ok(hunks_by_filepath)
}
// list the virtual branches and their file statuses (statusi?)
pub fn get_status_by_branch(
gb_repository: &gb_repository::Repository,
project_repository: &project_repository::Repository<'_>,
) -> Result<Vec<(branch::Branch, Vec<VirtualBranchFile>)>> {
let current_session = gb_repository
.get_or_create_current_session()
.context("failed to get or create currnt session")?;
let current_session_reader = sessions::Reader::open(gb_repository, &current_session)
.context("failed to open current session")?;
let target_reader = target::Reader::new(&current_session_reader);
let default_target = match target_reader.read_default() {
Ok(target) => Ok(target),
Err(reader::Error::NotFound) => {
println!(" no base sha set, run butler setup");
return Ok(vec![]);
}
Err(e) => Err(e),
}
.context("failed to read default target")?;
let diff = project_repository
.workdir_diff(&default_target.sha)
.context(format!(
"failed to get diff workdir with {}",
default_target.sha
))?;
let hunks_by_filepath = diff_to_hunks_by_filepath(diff, project_repository)?;
let mut virtual_branches = Iterator::new(&current_session_reader) let mut virtual_branches = Iterator::new(&current_session_reader)
.context("failed to read virtual branches")? .context("failed to read virtual branches")?
@ -628,12 +720,7 @@ pub fn get_status_by_branch(
Ok(statuses) Ok(statuses)
} }
pub fn commit( fn get_default_target(gb_repository: &gb_repository::Repository) -> Result<target::Target> {
gb_repository: &gb_repository::Repository,
project_repository: &project_repository::Repository,
branch_id: &str,
message: &str,
) -> Result<()> {
let current_session = gb_repository let current_session = gb_repository
.get_or_create_current_session() .get_or_create_current_session()
.expect("failed to get or create currnt session"); .expect("failed to get or create currnt session");
@ -645,34 +732,52 @@ pub fn commit(
Ok(target) => target, Ok(target) => target,
Err(e) => panic!("failed to read default target: {}", e), Err(e) => panic!("failed to read default target: {}", e),
}; };
Ok(default_target)
}
fn write_tree(
gb_repository: &gb_repository::Repository,
project_repository: &project_repository::Repository,
files: &Vec<VirtualBranchFile>,
) -> Result<git2::Oid> {
let default_target = get_default_target(gb_repository)?;
// read the base sha into an index
let git_repository = &project_repository.git_repository;
let base_commit = git_repository.find_commit(default_target.sha).unwrap();
let base_tree = base_commit.tree().unwrap();
let mut index = git_repository.index().unwrap();
index.read_tree(&base_tree).unwrap();
// now update the index with content in the working directory for each file
for file in files {
// convert this string to a Path
let file = std::path::Path::new(&file.path);
// TODO: deal with removals too
index.add_path(file).unwrap();
}
// now write out the tree
let tree_oid = index.write_tree().unwrap();
Ok(tree_oid)
}
pub fn commit(
gb_repository: &gb_repository::Repository,
project_repository: &project_repository::Repository,
branch_id: &str,
message: &str,
) -> Result<()> {
// get the files to commit // get the files to commit
let statuses = get_status_by_branch(gb_repository, project_repository) let statuses = get_status_by_branch(gb_repository, project_repository)
.expect("failed to get status by branch"); .expect("failed to get status by branch");
for (mut branch, files) in statuses { for (mut branch, files) in statuses {
if branch.id == branch_id { if branch.id == branch_id {
// read the base sha into an index let tree_oid = write_tree(gb_repository, project_repository, &files)?;
let git_repository = &project_repository.git_repository;
let base_commit = git_repository.find_commit(default_target.sha).unwrap();
let base_tree = base_commit.tree().unwrap();
let parent_commit = git_repository.find_commit(branch.head).unwrap();
let mut index = git_repository.index().unwrap();
index.read_tree(&base_tree).unwrap();
// now update the index with content in the working directory for each file
for file in files {
// convert this string to a Path
let file = std::path::Path::new(&file.path);
// TODO: deal with removals too
index.add_path(file).unwrap();
}
// now write out the tree
let tree_oid = index.write_tree().unwrap();
// only commit if it's a new tree
if tree_oid != branch.tree { if tree_oid != branch.tree {
let git_repository = &project_repository.git_repository;
let parent_commit = git_repository.find_commit(branch.head).unwrap();
let tree = git_repository.find_tree(tree_oid).unwrap(); let tree = git_repository.find_tree(tree_oid).unwrap();
// now write a commit // now write a commit
let (author, committer) = gb_repository.git_signatures().unwrap(); let (author, committer) = gb_repository.git_signatures().unwrap();
@ -734,6 +839,80 @@ mod tests {
Ok(repository) Ok(repository)
} }
#[test]
fn commit_on_branch_then_change_file_then_get_status() -> Result<()> {
let repository = test_repository()?;
let project = projects::Project::try_from(&repository)?;
let gb_repo_path = tempdir()?.path().to_str().unwrap().to_string();
let storage = storage::Storage::from_path(tempdir()?.path());
let user_store = users::Storage::new(storage.clone());
let project_store = projects::Storage::new(storage);
project_store.add_project(&project)?;
let file_path = std::path::Path::new("test.txt");
std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\n",
)?;
let file_path2 = std::path::Path::new("test2.txt");
std::fs::write(
std::path::Path::new(&project.path).join(file_path2),
"line5\nline6\nline7\nline8\n",
)?;
commit_all(&repository)?;
let gb_repo = gb_repository::Repository::open(
gb_repo_path,
project.id.clone(),
project_store,
user_store,
)?;
let project_repository = project_repository::Repository::open(&project)?;
target::Writer::new(&gb_repo).write_default(&target::Target {
name: "origin".to_string(),
remote: "origin".to_string(),
sha: repository.head().unwrap().target().unwrap(),
})?;
let branch1_id = create_virtual_branch(&gb_repo, "test_branch")
.expect("failed to create virtual branch");
let branch_writer = branch::Writer::new(&gb_repo);
branch_writer.write_selected(&Some(branch1_id.clone()))?;
std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"line0\nline1\nline2\nline3\nline4\n",
)?;
let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch = &branches[0];
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.commits.len(), 0);
// commit
commit(&gb_repo, &project_repository, &branch1_id, "test commit")?;
// status (no files)
let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch = &branches[0];
assert_eq!(branch.files.len(), 0);
assert_eq!(branch.commits.len(), 1);
std::fs::write(
std::path::Path::new(&project.path).join(file_path2),
"line5\nline6\nlineBLAH\nline7\nline8\n",
)?;
// should have just the last change now, the other line is committed
let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch = &branches[0];
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.commits.len(), 1);
Ok(())
}
#[test] #[test]
fn create_branch() -> Result<()> { fn create_branch() -> Result<()> {
let repository = test_repository()?; let repository = test_repository()?;

View File

@ -41,6 +41,7 @@ export async function load(e: PageLoadEvent) {
const branches: Branch[] = sortBranchHunks( const branches: Branch[] = sortBranchHunks(
plainToInstance(Branch, await getVirtualBranches({ projectId })) plainToInstance(Branch, await getVirtualBranches({ projectId }))
); );
console.log(branches);
return { projectId, target, remoteBranches, remoteBranchesData, branches }; return { projectId, target, remoteBranches, remoteBranchesData, branches };
} }

View File

@ -87,12 +87,12 @@
on:consider={handleDndEvent} on:consider={handleDndEvent}
on:finalize={handleDndEvent} on:finalize={handleDndEvent}
> >
{#each branches.filter((c) => c.active) as { id, name, files, description } (id)} {#each branches.filter((c) => c.active) as { id, name, files, commits, description } (id)}
<Lane <Lane
bind:name bind:name
bind:commitMessage={description} bind:commitMessage={description}
bind:files bind:files
commits={testCommits} {commits}
on:empty={handleEmpty} on:empty={handleEmpty}
{projectId} {projectId}
branchId={id} branchId={id}

View File

@ -192,31 +192,33 @@
</div> </div>
{/each} {/each}
</div> </div>
<div class="relative"> {#if remoteCommits.length > 0}
<!-- Commit bubble track --> <div class="relative">
<div class="absolute top-0 h-full w-0.5 bg-light-600" style="left: 0.925rem" /> <!-- Commit bubble track -->
<!-- Section title for remote commits --> <div class="absolute top-0 h-full w-0.5 bg-light-600" style="left: 0.925rem" />
<div class="flex w-full px-2 pb-4"> <!-- Section title for remote commits -->
<div class="z-10 w-6">
<div
class="h-4 w-4 rounded-full border-2 border-light-200 bg-light-200 text-white dark:border-dark-200 dark:bg-dark-200 dark:text-black"
>
<!-- Target HEAD commit bubble -->
<IconGithub />
</div>
</div>
<div class="flex-grow">Pushed to origin/master</div>
</div>
{#each remoteCommits as commit (commit.id)}
<div class="flex w-full px-2 pb-4"> <div class="flex w-full px-2 pb-4">
<div class="z-10 w-6 py-2"> <div class="z-10 w-6">
<!-- Pushed commit bubble -->
<div <div
class="rounded--b-sm h-4 w-4 rounded-full border-2 border-light-200 bg-light-600 dark:border-dark-200 dark:bg-dark-200" class="h-4 w-4 rounded-full border-2 border-light-200 bg-light-200 text-white dark:border-dark-200 dark:bg-dark-200 dark:text-black"
/> >
<!-- Target HEAD commit bubble -->
<IconGithub />
</div>
</div> </div>
<CommitCard {commit} /> <div class="flex-grow">Pushed to origin/master</div>
</div> </div>
{/each} {#each remoteCommits as commit (commit.id)}
</div> <div class="flex w-full px-2 pb-4">
<div class="z-10 w-6 py-2">
<!-- Pushed commit bubble -->
<div
class="rounded--b-sm h-4 w-4 rounded-full border-2 border-light-200 bg-light-600 dark:border-dark-200 dark:bg-dark-200"
/>
</div>
<CommitCard {commit} />
</div>
{/each}
</div>
{/if}
</div> </div>

View File

@ -24,6 +24,7 @@ export class Branch extends DndItem {
active!: boolean; active!: boolean;
@Type(() => File) @Type(() => File)
files!: File[]; files!: File[];
commits!: Commit[];
description!: string; description!: string;
} }