Skip to content

Commit

Permalink
copy_untracked should be copy_ignored (#98)
Browse files Browse the repository at this point in the history
Wrong design decision. Thanks to @lf- for catching this!

Closes #93
  • Loading branch information
9999years authored Nov 18, 2024
1 parent 0fce93d commit eddd725
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 79 deletions.
10 changes: 7 additions & 3 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,18 @@ enable_gh = false
#
# `man git-prole-add`
[add]
# When `git prole add` is used to create a new worktree, untracked files are
# copied to the new worktree from the current worktree by default.
# When `git prole add` is used to create a new worktree, `.gitignored` files
# are copied to the new worktree from the current worktree by default.
#
# This will allow you to get started quickly by copying build products and
# other configuration files over to the new worktree. However, copying these
# files can take some time, so this setting can be used to disable this
# behavior if needed.
copy_untracked = true
#
# Note: Untracked files which are not ignored will not be copied.
#
# See: `man 'gitignore(5)'`
copy_ignored = true

# Commands to run when a new worktree is added.
commands = [
Expand Down
71 changes: 39 additions & 32 deletions src/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use crate::git::GitLike;
use crate::git::LocalBranchRef;
use crate::AddWorktreeOpts;
use crate::PathDisplay;
use crate::StatusEntry;
use crate::Utf8Absolutize;

/// A plan for creating a new `git worktree`.
Expand All @@ -30,8 +31,28 @@ pub struct WorktreePlan<'a> {
git: AppGit<'a, Utf8PathBuf>,
destination: Utf8PathBuf,
branch: BranchStartPointPlan,
/// Relative paths to copy to the new worktree, if any.
copy_untracked: Vec<Utf8PathBuf>,
copy_ignored: Vec<StatusEntry>,
}

impl Display for WorktreePlan<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Creating worktree in {} {}",
self.destination.display_path_cwd(),
self.branch,
)?;

if !self.copy_ignored.is_empty() {
write!(
f,
"\nCopying {} ignored paths to new worktree",
self.copy_ignored.len()
)?;
}

Ok(())
}
}

