diff --git a/README.md b/README.md
index 0adc824..caea377 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@
Workflow •
Installation •
Usage •
+ Development •
Contribution •
Roadmap
@@ -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 9178ea9..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);
@@ -117,3 +124,9 @@ impl App {
area
}
}
+
+impl Default for App {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/src/components.rs b/src/components/mod.rs
similarity index 100%
rename from src/components.rs
rename to src/components/mod.rs
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..735282d 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);
- }
-
- 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;
+ return false;
+ }
+ 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,51 +204,11 @@ 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"
);
}
- #[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 = get_git_subdirs(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");
@@ -260,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",
@@ -275,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",
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);
- }
- };
- }
-}