Skip to content

Commit 620327e

Browse files
committedJan 21, 2025
git: spawn a separate git process for network operations
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. 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 --config='git.subprocess=true' <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 --config='git.subprocess=true' -b <branch> ``` Issues Closed With a grain of salt, but most of these problems should be fixed (or at least checked if they are fixed). They are the ones listed in jj-vcs#4979 . SSH: - jj-vcs#63 - jj-vcs#440 - jj-vcs#1455 - jj-vcs#1507 - jj-vcs#2931 - jj-vcs#2958 - jj-vcs#3322 - jj-vcs#4101 - jj-vcs#4333 - jj-vcs#4386 - jj-vcs#4488 - jj-vcs#4591 - jj-vcs#4802 - jj-vcs#4870 - jj-vcs#4937 - jj-vcs#4978 - jj-vcs#5120 - jj-vcs#5166 Clone/fetch/push/pull: - jj-vcs#360 - jj-vcs#1278 - jj-vcs#1957 - jj-vcs#2295 - jj-vcs#3851 - jj-vcs#4177 - jj-vcs#4682 - jj-vcs#4719 - jj-vcs#4889 - jj-vcs#5147 - jj-vcs#5238 Notable Holdouts: - Interactive HTTP authentication (jj-vcs#401, jj-vcs#469) - libssh2-sys dependency on windows problem (can only be removed if/when we get rid of libgit2): jj-vcs#3984
1 parent 5fb7a3a commit 620327e

18 files changed

+2340
-293
lines changed
 

‎.github/workflows/build.yml

+12-1
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,20 @@ jobs:
8686
tool: nextest
8787
- name: Build
8888
run: cargo build --workspace --all-targets --verbose ${{ matrix.cargo_flags }}
89-
- name: Test
89+
- name: Test [non-windows]
90+
if: ${{ matrix.os != 'windows-latest' }}
91+
run: |
92+
cargo nextest run --workspace --all-targets --verbose ${{ matrix.cargo_flags }}
93+
env:
94+
RUST_BACKTRACE: 1
95+
CARGO_TERM_COLOR: always
96+
- name: Test [windows]
97+
if: ${{ matrix.os == 'windows-latest' }}
9098
run: cargo nextest run --workspace --all-targets --verbose ${{ matrix.cargo_flags }}
9199
env:
100+
# tests clear the PATH variable (but is only felt on windows),
101+
# so we need to set the git executable
102+
TEST_GIT_EXECUTABLE_PATH: 'C:\Program Files\Git\bin\git.exe'
92103
RUST_BACKTRACE: 1
93104
CARGO_TERM_COLOR: always
94105

‎CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6868

6969
### New features
7070

71+
* `jj git {push,clone,fetch}` can now spawn an external `git` subprocess, via
72+
the `git.subprocess = true` config knob. This provides an alternative that,
73+
when turned on, fixes SSH bugs when interacting with Git remotes due to
74+
`libgit2`s limitations [#4979](https://github.com/jj-vcs/jj/issues/4979).
75+
7176
* `jj evolog` and `jj op log` now accept `--reversed`.
7277

7378
* `jj restore` now supports `-i`/`--interactive` selection.

‎cli/src/commands/git/clone.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,7 @@ fn fetch_new_remote(
204204
)?;
205205
let git_settings = workspace_command.settings().git_settings()?;
206206
let mut fetch_tx = workspace_command.start_transaction();
207-
208-
let stats = with_remote_git_callbacks(ui, None, |cb| {
207+
let stats = with_remote_git_callbacks(ui, None, &git_settings, |cb| {
209208
git::fetch(
210209
fetch_tx.repo_mut(),
211210
&git_repo,
@@ -222,6 +221,7 @@ fn fetch_new_remote(
222221
}
223222
GitFetchError::GitImportError(err) => CommandError::from(err),
224223
GitFetchError::InternalGitError(err) => map_git_error(err),
224+
GitFetchError::Subprocess(err) => user_error(err),
225225
GitFetchError::InvalidBranchPattern => {
226226
unreachable!("we didn't provide any globs")
227227
}

‎cli/src/commands/git/push.rs

+17-3
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,23 @@ pub fn cmd_git_push(
357357
let mut sideband_progress_callback = |progress_message: &[u8]| {
358358
_ = writer.write(ui, progress_message);
359359
};
360-
with_remote_git_callbacks(ui, Some(&mut sideband_progress_callback), |cb| {
361-
git::push_branches(tx.repo_mut(), &git_repo, &remote, &targets, cb)
362-
})
360+
361+
let git_settings = tx.settings().git_settings()?;
362+
with_remote_git_callbacks(
363+
ui,
364+
Some(&mut sideband_progress_callback),
365+
&git_settings,
366+
|cb| {
367+
git::push_branches(
368+
tx.repo_mut(),
369+
&git_repo,
370+
&git_settings,
371+
&remote,
372+
&targets,
373+
cb,
374+
)
375+
},
376+
)
363377
.map_err(|err| match err {
364378
GitPushError::InternalGitError(err) => map_git_error(err),
365379
GitPushError::RefInUnexpectedLocation(refs) => user_error_with_hint(

‎cli/src/config-schema.json

+10
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,16 @@
380380
"type": "boolean",
381381
"description": "Whether jj should sign commits before pushing",
382382
"default": "false"
383+
},
384+
"subprocess": {
385+
"type": "boolean",
386+
"description": "Whether jj spawns a git subprocess for network operations (push/fetch/clone)",
387+
"default": false
388+
},
389+
"executable-path": {
390+
"type": "string",
391+
"description": "Path to the git executable",
392+
"default": "git"
383393
}
384394
}
385395
},

‎cli/src/git_util.rs

+33-21
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ use jj_lib::op_store::RefTarget;
4141
use jj_lib::op_store::RemoteRef;
4242
use jj_lib::repo::ReadonlyRepo;
4343
use jj_lib::repo::Repo;
44+
use jj_lib::settings::GitSettings;
4445
use jj_lib::store::Store;
4546
use jj_lib::str_util::StringPattern;
4647
use jj_lib::workspace::Workspace;
@@ -282,29 +283,40 @@ type SidebandProgressCallback<'a> = &'a mut dyn FnMut(&[u8]);
282283
pub fn with_remote_git_callbacks<T>(
283284
ui: &Ui,
284285
sideband_progress_callback: Option<SidebandProgressCallback<'_>>,
286+
git_settings: &GitSettings,
285287
f: impl FnOnce(git::RemoteCallbacks<'_>) -> T,
286288
) -> T {
287-
let mut callbacks = git::RemoteCallbacks::default();
288-
let mut progress_callback = None;
289-
if let Some(mut output) = ui.progress_output() {
290-
let mut progress = Progress::new(Instant::now());
291-
progress_callback = Some(move |x: &git::Progress| {
292-
_ = progress.update(Instant::now(), x, &mut output);
293-
});
289+
if git_settings.subprocess {
290+
// TODO: with git2, we are able to display progress from the data that is given
291+
// With the git processes themselves, this is significantly harder, as it
292+
// requires parsing the output directly
293+
//
294+
// In any case, this would be the place to add that funcionalty
295+
f(git::RemoteCallbacks::default())
296+
} else {
297+
let mut callbacks = git::RemoteCallbacks::default();
298+
let mut progress_callback = None;
299+
if let Some(mut output) = ui.progress_output() {
300+
let mut progress = Progress::new(Instant::now());
301+
progress_callback = Some(move |x: &git::Progress| {
302+
_ = progress.update(Instant::now(), x, &mut output);
303+
});
304+
}
305+
callbacks.progress = progress_callback
306+
.as_mut()
307+
.map(|x| x as &mut dyn FnMut(&git::Progress));
308+
callbacks.sideband_progress =
309+
sideband_progress_callback.map(|x| x as &mut dyn FnMut(&[u8]));
310+
let mut get_ssh_keys = get_ssh_keys; // Coerce to unit fn type
311+
callbacks.get_ssh_keys = Some(&mut get_ssh_keys);
312+
let mut get_pw =
313+
|url: &str, _username: &str| pinentry_get_pw(url).or_else(|| terminal_get_pw(ui, url));
314+
callbacks.get_password = Some(&mut get_pw);
315+
let mut get_user_pw =
316+
|url: &str| Some((terminal_get_username(ui, url)?, terminal_get_pw(ui, url)?));
317+
callbacks.get_username_password = Some(&mut get_user_pw);
318+
f(callbacks)
294319
}
295-
callbacks.progress = progress_callback
296-
.as_mut()
297-
.map(|x| x as &mut dyn FnMut(&git::Progress));
298-
callbacks.sideband_progress = sideband_progress_callback.map(|x| x as &mut dyn FnMut(&[u8]));
299-
let mut get_ssh_keys = get_ssh_keys; // Coerce to unit fn type
300-
callbacks.get_ssh_keys = Some(&mut get_ssh_keys);
301-
let mut get_pw =
302-
|url: &str, _username: &str| pinentry_get_pw(url).or_else(|| terminal_get_pw(ui, url));
303-
callbacks.get_password = Some(&mut get_pw);
304-
let mut get_user_pw =
305-
|url: &str| Some((terminal_get_username(ui, url)?, terminal_get_pw(ui, url)?));
306-
callbacks.get_username_password = Some(&mut get_user_pw);
307-
f(callbacks)
308320
}
309321

310322
pub fn print_git_import_stats(
@@ -635,7 +647,7 @@ pub fn git_fetch(
635647
let git_settings = tx.settings().git_settings()?;
636648

637649
for remote in remotes {
638-
let stats = with_remote_git_callbacks(ui, None, |cb| {
650+
let stats = with_remote_git_callbacks(ui, None, &git_settings, |cb| {
639651
git::fetch(
640652
tx.repo_mut(),
641653
git_repo,

‎cli/tests/common/mod.rs

+13
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ impl Default for TestEnvironment {
8080
'format_time_range(time_range)' = 'time_range.start() ++ " - " ++ time_range.end()'
8181
"#,
8282
);
83+
8384
env
8485
}
8586
}
@@ -280,6 +281,18 @@ impl TestEnvironment {
280281
self.env_vars.insert(key.into(), val.into());
281282
}
282283

284+
pub fn set_up_git_subprocessing(&self) {
285+
self.add_config("git.subprocess = true");
286+
287+
// add a git path from env: this is only used if git.subprocess = true
288+
if let Ok(git_executable_path) = std::env::var("TEST_GIT_EXECUTABLE_PATH") {
289+
self.add_config(format!(
290+
"git.executable-path = {}",
291+
to_toml_value(git_executable_path)
292+
));
293+
}
294+
}
295+
283296
pub fn current_operation_id(&self, repo_path: &Path) -> String {
284297
let id_and_newline =
285298
self.jj_cmd_success(repo_path, &["debug", "operation", "--display=id"]);

0 commit comments

Comments
 (0)