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")
}