Compare commits
5 Commits
v0.1
...
09f22edf49
| Author | SHA1 | Date | |
|---|---|---|---|
| 09f22edf49 | |||
| b0746c95b5 | |||
| 153d09f3ef | |||
| 74a7772a29 | |||
| 5df6dcb053 |
29
README.md
29
README.md
@@ -79,6 +79,35 @@ $ grm status
|
|||||||
+----------+------------+----------------------------------+--------+---------+
|
+----------+------------+----------------------------------+--------+---------+
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Manage worktrees for projects
|
||||||
|
|
||||||
|
Optionally, GRM can also set up a repository to support multiple worktrees. See
|
||||||
|
[the git documentation](https://git-scm.com/docs/git-worktree) for details about
|
||||||
|
worktrees. Long story short: Worktrees allow you to have multiple independent
|
||||||
|
checkouts of the same repository in different directories, backed by a single
|
||||||
|
git repository.
|
||||||
|
|
||||||
|
To use this, specify `worktree_setup = true` for a repo in your configuration.
|
||||||
|
After the sync, you will see that the target directory is empty. Actually, the
|
||||||
|
repository was bare-cloned into a hidden directory: `.git-main-working-tree`.
|
||||||
|
Don't touch it! GRM provides a command to manage working trees.
|
||||||
|
|
||||||
|
Use `grm worktree add <name>` to create a new checkout of a new branch into
|
||||||
|
a subdirectory. An example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ grm worktree add mybranch
|
||||||
|
$ cd ./mybranch
|
||||||
|
$ git status
|
||||||
|
On branch mybranch
|
||||||
|
|
||||||
|
nothing to commit, working tree clean
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're done with your worktree, use `grm worktree delete <name>` to remove it.
|
||||||
|
GRM will refuse to delete worktrees that contain uncommitted or unpushed changes,
|
||||||
|
otherwise you might lose work.
|
||||||
|
|
||||||
# Why?
|
# Why?
|
||||||
|
|
||||||
I have a **lot** of repositories on my machines. My own stuff, forks, quick
|
I have a **lot** of repositories on my machines. My own stuff, forks, quick
|
||||||
|
|||||||
33
src/cmd.rs
33
src/cmd.rs
@@ -28,6 +28,12 @@ pub enum SubCommand {
|
|||||||
Find(Find),
|
Find(Find),
|
||||||
#[clap(about = "Show status of configured repositories")]
|
#[clap(about = "Show status of configured repositories")]
|
||||||
Status(OptionalConfig),
|
Status(OptionalConfig),
|
||||||
|
#[clap(
|
||||||
|
visible_alias = "wt",
|
||||||
|
about = "Manage worktrees"
|
||||||
|
)]
|
||||||
|
Worktree(Worktree),
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@@ -45,11 +51,7 @@ pub struct Sync {
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap()]
|
#[clap()]
|
||||||
pub struct OptionalConfig {
|
pub struct OptionalConfig {
|
||||||
#[clap(
|
#[clap(short, long, about = "Path to the configuration file")]
|
||||||
short,
|
|
||||||
long,
|
|
||||||
about = "Path to the configuration file"
|
|
||||||
)]
|
|
||||||
pub config: Option<String>,
|
pub config: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +61,27 @@ pub struct Find {
|
|||||||
pub path: String,
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
pub struct Worktree {
|
||||||
|
#[clap(subcommand, name = "action")]
|
||||||
|
pub action: WorktreeAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
pub enum WorktreeAction {
|
||||||
|
#[clap(about = "Add a new worktree")]
|
||||||
|
Add(WorktreeActionArgs),
|
||||||
|
#[clap(about = "Add an existing worktree")]
|
||||||
|
Delete(WorktreeActionArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
pub struct WorktreeActionArgs {
|
||||||
|
#[clap(about = "Name of the worktree")]
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn parse() -> Opts {
|
pub fn parse() -> Opts {
|
||||||
Opts::parse()
|
Opts::parse()
|
||||||
}
|
}
|
||||||
|
|||||||
291
src/lib.rs
291
src/lib.rs
@@ -10,13 +10,15 @@ mod repo;
|
|||||||
use config::{Config, Tree};
|
use config::{Config, Tree};
|
||||||
use output::*;
|
use output::*;
|
||||||
|
|
||||||
use comfy_table::{Table, Cell};
|
use comfy_table::{Cell, Table};
|
||||||
|
|
||||||
use repo::{
|
use repo::{
|
||||||
clone_repo, detect_remote_type, get_repo_status, init_repo, open_repo, Remote, Repo,
|
clone_repo, detect_remote_type, get_repo_status, init_repo, open_repo, Remote,
|
||||||
RepoErrorKind, RemoteTrackingStatus
|
RemoteTrackingStatus, Repo, RepoErrorKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree";
|
||||||
|
|
||||||
fn path_as_string(path: &Path) -> String {
|
fn path_as_string(path: &Path) -> String {
|
||||||
path.to_path_buf().into_os_string().into_string().unwrap()
|
path.to_path_buf().into_os_string().into_string().unwrap()
|
||||||
}
|
}
|
||||||
@@ -65,21 +67,36 @@ fn sync_trees(config: Config) {
|
|||||||
|
|
||||||
for repo in &repos {
|
for repo in &repos {
|
||||||
let repo_path = root_path.join(&repo.name);
|
let repo_path = root_path.join(&repo.name);
|
||||||
|
let actual_git_directory = get_actual_git_directory(&repo_path, repo.worktree_setup);
|
||||||
|
|
||||||
let mut repo_handle = None;
|
let mut repo_handle = None;
|
||||||
|
|
||||||
if repo_path.exists() {
|
if repo_path.exists() {
|
||||||
repo_handle = Some(open_repo(&repo_path).unwrap_or_else(|error| {
|
if repo.worktree_setup && !actual_git_directory.exists() {
|
||||||
print_repo_error(&repo.name, &format!("Opening repository failed: {}", error));
|
print_repo_error(
|
||||||
|
&repo.name,
|
||||||
|
&format!("Repo already exists, but is not using a worktree setup"),
|
||||||
|
);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}));
|
}
|
||||||
|
repo_handle = Some(open_repo(&repo_path, repo.worktree_setup).unwrap_or_else(
|
||||||
|
|error| {
|
||||||
|
print_repo_error(
|
||||||
|
&repo.name,
|
||||||
|
&format!("Opening repository failed: {}", error),
|
||||||
|
);
|
||||||
|
process::exit(1);
|
||||||
|
},
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
if matches!(&repo.remotes, None) || repo.remotes.as_ref().unwrap().len().clone() == 0 {
|
if matches!(&repo.remotes, None)
|
||||||
|
|| repo.remotes.as_ref().unwrap().len().clone() == 0
|
||||||
|
{
|
||||||
print_repo_action(
|
print_repo_action(
|
||||||
&repo.name,
|
&repo.name,
|
||||||
"Repository does not have remotes configured, initializing new",
|
"Repository does not have remotes configured, initializing new",
|
||||||
);
|
);
|
||||||
repo_handle = match init_repo(&repo_path) {
|
repo_handle = match init_repo(&repo_path, repo.worktree_setup) {
|
||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
print_repo_success(&repo.name, "Repository created");
|
print_repo_success(&repo.name, "Repository created");
|
||||||
Some(r)
|
Some(r)
|
||||||
@@ -95,7 +112,7 @@ fn sync_trees(config: Config) {
|
|||||||
} else {
|
} else {
|
||||||
let first = repo.remotes.as_ref().unwrap().first().unwrap();
|
let first = repo.remotes.as_ref().unwrap().first().unwrap();
|
||||||
|
|
||||||
match clone_repo(first, &repo_path) {
|
match clone_repo(first, &repo_path, repo.worktree_setup) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
print_repo_success(&repo.name, "Repository successfully cloned");
|
print_repo_success(&repo.name, "Repository successfully cloned");
|
||||||
}
|
}
|
||||||
@@ -110,8 +127,9 @@ fn sync_trees(config: Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(remotes) = &repo.remotes {
|
if let Some(remotes) = &repo.remotes {
|
||||||
let repo_handle = repo_handle
|
let repo_handle = repo_handle.unwrap_or_else(|| {
|
||||||
.unwrap_or_else(|| open_repo(&repo_path).unwrap_or_else(|_| process::exit(1)));
|
open_repo(&repo_path, repo.worktree_setup).unwrap_or_else(|_| process::exit(1))
|
||||||
|
});
|
||||||
|
|
||||||
let current_remotes: Vec<String> = match repo_handle.remotes() {
|
let current_remotes: Vec<String> = match repo_handle.remotes() {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
@@ -190,7 +208,7 @@ fn sync_trees(config: Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let current_repos = find_repos_without_details(&root_path).unwrap();
|
let current_repos = find_repos_without_details(&root_path).unwrap();
|
||||||
for repo in current_repos {
|
for (repo, _) in current_repos {
|
||||||
let name = path_as_string(repo.strip_prefix(&root_path).unwrap());
|
let name = path_as_string(repo.strip_prefix(&root_path).unwrap());
|
||||||
if !repos.iter().any(|r| r.name == name) {
|
if !repos.iter().any(|r| r.name == name) {
|
||||||
print_warning(&format!("Found unmanaged repository: {}", name));
|
print_warning(&format!("Found unmanaged repository: {}", name));
|
||||||
@@ -199,12 +217,16 @@ fn sync_trees(config: Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_repos_without_details(path: &Path) -> Option<Vec<PathBuf>> {
|
fn find_repos_without_details(path: &Path) -> Option<Vec<(PathBuf, bool)>> {
|
||||||
let mut repos: Vec<PathBuf> = Vec::new();
|
let mut repos: Vec<(PathBuf, bool)> = Vec::new();
|
||||||
|
|
||||||
let git_dir = path.join(".git");
|
let git_dir = path.join(".git");
|
||||||
|
let git_worktree = path.join(GIT_MAIN_WORKTREE_DIRECTORY);
|
||||||
|
|
||||||
if git_dir.exists() {
|
if git_dir.exists() {
|
||||||
repos.push(path.to_path_buf());
|
repos.push((path.to_path_buf(), false));
|
||||||
|
} else if git_worktree.exists() {
|
||||||
|
repos.push((path.to_path_buf(), true));
|
||||||
} else {
|
} else {
|
||||||
match fs::read_dir(path) {
|
match fs::read_dir(path) {
|
||||||
Ok(contents) => {
|
Ok(contents) => {
|
||||||
@@ -238,14 +260,29 @@ fn find_repos_without_details(path: &Path) -> Option<Vec<PathBuf>> {
|
|||||||
Some(repos)
|
Some(repos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_actual_git_directory(path: &Path, is_worktree: bool) -> PathBuf {
|
||||||
|
match is_worktree {
|
||||||
|
false => path.to_path_buf(),
|
||||||
|
true => path.join(GIT_MAIN_WORKTREE_DIRECTORY),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn find_repos(root: &Path) -> Option<Vec<Repo>> {
|
fn find_repos(root: &Path) -> Option<Vec<Repo>> {
|
||||||
let mut repos: Vec<Repo> = Vec::new();
|
let mut repos: Vec<Repo> = Vec::new();
|
||||||
|
|
||||||
for path in find_repos_without_details(root).unwrap() {
|
for (path, is_worktree) in find_repos_without_details(root).unwrap() {
|
||||||
let repo = match open_repo(&path) {
|
let repo = match open_repo(&path, is_worktree) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
print_error(&format!("Error opening repo {}: {}", path.display(), e));
|
print_error(&format!(
|
||||||
|
"Error opening repo {}{}: {}",
|
||||||
|
path.display(),
|
||||||
|
match is_worktree {
|
||||||
|
true => " as worktree",
|
||||||
|
false => "",
|
||||||
|
},
|
||||||
|
e
|
||||||
|
));
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -330,6 +367,7 @@ fn find_repos(root: &Path) -> Option<Vec<Repo>> {
|
|||||||
false => path_as_string(path.strip_prefix(&root).unwrap()),
|
false => path_as_string(path.strip_prefix(&root).unwrap()),
|
||||||
},
|
},
|
||||||
remotes,
|
remotes,
|
||||||
|
worktree_setup: is_worktree,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Some(repos)
|
Some(repos)
|
||||||
@@ -386,41 +424,56 @@ fn add_repo_status(table: &mut Table, repo_name: &str, repo_handle: &git2::Repos
|
|||||||
out.push(format!("Deleted: {}\n", changes.files_deleted))
|
out.push(format!("Deleted: {}\n", changes.files_deleted))
|
||||||
}
|
}
|
||||||
out.into_iter().collect::<String>().trim().to_string()
|
out.into_iter().collect::<String>().trim().to_string()
|
||||||
},
|
}
|
||||||
None => String::from("No changes"),
|
None => String::from("\u{2714}"),
|
||||||
},
|
},
|
||||||
&repo_status.branches.iter().map(|(branch_name, remote_branch)| {
|
&repo_status
|
||||||
format!("branch: {}{}\n",
|
.branches
|
||||||
&branch_name,
|
.iter()
|
||||||
&match remote_branch {
|
.map(|(branch_name, remote_branch)| {
|
||||||
None => String::from(" <!local>"),
|
format!(
|
||||||
Some((remote_branch_name, remote_tracking_status)) => {
|
"branch: {}{}\n",
|
||||||
format!(" <{}>{}",
|
&branch_name,
|
||||||
remote_branch_name,
|
&match remote_branch {
|
||||||
&match remote_tracking_status {
|
None => String::from(" <!local>"),
|
||||||
RemoteTrackingStatus::UpToDate => String::from(" \u{2714}"),
|
Some((remote_branch_name, remote_tracking_status)) => {
|
||||||
RemoteTrackingStatus::Ahead(d) => format!(" [+{}]", &d),
|
format!(
|
||||||
RemoteTrackingStatus::Behind(d) => format!(" [-{}]", &d),
|
" <{}>{}",
|
||||||
RemoteTrackingStatus::Diverged(d1, d2) => format!(" [-{}/+{}]", &d1,&d2),
|
remote_branch_name,
|
||||||
}
|
&match remote_tracking_status {
|
||||||
)
|
RemoteTrackingStatus::UpToDate => String::from(" \u{2714}"),
|
||||||
|
RemoteTrackingStatus::Ahead(d) => format!(" [+{}]", &d),
|
||||||
|
RemoteTrackingStatus::Behind(d) => format!(" [-{}]", &d),
|
||||||
|
RemoteTrackingStatus::Diverged(d1, d2) =>
|
||||||
|
format!(" [-{}/+{}]", &d1, &d2),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
})
|
||||||
}).collect::<String>().trim().to_string(),
|
.collect::<String>()
|
||||||
|
.trim()
|
||||||
|
.to_string(),
|
||||||
&match repo_status.head {
|
&match repo_status.head {
|
||||||
Some(head) => head,
|
Some(head) => head,
|
||||||
None => String::from("Empty"),
|
None => String::from("Empty"),
|
||||||
},
|
},
|
||||||
&repo_status.remotes.iter().map(|r| format!("{}\n", r)).collect::<String>().trim().to_string(),
|
&repo_status
|
||||||
|
.remotes
|
||||||
|
.iter()
|
||||||
|
.map(|r| format!("{}\n", r))
|
||||||
|
.collect::<String>()
|
||||||
|
.trim()
|
||||||
|
.to_string(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_single_repo_status(path: &Path) {
|
fn show_single_repo_status(path: &Path, is_worktree: bool) {
|
||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
add_table_header(&mut table);
|
add_table_header(&mut table);
|
||||||
|
|
||||||
let repo_handle = open_repo(path);
|
let repo_handle = open_repo(path, is_worktree);
|
||||||
|
|
||||||
if let Err(error) = repo_handle {
|
if let Err(error) = repo_handle {
|
||||||
if error.kind == RepoErrorKind::NotFound {
|
if error.kind == RepoErrorKind::NotFound {
|
||||||
@@ -435,14 +488,14 @@ fn show_single_repo_status(path: &Path) {
|
|||||||
None => {
|
None => {
|
||||||
print_warning("Cannot detect repo name. Are you working in /?");
|
print_warning("Cannot detect repo name. Are you working in /?");
|
||||||
String::from("unknown")
|
String::from("unknown")
|
||||||
},
|
}
|
||||||
Some(file_name) => match file_name.to_str() {
|
Some(file_name) => match file_name.to_str() {
|
||||||
None => {
|
None => {
|
||||||
print_warning("Name of current directory is not valid UTF-8");
|
print_warning("Name of current directory is not valid UTF-8");
|
||||||
String::from("invalid")
|
String::from("invalid")
|
||||||
},
|
}
|
||||||
Some(name) => name.to_string(),
|
Some(name) => name.to_string(),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
add_repo_status(&mut table, &repo_name, &repo_handle.unwrap());
|
add_repo_status(&mut table, &repo_name, &repo_handle.unwrap());
|
||||||
@@ -463,15 +516,21 @@ fn show_status(config: Config) {
|
|||||||
let repo_path = root_path.join(&repo.name);
|
let repo_path = root_path.join(&repo.name);
|
||||||
|
|
||||||
if !repo_path.exists() {
|
if !repo_path.exists() {
|
||||||
print_repo_error(&repo.name, &"Repository does not exist. Run sync?".to_string());
|
print_repo_error(
|
||||||
|
&repo.name,
|
||||||
|
&"Repository does not exist. Run sync?".to_string(),
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let repo_handle = open_repo(&repo_path);
|
let repo_handle = open_repo(&repo_path, repo.worktree_setup);
|
||||||
|
|
||||||
if let Err(error) = repo_handle {
|
if let Err(error) = repo_handle {
|
||||||
if error.kind == RepoErrorKind::NotFound {
|
if error.kind == RepoErrorKind::NotFound {
|
||||||
print_repo_error(&repo.name, &"No git repository found. Run sync?".to_string());
|
print_repo_error(
|
||||||
|
&repo.name,
|
||||||
|
&"No git repository found. Run sync?".to_string(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
print_repo_error(&repo.name, &format!("Opening repository failed: {}", error));
|
print_repo_error(&repo.name, &format!("Opening repository failed: {}", error));
|
||||||
}
|
}
|
||||||
@@ -500,31 +559,30 @@ pub fn run() {
|
|||||||
};
|
};
|
||||||
sync_trees(config);
|
sync_trees(config);
|
||||||
}
|
}
|
||||||
cmd::SubCommand::Status(args) => {
|
cmd::SubCommand::Status(args) => match &args.config {
|
||||||
match &args.config {
|
Some(config_path) => {
|
||||||
Some(config_path) => {
|
let config = match config::read_config(config_path) {
|
||||||
let config = match config::read_config(config_path) {
|
Ok(c) => c,
|
||||||
Ok(c) => c,
|
Err(e) => {
|
||||||
Err(e) => {
|
print_error(&e);
|
||||||
print_error(&e);
|
process::exit(1);
|
||||||
process::exit(1);
|
}
|
||||||
}
|
};
|
||||||
};
|
show_status(config);
|
||||||
show_status(config);
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
let dir = match std::env::current_dir(){
|
|
||||||
Ok(d) => d,
|
|
||||||
Err(e) => {
|
|
||||||
print_error(&format!("Could not open current directory: {}", e));
|
|
||||||
process::exit(1);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
show_single_repo_status(&dir);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
None => {
|
||||||
|
let dir = match std::env::current_dir() {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Could not open current directory: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let has_worktree = dir.join(GIT_MAIN_WORKTREE_DIRECTORY).exists();
|
||||||
|
show_single_repo_status(&dir, has_worktree);
|
||||||
|
}
|
||||||
|
},
|
||||||
cmd::SubCommand::Find(find) => {
|
cmd::SubCommand::Find(find) => {
|
||||||
let path = Path::new(&find.path);
|
let path = Path::new(&find.path);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
@@ -541,9 +599,98 @@ pub fn run() {
|
|||||||
trees: vec![find_in_tree(path).unwrap()],
|
trees: vec![find_in_tree(path).unwrap()],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
println!("{:#?}", config);
|
||||||
let toml = toml::to_string(&config).unwrap();
|
let toml = toml::to_string(&config).unwrap();
|
||||||
|
|
||||||
print!("{}", toml);
|
print!("{}", toml);
|
||||||
}
|
},
|
||||||
|
cmd::SubCommand::Worktree(args) => {
|
||||||
|
let dir = match std::env::current_dir() {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Could not open current directory: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match args.action {
|
||||||
|
cmd::WorktreeAction::Add(action_args) => {
|
||||||
|
let repo = match open_repo(&dir, true) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
match e.kind {
|
||||||
|
RepoErrorKind::NotFound => print_error(&"Current directory does not contain a worktree setup"),
|
||||||
|
_ => print_error(&format!("Error opening repo: {}", e)),
|
||||||
|
}
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let worktrees = repo.worktrees().unwrap().iter().map(|e| e.unwrap()).collect::<String>();
|
||||||
|
if worktrees.contains(&action_args.name) {
|
||||||
|
print_error("Worktree directory already exists");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
match repo.worktree(&action_args.name, &dir.join(&action_args.name), None) {
|
||||||
|
Ok(_) => print_success(&format!("Worktree {} created", &action_args.name)),
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Error creating worktree: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
cmd::WorktreeAction::Delete(action_args) => {
|
||||||
|
let worktree_dir = dir.join(&action_args.name);
|
||||||
|
if !worktree_dir.exists() {
|
||||||
|
print_error(&format!("{} does not exist", &action_args.name));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
let repo = match open_repo(&worktree_dir, false) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Error opening repo: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let status = get_repo_status(&repo);
|
||||||
|
if let Some(_) = status.changes {
|
||||||
|
println!("Changes found in worktree, refusing to delete!");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut branch = repo.find_branch(&action_args.name, git2::BranchType::Local).unwrap();
|
||||||
|
match branch.upstream() {
|
||||||
|
Ok(remote_branch) => {
|
||||||
|
let (ahead, behind) = repo
|
||||||
|
.graph_ahead_behind(
|
||||||
|
branch.get().peel_to_commit().unwrap().id(),
|
||||||
|
remote_branch.get().peel_to_commit().unwrap().id(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if (ahead, behind) != (0, 0) {
|
||||||
|
print_error(&format!("Branch {} is not in line with remote branch, refusing to delete worktree!", &action_args.name));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
print_error(&format!("No remote tracking branch for branch {} found, refusing to delete worktree!", &action_args.name));
|
||||||
|
process::exit(1);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
match std::fs::remove_dir_all(&worktree_dir) {
|
||||||
|
Ok(_) => print_success(&format!("Worktree {} deleted", &action_args.name)),
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Error deleting {}: {}", &worktree_dir.display(), e));
|
||||||
|
process::exit(1);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
repo.find_worktree(&action_args.name).unwrap().prune(None).unwrap();
|
||||||
|
branch.delete().unwrap();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
71
src/repo.rs
71
src/repo.rs
@@ -46,10 +46,18 @@ pub struct Remote {
|
|||||||
pub remote_type: RemoteType,
|
pub remote_type: RemoteType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn worktree_setup_default() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Repo {
|
pub struct Repo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
|
#[serde(default = "worktree_setup_default")]
|
||||||
|
pub worktree_setup: bool,
|
||||||
|
|
||||||
pub remotes: Option<Vec<Remote>>,
|
pub remotes: Option<Vec<Remote>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,8 +175,16 @@ pub fn detect_remote_type(remote_url: &str) -> Option<RemoteType> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_repo(path: &Path) -> Result<Repository, RepoError> {
|
pub fn open_repo(path: &Path, is_worktree: bool) -> Result<Repository, RepoError> {
|
||||||
match Repository::open(path) {
|
let open_func = match is_worktree {
|
||||||
|
true => Repository::open_bare,
|
||||||
|
false => Repository::open,
|
||||||
|
};
|
||||||
|
let path = match is_worktree {
|
||||||
|
true => path.join(super::GIT_MAIN_WORKTREE_DIRECTORY),
|
||||||
|
false => path.to_path_buf(),
|
||||||
|
};
|
||||||
|
match open_func(path) {
|
||||||
Ok(r) => Ok(r),
|
Ok(r) => Ok(r),
|
||||||
Err(e) => match e.code() {
|
Err(e) => match e.code() {
|
||||||
git2::ErrorCode::NotFound => Err(RepoError::new(RepoErrorKind::NotFound)),
|
git2::ErrorCode::NotFound => Err(RepoError::new(RepoErrorKind::NotFound)),
|
||||||
@@ -179,24 +195,43 @@ pub fn open_repo(path: &Path) -> Result<Repository, RepoError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_repo(path: &Path) -> Result<Repository, Box<dyn std::error::Error>> {
|
pub fn init_repo(path: &Path, is_worktree: bool) -> Result<Repository, Box<dyn std::error::Error>> {
|
||||||
match Repository::init(path) {
|
match is_worktree {
|
||||||
Ok(r) => Ok(r),
|
false => match Repository::init(path) {
|
||||||
Err(e) => Err(Box::new(e)),
|
Ok(r) => Ok(r),
|
||||||
|
Err(e) => Err(Box::new(e)),
|
||||||
|
},
|
||||||
|
true => match Repository::init_bare(path.join(super::GIT_MAIN_WORKTREE_DIRECTORY)) {
|
||||||
|
Ok(r) => Ok(r),
|
||||||
|
Err(e) => Err(Box::new(e)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clone_repo(remote: &Remote, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn clone_repo(
|
||||||
|
remote: &Remote,
|
||||||
|
path: &Path,
|
||||||
|
is_worktree: bool,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let clone_target = match is_worktree {
|
||||||
|
false => path.to_path_buf(),
|
||||||
|
true => path.join(super::GIT_MAIN_WORKTREE_DIRECTORY),
|
||||||
|
};
|
||||||
|
|
||||||
print_action(&format!(
|
print_action(&format!(
|
||||||
"Cloning into \"{}\" from \"{}\"",
|
"Cloning into \"{}\" from \"{}\"",
|
||||||
&path.display(),
|
&clone_target.display(),
|
||||||
&remote.url
|
&remote.url
|
||||||
));
|
));
|
||||||
match remote.remote_type {
|
match remote.remote_type {
|
||||||
RemoteType::Https => match Repository::clone(&remote.url, &path) {
|
RemoteType::Https => {
|
||||||
Ok(_) => Ok(()),
|
let mut builder = git2::build::RepoBuilder::new();
|
||||||
Err(e) => Err(Box::new(e)),
|
builder.bare(is_worktree);
|
||||||
},
|
match builder.clone(&remote.url, &clone_target) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(Box::new(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
RemoteType::Ssh => {
|
RemoteType::Ssh => {
|
||||||
let mut callbacks = RemoteCallbacks::new();
|
let mut callbacks = RemoteCallbacks::new();
|
||||||
callbacks.credentials(|_url, username_from_url, _allowed_types| {
|
callbacks.credentials(|_url, username_from_url, _allowed_types| {
|
||||||
@@ -207,9 +242,10 @@ pub fn clone_repo(remote: &Remote, path: &Path) -> Result<(), Box<dyn std::error
|
|||||||
fo.remote_callbacks(callbacks);
|
fo.remote_callbacks(callbacks);
|
||||||
|
|
||||||
let mut builder = git2::build::RepoBuilder::new();
|
let mut builder = git2::build::RepoBuilder::new();
|
||||||
|
builder.bare(is_worktree);
|
||||||
builder.fetch_options(fo);
|
builder.fetch_options(fo);
|
||||||
|
|
||||||
match builder.clone(&remote.url, path) {
|
match builder.clone(&remote.url, &clone_target) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(e) => Err(Box::new(e)),
|
Err(e) => Err(Box::new(e)),
|
||||||
}
|
}
|
||||||
@@ -237,7 +273,9 @@ pub fn get_repo_status(repo: &git2::Repository) -> RepoStatus {
|
|||||||
false => Some(repo.head().unwrap().shorthand().unwrap().to_string()),
|
false => Some(repo.head().unwrap().shorthand().unwrap().to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let statuses = repo.statuses(None).unwrap();
|
let statuses = repo
|
||||||
|
.statuses(Some(git2::StatusOptions::new().include_ignored(false).include_untracked(true)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let changes = match statuses.is_empty() {
|
let changes = match statuses.is_empty() {
|
||||||
true => None,
|
true => None,
|
||||||
@@ -264,6 +302,11 @@ pub fn get_repo_status(repo: &git2::Repository) -> RepoStatus {
|
|||||||
files_deleted += 1;
|
files_deleted += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (files_new, files_modified, files_deleted) == (0, 0, 0) {
|
||||||
|
panic!(
|
||||||
|
"is_empty() returned true, but no file changes were detected. This is a bug!"
|
||||||
|
);
|
||||||
|
}
|
||||||
Some(RepoChanges {
|
Some(RepoChanges {
|
||||||
files_new,
|
files_new,
|
||||||
files_modified,
|
files_modified,
|
||||||
|
|||||||
Reference in New Issue
Block a user