Skip to content

Commit 0409e47

Browse files
committed
Auto merge of #4365 - Turbo87:git-run-command, r=locks
Repository: Extract `run_command()` function This new function makes it easier to run arbitrary `git` commands in the working folder of the local index clone. It was extracted from the `squash_index()` background worker tasks, which uses it to run `git push --force-with-lease`, which is not yet supported by the `git2` crate. The main goal of this PR is to reduce the visibility of the `checkout_path` and `credentials` fields on the `Repository`, which outside code should ideally not have any access to. Note that this includes the commits from and is based on #4363.
2 parents 0fef2f4 + eb2f671 commit 0409e47

File tree

2 files changed

+86
-52
lines changed

2 files changed

+86
-52
lines changed

src/git.rs

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
use anyhow::{anyhow, Context};
12
use std::collections::HashMap;
3+
use std::io::Write;
24
use std::path::{Path, PathBuf};
5+
use std::process::Command;
36

47
use swirl::PerformError;
58
use tempfile::TempDir;
@@ -48,6 +51,45 @@ impl Credentials {
4851
}
4952
}
5053
}
54+
55+
/// Write the SSH key to a temporary file and return the path. The file is
56+
/// deleted once the returned path is dropped.
57+
///
58+
/// This function can be used when running `git push` instead of using the
59+
/// `git2` crate for pushing commits to remote git servers.
60+
///
61+
/// Note: On Linux this function creates the temporary file in `/dev/shm` to
62+
/// avoid writing it to disk.
63+
///
64+
/// # Errors
65+
///
66+
/// - If non-SSH credentials are use, `Err` is returned.
67+
/// - If creation of the temporary file fails, `Err` is returned.
68+
///
69+
fn write_temporary_ssh_key(&self) -> anyhow::Result<tempfile::TempPath> {
70+
let key = match self {
71+
Credentials::Ssh { key } => key,
72+
_ => return Err(anyhow!("SSH key not available")),
73+
};
74+
75+
let dir = if cfg!(target_os = "linux") {
76+
// When running on production, ensure the file is created in tmpfs and not persisted to disk
77+
"/dev/shm".into()
78+
} else {
79+
// For other platforms, default to std::env::tempdir()
80+
std::env::temp_dir()
81+
};
82+
83+
let mut temp_key_file = tempfile::Builder::new()
84+
.tempfile_in(dir)
85+
.context("Failed to create temporary file")?;
86+
87+
temp_key_file
88+
.write_all(key.as_bytes())
89+
.context("Failed to write SSH key to temporary file")?;
90+
91+
Ok(temp_key_file.into_temp_path())
92+
}
5193
}
5294

5395
#[derive(Serialize, Deserialize, Debug)]
@@ -137,10 +179,9 @@ impl RepositoryConfig {
137179
}
138180

139181
pub struct Repository {
140-
/// bla
141-
pub checkout_path: TempDir,
182+
checkout_path: TempDir,
142183
repository: git2::Repository,
143-
pub credentials: Credentials,
184+
credentials: Credentials,
144185
}
145186

146187
impl Repository {
@@ -346,4 +387,32 @@ impl Repository {
346387

347388
Ok(())
348389
}
390+
391+
/// Runs the specified `git` command in the working directory of the local
392+
/// crate index repository.
393+
///
394+
/// This function also temporarily sets the `GIT_SSH_COMMAND` environment
395+
/// variable to ensure that `git push` commands are able to succeed.
396+
pub fn run_command(&self, command: &mut Command) -> Result<(), PerformError> {
397+
let checkout_path = self.checkout_path.path();
398+
command.current_dir(checkout_path);
399+
400+
let temp_key_path = self.credentials.write_temporary_ssh_key()?;
401+
command.env(
402+
"GIT_SSH_COMMAND",
403+
format!(
404+
"ssh -o StrictHostKeyChecking=accept-new -i {}",
405+
temp_key_path.display()
406+
),
407+
);
408+
409+
let output = command.output()?;
410+
if !output.status.success() {
411+
let stderr = String::from_utf8_lossy(&output.stderr);
412+
let message = format!("Running git command failed with: {}", stderr);
413+
return Err(message.into());
414+
}
415+
416+
Ok(())
417+
}
349418
}

src/worker/git.rs

Lines changed: 14 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use crate::background_jobs::Environment;
2-
use crate::git::{Crate, Credentials};
2+
use crate::git::Crate;
33
use crate::schema;
44
use anyhow::Context;
55
use chrono::Utc;
66
use diesel::prelude::*;
77
use std::fs::{self, OpenOptions};
8-
use std::io::prelude::*;
8+
use std::process::Command;
99
use swirl::PerformError;
1010

1111
#[swirl::background_job]
@@ -99,53 +99,18 @@ pub fn squash_index(env: &Environment) -> Result<(), PerformError> {
9999

100100
// Shell out to git because libgit2 does not currently support push leases
101101

102-
let key = match &repo.credentials {
103-
Credentials::Ssh { key } => key,
104-
Credentials::Http { .. } => {
105-
return Err(String::from("squash_index: Password auth not supported").into())
106-
}
107-
_ => return Err(String::from("squash_index: Could not determine credentials").into()),
108-
};
109-
110-
// When running on production, ensure the file is created in tmpfs and not persisted to disk
111-
#[cfg(target_os = "linux")]
112-
let mut temp_key_file = tempfile::Builder::new().tempfile_in("/dev/shm")?;
113-
114-
// For other platforms, default to std::env::tempdir()
115-
#[cfg(not(target_os = "linux"))]
116-
let mut temp_key_file = tempfile::Builder::new().tempfile()?;
117-
118-
temp_key_file.write_all(key.as_bytes())?;
119-
120-
let checkout_path = repo.checkout_path.path();
121-
let output = std::process::Command::new("git")
122-
.current_dir(checkout_path)
123-
.env(
124-
"GIT_SSH_COMMAND",
125-
format!(
126-
"ssh -o StrictHostKeyChecking=accept-new -i {}",
127-
temp_key_file.path().display()
128-
),
129-
)
130-
.args(&[
131-
"push",
132-
// Both updates should succeed or fail together
133-
"--atomic",
134-
"origin",
135-
// Overwrite master, but only if it server matches the expected value
136-
&format!("--force-with-lease=refs/heads/master:{}", original_head),
137-
// The new squashed commit is pushed to master
138-
"HEAD:refs/heads/master",
139-
// The previous value of HEAD is pushed to a snapshot branch
140-
&format!("{}:refs/heads/snapshot-{}", original_head, now),
141-
])
142-
.output()?;
143-
144-
if !output.status.success() {
145-
let stderr = String::from_utf8_lossy(&output.stderr);
146-
let message = format!("Running git command failed with: {}", stderr);
147-
return Err(message.into());
148-
}
102+
repo.run_command(Command::new("git").args(&[
103+
"push",
104+
// Both updates should succeed or fail together
105+
"--atomic",
106+
"origin",
107+
// Overwrite master, but only if it server matches the expected value
108+
&format!("--force-with-lease=refs/heads/master:{}", original_head),
109+
// The new squashed commit is pushed to master
110+
"HEAD:refs/heads/master",
111+
// The previous value of HEAD is pushed to a snapshot branch
112+
&format!("{}:refs/heads/snapshot-{}", original_head, now),
113+
]))?;
149114

150115
println!("The index has been successfully squashed.");
151116

0 commit comments

Comments
 (0)