Skip to content

Commit bd3fe69

Browse files
committedJan 20, 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 dc6ebe1 commit bd3fe69

18 files changed

+2480
-297
lines changed
 

‎.github/workflows/build.yml

+13-1
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,21 @@ 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+
# tests clear the PATH variable, so we need to set the git executable
95+
TEST_GIT_EXECUTABLE_PATH: 'git'
96+
RUST_BACKTRACE: 1
97+
CARGO_TERM_COLOR: always
98+
- name: Test [windows]
99+
if: ${{ matrix.os == 'windows-latest' }}
90100
run: cargo nextest run --workspace --all-targets --verbose ${{ matrix.cargo_flags }}
91101
env:
102+
# tests clear the PATH variable, so we need to set the git executable
103+
TEST_GIT_EXECUTABLE_PATH: 'C:\Program Files\Git\bin\git.exe'
92104
RUST_BACKTRACE: 1
93105
CARGO_TERM_COLOR: always
94106

‎CHANGELOG.md

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

6363
### New features
6464

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

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

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,7 @@ fn do_git_clone(
200200
git_repo.remote(remote_name, source).unwrap();
201201
let git_settings = workspace_command.settings().git_settings()?;
202202
let mut fetch_tx = workspace_command.start_transaction();
203-
204-
let stats = with_remote_git_callbacks(ui, None, |cb| {
203+
let stats = with_remote_git_callbacks(ui, None, &git_settings, |cb| {
205204
git::fetch(
206205
fetch_tx.repo_mut(),
207206
&git_repo,
@@ -213,11 +212,12 @@ fn do_git_clone(
213212
)
214213
})
215214
.map_err(|err| match err {
216-
GitFetchError::NoSuchRemote(_) => {
217-
panic!("shouldn't happen as we just created the git remote")
215+
GitFetchError::NoSuchRemote(repo_name) => {
216+
user_error(format!("Could not find repository at '{repo_name}'"))
218217
}
219218
GitFetchError::GitImportError(err) => CommandError::from(err),
220219
GitFetchError::InternalGitError(err) => map_git_error(err),
220+
GitFetchError::Subprocess(err) => user_error(err),
221221
GitFetchError::InvalidBranchPattern => {
222222
unreachable!("we didn't provide any globs")
223223
}

‎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
@@ -377,6 +377,16 @@
377377
"type": "boolean",
378378
"description": "Whether jj should sign commits before pushing",
379379
"default": "false"
380+
},
381+
"subprocess": {
382+
"type": "boolean",
383+
"description": "Whether jj spawns a git subprocess for network operations (push/fetch/clone)",
384+
"default": false
385+
},
386+
"executable-path": {
387+
"type": "string",
388+
"description": "Path to the git executable",
389+
"default": "git"
380390
}
381391
}
382392
},

‎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)