Skip to content

feat: find tree root via a user-specified command #571

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

Merged
merged 1 commit into from
Apr 23, 2025

Conversation

brianmcgee
Copy link
Member

Closes #530

@brianmcgee brianmcgee force-pushed the feat/tree-root-cmd branch 2 times, most recently from 6d5cb7a to 36c37db Compare April 18, 2025 11:16
@brianmcgee brianmcgee marked this pull request as ready for review April 18, 2025 11:23
@brianmcgee brianmcgee requested review from zimbatm, Mic92 and jfly April 18, 2025 11:23
Copy link
Member

@zimbatm zimbatm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's good

@brianmcgee brianmcgee force-pushed the feat/tree-root-cmd branch 3 times, most recently from e1b66b6 to 32bedf9 Compare April 20, 2025 15:56
@brianmcgee
Copy link
Member Author

@MattSturgeon, do you mind giving this a review as well?

@brianmcgee brianmcgee force-pushed the feat/tree-root-cmd branch 2 times, most recently from 7b01ed5 to 10b377b Compare April 21, 2025 07:09
Copy link
Contributor

@MattSturgeon MattSturgeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

The concept looks good to me, thanks! I'll defer to @jfly for the more technical review.

shlex

I do wonder about the merit of using shlex vs either requiring that multi-arg commands be configured as a list, or evaluated using a real shell (i.e. bash).

treefmt-nix

Slightly off topic, but I wonder what implications this has for treefmt-nix?

Now that git rev-parse --show-toplevel is used by default, does that mean that treefmt-nix no longer needs to provide a default for tree-root-file, projectRootFile (default ".git/config"), projectRoot (default self1), etc?

Also, how should the change be communicate to users who have gotten in the habit of setting tree-root-file or projectRootFile themselves, as that is now likely to be redundant?

Footnotes

  1. Tangent: does projectRoot's current default self make sense? Since that'll be an in-store path, and won't ever be used anyway because projectRootFile is always defined

@brianmcgee
Copy link
Member Author

Took another pass and fixed some bad assumptions.

Fresh reviews have been requested 🙏

@brianmcgee brianmcgee requested a review from zimbatm April 21, 2025 15:47
Copy link
Collaborator

@jfly jfly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the new determineTreeRoot function to be way more readable than before, thank you!

@brianmcgee brianmcgee force-pushed the feat/tree-root-cmd branch 2 times, most recently from 8ce083e to 96a2d04 Compare April 22, 2025 11:12
@brianmcgee
Copy link
Member Author

treefmt-nix

Slightly off topic, but I wonder what implications this has for treefmt-nix?

Now that git rev-parse --show-toplevel is used by default, does that mean that treefmt-nix no longer needs to provide a default for tree-root-file, projectRootFile (default ".git/config"), projectRoot (default self1), etc?

Correct.

The default root file of .git/config can be removed. I'll apply that change when I next update treefmt-nix to incorporate this change (after the release).

Also, how should the change be communicate to users who have gotten in the habit of setting tree-root-file or projectRootFile themselves, as that is now likely to be redundant?

This is a good point. It will be in the release notes, and I can point it out on Fosstodon as well.

A Nixos discourse post might also be appropriate?

Copy link
Collaborator

@jfly jfly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

