1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
//! Saving and loading of on-disk information for the `git branchless test`
//! subcommand. This isn't part of Git itself, but multiple `git-branchless`
//! subsystems need to know about it, similar to snapshotting.
//!
//! Regrettably, this adds `serde` as a new dependency to `git-branchless-lib`,
//! which will increase build times.
use std::{fmt::Display, path::PathBuf};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use super::{Commit, NonZeroOid, Repo};
/// The exit status to use when a test command succeeds.
pub const TEST_SUCCESS_EXIT_CODE: i32 = 0;
/// The exit status to use when a test command intends to skip the provided commit.
/// This exit code is used officially by several source control systems:
///
/// - Git: "Note that the script (my_script in the above example) should exit
/// with code 0 if the current source code is good/old, and exit with a code
/// between 1 and 127 (inclusive), except 125, if the current source code is
/// bad/new."
/// - Mercurial: "The exit status of the command will be used to mark revisions
/// as good or bad: status 0 means good, 125 means to skip the revision, 127
/// (command not found) will abort the bisection, and any other non-zero exit
/// status means the revision is bad."
///
/// And it's become the de-facto standard for custom bisection scripts for other
/// source control systems as well.
pub const TEST_INDETERMINATE_EXIT_CODE: i32 = 125;
/// Similarly to `INDETERMINATE_EXIT_CODE`, this exit code is used officially by
/// `git-bisect` and others to abort the process. It's also typically raised by
/// the shell when the command is not found, so it's technically ambiguous
/// whether the command existed or not. Nonetheless, it's intuitive for a
/// failure to run a given command to abort the process altogether, so it
/// shouldn't be too confusing in practice.
pub const TEST_ABORT_EXIT_CODE: i32 = 127;
/// Convert a command string into a string that's safe to use as a filename.
pub fn make_test_command_slug(command: String) -> String {
command.replace(['/', ' ', '\n'], "__")
}
/// A version of `NonZeroOid` that can be serialized and deserialized. This
/// exists in case we want to move this type (back) into a separate module which
/// has a `serde` dependency in the interest of improving build times.
#[derive(Debug)]
pub struct SerializedNonZeroOid(pub NonZeroOid);
impl Serialize for SerializedNonZeroOid {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0.to_string())
}
}
impl<'de> Deserialize<'de> for SerializedNonZeroOid {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
let oid: NonZeroOid = s.parse().map_err(|_| {
de::Error::invalid_value(de::Unexpected::Str(&s), &"a valid non-zero OID")
})?;
Ok(SerializedNonZeroOid(oid))
}
}
/// A test command to run.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum TestCommand {
/// A full command string (to be passed to a shell for processing).
String(String),
/// A list of arguments. The first argument indicates the program to invoke.
Args(Vec<String>),
}
impl Display for TestCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TestCommand::String(s) => write!(f, "{s}"),
TestCommand::Args(args) => {
for (i, arg) in args.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{}", shell_words::quote(arg))?;
}
Ok(())
}
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct SerializedTestResult {
pub command: TestCommand,
pub exit_code: i32,
pub head_commit_oid: Option<SerializedNonZeroOid>,
pub snapshot_tree_oid: Option<SerializedNonZeroOid>,
#[serde(default)]
pub interactive: bool,
}
/// Get the directory where the results of running tests are stored.
fn get_test_dir(repo: &Repo) -> PathBuf {
repo.get_path().join("branchless").join("test")
}
/// Get the directory where the result of tests for a particular commit are
/// stored. Tests are keyed by tree OID, not commit OID, so that they can be
/// cached based on the contents of the commit, rather than its specific commit
/// hash. This means that we can cache the results of tests for commits that
/// have been amended or rebased.
pub fn get_test_tree_dir(repo: &Repo, commit: &Commit) -> PathBuf {
get_test_dir(repo).join(commit.get_tree_oid().to_string())
}
/// Get the directory where the locks for running tests are stored.
pub fn get_test_locks_dir(repo: &Repo) -> PathBuf {
get_test_dir(repo).join("locks")
}
/// Get the directory where the worktrees for running tests are stored.
pub fn get_test_worktrees_dir(repo: &Repo) -> PathBuf {
get_test_dir(repo).join("worktrees")
}
/// Get the path to the file where the latest test command is stored.
pub fn get_latest_test_command_path(repo: &Repo) -> PathBuf {
get_test_dir(repo).join("latest-command")
}