Skip to content

Commit eddd725

Browse files
authored
copy_untracked should be copy_ignored (#98)
Wrong design decision. Thanks to @lf- for catching this! Closes #93
1 parent 0fce93d commit eddd725

11 files changed

+241
-79
lines changed

config.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,18 @@ enable_gh = false
4848
#
4949
# `man git-prole-add`
5050
[add]
51-
# When `git prole add` is used to create a new worktree, untracked files are
52-
# copied to the new worktree from the current worktree by default.
51+
# When `git prole add` is used to create a new worktree, `.gitignored` files
52+
# are copied to the new worktree from the current worktree by default.
5353
#
5454
# This will allow you to get started quickly by copying build products and
5555
# other configuration files over to the new worktree. However, copying these
5656
# files can take some time, so this setting can be used to disable this
5757
# behavior if needed.
58-
copy_untracked = true
58+
#
59+
# Note: Untracked files which are not ignored will not be copied.
60+
#
61+
# See: `man 'gitignore(5)'`
62+
copy_ignored = true
5963

6064
# Commands to run when a new worktree is added.
6165
commands = [

src/add.rs

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use crate::git::GitLike;
2222
use crate::git::LocalBranchRef;
2323
use crate::AddWorktreeOpts;
2424
use crate::PathDisplay;
25+
use crate::StatusEntry;
2526
use crate::Utf8Absolutize;
2627

2728
/// A plan for creating a new `git worktree`.
@@ -30,8 +31,28 @@ pub struct WorktreePlan<'a> {
3031
git: AppGit<'a, Utf8PathBuf>,
3132
destination: Utf8PathBuf,
3233
branch: BranchStartPointPlan,
33-
/// Relative paths to copy to the new worktree, if any.
34-
copy_untracked: Vec<Utf8PathBuf>,
34+
copy_ignored: Vec<StatusEntry>,
35+
}
36+
37+
impl Display for WorktreePlan<'_> {
38+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39+
write!(
40+
f,
41+
"Creating worktree in {} {}",
42+
self.destination.display_path_cwd(),
43+
self.branch,
44+
)?;
45+
46+
if !self.copy_ignored.is_empty() {
47+
write!(
48+
f,
49+
"\nCopying {} ignored paths to new worktree",
50+
self.copy_ignored.len()
51+
)?;
52+
}
53+
54+
Ok(())
55+
}
3556
}
3657