config/config.go Outdated
Comment on lines 415 to 420
if treeRoot == "" {
return "", fmt.Errorf("empty output received after executing tree-root-cmd: %s", cfg.TreeRootCmd)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally would prefer to put this check up a layer in determineTreeRoot. If somebody explicitly specifies --tree-root "", I'd also like them to get this error, rather than having it fall back to the current directory.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If somebody explicitly specifies --tree-root " "

I was about to comment that "" would be treated as tree-root being undefined, however if a whitespace-only value is defined then you're right, it would run into this edge-case.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, good point. I didn't remember we're treating "unset" and "set to empty string" as the same thing here.

I still do feel like it would make more sense to do this check as part of the "is this potential root a valid path? let's normalize it" codepath, and not in the implementation of a specific root discovery mechanism.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite following what is being advocated for here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe @jfly is suggesting the new assertion for non-empty (trimmed) stdout could also apply to other methods of setting the tree root.

Currently it is possible to explicitly define tree-root = " ", which would bypass the tree-root != "" checks.


IMO this begs the question of whether trimming stdout is the correct approach anyway. If not, this argument would be kinda irrelevant.

However trimming is good enough for an initial implementation, since valid filepaths with leading/trailing whitespace seem very unlikely, and the command printing additional blank lines is rather likely.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this begs the question of whether trimming stdout is the correct approach anyway.

In hindsight, I believe the most correct approach here would be to split on \n, then remove the empty lines.

You can then assert that the remaining line count is 1, and also use the lines slice to log the stdout line-by-line.

lines := strings.Split(stdout, "\n")
nonEmptyLines := slices.DeleteFunc(lines, func(line string) bool {
	return line == ""
}

switch (len(nonEmptyLines)) {
case 1:
	// no-op

case 0:
	return "", fmt.Errorf("empty output received after executing tree-root-cmd: %s", cfg.TreeRootCmd)

default:
	// Log all stdout line-by-line, including the blank lines
	logger := log.WithPrefix("tree-root-cmd | stdout")
	for _, line := range lines {
		logger.Error(line)
	}
	return "", fmt.Errorf("tree-root-cmd cannot output multiple lines: %s", cfg.TreeRootCmd)
}

As said before, trimming is fine for the initial impl, I can open a follow up PR for this if preferred.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All mechanisms must pass through the same stage of realising their output as absolute paths, which should throw an error for any malformed paths. Trimming the output of the command execution and enforcing a single line seems prudent, specifically for the tree-root-cmd pathway.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which should throw an error for any malformed paths

This is the part that bothers me: IIRC, we're using go's built-in Path.abs to do this, which accepts empty strings rather than erroring.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the part that bothers me: IIRC, we're using go's built-in Path.abs to do this, which accepts empty strings rather than erroring.

I believe it is impossible to define a truly empty tree-root path; attempting to set tree-root = "" will be treated as undefined. Returning an empty string from tree-root-cmd will be caught here.

As for blank/whitespace paths, a whitepace string is technically a valid relative path. On some filesystems you can name a file/dir " " (if you really wanted to).

This is one reason why the current impl splits stdout into lines, instead of just trimming it.

Copy link
Contributor

@MattSturgeon MattSturgeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall. Thanks again for working on this!

A few minor things outstanding most of which you probably don't need to block on if you don't want to:

@brianmcgee brianmcgee force-pushed the feat/tree-root-cmd branch 2 times, most recently from 385c112 to 984050e Compare April 23, 2025 12:02
Copy link
Contributor

@MattSturgeon MattSturgeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to review again with such minor comments... Despite the noise, the PR is looking really good!

config/config.go Outdated
Comment on lines 415 to 420
if treeRoot == "" {
return "", fmt.Errorf("empty output received after executing tree-root-cmd: %s", cfg.TreeRootCmd)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this begs the question of whether trimming stdout is the correct approach anyway.

In hindsight, I believe the most correct approach here would be to split on \n, then remove the empty lines.

You can then assert that the remaining line count is 1, and also use the lines slice to log the stdout line-by-line.

lines := strings.Split(stdout, "\n")
nonEmptyLines := slices.DeleteFunc(lines, func(line string) bool {
	return line == ""
}

switch (len(nonEmptyLines)) {
case 1:
	// no-op

case 0:
	return "", fmt.Errorf("empty output received after executing tree-root-cmd: %s", cfg.TreeRootCmd)

default:
	// Log all stdout line-by-line, including the blank lines
	logger := log.WithPrefix("tree-root-cmd | stdout")
	for _, line := range lines {
		logger.Error(line)
	}
	return "", fmt.Errorf("tree-root-cmd cannot output multiple lines: %s", cfg.TreeRootCmd)
}

As said before, trimming is fine for the initial impl, I can open a follow up PR for this if preferred.

@brianmcgee
Copy link
Member Author

Sorry to review again with such minor comments... Despite the noise, the PR is looking really good!

I don't mind, I'd rather get it right.

Copy link
Contributor

@MattSturgeon MattSturgeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only things outstanding for me are

Neither of these are blocking IMO, so I approve 🎉

(I haven't tested locally, but the diff & tests all look good)

Signed-off-by: Brian McGee <[email protected]>
Co-authored-by: Matt Sturgeon <[email protected]>
Signed-off-by: Brian McGee <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Dynamic tree root detection
5 participants