-
Notifications
You must be signed in to change notification settings - Fork 739
Allow ln destination to be an existing directory, fixes #832 #833
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Codecov Report
@@ Coverage Diff @@
## master #833 +/- ##
==========================================
+ Coverage 95.49% 95.53% +0.03%
==========================================
Files 34 34
Lines 1266 1276 +10
==========================================
+ Hits 1209 1219 +10
Misses 57 57
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good, a few things to fix. Thanks for fixing this up, we've received an unusually high number of bug reports related to ln()
recently (let me know if you're interested in fixing those too)...
test/ln.js
Outdated
const result = shell.ln('-s', 'file1', './'); | ||
t.truthy(shell.error()); | ||
t.is(result.code, 1); | ||
shell.cd('..'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line isn't necessary. We restore state inside beforeEach().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure that's working correctly? If I omit restoring the original working directory, the following test will fail, afaict due to non-existing file paths.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, nevermind. I get the described behaviour when I remove the shell.cd('..')
at line 158, not here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed line 56.
Re-added line 56, see comment below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this is a bug. As a safer workaround, could you edit these lines:
Lines 18 to 20 in 9035b27
test.afterEach.always(t => { | |
shell.rm('-rf', t.context.tmp); | |
}); |
to include process.chdir(CWD)
before the rm()
call? I've filed #834 to provide a better, long-term solution.
The problem with cd('..')
is that our code might throw exceptions, causing the cd('..')
to not be executed (and so state would never be restored). Also, I think ava stops running a test case as soon as the first assertion fails, which is another case where cd('..')
would be skipped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I've already had that case where an unrelated test would fail just because the previous one couldn't cd('..')
anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added chdir
ing to the CWD
after each test and also removed all now-gratuitous cd('..')
lines in the file.
test/ln.js
Outdated
test('Inside existing directory', t => { | ||
shell.cd(t.context.tmp); | ||
const result = shell.ln('-s', 'external/node_script.js', './'); | ||
t.is(result.code, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: also assert .stderr
and shell.error()
are empty
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going to add those. It's not been done anywhere in the ln()
tests, and I mostly got inspiration from the other tests around there. 🙈
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check.
test/ln.js
Outdated
fs.readFileSync('external/node_script.js').toString(), | ||
fs.readFileSync('node_script.js').toString() | ||
); | ||
shell.rm('node_script.js'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cleanup isn't necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed that line.
src/ln.js
Outdated
@@ -35,6 +35,10 @@ function _ln(options, source, dest) { | |||
var isAbsolute = (path.resolve(source) === sourcePath); | |||
dest = path.resolve(process.cwd(), String(dest)); | |||
|
|||
if (fs.existsSync(dest) && common.statFollowLinks(dest).isDirectory(dest)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think isDirectory()
accepts arguments: https://nodejs.org/api/fs.html#fs_stats_isdirectory
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, right. That was definitely not on purpose. 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
src/ln.js
Outdated
@@ -35,6 +35,10 @@ function _ln(options, source, dest) { | |||
var isAbsolute = (path.resolve(source) === sourcePath); | |||
dest = path.resolve(process.cwd(), String(dest)); | |||
|
|||
if (fs.existsSync(dest) && common.statFollowLinks(dest).isDirectory(dest)) { | |||
dest = path.resolve(dest, path.basename(sourcePath)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be path.join()
. The docs for path.resolve()
claim it produces an absolute path. ln -s
actually cares if it's passed an absolute path vs. relative path, however, and path.join()
preserves that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, that shouldn't matter here, should it? It has been path.resolve
d three lines earlier anyway.
So given that we are guaranteed that dest
already is an absolute path, path.join
and path.resolve
should return the exact same result and I decided to go for the one that was used in the code around it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. I've replaced resolve
with join
. Shouldn't change anything, but it doesn't hurt either. 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, you're right. It's ok to resolve
the destination, but not OK to resolve the source. Either join
or resolve
is fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good, a few things to fix. Thanks for fixing this up, we've received an unusually high number of bug reports related to ln()
recently (let me know if you're interested in fixing those too)...
I'm going to take a look at your comments tomorrow, it's already night time over here. Thanks for the initial feedback. 🙂 Regaring the other |
Okay, I've adjusted the the things you mentioned. Let me know if there's anything else I can help with. 🙂 EDIT: Seems that, for some reason, the temporary test folders are not cleaned up correctly. AppVeyor tries to lint them. Let's see if I can find the cause. EDIT 2: Weirdly, this was actually caused by removing the working directory cleanup line. I've re-added it to make tests work again. |
It somehow caused temporary test folders not to get cleaned up correctly.
I think #829 is low-hanging. |
test/ln.js
Outdated
const result = shell.ln('-s', 'file1', './'); | ||
t.truthy(shell.error()); | ||
t.is(result.code, 1); | ||
shell.cd('..'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this is a bug. As a safer workaround, could you edit these lines:
Lines 18 to 20 in 9035b27
test.afterEach.always(t => { | |
shell.rm('-rf', t.context.tmp); | |
}); |
to include process.chdir(CWD)
before the rm()
call? I've filed #834 to provide a better, long-term solution.
The problem with cd('..')
is that our code might throw exceptions, causing the cd('..')
to not be executed (and so state would never be restored). Also, I think ava stops running a test case as soon as the first assertion fails, which is another case where cd('..')
would be skipped.
src/ln.js
Outdated
@@ -35,6 +35,10 @@ function _ln(options, source, dest) { | |||
var isAbsolute = (path.resolve(source) === sourcePath); | |||
dest = path.resolve(process.cwd(), String(dest)); | |||
|
|||
if (fs.existsSync(dest) && common.statFollowLinks(dest).isDirectory(dest)) { | |||
dest = path.resolve(dest, path.basename(sourcePath)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, you're right. It's ok to resolve
the destination, but not OK to resolve the source. Either join
or resolve
is fine.
Added a commit which should fix #829, possibly also #830. I can't say for sure since both issues don't provide concrete examples. This commit now allows you to use dead symlinks
(This thing is kind of turning into a "general fixing of some |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
his thing is kind of turning into a "general fixing of some ln() issues" branch, I hope you're okay with that.
My preference is to split into multiple PRs. We squash PRs to a single commit, and it's nice to have commits focused on a single root issue.
But I won't reject the PR if it's too much trouble to split. I left feedback on the new changes regardless, it's up to you if you want to cherry pick to a new branch.
t.is(result.code, 0); | ||
t.falsy(result.stderr); | ||
t.falsy(shell.error()); | ||
fs.lstatSync('link-to-dead-link'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we turn this into a real assertion? This would be a good use of common.isBrokenLink()
.
@@ -161,6 +190,18 @@ test('-sf option', t => { | |||
}); | |||
}); | |||
|
|||
test('Override dead destination links with -sf', t => { | |||
shell.cd(t.context.tmp); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
optional: you could assert the precondition (that this link is in fact a dead link). I'd be fine with t.falsy(fs.existsSync('badlink'))
.
@@ -47,7 +59,16 @@ function _ln(options, source, dest) { | |||
var isWindows = process.platform === 'win32'; | |||
var linkType = isWindows ? 'file' : null; | |||
var resolvedSourcePath = isAbsolute ? sourcePath : path.resolve(process.cwd(), path.dirname(dest), source); | |||
if (!fs.existsSync(resolvedSourcePath)) { | |||
|
|||
var resolvedSourceExists; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about we move this to common.isBrokenLink(nameOfPossibleLink)
? This is something I wanted for #835 anyway (and it makes more sense in common
than utils
). Some ideas for the behavior:
isBrokenLink(pointsToMissingFile) === true
isBrokenLink(realRegularFile) // throws an exception
isBrokenLink(someDirectory) // throws an exception
isBrokenLink(doesNotExist) // throws an exception
isBrokenLink(pointsToFileOrDirectory) === false
isBrokenLink(pointsToAnotherBrokenLink) === true // `ls` highlights this just like other broken links
WDYT?
resolvedSourceExists = false; | ||
} | ||
|
||
if (!resolvedSourceExists) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, was this behavior ever correct with ln -s
?
$ ls this-does-not-exist
ls: cannot access 'this-does-not-exist': No such file or directory
$ rm bad-link
$ ln -s this-does-not-exist bad-link
$ ls bad-link # it's highlighted link a broken link
bad-link
I think, long term, the correct logic is if (!resolvedSourceExists && !options.symlink)
. I wonder if this would be too big of a change (@freitagbr , what do you think?).
@loilo which bug does this address?
EDIT: Removed this comment. That wasn't supposed to be posted here, I wanted to create a Gist that helped me remember how to split a pull request — sorry. |
I think so too, yes. Didn't have the time to handle this in the last days, I'll take a look around the weekend. |
Oh dear, I totally forgot this. 😅 |
So, I looked into it and if you don't mind, I'd like to give that task back to you. I'm currently on vacation with my family and couldn't re-wrap my head around the codebase as quickly as I'd have needed to. Otherwise I could set a reminder for after my vacation in 1-2 weeks. 🙂 |
This allows symlinking destinations to be existing directories. Symlinks will be placed inside those, as described in #832.