github.com/charypar/monobuild@v0.0.0-20211122220434-fd884ed50212/rs/src/git.rs (about)

     1  use thiserror::Error;
     2  
     3  pub type Commit = String;
     4  pub type Command = Vec<String>;
     5  
     6  pub enum Mode {
     7      Feature(String), // base branch, e.g. 'main'
     8      Main(String),    // base commit, e.g. 'HEAD^1'
     9  }
    10  
    11  #[derive(PartialEq, Error, Debug)]
    12  pub enum GitError {
    13      #[error("Cannot find merge base with branch {0}: {1}")]
    14      MergeBase(String, String), // base branch, error
    15      #[error("Finding changed files failed: {0}")]
    16      Diff(String), // error
    17  }
    18  
    19  pub struct Git<Executor>
    20  where
    21      Executor: FnMut(Command) -> Result<String, String>,
    22  {
    23      // Inversion of control for command execution to make Git pure
    24      // and easier to test
    25      executor: Executor,
    26  }
    27  
    28  impl<Executor> Git<Executor>
    29  where
    30      Executor: FnMut(Command) -> Result<String, String>,
    31  {
    32      pub fn new(executor: Executor) -> Self {
    33          Self { executor }
    34      }
    35  
    36      pub fn diff_base(&mut self, mode: Mode) -> Result<Commit, GitError> {
    37          match mode {
    38              Mode::Feature(base_branch) => self
    39                  .execute(["git", "merge-base", base_branch.as_ref(), "HEAD"])
    40                  .map(|base| base.trim_end().to_string())
    41                  .map_err(|e| GitError::MergeBase(base_branch, e.to_string())),
    42              Mode::Main(base_commit) => Ok(base_commit.trim_end().to_string()),
    43          }
    44      }
    45  
    46      pub fn diff(&mut self, mode: Mode) -> Result<Vec<String>, GitError> {
    47          let base = self.diff_base(mode)?;
    48  
    49          self.execute([
    50              "git",
    51              "diff",
    52              "--no-commit-id",
    53              "--name-only",
    54              "-r",
    55              base.as_ref(),
    56          ])
    57          .map(|files| {
    58              files
    59                  .trim_end()
    60                  .split("\n")
    61                  .map(|f| f.to_string())
    62                  .collect()
    63          })
    64          .map_err(|e| GitError::Diff(e.to_string()))
    65      }
    66  
    67      fn execute<'a>(
    68          &mut self,
    69          command: impl IntoIterator<Item = &'a str>,
    70      ) -> Result<String, String> {
    71          (self.executor)(command.into_iter().map(|p| p.to_string()).collect())
    72      }
    73  }
    74  
    75  #[cfg(test)]
    76  mod test {
    77      mod diff_base {
    78          use super::super::*;
    79  
    80          #[test]
    81          fn base_on_feature_branch() {
    82              let mut actual_command: Option<Command> = None;
    83              let expected_command = Some(vec![
    84                  "git".into(),
    85                  "merge-base".into(),
    86                  "main".into(),
    87                  "HEAD".into(),
    88              ]);
    89  
    90              let mock_exec = |cmd: Command| -> Result<String, String> {
    91                  actual_command = Some(cmd);
    92  
    93                  Ok("abc\n".to_string()) // check new line is trimmed
    94              };
    95  
    96              let mut git = Git::new(mock_exec);
    97  
    98              let actual = git.diff_base(Mode::Feature("main".to_string()));
    99              let expected = Ok("abc".to_string());
   100  
   101              assert_eq!(actual, expected);
   102              assert_eq!(actual_command, expected_command);
   103          }
   104  
   105          #[test]
   106          fn base_on_main_branch() {
   107              let mut actual_command: Option<Command> = None;
   108              let expected_command = None;
   109  
   110              let mock_exec = |cmd: Command| -> Result<String, String> {
   111                  actual_command = Some(cmd);
   112  
   113                  Ok("abc\n".to_string())
   114              };
   115  
   116              let mut git = Git::new(mock_exec);
   117  
   118              let actual = git.diff_base(Mode::Main("HEAD^1".to_string()));
   119              let expected = Ok("HEAD^1".to_string());
   120  
   121              assert_eq!(actual, expected);
   122              assert_eq!(actual_command, expected_command);
   123          }
   124      }
   125  
   126      mod diff {
   127          use super::super::*;
   128  
   129          #[test]
   130          fn diff_on_feature_branch() {
   131              let mut actual_commands: Vec<Command> = vec![];
   132              let expected_command: Vec<String> = vec![
   133                  "git".into(),
   134                  "diff".into(),
   135                  "--no-commit-id".into(),
   136                  "--name-only".into(),
   137                  "-r".into(),
   138                  "main".into(),
   139              ];
   140  
   141              let mock_exec = |cmd: Command| -> Result<String, String> {
   142                  actual_commands.push(cmd);
   143  
   144                  if actual_commands.len() < 2 {
   145                      Ok("main\n".to_string())
   146                  } else {
   147                      Ok("one\ntwo\nthree\n".to_string())
   148                  }
   149              };
   150  
   151              let mut git = Git::new(mock_exec);
   152  
   153              let actual = git.diff(Mode::Feature("main".to_string()));
   154              let expected = Ok(vec![
   155                  "one".to_string(),
   156                  "two".to_string(),
   157                  "three".to_string(),
   158              ]);
   159  
   160              assert_eq!(actual, expected);
   161              assert_eq!(actual_commands[1], expected_command);
   162          }
   163  
   164          #[test]
   165          fn diff_on_main_branch() {
   166              let mut actual_commands: Vec<Command> = vec![];
   167              let expected_command: Vec<String> = vec![
   168                  "git".into(),
   169                  "diff".into(),
   170                  "--no-commit-id".into(),
   171                  "--name-only".into(),
   172                  "-r".into(),
   173                  "HEAD^1".into(),
   174              ];
   175  
   176              let mock_exec = |cmd: Command| -> Result<String, String> {
   177                  actual_commands.push(cmd);
   178  
   179                  Ok("one\ntwo\nthree\n".to_string())
   180              };
   181  
   182              let mut git = Git::new(mock_exec);
   183  
   184              let actual = git.diff(Mode::Main("HEAD^1".to_string()));
   185              let expected = Ok(vec![
   186                  "one".to_string(),
   187                  "two".to_string(),
   188                  "three".to_string(),
   189              ]);
   190  
   191              assert_eq!(actual, expected);
   192              assert_eq!(actual_commands[0], expected_command);
   193          }
   194      }
   195  }