-
Notifications
You must be signed in to change notification settings - Fork 363
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
git: spawn a separate git process for network operations #5228
base: main
Are you sure you want to change the base?
Conversation
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
That seems okay. It's also good to reorganize the current error enum as needed.
I think it's best to add a config knob to turn the shelling-out backend on. If the implementation gets stable enough, we can change the default, and eventually remove the git2 backend. I think the flag can be checked by CLI layer, but that's not a requirement. Regarding the code layout, maybe better to add new module for shelling-out implementation? The current git.rs isn't small, and there wouldn't be many codes to be shared. Thanks! |
cli/tests/test_git_clone.rs
Outdated
#[duplicate_item( | ||
test_git_clone shell_git_config; | ||
[test_git_clone_shell] ["git.shell = true"]; | ||
[test_git_clone_git2] [""]; | ||
)] |
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.
We currently already use test-case
to parametrize various test cases in the codebase. I don't think it makes sense to pull in another dependency to do this.
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 sounds amazing, will look into it
Hi! Thanks for the feedback! In the meantime, I was trying to make CI pass and did away with the feature flag (which was always my intention anyways but wanted to receive feedback).
EDIT: I misread a comment where I thought it was saying no config knob. sorry about that |
Can you also squash it down to one commit and change the commit message/PR title to |
Getting rid of libgit2 is likely inevitable at this rate. It is only used for push/pull and is a large 3rd party dependency that we have probably outgrown by now, I'm almost certain the number one most-repeated bug report is "push does not work", with many users compiling themselves to fix. In reality I think it will probably remain a long tail of issues, things that won't work right, because the existing Git code, tools, and installers have a lot of quirks figured out for many various OS/workflow combinations, e.g. something like Git for Windows Credential Manager, or auto-ssh key unlock via secure enclaves, etc. In the future if it supported this stuff really well, bringing back the code probably wouldn't be too bad. Or maybe Gitoxide will get good support, which would be even better. But in the meantime, assuming this improves the user experience now, and assuming we turn it on by default at some point, it's probably no longer worth keeping git2 around. |
Thanks for taking up this task! I’ll try to do a more thorough review later but for now I wanted to mention that this should use the fancy Git (Also +1 to getting rid of |
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.
Exciting work!
cli/src/commands/git/clone.rs
Outdated
/// Run git clone as a forked git process | ||
#[arg(long)] | ||
shell: bool, |
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.
suggestion: I'd recommend you actually drop the --shell
argument altogether (from here and elsewhere). For one-off invocations, I believe you can write something like jj --config='git.shell = true
git push. I agree with your comment in the top-level thread that it seems unlikely to want to toggle it frequently, so using the more verbose
--config` version of the command seems like an acceptable trade-off vs polluting the command-line interface.
cli/tests/test_git_clone.rs
Outdated
// this test has slightly different behaviour from git2, because it doesn't support shallow clones | ||
// on a local transport |
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.
comment: IIRC you can get it to support shallow clones if you use a file URI instead of just a path (like file:///path/to/repo
). Is there a specific reason to prefer to do or not do 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.
This refers to
jj/cli/tests/test_git_clone.rs
Line 597 in cc01531
// local transport does not support shallow clones so we just test that the |
Basically, the git2 implementation errors out here, because of the underlying git2 behaviour regarding shallow clones on local transports. In its test this is fine, but git
cannot replicate the erroneous git2
behaviour, hence why the tests are manually separated out (instead of using test_case
)
lib/src/git.rs
Outdated
// there are two options: | ||
// | ||
// a bare git repo, which has a parent named .jj that sits on the workspace root | ||
// or | ||
// a colocated .git dir, which is already on the workspace root |
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.
suggestion (non-blocking): I think it's probably better to promote this to a doc-comment (///
)? (Assuming that this function makes it into the final version.)
lib/src/git.rs
Outdated
let mut it = git_dir.ancestors(); | ||
for path in it.by_ref() { | ||
if path.file_name() == Some(OsStr::new(".jj")) { | ||
break; | ||
} | ||
} | ||
|
||
it.next().map(|x| x.to_path_buf()).ok_or(format!( | ||
"could not find .jj dir in git dir path: {}", | ||
git_dir.display() | ||
)) |
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.
suggestion: Perhaps you want something like it.skip_while(/* isn't .jj */).next()
? (Assuming that this function makes it into the final version.)
// about it on clone. | ||
|
||
let default_branch = fork_git_remote_show(git_dir, &work_tree, remote_name)?; | ||
tracing::debug!(default_branch = default_branch); |
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.
suggestion: I'd include a message in the debug!
here to make it easier to understand what operation it's associated with.
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.
All the spawn_
functions start with a tracing::debug!
which includes the command it's going to fork to. Are you suggesting adding something here too?
lib/src/git.rs
Outdated
git_err | ||
}; | ||
if let Some(e) = git_err { | ||
return e; |
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.
suggestion: It seems like you don't need to assign git_err
to a variable, and can just return it directly on line 2271?
lib/src/git.rs
Outdated
) -> Result<(), GitFetchError> { | ||
let depth_arg = depth.map(|x| format!("--depth={x}")); | ||
tracing::debug!( | ||
"shelling out to `git --git-dir={} --work-tree={} fetch --prune{} {} {}", |
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.
suggestion: You may want to print this as part of regular output.
issue: I would strongly recommend constructing the args
vec and then logging/rendering/printing from it directly, so that there's a single source of truth. Otherwise, we risk having the reported command invocation and the actual command invocation falling out of sync, which will be very confusing to debug. (You can use cmd.args
instead of cmd.arg
to provide a list of arguments. You can also use cmd.get_args
to get the list of arguments it's about to be called with for reporting purposes, if you don't want to maintain a separate variable.)
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.
+1 on constructing it from the args
on printing it as part as the regular output, this would break all the tests, as they rely on the output matching. Any suggestions on how to work around 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.
I personally don’t think that printing it by default is a good idea. I think we should just try and match the existing jj git push
UX as closely as possible; I was even thinking about passing verbose flags to parse the terminal output for progress bar updates. By the way you’ll probably also have an easier time parsing the final output if you pass --porcelain
, since Git doesn’t guarantee stable machine‐readable output otherwise. (Sorry if some of this is already stuff you’re doing, just sending scattershot thoughts from when I was looking into this before I can give the code a proper read‐through!)
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.
The only two commands where we are getting output from currently (git remote show
and git ls-remote
) don't have a porcelain flag. We ignore the stdout output from both fetch
, push
and branch
.
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.
To be honest, the main problem parsing is with errors, where --porcelain
doesn't really help.
Progress bar updates (to match the current UX) sound really hard.
Not sure how to live parse it (especially with the \r
being thrown around). I checked if porcelain would help there, but it doesn't.
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 wasn’t meaning to suggest you try to parse the updates; it’s something I was interested in doing but it’s certainly gnarly and not required for an MVP :)
I’m surprised you don’t use the final output from git fetch
, though.
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.
On success it's not a problem, only need to do it for errors, which are not affected by --porcelain
:-)
It's also due to the fact it fetches one at a time, so we can check success on each one of them
lib/src/git.rs
Outdated
}?; | ||
|
||
let output = remote_git.wait_with_output()?; | ||
let _ = convert_git_fetch_output_to_fetch_result(output, prunes)?; |
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.
suggestion: I would annotate the type of the result variable here (or use the result), as it can catch issues when the return value of the function changes and now it's meant to be used.
lib/src/git.rs
Outdated
return Ok(output); | ||
} | ||
|
||
let lossy_err = String::from_utf8_lossy(&output.stderr).to_string(); |
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.
comment: A lot of the error-detection here seems to be duplicated with other functions in this file; maybe worth unifying in the final version.
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.
Part of the reason for getting it split like this was due to the fact that some commands issue different errors (while others issue the same). I've reworked it to address your comments but feel that having them separate by call is the right move. If we have a particular error we want to handle that comes from only one git command we can then parse it out just there.
lib/src/git.rs
Outdated
))) | ||
} | ||
|
||
fn fork_git_fetch( |
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.
question: Why are these function called fork_*
? Does fork
here just mean "spawn a subprocess"? I might call them spawn_*
in that case.
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.
no good reason: renamed
I looked into this, but am unsure this completely mimics the behaviour as it stands right now. The patch uses |
I agree. In fact, there is a current clippy warning about a function with too many arguments that is hard to go around without it. PS: I intend to work on the remaining issues on top of |
The version with the argument lets you specify the exact expected commit for each remote ref, so you should be able to implement the current logic on top of it. I think |
daa498e
to
5696825
Compare
cli/src/commands/git/clone.rs
Outdated
GitFetchError::NoSuchRemote(_) => { | ||
panic!("shouldn't happen as we just created the git remote") | ||
GitFetchError::NoSuchRemote(repo_name) => { | ||
user_error(format!("could not find repository at '{repo_name}'")) |
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: i think we usually start messages with uppercase
cli/src/git_util.rs
Outdated
|
||
pub(crate) fn get_config_git_shell(command: &CommandHelper) -> Result<bool, ConfigGetError> { | ||
command | ||
.config_env() |
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.
Use .settings()
instead. raw_config()
's help says "Use this only if the unmodified config data is needed."
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.
thanks so much! ran around for a long time looking for exactly this and used raw_config
as a last resort
lib/src/git_shell.rs
Outdated
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: I'm not sure if "shell out" necessarily means subprocessing to a shell, but "subprocess" seems clearer either way, so maybe git_subprocess.rs
?
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.
do you think this should be done patch-wise? the config + variable names + function names + test names all refer to this process as shell
ing out
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 we should do it consistently. So if we decide to not call it "shell out", then we should consistently not call it that. The alternative is that we decide that it's okay to call it that even if it's inaccurate.
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, a possible approach is:
- the functions that spawn a git process would be, e.g.,
GitSubprocessContext::spawn_fetch
- rework other function names from
shell
-->subprocess
Does this share functionality with #4759 ? At a surface level, it seems similar. |
I don't think it does. This PR is about using |
Re: The particular test case that fails regards if we are pushing a branch deletion, and the branch was already deleted on the remote (and as such, is not at the expected commit)
Would be great to get perspective on what to do here! cc @emilazy |
9a733d2
to
bb5e58c
Compare
6ccb197
to
16004f1
Compare
I think minor behavior difference is acceptable if that reduces the complexity. (We can also start with a simple implementation, and fill a gap later if needed.) |
b4a0bcf
to
6a1ea14
Compare
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.
cli: shell out to git for network operations
Nit on the commit message: This feature is not just about the CLI. I'd say the most important changes are in the library. "git" seems like a better topic.
Reasoning: `jj` fails to push/fetch over ssh depending on the system. Issue jj-vcs#4979 lists over 20 related issues on this and proposes spawning a `git` subprocess for tasks related to the network (in fact, just push/fetch are enough). This PR implements this. Users can either enable shelling out to git in a config file: ```toml [git] subprocess = true ``` Implementation Details: This PR implements shelling out to `git` via `std::process::Command`. There are 2 sharp edges with the patch: - it relies on having to parse out git errors to match the error codes (and parsing git2's errors in one particular instance to match the error behaviour). This seems mostly unavoidable - to ensure matching behaviour with git2, the tests are maintained across the two implementations. This is done using test_case, as with the rest of the codebase Testing: Run the rust tests: ``` $ cargo test ``` Build: ``` $ cargo build ``` Clone a private repo: ``` $ path/to/jj git clone --shell <REPO_SSH_URL> ``` Create new commit and push ``` $ echo "TEST" > this_is_a_test_file.txt $ path/to/jj describe -m 'test commit' $ path/to/jj git push --shell -b <branch> ``` fix
1e8cbe7
to
fccd31b
Compare
Reasoning:
jj
fails to push/fetch over ssh depending on the system.Issue #4979 lists over 20 related issues on this and proposes spawning
a
git
subprocess for tasks related to the network (in fact, just push/fetchare enough).
This PR implements this.
Users can either enable shelling out to git in a config file:
Implementation Details:
This PR implements shelling out to
git
viastd::process::Command
.There are 2 sharp edges with the patch:
it relies on having to parse out git errors to match the error codes
(and parsing git2's errors in one particular instance to match the
error behaviour). This seems mostly unavoidable
to ensure matching behaviour with git2, the tests are maintained across the
two implementations. This is done using test_case, as with the rest
of the codebase
Testing:
Run the rust tests:
Build:
Clone a private repo:
Create new commit and push
Checklist
If applicable:
CHANGELOG.md