impl<'a> WorktreePlan<'a> {
Expand All @@ -53,19 +74,24 @@ impl<'a> WorktreePlan<'a> {
let git = git.with_current_dir(worktree);
let branch = BranchStartPointPlan::new(&git, args)?;
let destination = Self::destination_plan(&git, args, &branch)?;
let copy_untracked = Self::untracked_plan(&git)?;
let copy_ignored = Self::copy_ignored_plan(&git)?;
Ok(Self {
git,
branch,
destination,
copy_untracked,
copy_ignored,
})
}

#[instrument(level = "trace")]
fn untracked_plan(git: &AppGit<'_, Utf8PathBuf>) -> miette::Result<Vec<Utf8PathBuf>> {
if git.config.file.add.copy_untracked() && git.worktree().is_inside()? {
git.status().untracked_files()
fn copy_ignored_plan(git: &AppGit<'_, Utf8PathBuf>) -> miette::Result<Vec<StatusEntry>> {
if git.config.file.add.copy_ignored() && git.worktree().is_inside()? {
Ok(git
.status()
.get()?
.into_iter()
.filter(|entry| entry.is_ignored())
.collect())
} else {
Ok(Vec::new())
}
Expand Down Expand Up @@ -136,15 +162,17 @@ impl<'a> WorktreePlan<'a> {
}

#[instrument(level = "trace")]
fn copy_untracked(&self) -> miette::Result<()> {
if self.copy_untracked.is_empty() {
fn copy_ignored(&self) -> miette::Result<()> {
if self.copy_ignored.is_empty() {
return Ok(());
}

tracing::info!(
"Copying untracked files to {}",
self.destination.display_path_cwd()
);
for path in &self.copy_untracked {
for entry in &self.copy_ignored {
let path = &entry.path;
let from = self.git.get_current_dir().join(path);
let to = self.destination.join(path);
tracing::trace!(
Expand Down Expand Up @@ -190,7 +218,7 @@ impl<'a> WorktreePlan<'a> {
}

command.status_checked()?;
self.copy_untracked()?;
self.copy_ignored()?;
self.run_commands()?;
Ok(())
}
Expand All @@ -217,27 +245,6 @@ impl<'a> WorktreePlan<'a> {
}
}

impl Display for WorktreePlan<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Creating worktree in {} {}",
self.destination.display_path_cwd(),
self.branch,
)?;

if !self.copy_untracked.is_empty() {
write!(
f,
"\nCopying {} untracked paths to new worktree",
self.copy_untracked.len()
)?;
}

Ok(())
}
}

/// Where to start a worktree at.
#[derive(Debug, Clone)]
enum StartPoint {
Expand Down
15 changes: 11 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,18 @@ impl CloneConfig {
#[serde(default)]
pub struct AddConfig {
copy_untracked: Option<bool>,
copy_ignored: Option<bool>,
commands: Vec<ShellCommand>,
branch_replacements: Vec<BranchReplacement>,
}

impl AddConfig {
pub fn copy_untracked(&self) -> bool {
self.copy_untracked.unwrap_or(true)
pub fn copy_ignored(&self) -> bool {
if let Some(copy_untracked) = self.copy_untracked {
tracing::warn!("`add.copy_untracked` has been replaced with `add.copy_ignored`");
return copy_untracked;
}
self.copy_ignored.unwrap_or(true)
}

pub fn commands(&self) -> &[ShellCommand] {
Expand Down Expand Up @@ -253,7 +258,8 @@ mod tests {
enable_gh: Some(false)
},
add: AddConfig {
copy_untracked: Some(true),
copy_untracked: None,
copy_ignored: Some(true),
commands: vec![],
branch_replacements: vec![],
}
Expand All @@ -270,7 +276,8 @@ mod tests {
enable_gh: Some(empty_config.clone.enable_gh()),
},
add: AddConfig {
copy_untracked: Some(empty_config.add.copy_untracked()),
copy_untracked: None,
copy_ignored: Some(empty_config.add.copy_ignored()),
commands: empty_config
.add
.commands()
Expand Down
50 changes: 27 additions & 23 deletions src/git/status.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::fmt::Debug;
use std::fmt::Display;
use std::iter;
use std::ops::Deref;
use std::str::FromStr;

use camino::Utf8PathBuf;
Expand Down Expand Up @@ -57,29 +58,6 @@ where
}
})?)
}

/// List untracked files and directories.
#[instrument(level = "trace")]
pub fn untracked_files(&self) -> miette::Result<Vec<Utf8PathBuf>> {
Ok(self
.0
.command()
.args([
"ls-files",
// Show untracked (e.g. ignored) files.
"--others",
// If a whole directory is classified as other, show just its name and not its
// whole contents.
"--directory",
"-z",
])
.output_checked_utf8()?
.stdout
.split('\0')
.filter(|path| !path.is_empty())
.map(Utf8PathBuf::from)
.collect())
}
}

/// The status code of a particular file. Each [`StatusEntry`] has two of these.
Expand Down Expand Up @@ -193,6 +171,10 @@ impl StatusEntry {
})
}

pub fn is_ignored(&self) -> bool {
self.codes().any(|code| matches!(code, StatusCode::Ignored))
}

pub fn parser(input: &mut &str) -> PResult<Self> {
let left = StatusCode::parser.parse_next(input)?;
let right = StatusCode::parser.parse_next(input)?;
Expand Down Expand Up @@ -271,6 +253,28 @@ impl Status {
let (entries, _eof) = repeat_till(1.., StatusEntry::parser, eof).parse_next(input)?;
Ok(Self { entries })
}

pub fn iter(&self) -> std::slice::Iter<'_, StatusEntry> {
self.entries.iter()
}
}

impl IntoIterator for Status {
type Item = StatusEntry;

type IntoIter = std::vec::IntoIter<Self::Item>;

fn into_iter(self) -> Self::IntoIter {
self.entries.into_iter()
}
}

impl Deref for Status {
type Target = Vec<StatusEntry>;

fn deref(&self) -> &Self::Target {
&self.entries
}
}

impl FromStr for Status {
Expand Down
64 changes: 64 additions & 0 deletions tests/add_copy_ignored_broken_symlink.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use command_error::CommandExt;
use test_harness::GitProle;
use test_harness::WorktreeState;

#[test]
fn add_copy_ignored_broken_symlink() -> miette::Result<()> {
let prole = GitProle::new()?;

prole.setup_worktree_repo("my-repo")?;

prole.sh(r#"
cd my-repo/main || exit

echo "my-cool-symlink" >> .gitignore
echo "symlink-to-directory" >> .gitignore
echo "untracked-dir" >> .gitignore
git add .gitignore
git commit -m "Add .gitignore"

ln -s does-not-exist my-cool-symlink
mkdir untracked-dir
ln -s does-not-exist untracked-dir/my-cooler-symlink
ln -s untracked-dir symlink-to-directory
"#)?;

prole
.cd_cmd("my-repo/main")
.args(["add", "puppy"])
.status_checked()?;

prole
.repo_state("my-repo")
.worktrees([
WorktreeState::new_bare(),
WorktreeState::new("main").branch("main").status([
"!! my-cool-symlink",
"!! symlink-to-directory",
"!! untracked-dir/",
]),
WorktreeState::new("puppy")
.branch("puppy")
.upstream("main")
.status([
"!! my-cool-symlink",
"!! symlink-to-directory",
"!! untracked-dir/",
]),
])
.assert();

let link = prole.path("my-repo/puppy/my-cool-symlink");
assert!(link.symlink_metadata().unwrap().is_symlink());
assert_eq!(link.read_link_utf8().unwrap(), "does-not-exist");

let link = prole.path("my-repo/puppy/symlink-to-directory");
assert!(link.symlink_metadata().unwrap().is_symlink());
assert_eq!(link.read_link_utf8().unwrap(), "untracked-dir");

let link = prole.path("my-repo/puppy/untracked-dir/my-cooler-symlink");
assert!(link.symlink_metadata().unwrap().is_symlink());
assert_eq!(link.read_link_utf8().unwrap(), "does-not-exist");

Ok(())
}
16 changes: 5 additions & 11 deletions tests/add_copy_untracked_broken_symlink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,22 @@ fn add_copy_untracked_broken_symlink() -> miette::Result<()> {
"?? symlink-to-directory",
"?? untracked-dir/",
]),
// Untracked files are not copied!
WorktreeState::new("puppy")
.branch("puppy")
.upstream("main")
.status([
"?? my-cool-symlink",
"?? symlink-to-directory",
"?? untracked-dir/",
]),
.status([]),
])
.assert();

let link = prole.path("my-repo/puppy/my-cool-symlink");
assert!(link.symlink_metadata().unwrap().is_symlink());
assert_eq!(link.read_link_utf8().unwrap(), "does-not-exist");
assert!(!link.exists());

let link = prole.path("my-repo/puppy/symlink-to-directory");
assert!(link.symlink_metadata().unwrap().is_symlink());
assert_eq!(link.read_link_utf8().unwrap(), "untracked-dir");
assert!(!link.exists());

let link = prole.path("my-repo/puppy/untracked-dir/my-cooler-symlink");
assert!(link.symlink_metadata().unwrap().is_symlink());
assert_eq!(link.read_link_utf8().unwrap(), "does-not-exist");
assert!(!link.exists());

Ok(())
}
5 changes: 5 additions & 0 deletions tests/add_from_container_no_default_branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ fn add_from_container_no_default_branch() -> miette::Result<()> {
cd puppy || exit
git switch -c puppy
git branch -D main

echo 'puppy-file' > .gitignore
git add .gitignore
git commit -m 'Add .gitignore'

echo puppyyyy > puppy-file
"#)?;

Expand Down
5 changes: 5 additions & 0 deletions tests/add_from_non_worktree_repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ fn add_from_non_worktree_repo() -> miette::Result<()> {
cd my-repo || exit
git switch -c puppy
git branch -D main

echo 'puppy-file' > .gitignore
git add .gitignore
git commit -m 'Add .gitignore'

echo puppyyyy > puppy-file
"#)?;

Expand Down
Loading

0 comments on commit eddd725

Please sign in to comment.