mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2025-01-08 19:06:38 +03:00
Add basic committing to the client (#484)
* move commit to virtual_branches code, add bridge for JS * actually run commit() from the client * simple remote branches list, for later * better remote branches * adds more data to remote branches * test file movement * added move_files test and remove path from all branches that match * fix move with duplicate entries
This commit is contained in:
parent
e839000a0b
commit
971059e179
@ -351,6 +351,20 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn commit_virtual_branch(
|
||||
&self,
|
||||
project_id: &str,
|
||||
branch: &str,
|
||||
message: &str,
|
||||
) -> Result<()> {
|
||||
let gb_repository = self.gb_repository(project_id)?;
|
||||
let project = self.gb_project(project_id)?;
|
||||
let project_repository = project_repository::Repository::open(&project)
|
||||
.context("failed to open project repository")?;
|
||||
virtual_branches::commit(&gb_repository, &project_repository, branch, message)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn upsert_bookmark(&self, bookmark: &bookmarks::Bookmark) -> Result<()> {
|
||||
let gb_repository = self.gb_repository(&bookmark.project_id)?;
|
||||
let writer = bookmarks::Writer::new(&gb_repository).context("failed to open writer")?;
|
||||
@ -441,6 +455,17 @@ impl App {
|
||||
project_repository.git_remote_branches()
|
||||
}
|
||||
|
||||
pub fn git_remote_branches_data(
|
||||
&self,
|
||||
project_id: &str,
|
||||
) -> Result<Vec<virtual_branches::RemoteBranch>> {
|
||||
let gb_repository = self.gb_repository(project_id)?;
|
||||
let project = self.gb_project(project_id)?;
|
||||
let project_repository = project_repository::Repository::open(&project)
|
||||
.context("failed to open project repository")?;
|
||||
virtual_branches::remote_branches(&gb_repository, &project_repository)
|
||||
}
|
||||
|
||||
pub fn git_head(&self, project_id: &str) -> Result<String> {
|
||||
let project = self.gb_project(project_id)?;
|
||||
let project_repository = project_repository::Repository::open(&project)
|
||||
|
@ -3,7 +3,7 @@ use colored::Colorize;
|
||||
use dialoguer::{console::Term, theme::ColorfulTheme, Input, MultiSelect, Select};
|
||||
|
||||
use git2::Repository;
|
||||
use std::time;
|
||||
use std::{collections::HashMap, time};
|
||||
use uuid::Uuid;
|
||||
|
||||
use git_butler_tauri::{
|
||||
@ -79,11 +79,21 @@ fn main() {
|
||||
"setup" => run_setup(butler), // sets target sha from remote branch
|
||||
"commit" => run_commit(butler), // creates trees from the virtual branch content and creates a commit
|
||||
"branches" => run_branches(butler),
|
||||
"remotes" => run_remotes(butler),
|
||||
"flush" => run_flush(butler), // artificially forces a session flush
|
||||
_ => println!("Unknown command: {}", args.command),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_remotes(butler: ButlerCli) {
|
||||
let branches =
|
||||
virtual_branches::remote_branches(&butler.gb_repository, &butler.project_repository())
|
||||
.unwrap();
|
||||
for branch in branches {
|
||||
println!("{:?}", branch);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_flush(butler: ButlerCli) {
|
||||
println!("Flushing sessions");
|
||||
butler.gb_repository.flush().unwrap();
|
||||
@ -108,7 +118,6 @@ fn run_branches(butler: ButlerCli) {
|
||||
|
||||
fn run_commit(butler: ButlerCli) {
|
||||
// get the branch to commit
|
||||
|
||||
let current_session = butler
|
||||
.gb_repository
|
||||
.get_or_create_current_session()
|
||||
@ -123,17 +132,18 @@ fn run_commit(butler: ButlerCli) {
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let branch_names = virtual_branches
|
||||
.iter()
|
||||
.map(|b| b.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut ids = Vec::new();
|
||||
let mut names = Vec::new();
|
||||
for branch in virtual_branches {
|
||||
ids.push(branch.id);
|
||||
names.push(branch.name);
|
||||
}
|
||||
let selection = Select::with_theme(&ColorfulTheme::default())
|
||||
.items(&branch_names)
|
||||
.items(&names)
|
||||
.default(0)
|
||||
.interact_on_opt(&Term::stderr())
|
||||
.unwrap();
|
||||
let commit_branch = branch_names[selection.unwrap()].clone();
|
||||
let commit_branch = ids[selection.unwrap()].clone();
|
||||
println!("Committing virtual branch {}", commit_branch.red());
|
||||
|
||||
// get the commit message
|
||||
@ -141,74 +151,12 @@ fn run_commit(butler: ButlerCli) {
|
||||
.with_prompt("Commit message")
|
||||
.interact_text()
|
||||
.unwrap();
|
||||
|
||||
let target_reader = virtual_branches::target::Reader::new(¤t_session_reader);
|
||||
let default_target = match target_reader.read_default() {
|
||||
Ok(target) => target,
|
||||
Err(reader::Error::NotFound) => return,
|
||||
Err(e) => panic!("failed to read default target: {}", e),
|
||||
};
|
||||
|
||||
// get the files to commit
|
||||
let statuses =
|
||||
virtual_branches::get_status_by_branch(&butler.gb_repository, &butler.project_repository())
|
||||
.expect("failed to get status by branch");
|
||||
for (mut branch, files) in statuses {
|
||||
if branch.name == commit_branch {
|
||||
println!(" branch: {}", branch.id.blue());
|
||||
println!(" base: {}", default_target.sha.to_string().blue());
|
||||
|
||||
// read the base sha into an index
|
||||
let git_repository = butler.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 {
|
||||
println!("{}", file.path);
|
||||
// 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 {
|
||||
let tree = git_repository.find_tree(tree_oid).unwrap();
|
||||
// now write a commit
|
||||
let (author, committer) = butler.gb_repository.git_signatures().unwrap();
|
||||
let commit_oid = git_repository
|
||||
.commit(
|
||||
None,
|
||||
&author,
|
||||
&committer,
|
||||
&message,
|
||||
&tree,
|
||||
&[&parent_commit],
|
||||
)
|
||||
.unwrap();
|
||||
// write this new commit to the virtual branch
|
||||
println!(" commit: {}", commit_oid.to_string().blue());
|
||||
|
||||
// update the virtual branch head
|
||||
branch.tree = tree_oid;
|
||||
branch.head = commit_oid;
|
||||
let writer = virtual_branches::branch::Writer::new(&butler.gb_repository);
|
||||
writer.write(&branch).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create the tree
|
||||
|
||||
// create the commit
|
||||
virtual_branches::commit(
|
||||
&butler.gb_repository,
|
||||
&butler.project_repository(),
|
||||
&commit_branch,
|
||||
&message,
|
||||
);
|
||||
}
|
||||
|
||||
fn run_new(butler: ButlerCli) {
|
||||
@ -268,6 +216,7 @@ fn run_move(butler: ButlerCli) {
|
||||
.iter()
|
||||
.map(|i| files[*i].clone().into())
|
||||
.collect::<Vec<_>>();
|
||||
println!("Selected files: {:?}", selected_files);
|
||||
|
||||
let current_session = butler
|
||||
.gb_repository
|
||||
@ -283,30 +232,20 @@ fn run_move(butler: ButlerCli) {
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// get the branch to move to
|
||||
let branch_names = virtual_branches
|
||||
.iter()
|
||||
.map(|b| b.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let mut ids = Vec::new();
|
||||
let mut names = Vec::new();
|
||||
for branch in virtual_branches {
|
||||
ids.push(branch.id);
|
||||
names.push(branch.name);
|
||||
}
|
||||
let selection = Select::with_theme(&ColorfulTheme::default())
|
||||
.items(&branch_names)
|
||||
.items(&names)
|
||||
.default(0)
|
||||
.interact_on_opt(&Term::stderr())
|
||||
.unwrap();
|
||||
let new_branch = branch_names[selection.unwrap()].clone();
|
||||
let target_branch_id = ids[selection.unwrap()].clone();
|
||||
|
||||
let target_branch_id = virtual_branches
|
||||
.iter()
|
||||
.find(|b| b.name == new_branch)
|
||||
.unwrap()
|
||||
.id
|
||||
.clone();
|
||||
|
||||
println!(
|
||||
"Moving {} files to {}",
|
||||
selected_files.len(),
|
||||
new_branch.red()
|
||||
);
|
||||
println!("Moving {} files", selected_files.len());
|
||||
virtual_branches::move_files(&butler.gb_repository, &target_branch_id, &selected_files)
|
||||
.expect("failed to move files");
|
||||
}
|
||||
|
@ -441,6 +441,19 @@ async fn git_remote_branches(
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
#[timed(duration(printer = "debug!"))]
|
||||
#[tauri::command(async)]
|
||||
async fn git_remote_branches_data(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: &str,
|
||||
) -> Result<Vec<virtual_branches::RemoteBranch>, Error> {
|
||||
let app = handle.state::<app::App>();
|
||||
let branches = app
|
||||
.git_remote_branches_data(project_id)
|
||||
.with_context(|| format!("failed to get git branches for project {}", project_id))?;
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
#[timed(duration(printer = "debug!"))]
|
||||
#[tauri::command(async)]
|
||||
async fn git_head(handle: tauri::AppHandle, project_id: &str) -> Result<String, Error> {
|
||||
@ -594,6 +607,19 @@ async fn move_virtual_branch_files(
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
#[timed(duration(printer = "debug!"))]
|
||||
#[tauri::command(async)]
|
||||
async fn commit_virtual_branch(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: &str,
|
||||
branch: &str,
|
||||
message: &str,
|
||||
) -> Result<(), Error> {
|
||||
let app = handle.state::<app::App>();
|
||||
let target = app.commit_virtual_branch(project_id, branch, message)?;
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
#[timed(duration(printer = "debug!"))]
|
||||
#[tauri::command(async)]
|
||||
async fn get_target_data(
|
||||
@ -745,6 +771,7 @@ fn main() {
|
||||
git_match_paths,
|
||||
git_branches,
|
||||
git_remote_branches,
|
||||
git_remote_branches_data,
|
||||
git_head,
|
||||
git_switch_branch,
|
||||
git_commit,
|
||||
@ -760,6 +787,7 @@ fn main() {
|
||||
list_virtual_branches,
|
||||
create_virtual_branch,
|
||||
move_virtual_branch_files,
|
||||
commit_virtual_branch,
|
||||
get_target_data,
|
||||
set_target_branch,
|
||||
])
|
||||
|
@ -2,7 +2,10 @@ pub mod branch;
|
||||
mod iterator;
|
||||
pub mod target;
|
||||
|
||||
use std::{collections::HashMap, path, time, vec};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path, time, vec,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use filetime::FileTime;
|
||||
@ -41,6 +44,153 @@ pub struct VirtualBranchHunk {
|
||||
pub file_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoteBranch {
|
||||
sha: String,
|
||||
branch: String,
|
||||
name: String,
|
||||
description: String,
|
||||
last_commit_ts: u128,
|
||||
first_commit_ts: u128,
|
||||
ahead: u32,
|
||||
behind: u32,
|
||||
upstream: String,
|
||||
authors: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn remote_branches(
|
||||
gb_repository: &gb_repository::Repository,
|
||||
project_repository: &project_repository::Repository,
|
||||
) -> Result<Vec<RemoteBranch>> {
|
||||
// get the current target
|
||||
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, ¤t_session)
|
||||
.context("failed to open current session")?;
|
||||
|
||||
let target_reader = target::Reader::new(¤t_session_reader);
|
||||
let default_target = match target_reader.read_default() {
|
||||
Ok(target) => Ok(target),
|
||||
Err(reader::Error::NotFound) => return Ok(vec![]),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
.context("failed to read default target")?;
|
||||
|
||||
let main_oid = default_target.sha;
|
||||
|
||||
let current_time = time::SystemTime::now();
|
||||
let too_old = time::Duration::from_secs(86_400 * 180); // 180 days (6 months) is too old
|
||||
|
||||
let repo = &project_repository.git_repository;
|
||||
let mut branches: Vec<RemoteBranch> = Vec::new();
|
||||
for branch in repo.branches(Some(git2::BranchType::Remote))? {
|
||||
let (branch, _) = branch?;
|
||||
let branch_name = branch.get().name().unwrap();
|
||||
let upstream_branch = branch.upstream();
|
||||
match branch.get().target() {
|
||||
Some(branch_oid) => {
|
||||
// get the branch ref
|
||||
let branch_commit = repo.find_commit(branch_oid).ok().unwrap();
|
||||
|
||||
// figure out if the last commit on this branch is too old to consider
|
||||
let branch_time = branch_commit.time();
|
||||
// convert git::Time to SystemTime
|
||||
let branch_time = time::UNIX_EPOCH
|
||||
+ time::Duration::from_secs(branch_time.seconds().try_into().unwrap());
|
||||
let duration = current_time.duration_since(branch_time).unwrap();
|
||||
if duration > too_old {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut revwalk = repo.revwalk().unwrap();
|
||||
revwalk.set_sorting(git2::Sort::TOPOLOGICAL).unwrap();
|
||||
revwalk.push(main_oid).unwrap();
|
||||
revwalk.hide(branch_oid).unwrap();
|
||||
|
||||
let mut count_behind = 0;
|
||||
for oid in revwalk {
|
||||
if oid.unwrap() == branch_oid {
|
||||
break;
|
||||
}
|
||||
count_behind += 1;
|
||||
if count_behind > 100 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut revwalk2 = repo.revwalk().unwrap();
|
||||
revwalk2.set_sorting(git2::Sort::TOPOLOGICAL).unwrap();
|
||||
revwalk2.push(branch_oid).unwrap();
|
||||
revwalk2.hide(main_oid).unwrap();
|
||||
|
||||
let mut min_time = None;
|
||||
let mut max_time = None;
|
||||
let mut count_ahead = 0;
|
||||
let mut authors = HashSet::new();
|
||||
for oid in revwalk2 {
|
||||
let oid = oid.unwrap();
|
||||
if oid == main_oid {
|
||||
break;
|
||||
}
|
||||
let commit = repo.find_commit(oid).ok().unwrap();
|
||||
let timestamp = commit.time().seconds() as u128;
|
||||
|
||||
if min_time.is_none() || timestamp < min_time.unwrap() {
|
||||
min_time = Some(timestamp);
|
||||
}
|
||||
|
||||
if max_time.is_none() || timestamp > max_time.unwrap() {
|
||||
max_time = Some(timestamp);
|
||||
}
|
||||
|
||||
// find the signature for this commit
|
||||
let commit = repo.find_commit(oid).ok().unwrap();
|
||||
let signature = commit.author();
|
||||
authors.insert(signature.email().unwrap().to_string());
|
||||
|
||||
count_ahead += 1;
|
||||
}
|
||||
|
||||
let upstream_branch_name = match upstream_branch {
|
||||
Ok(upstream_branch) => upstream_branch.get().name().unwrap_or("").to_string(),
|
||||
Err(e) => "".to_string(),
|
||||
};
|
||||
|
||||
branches.push(RemoteBranch {
|
||||
sha: branch_oid.to_string(),
|
||||
branch: branch_name.to_string(),
|
||||
name: branch_name.to_string(),
|
||||
description: "".to_string(),
|
||||
last_commit_ts: max_time.unwrap_or(0),
|
||||
first_commit_ts: min_time.unwrap_or(0),
|
||||
ahead: count_ahead,
|
||||
behind: count_behind,
|
||||
upstream: upstream_branch_name,
|
||||
authors: authors.into_iter().collect(),
|
||||
});
|
||||
}
|
||||
None => {
|
||||
// this is a detached head
|
||||
branches.push(RemoteBranch {
|
||||
sha: "".to_string(),
|
||||
branch: branch_name.to_string(),
|
||||
name: branch_name.to_string(),
|
||||
description: "".to_string(),
|
||||
last_commit_ts: 0,
|
||||
first_commit_ts: 0,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
upstream: "".to_string(),
|
||||
authors: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
pub fn list_virtual_branches(
|
||||
gb_repository: &gb_repository::Repository,
|
||||
project_repository: &project_repository::Repository,
|
||||
@ -135,19 +285,20 @@ pub fn move_files(
|
||||
.clone();
|
||||
|
||||
for ownership in to_move {
|
||||
let mut source_branch = virtual_branches
|
||||
// take the file out of all branches (in case of accidental duplication)
|
||||
let source_branches = virtual_branches
|
||||
.iter()
|
||||
.find(|b| b.ownership.contains(ownership))
|
||||
.context(format!("failed to find source branch for {}", ownership))?
|
||||
.clone();
|
||||
.filter(|b| b.ownership.contains(ownership));
|
||||
|
||||
source_branch.ownership.retain(|o| !o.eq(ownership));
|
||||
source_branch.ownership.sort();
|
||||
source_branch.ownership.dedup();
|
||||
|
||||
writer
|
||||
.write(&source_branch)
|
||||
.context(format!("failed to write source branch for {}", ownership))?;
|
||||
for source_branch in source_branches {
|
||||
let mut source_branch = source_branch.clone();
|
||||
source_branch.ownership.retain(|o| !o.eq(ownership));
|
||||
source_branch.ownership.sort();
|
||||
source_branch.ownership.dedup();
|
||||
writer
|
||||
.write(&source_branch)
|
||||
.context(format!("failed to find source branch for {}", ownership))?
|
||||
}
|
||||
|
||||
target_branch.ownership.push(ownership.clone());
|
||||
target_branch.ownership.sort();
|
||||
@ -449,6 +600,76 @@ pub fn get_status_by_branch(
|
||||
Ok(statuses)
|
||||
}
|
||||
|
||||
pub fn commit(
|
||||
gb_repository: &gb_repository::Repository,
|
||||
project_repository: &project_repository::Repository,
|
||||
branch_id: &str,
|
||||
message: &str,
|
||||
) -> Result<()> {
|
||||
let current_session = gb_repository
|
||||
.get_or_create_current_session()
|
||||
.expect("failed to get or create currnt session");
|
||||
let current_session_reader = sessions::Reader::open(&gb_repository, ¤t_session)
|
||||
.expect("failed to open current session reader");
|
||||
|
||||
let target_reader = target::Reader::new(¤t_session_reader);
|
||||
let default_target = match target_reader.read_default() {
|
||||
Ok(target) => target,
|
||||
Err(e) => panic!("failed to read default target: {}", e),
|
||||
};
|
||||
|
||||
// get the files to commit
|
||||
let statuses = get_status_by_branch(&gb_repository, &project_repository)
|
||||
.expect("failed to get status by branch");
|
||||
for (mut branch, files) in statuses {
|
||||
if branch.id == branch_id {
|
||||
// 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 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 {
|
||||
let tree = git_repository.find_tree(tree_oid).unwrap();
|
||||
// now write a commit
|
||||
let (author, committer) = gb_repository.git_signatures().unwrap();
|
||||
let commit_oid = git_repository
|
||||
.commit(
|
||||
None,
|
||||
&author,
|
||||
&committer,
|
||||
&message,
|
||||
&tree,
|
||||
&[&parent_commit],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// update the virtual branch head
|
||||
branch.tree = tree_oid;
|
||||
branch.head = commit_oid;
|
||||
let writer = branch::Writer::new(&gb_repository);
|
||||
writer.write(&branch).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempfile::tempdir;
|
||||
@ -630,4 +851,74 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_files_scott() -> 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 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 file_path = std::path::Path::new("test.txt");
|
||||
std::fs::write(
|
||||
std::path::Path::new(&project.path).join(file_path),
|
||||
"line1\nline2\n",
|
||||
)?;
|
||||
|
||||
let file_path2 = std::path::Path::new("test2.txt");
|
||||
std::fs::write(
|
||||
std::path::Path::new(&project.path).join(file_path2),
|
||||
"line1\nline2\n",
|
||||
)?;
|
||||
|
||||
let branch1_id = create_virtual_branch(&gb_repo, "test_branch")
|
||||
.expect("failed to create virtual branch");
|
||||
let branch2_id = create_virtual_branch(&gb_repo, "test_branch2")
|
||||
.expect("failed to create virtual branch");
|
||||
|
||||
let session = gb_repo.get_or_create_current_session().unwrap();
|
||||
let session_reader = sessions::Reader::open(&gb_repo, &session).unwrap();
|
||||
|
||||
// this should automatically move the file to branch2
|
||||
let status =
|
||||
get_status_by_branch(&gb_repo, &project_repository).expect("failed to get status");
|
||||
|
||||
let vbranch_reader = branch::Reader::new(&session_reader);
|
||||
|
||||
move_files(&gb_repo, &branch1_id, &vec!["test.txt".to_string().into()]).unwrap();
|
||||
move_files(&gb_repo, &branch2_id, &vec!["test2.txt".to_string().into()]).unwrap();
|
||||
|
||||
let branch1 = vbranch_reader.read(&branch1_id).unwrap();
|
||||
let branch2 = vbranch_reader.read(&branch2_id).unwrap();
|
||||
|
||||
assert_eq!(branch1.ownership.len(), 1);
|
||||
assert_eq!(
|
||||
branch2
|
||||
.ownership
|
||||
.first()
|
||||
.unwrap()
|
||||
.file_path
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"test2.txt"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,7 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let { projectId, target, branches, remoteBranches } = data;
|
||||
let { projectId, target, branches, remoteBranches, remoteBranchesData } = data;
|
||||
let targetChoice = 'origin/master'; // prob should check if it exists
|
||||
|
||||
async function setTarget() {
|
||||
@ -31,7 +30,7 @@
|
||||
|
||||
{#if target}
|
||||
<div class="flex w-full max-w-full">
|
||||
<Tray bind:branches />
|
||||
<Tray bind:branches remoteBranches={remoteBranchesData} />
|
||||
<Board {projectId} bind:branches on:newBranch={handleNewBranch} />
|
||||
</div>
|
||||
{:else}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { Branch } from './types';
|
||||
import { Branch, type BranchData } from './types';
|
||||
import { invoke } from '$lib/ipc';
|
||||
import type { PageLoadEvent } from './$types';
|
||||
|
||||
@ -15,6 +15,10 @@ async function getTargetData(params: { projectId: string }) {
|
||||
return invoke<object>('get_target_data', params);
|
||||
}
|
||||
|
||||
async function getRemoteBranchesData(params: { projectId: string }) {
|
||||
return invoke<Array<BranchData>>('git_remote_branches_data', params);
|
||||
}
|
||||
|
||||
function sortBranchHunks(branches: Branch[]): Branch[] {
|
||||
for (const branch of branches) {
|
||||
for (const file of branch.files) {
|
||||
@ -24,12 +28,18 @@ function sortBranchHunks(branches: Branch[]): Branch[] {
|
||||
return branches;
|
||||
}
|
||||
|
||||
function sortBranchData(branchData: BranchData[]): BranchData[] {
|
||||
// sort remote_branches_data by date
|
||||
return branchData.sort((a, b) => b.lastCommitTs - a.lastCommitTs);
|
||||
}
|
||||
|
||||
export async function load(e: PageLoadEvent) {
|
||||
const projectId = e.params.projectId;
|
||||
const target = await getTargetData({ projectId });
|
||||
const remoteBranches = await getRemoteBranches({ projectId });
|
||||
const remoteBranchesData = sortBranchData(await getRemoteBranchesData({ projectId }));
|
||||
const branches: Branch[] = sortBranchHunks(
|
||||
plainToInstance(Branch, await getVirtualBranches({ projectId }))
|
||||
);
|
||||
return { projectId, target, remoteBranches, branches };
|
||||
return { projectId, target, remoteBranches, remoteBranchesData, branches };
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
import { IconBranch } from '$lib/icons';
|
||||
import { IconTriangleUp, IconTriangleDown } from '$lib/icons';
|
||||
import { Button } from '$lib/components';
|
||||
import { message } from '@tauri-apps/api/dialog';
|
||||
|
||||
export let branchId: string;
|
||||
export let name: string;
|
||||
@ -24,6 +25,9 @@
|
||||
const move_files = async (params: { projectId: string; branch: string; paths: Array<string> }) =>
|
||||
invoke<object>('move_virtual_branch_files', params);
|
||||
|
||||
const commit_branch = async (params: { projectId: string; branch: string; message: string }) =>
|
||||
invoke<object>('commit_virtual_branch', params);
|
||||
|
||||
function handleDndEvent(e: CustomEvent<DndEvent<File | Hunk>>) {
|
||||
const newItems = e.detail.items;
|
||||
const fileItems = newItems.filter((item) => item instanceof File) as File[];
|
||||
@ -70,6 +74,17 @@
|
||||
descriptionHeight = textArea.scrollHeight;
|
||||
}
|
||||
|
||||
function commit() {
|
||||
console.log('commit', textArea.value, projectId, branchId);
|
||||
commit_branch({
|
||||
projectId: projectId,
|
||||
branch: branchId,
|
||||
message: textArea.value
|
||||
}).then((res) => {
|
||||
console.log(res);
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
updateTextArea();
|
||||
});
|
||||
@ -99,7 +114,7 @@
|
||||
width="full-width"
|
||||
color="purple"
|
||||
on:click={() => {
|
||||
console.log("i'd like to commit some day but im not sure i'm ready for it");
|
||||
commit();
|
||||
}}>Commit</Button
|
||||
>
|
||||
</div>
|
||||
|
@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Checkbox } from '$lib/components';
|
||||
import type { Branch } from './types';
|
||||
import type { Branch, BranchData } from './types';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
export let branches: Branch[];
|
||||
export let remoteBranches: BranchData[];
|
||||
</script>
|
||||
|
||||
<div class="gb-text-2 w-80 shrink-0 px-2">
|
||||
@ -17,4 +19,29 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if remoteBranches}
|
||||
<div class="flex flex-col">
|
||||
<div class="py-4 text-lg font-bold">Remote Branches</div>
|
||||
{#each remoteBranches as branch}
|
||||
<div class="flex flex-col justify-between rounded-lg p-2" title={branch.branch}>
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="cursor-pointer">
|
||||
{branch.branch.replace('refs/remotes/', '')}
|
||||
</div>
|
||||
<div>{branch.ahead}/{branch.behind}</div>
|
||||
</div>
|
||||
{#if branch.lastCommitTs > 0}
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-sm">{formatDistanceToNow(branch.lastCommitTs * 1000)}</div>
|
||||
<div>
|
||||
{#each branch.authors as author}
|
||||
{author[0]}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -26,3 +26,16 @@ export class Branch extends DndItem {
|
||||
files!: File[];
|
||||
description!: string;
|
||||
}
|
||||
|
||||
export type BranchData = {
|
||||
sha: string;
|
||||
branch: string;
|
||||
name: string;
|
||||
description: string;
|
||||
lastCommitTs: number;
|
||||
firstCommitTs: number;
|
||||
ahead: number;
|
||||
behind: number;
|
||||
upstream: string;
|
||||
authors: string[];
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user