From 11c5f2ab55508e96b9ed13786b5c76afefe9b997 Mon Sep 17 00:00:00 2001 From: Muzaffar Omer Date: Tue, 27 May 2025 05:03:25 +0200 Subject: [PATCH 1/5] feat: restructure modules under lib --- src/app.rs | 6 ++++++ src/lib.rs | 27 +++++++++++++++++++++++++++ src/logs.rs | 4 ++-- src/main.rs | 24 ++---------------------- 4 files changed, 37 insertions(+), 24 deletions(-) create mode 100644 src/lib.rs diff --git a/src/app.rs b/src/app.rs index 9178ea9..c810ae3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -117,3 +117,9 @@ impl App { area } } + +impl Default for App { + fn default() -> Self { + Self::new() + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e00a08c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,27 @@ +pub mod app; +mod cli; +mod components; +mod dirs; +mod git; +pub mod logs; + +use std::io; + +use components::EventState; +use ratatui::{ + backend::Backend, + crossterm::event::{self, Event}, + Terminal, +}; + +pub fn run_app(terminal: &mut Terminal, app: &mut app::App) -> io::Result { + loop { + terminal.draw(|f| app.draw(f))?; + + if let Event::Key(key) = event::read()? { + if app.handle_key(key) == EventState::NotConsumed { + break Ok(false); + } + }; + } +} diff --git a/src/logs.rs b/src/logs.rs index 98a38aa..9bd65ec 100644 --- a/src/logs.rs +++ b/src/logs.rs @@ -1,10 +1,10 @@ -use crate::dirs; - use color_eyre::eyre::Result; use lazy_static::lazy_static; use tracing_error::ErrorLayer; use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, Layer}; +use crate::dirs; + lazy_static! { pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string(); pub static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone()); diff --git a/src/main.rs b/src/main.rs index f92dfc9..d13acf8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,9 @@ -mod app; -mod cli; -mod components; -mod dirs; -mod git; -mod logs; - +use devspace::{app, logs, run_app}; use std::{error::Error, io}; -use components::EventState; use ratatui::{ - backend::{Backend, CrosstermBackend}, + backend::CrosstermBackend, crossterm::{ - event::{self, Event}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }, @@ -40,15 +32,3 @@ fn restore_terminal(terminal: &mut Terminal>) -> io execute!(terminal.backend_mut(), LeaveAlternateScreen,)?; terminal.show_cursor() } - -fn run_app(terminal: &mut Terminal, app: &mut app::App) -> io::Result { - loop { - terminal.draw(|f| app.draw(f))?; - - if let Event::Key(key) = event::read()? { - if app.handle_key(key) == EventState::NotConsumed { - break Ok(false); - } - }; - } -} From 1f5ad207f123c54eabe3649d9179ece3c032b599 Mon Sep 17 00:00:00 2001 From: Muzaffar Omer Date: Tue, 27 May 2025 05:12:52 +0200 Subject: [PATCH 2/5] feat: move all the code related to components under components dir --- src/components.rs | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/components.rs diff --git a/src/components.rs b/src/components.rs deleted file mode 100644 index d923eea..0000000 --- a/src/components.rs +++ /dev/null @@ -1,18 +0,0 @@ -mod create_worktree; -mod filter; -mod list; -mod repositories; -mod worktrees; - -pub use create_worktree::CreateWorktreeComponent; -use ratatui::style::{palette::tailwind::SLATE, Modifier, Style}; -pub use repositories::RepositoriesComponent; -pub use worktrees::WorktreesComponent; - -const SELECTED_STYLE: Style = Style::new().bg(SLATE.c800).add_modifier(Modifier::BOLD); - -#[derive(PartialEq, Debug)] -pub enum EventState { - Consumed, - NotConsumed, -} From 997574da69aedabbad26a9122b2b991ed2589216 Mon Sep 17 00:00:00 2001 From: Muzaffar Omer Date: Tue, 27 May 2025 05:17:15 +0200 Subject: [PATCH 3/5] fix: push missing mod.rs --- src/components/mod.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/components/mod.rs diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..d923eea --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,18 @@ +mod create_worktree; +mod filter; +mod list; +mod repositories; +mod worktrees; + +pub use create_worktree::CreateWorktreeComponent; +use ratatui::style::{palette::tailwind::SLATE, Modifier, Style}; +pub use repositories::RepositoriesComponent; +pub use worktrees::WorktreesComponent; + +const SELECTED_STYLE: Style = Style::new().bg(SLATE.c800).add_modifier(Modifier::BOLD); + +#[derive(PartialEq, Debug)] +pub enum EventState { + Consumed, + NotConsumed, +} From c4963107c18b6a1f3528bf7f62af716a548df330 Mon Sep 17 00:00:00 2001 From: Muzaffar Mohammed Date: Sat, 9 Aug 2025 07:44:39 +0200 Subject: [PATCH 4/5] feat: delete worktrees --- README.md | 52 +++++++++++- src/app.rs | 17 ++-- src/components/worktrees.rs | 17 +++- src/git.rs | 163 +++++++++++++++++++----------------- 4 files changed, 164 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 0adc824..caea377 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ WorkflowInstallationUsage • + DevelopmentContributionRoadmap

@@ -74,6 +75,7 @@ Lists the worktrees that exist under the worktrees directory. | `Ctrl + N` or `Down Arrow` | Move down in the list | | `Ctrl + P` or `Up Arrow` | Move up in the list | | `Ctrl + D` | Switch to `Repositories Mode` | +| `Ctrl + X` | Delete the selected worktree | | `Enter` | Copy the full path of the selected worktree to the clipboard and exit | ### Repositories Mode @@ -94,6 +96,54 @@ Creates new worktree in the selected repository. | ------------- | ------------- | | `Enter` | Create new worktree with the provided branch name in the selected repository and switch to `Worktrees Mode` | +# Development + +To enable debugging in this project, you need to set the `RUST_LOG` environment variable. The project uses the `tracing` library for logging, and the log level is configured via this environment variable. + +### Set the `RUST_LOG` Environment Variable + +You can set the `RUST_LOG` environment variable to control the verbosity of the logs. Here are a few examples: + +- **To see all `info` level logs (the default):** + ```bash + export RUST_LOG=info + ``` + +- **To enable `debug` level logging for all crates:** + ```bash + export RUST_LOG=debug + ``` + +- **To enable `debug` level logging only for the `devspace` crate:** + ```bash + export RUST_LOG=devspace=debug + ``` + +- **For the most verbose logging, you can use `trace`:** + ```bash + export RUST_LOG=trace + ``` + +### Run the Application + +After setting the environment variable, run the application as you normally would: + +```bash +devspace +``` + +### View the Logs + +The log output is written to a file named `devspace.log`. This file is located in the application's data directory. On Linux, this is typically `~/.local/share/devspace/devspace.log`. + +You can view the logs in real-time by using the `tail` command: + +```bash +tail -f ~/.local/share/devspace/devspace.log +``` + +By adjusting the `RUST_LOG` environment variable, you can control the level of detail in the logs, which is very helpful for debugging. + # Contribution So far there are no specific rules for contributions. Pull requests are very welcome. Feel free to checkout the repo and submit new PRs for fixes or new feature requests. @@ -101,7 +151,7 @@ So far there are no specific rules for contributions. Pull requests are very wel # Roadmap - [x] Create new worktrees. -- [ ] Delete worktrees. +- [x] Delete worktrees. - [ ] Show the status of worktrees (e.g. stale, active ...etc.). - [ ] Add metadata to worktrees, e.g. JIRA links, PR links ...etc. diff --git a/src/app.rs b/src/app.rs index c810ae3..8b20939 100644 --- a/src/app.rs +++ b/src/app.rs @@ -62,13 +62,20 @@ impl App { Focus::Worktrees => { let result = self.worktrees.handle_key(key); if result == EventState::Consumed { - result - } else if key.modifiers == KeyModifiers::CONTROL && key.code == KeyCode::Char('d') { + return result; + } + + if key.modifiers == KeyModifiers::CONTROL && key.code == KeyCode::Char('d') { self.focus = Focus::Repositories; - EventState::Consumed - } else { - EventState::NotConsumed + return EventState::Consumed; + } + + if key.modifiers == KeyModifiers::CONTROL && key.code == KeyCode::Char('x') { + self.worktrees.delete_selected_worktree(); + return EventState::Consumed; } + + EventState::NotConsumed } Focus::Repositories => { let result = self.repositories.handle_key(key); diff --git a/src/components/worktrees.rs b/src/components/worktrees.rs index cf706d9..7a10c28 100644 --- a/src/components/worktrees.rs +++ b/src/components/worktrees.rs @@ -1,3 +1,4 @@ +use crate::git; use arboard::Clipboard; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::{ @@ -112,10 +113,22 @@ impl WorktreesComponent { self.selected_index = new_worktree_index; } - pub fn selected_worktree(&mut self) -> Option<&Worktree> { + pub fn delete_selected_worktree(&mut self) { + if let Some(selected_worktree) = self.selected_worktree() { + if let Err(error) = git::delete_worktree(&selected_worktree) { + error!("Could not delete the worktree. Error: {}", error); + } else { + self.worktrees.retain(|w| !w.path().eq(selected_worktree.path())); + self.state.select(None); + self.selected_index = None; + } + } + } + + pub fn selected_worktree(&mut self) -> Option { match self.selected_index { Some(index) => match self.filtered_items().get(index) { - Some(worktree) => Some(worktree), + Some(worktree) => Some((*worktree).clone()), None => None, }, None => None, diff --git a/src/git.rs b/src/git.rs index 3d0e388..4b31c2c 100644 --- a/src/git.rs +++ b/src/git.rs @@ -5,13 +5,34 @@ use std::{ io, path::{Path, PathBuf}, }; -use tracing::error; +use tracing::{debug, error}; pub struct Worktree(git2::Worktree); impl Worktree { pub fn path(&self) -> &str { self.0.path().to_str().unwrap() } + + pub fn name(&self) -> &str { + self.0.name().unwrap() + } +} + +impl Clone for Worktree { + fn clone(&self) -> Self { + let repo = git2::Repository::discover(self.path()).unwrap(); + let worktree = repo.find_worktree(self.name()).unwrap(); + Self(worktree) + } +} + +pub fn delete_worktree(worktree: &Worktree) -> io::Result<()> { + let worktree_path = Path::new(worktree.path()); + if worktree_path.exists() { + fs::remove_dir_all(worktree_path)?; + } + worktree.0.prune(None).unwrap(); + Ok(()) } pub struct Repository(git2::Repository); @@ -27,7 +48,6 @@ impl Repository { let repo_worktrees_dir = PathBuf::from(worktrees_dir).join(self.name()); let new_worktree_dir = PathBuf::from(&repo_worktrees_dir).join(worktree_name); - // Create the directory to store the worktrees of the selected repository let _ = fs::create_dir_all(&repo_worktrees_dir); let mut create_worktree_options = WorktreeAddOptions::new(); @@ -45,17 +65,10 @@ impl Repository { "Could not create the worktree {}. Error: {}", worktree_name, error ); - // None } } } - // fn cleanup(&self) { - // TODO: remove the worktree directory if exists - // TODO: remove the branch if exists - // TODO: remove the worktree from the repo/.git/worktree directory - // } - pub fn name(&self) -> String { let path = String::from(self.0.path().to_str().unwrap()); path.replace("/.git/", "") @@ -86,88 +99,86 @@ impl Repository { } pub fn list_repositories(path: &str) -> Vec { - match list_git_dirs(path) { - Ok(git_dirs) => git_dirs - .iter() - .filter_map(|dir| match Repository::from_path(dir) { - Ok(created_repo) => Some(created_repo), - Err(err) => { - error!( - "Could not create repository from path {}. Error: {}", - dir, err - ); - None - } - }) - .collect(), - Err(err) => { - error!( - "Could not retrieve the git directories for repositories: {}", - err - ); - Vec::new() - } - } -} - -pub fn list_git_dirs(path: &str) -> io::Result> { - let mut git_dirs: Vec = vec![]; - for entry in get_git_subdirs(Path::new(path))? { - if let Some(entry_path) = entry.to_str() { - git_dirs.push(entry_path.to_string()); - } - } + debug!("Listing repositories in: {}", path); + let repositories: Vec = find_git_dirs(Path::new(path)) + .iter() + .filter_map(|dir| match Repository::from_path(dir) { + Ok(created_repo) => Some(created_repo), + Err(err) => { + error!( + "Could not create repository from path {}. Error: {}", + dir, err + ); + None + } + }) + .collect(); - Ok(git_dirs) + repositories } -pub fn is_git_dir(dir: &Path) -> io::Result { +fn is_git_dir(dir: &Path) -> bool { if !dir.is_dir() { - return Ok(false); + return false; } - - let entries = read_dir(dir)?; - let mut result = false; - for entry in entries.flatten() { - let path = entry.path(); - if path.file_name() == Some(OsStr::new(".git")) { - result = true; - break; + match read_dir(dir) { + Ok(entries) => { + let mut result = false; + for entry in entries.flatten() { + let path = entry.path(); + if path.file_name() == Some(OsStr::new(".git")) { + result = true; + break; + } + } + result + } + Err(err) => { + error!("Could not read the directory {}: {}", dir.display(), err); + return false; } } - - Ok(result) } -pub fn get_git_subdirs(path: &Path) -> io::Result> { - let mut git_subdirs: Vec = Vec::new(); +fn find_git_dirs(path: &Path) -> Vec { + let mut git_dirs: Vec = vec![]; if !path.is_dir() { - return Ok(git_subdirs); + return git_dirs; } - if is_git_dir(path)? { - git_subdirs.push(path.to_path_buf()); - return Ok(git_subdirs); + if is_git_dir(path) { + debug!("Found git repository at: {:?}", path); + git_dirs.push(path.to_path_buf().display().to_string()); + return git_dirs; } - let entries = read_dir(path)?; - for entry in entries.flatten() { - if !entry.path().is_dir() { - continue; + return match read_dir(path) { + Err(err) => { + error!("Could not read the directory {}: {}", path.display(), err); + return git_dirs; } + Ok(entries) => { + for entry in entries.flatten() { + if !entry.path().is_dir() { + continue; + } - if let Ok(true) = is_git_dir(&entry.path()) { - git_subdirs.push(entry.path().to_path_buf()); - } else { - for entry in read_dir(entry.path())?.flatten() { - let entry_git_subdirs = get_git_subdirs(&entry.path())?; - git_subdirs.extend(entry_git_subdirs); + if let true = is_git_dir(&entry.path()) { + debug!("Found git repository at: {:?}", entry.path()); + git_dirs.push(entry.path().to_path_buf().display().to_string()); + } else { + debug!( + "No git repository found at: {:?}, continuing search", + entry.path() + ); + let sub_entries = find_git_dirs(&entry.path()); + git_dirs.extend(sub_entries); + } } + git_dirs } - } - - Ok(git_subdirs) + }; } #[cfg(test)] @@ -180,8 +191,7 @@ mod tests { fn test_not_git_dir() { let temp_dir = tempdir().expect("Could not create temporary directory"); assert!( - !is_git_dir(temp_dir.path()) - .expect("is_git_dir failed to check whether temporary path is git directory"), + !is_git_dir(temp_dir.path()), "Expected is_git_dir to be false, but it was true" ); } @@ -194,8 +204,7 @@ mod tests { .expect("Could not create .git directory inside the temporary dir"); assert!( - is_git_dir(temp_dir.path()) - .expect("is_git_dir failed to check whether temporary path is git directory"), + is_git_dir(temp_dir.path()), "Expected is_git_dir to be true, but it was false" ); } @@ -221,7 +230,7 @@ mod tests { }); } - let git_subdirs = get_git_subdirs(temp_dir.path()).unwrap(); + let git_subdirs = find_git_dirs(temp_dir.path()).unwrap(); for path in [ "first_git_dir/", From 27587bf6ad694e0e4e79b2981d01a7fc12f5a3fa Mon Sep 17 00:00:00 2001 From: Muzaffar Mohammed Date: Sat, 9 Aug 2025 09:55:11 +0200 Subject: [PATCH 5/5] fix: tests --- src/git.rs | 49 +------------------------------------------------ 1 file changed, 1 insertion(+), 48 deletions(-) diff --git a/src/git.rs b/src/git.rs index 4b31c2c..735282d 100644 --- a/src/git.rs +++ b/src/git.rs @@ -209,45 +209,6 @@ mod tests { ); } - #[test] - fn test_get_gitsubdirs() { - let temp_dir = tempdir().expect("Could not create temporary directory"); - - for path in [ - "first_git_dir/.git", - "second_git_dir/.git", - "third_git_dir/subdir/subdir/", - "fourth_git_dir/subdir/subdir/.git", - ] { - fs::DirBuilder::new() - .recursive(true) - .create(temp_dir.path().join(path)) - .unwrap_or_else(|_| { - panic!( - "Could not create {} directory inside the temporary dir", - path - ) - }); - } - - let git_subdirs = find_git_dirs(temp_dir.path()).unwrap(); - - for path in [ - "first_git_dir/", - "second_git_dir", - "fourth_git_dir/subdir/subdir", - ] { - let expected_dir = temp_dir.path().join(path); - assert!( - git_subdirs - .iter() - .any(|dir| dir.to_path_buf() == expected_dir), - "Expected {} to be listed in the git subdirectories, but it was not included", - expected_dir.to_str().unwrap() - ) - } - } - #[test] fn test_list() { let temp_dir = tempdir().expect("Could not create temporary directory"); @@ -269,14 +230,6 @@ mod tests { }); } - let git_subdirs = list_git_dirs( - temp_dir - .path() - .to_str() - .expect("Could not convert temporary path to string"), - ) - .expect("Could not list all git subdirectories"); - for path in [ "first_git_dir", "second_git_dir", @@ -284,7 +237,7 @@ mod tests { ] { let expected_dir = temp_dir.path().join(path); assert!( - git_subdirs + find_git_dirs(temp_dir.path()) .iter() .any(|dir| dir == expected_dir.to_str().unwrap()), "Expected {} to be listed in the git subdirectories, but it was not included",