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 }