diff --git a/src/git.rs b/src/git.rs index 8eada5c9b11..c8ff2905550 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,5 +1,8 @@ +use anyhow::{anyhow, Context}; use std::collections::HashMap; +use std::io::Write; use std::path::{Path, PathBuf}; +use std::process::Command; use swirl::PerformError; use tempfile::TempDir; @@ -48,6 +51,45 @@ impl Credentials { } } } + + /// Write the SSH key to a temporary file and return the path. The file is + /// deleted once the returned path is dropped. + /// + /// This function can be used when running `git push` instead of using the + /// `git2` crate for pushing commits to remote git servers. + /// + /// Note: On Linux this function creates the temporary file in `/dev/shm` to + /// avoid writing it to disk. + /// + /// # Errors + /// + /// - If non-SSH credentials are use, `Err` is returned. + /// - If creation of the temporary file fails, `Err` is returned. + /// + fn write_temporary_ssh_key(&self) -> anyhow::Result { + let key = match self { + Credentials::Ssh { key } => key, + _ => return Err(anyhow!("SSH key not available")), + }; + + let dir = if cfg!(target_os = "linux") { + // When running on production, ensure the file is created in tmpfs and not persisted to disk + "/dev/shm".into() + } else { + // For other platforms, default to std::env::tempdir() + std::env::temp_dir() + }; + + let mut temp_key_file = tempfile::Builder::new() + .tempfile_in(dir) + .context("Failed to create temporary file")?; + + temp_key_file + .write_all(key.as_bytes()) + .context("Failed to write SSH key to temporary file")?; + + Ok(temp_key_file.into_temp_path()) + } } #[derive(Serialize, Deserialize, Debug)] @@ -137,10 +179,9 @@ impl RepositoryConfig { } pub struct Repository { - /// bla - pub checkout_path: TempDir, + checkout_path: TempDir, repository: git2::Repository, - pub credentials: Credentials, + credentials: Credentials, } impl Repository { @@ -346,4 +387,32 @@ impl Repository { Ok(()) } + + /// Runs the specified `git` command in the working directory of the local + /// crate index repository. + /// + /// This function also temporarily sets the `GIT_SSH_COMMAND` environment + /// variable to ensure that `git push` commands are able to succeed. + pub fn run_command(&self, command: &mut Command) -> Result<(), PerformError> { + let checkout_path = self.checkout_path.path(); + command.current_dir(checkout_path); + + let temp_key_path = self.credentials.write_temporary_ssh_key()?; + command.env( + "GIT_SSH_COMMAND", + format!( + "ssh -o StrictHostKeyChecking=accept-new -i {}", + temp_key_path.display() + ), + ); + + let output = command.output()?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let message = format!("Running git command failed with: {}", stderr); + return Err(message.into()); + } + + Ok(()) + } } diff --git a/src/worker/git.rs b/src/worker/git.rs index f4ac6253932..33aa1ec67bf 100644 --- a/src/worker/git.rs +++ b/src/worker/git.rs @@ -1,11 +1,11 @@ use crate::background_jobs::Environment; -use crate::git::{Crate, Credentials}; +use crate::git::Crate; use crate::schema; use anyhow::Context; use chrono::Utc; use diesel::prelude::*; use std::fs::{self, OpenOptions}; -use std::io::prelude::*; +use std::process::Command; use swirl::PerformError; #[swirl::background_job] @@ -99,53 +99,18 @@ pub fn squash_index(env: &Environment) -> Result<(), PerformError> { // Shell out to git because libgit2 does not currently support push leases - let key = match &repo.credentials { - Credentials::Ssh { key } => key, - Credentials::Http { .. } => { - return Err(String::from("squash_index: Password auth not supported").into()) - } - _ => return Err(String::from("squash_index: Could not determine credentials").into()), - }; - - // When running on production, ensure the file is created in tmpfs and not persisted to disk - #[cfg(target_os = "linux")] - let mut temp_key_file = tempfile::Builder::new().tempfile_in("/dev/shm")?; - - // For other platforms, default to std::env::tempdir() - #[cfg(not(target_os = "linux"))] - let mut temp_key_file = tempfile::Builder::new().tempfile()?; - - temp_key_file.write_all(key.as_bytes())?; - - let checkout_path = repo.checkout_path.path(); - let output = std::process::Command::new("git") - .current_dir(checkout_path) - .env( - "GIT_SSH_COMMAND", - format!( - "ssh -o StrictHostKeyChecking=accept-new -i {}", - temp_key_file.path().display() - ), - ) - .args(&[ - "push", - // Both updates should succeed or fail together - "--atomic", - "origin", - // Overwrite master, but only if it server matches the expected value - &format!("--force-with-lease=refs/heads/master:{}", original_head), - // The new squashed commit is pushed to master - "HEAD:refs/heads/master", - // The previous value of HEAD is pushed to a snapshot branch - &format!("{}:refs/heads/snapshot-{}", original_head, now), - ]) - .output()?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let message = format!("Running git command failed with: {}", stderr); - return Err(message.into()); - } + repo.run_command(Command::new("git").args(&[ + "push", + // Both updates should succeed or fail together + "--atomic", + "origin", + // Overwrite master, but only if it server matches the expected value + &format!("--force-with-lease=refs/heads/master:{}", original_head), + // The new squashed commit is pushed to master + "HEAD:refs/heads/master", + // The previous value of HEAD is pushed to a snapshot branch + &format!("{}:refs/heads/snapshot-{}", original_head, now), + ]))?; println!("The index has been successfully squashed.");