3758
impl<'a> WorktreePlan<'a> {
@@ -53,19 +74,24 @@ impl<'a> WorktreePlan<'a> {
5374
let git = git.with_current_dir(worktree);
5475
let branch = BranchStartPointPlan::new(&git, args)?;
5576
let destination = Self::destination_plan(&git, args, &branch)?;
56-
let copy_untracked = Self::untracked_plan(&git)?;
77+
let copy_ignored = Self::copy_ignored_plan(&git)?;
5778
Ok(Self {
5879
git,
5980
branch,
6081
destination,
61-
copy_untracked,
82+
copy_ignored,
6283
})
6384
}
6485

6586
#[instrument(level = "trace")]
66-
fn untracked_plan(git: &AppGit<'_, Utf8PathBuf>) -> miette::Result<Vec<Utf8PathBuf>> {
67-
if git.config.file.add.copy_untracked() && git.worktree().is_inside()? {
68-
git.status().untracked_files()
87+
fn copy_ignored_plan(git: &AppGit<'_, Utf8PathBuf>) -> miette::Result<Vec<StatusEntry>> {
88+
if git.config.file.add.copy_ignored() && git.worktree().is_inside()? {
89+
Ok(git
90+
.status()
91+
.get()?
92+
.into_iter()
93+
.filter(|entry| entry.is_ignored())
94+
.collect())
6995
} else {
7096
Ok(Vec::new())
7197
}
@@ -136,15 +162,17 @@ impl<'a> WorktreePlan<'a> {
136162
}
137163

138164
#[instrument(level = "trace")]
139-
fn copy_untracked(&self) -> miette::Result<()> {
140-
if self.copy_untracked.is_empty() {
165+
fn copy_ignored(&self) -> miette::Result<()> {
166+
if self.copy_ignored.is_empty() {
141167
return Ok(());
142168
}
169+
143170
tracing::info!(
144171
"Copying untracked files to {}",
145172
self.destination.display_path_cwd()
146173
);
147-
for path in &self.copy_untracked {
174+
for entry in &self.copy_ignored {
175+
let path = &entry.path;
148176
let from = self.git.get_current_dir().join(path);
149177
let to = self.destination.join(path);
150178
tracing::trace!(
@@ -190,7 +218,7 @@ impl<'a> WorktreePlan<'a> {
190218
}
191219

192220
command.status_checked()?;
193-
self.copy_untracked()?;
221+
self.copy_ignored()?;
194222
self.run_commands()?;
195223
Ok(())
196224
}
@@ -217,27 +245,6 @@ impl<'a> WorktreePlan<'a> {
217245
}
218246
}
219247

220-
impl Display for WorktreePlan<'_> {
221-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222-
write!(
223-
f,
224-
"Creating worktree in {} {}",
225-
self.destination.display_path_cwd(),
226-
self.branch,
227-
)?;
228-
229-
if !self.copy_untracked.is_empty() {
230-
write!(
231-
f,
232-
"\nCopying {} untracked paths to new worktree",
233-
self.copy_untracked.len()
234-
)?;
235-
}
236-
237-
Ok(())
238-
}
239-
}
240-
241248
/// Where to start a worktree at.
242249
#[derive(Debug, Clone)]
243250
enum StartPoint {

src/config.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,18 @@ impl CloneConfig {
139139
#[serde(default)]
140140
pub struct AddConfig {
141141
copy_untracked: Option<bool>,
142+
copy_ignored: Option<bool>,
142143
commands: Vec<ShellCommand>,
143144
branch_replacements: Vec<BranchReplacement>,
144145
}
145146

146147
impl AddConfig {
147-
pub fn copy_untracked(&self) -> bool {
148-
self.copy_untracked.unwrap_or(true)
148+
pub fn copy_ignored(&self) -> bool {
149+
if let Some(copy_untracked) = self.copy_untracked {
150+
tracing::warn!("`add.copy_untracked` has been replaced with `add.copy_ignored`");
151+
return copy_untracked;
152+
}
153+
self.copy_ignored.unwrap_or(true)
149154
}
150155

151156
pub fn commands(&self) -> &[ShellCommand] {
@@ -253,7 +258,8 @@ mod tests {
253258
enable_gh: Some(false)
254259
},
255260
add: AddConfig {
256-
copy_untracked: Some(true),
261+
copy_untracked: None,
262+
copy_ignored: Some(true),
257263
commands: vec![],
258264
branch_replacements: vec![],
259265
}
@@ -270,7 +276,8 @@ mod tests {
270276
enable_gh: Some(empty_config.clone.enable_gh()),
271277
},
272278
add: AddConfig {
273-
copy_untracked: Some(empty_config.add.copy_untracked()),
279+
copy_untracked: None,
280+
copy_ignored: Some(empty_config.add.copy_ignored()),
274281
commands: empty_config
275282
.add
276283
.commands()

src/git/status.rs

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::fmt::Debug;
22
use std::fmt::Display;
33
use std::iter;
4+
use std::ops::Deref;
45
use std::str::FromStr;
56

67
use camino::Utf8PathBuf;
@@ -57,29 +58,6 @@ where
5758
}
5859
})?)
5960
}
60-
61-
/// List untracked files and directories.
62-
#[instrument(level = "trace")]
63-
pub fn untracked_files(&self) -> miette::Result<Vec<Utf8PathBuf>> {
64-
Ok(self
65-
.0
66-
.command()
67-
.args([
68-
"ls-files",
69-
// Show untracked (e.g. ignored) files.
70-
"--others",
71-
// If a whole directory is classified as other, show just its name and not its
72-
// whole contents.
73-
"--directory",
74-
"-z",
75-
])
76-
.output_checked_utf8()?
77-
.stdout
78-
.split('\0')
79-
.filter(|path| !path.is_empty())
80-
.map(Utf8PathBuf::from)
81-
.collect())
82-
}
8361
}
8462

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

174+
pub fn is_ignored(&self) -> bool {
175+
self.codes().any(|code| matches!(code, StatusCode::Ignored))
176+
}
177+
196178
pub fn parser(input: &mut &str) -> PResult<Self> {
197179
let left = StatusCode::parser.parse_next(input)?;
198180
let right = StatusCode::parser.parse_next(input)?;
@@ -271,6 +253,28 @@ impl Status {
271253
let (entries, _eof) = repeat_till(1.., StatusEntry::parser, eof).parse_next(input)?;
272254
Ok(Self { entries })
273255
}
256+
257+
pub fn iter(&self) -> std::slice::Iter<'_, StatusEntry> {
258+
self.entries.iter()
259+
}
260+
}
261+
262+
impl IntoIterator for Status {
263+
type Item = StatusEntry;
264+
265+
type IntoIter = std::vec::IntoIter<Self::Item>;
266+
267+
fn into_iter(self) -> Self::IntoIter {
268+
self.entries.into_iter()
269+
}
270+
}
271+
272+
impl Deref for Status {
273+
type Target = Vec<StatusEntry>;
274+
275+
fn deref(&self) -> &Self::Target {
276+
&self.entries
277+
}
274278
}
275279

276280
impl FromStr for Status {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use command_error::CommandExt;
2+
use test_harness::GitProle;
3+
use test_harness::WorktreeState;
4+
5+
#[test]
6+
fn add_copy_ignored_broken_symlink() -> miette::Result<()> {
7+
let prole = GitProle::new()?;
8+
9+
prole.setup_worktree_repo("my-repo")?;
10+
11+
prole.sh(r#"
12+
cd my-repo/main || exit
13+
14+
echo "my-cool-symlink" >> .gitignore
15+
echo "symlink-to-directory" >> .gitignore
16+
echo "untracked-dir" >> .gitignore
17+
git add .gitignore
18+
git commit -m "Add .gitignore"
19+
20+
ln -s does-not-exist my-cool-symlink
21+
mkdir untracked-dir
22+
ln -s does-not-exist untracked-dir/my-cooler-symlink
23+
ln -s untracked-dir symlink-to-directory
24+
"#)?;
25+
26+
prole
27+
.cd_cmd("my-repo/main")
28+
.args(["add", "puppy"])
29+
.status_checked()?;
30+
31+
prole
32+
.repo_state("my-repo")
33+
.worktrees([
34+
WorktreeState::new_bare(),
35+
WorktreeState::new("main").branch("main").status([
36+
"!! my-cool-symlink",
37+
"!! symlink-to-directory",
38+
"!! untracked-dir/",
39+
]),
40+
WorktreeState::new("puppy")
41+
.branch("puppy")
42+
.upstream("main")
43+
.status([
44+
"!! my-cool-symlink",
45+
"!! symlink-to-directory",
46+
"!! untracked-dir/",
47+
]),
48+
])
49+
.assert();
50+
51+
let link = prole.path("my-repo/puppy/my-cool-symlink");
52+
assert!(link.symlink_metadata().unwrap().is_symlink());
53+
assert_eq!(link.read_link_utf8().unwrap(), "does-not-exist");
54+
55+
let link = prole.path("my-repo/puppy/symlink-to-directory");
56+
assert!(link.symlink_metadata().unwrap().is_symlink());
57+
assert_eq!(link.read_link_utf8().unwrap(), "untracked-dir");
58+
59+
let link = prole.path("my-repo/puppy/untracked-dir/my-cooler-symlink");
60+
assert!(link.symlink_metadata().unwrap().is_symlink());
61+
assert_eq!(link.read_link_utf8().unwrap(), "does-not-exist");
62+
63+
Ok(())
64+
}

tests/add_copy_untracked_broken_symlink.rs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,22 @@ fn add_copy_untracked_broken_symlink() -> miette::Result<()> {
3030
"?? symlink-to-directory",
3131
"?? untracked-dir/",
3232
]),
33+
// Untracked files are not copied!
3334
WorktreeState::new("puppy")
3435
.branch("puppy")
3536
.upstream("main")
36-
.status([
37-
"?? my-cool-symlink",
38-
"?? symlink-to-directory",
39-
"?? untracked-dir/",
40-
]),
37+
.status([]),
4138
])
4239
.assert();
4340

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

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

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

5650
Ok(())
5751
}

tests/add_from_container_no_default_branch.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ fn add_from_container_no_default_branch() -> miette::Result<()> {
2020
cd puppy || exit
2121
git switch -c puppy
2222
git branch -D main
23+
24+
echo 'puppy-file' > .gitignore
25+
git add .gitignore
26+
git commit -m 'Add .gitignore'
27+
2328
echo puppyyyy > puppy-file
2429
"#)?;
2530

tests/add_from_non_worktree_repo.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ fn add_from_non_worktree_repo() -> miette::Result<()> {
1818
cd my-repo || exit
1919
git switch -c puppy
2020
git branch -D main
21+
22+
echo 'puppy-file' > .gitignore
23+
git add .gitignore
24+
git commit -m 'Add .gitignore'
25+
2126
echo puppyyyy > puppy-file
2227
"#)?;
2328

0 commit comments

Comments
 (0)