|
| 1 | +use std::process; |
| 2 | + |
| 3 | +use colored::*; |
| 4 | +use git2::{BranchType, Config, ConfigLevel, Error, ErrorCode, ObjectType, Repository}; |
| 5 | +use regex::Regex; |
| 6 | + |
| 7 | +use super::GitChain; |
| 8 | +use crate::types::*; |
| 9 | +use crate::{executable_name, Branch, Chain}; |
| 10 | + |
| 11 | +impl GitChain { |
| 12 | + pub fn init() -> Result<Self, Error> { |
| 13 | + let name_of_current_executable = executable_name(); |
| 14 | + |
| 15 | + let repo = Repository::discover(".")?; |
| 16 | + |
| 17 | + if repo.is_bare() { |
| 18 | + eprintln!( |
| 19 | + "Cannot run {} on bare git repository.", |
| 20 | + name_of_current_executable |
| 21 | + ); |
| 22 | + process::exit(1); |
| 23 | + } |
| 24 | + |
| 25 | + let git_chain = GitChain { |
| 26 | + repo, |
| 27 | + executable_name: name_of_current_executable, |
| 28 | + }; |
| 29 | + Ok(git_chain) |
| 30 | + } |
| 31 | + |
| 32 | + pub fn get_current_branch_name(&self) -> Result<String, Error> { |
| 33 | + let head = match self.repo.head() { |
| 34 | + Ok(head) => Some(head), |
| 35 | + Err(ref e) |
| 36 | + if e.code() == ErrorCode::UnbornBranch || e.code() == ErrorCode::NotFound => |
| 37 | + { |
| 38 | + None |
| 39 | + } |
| 40 | + Err(e) => return Err(e), |
| 41 | + }; |
| 42 | + |
| 43 | + let head = head.as_ref().and_then(|h| h.shorthand()); |
| 44 | + |
| 45 | + match head { |
| 46 | + Some(branch_name) => Ok(branch_name.to_string()), |
| 47 | + None => Err(Error::from_str("Unable to get current branch name.")), |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + pub fn get_local_git_config(&self) -> Result<Config, Error> { |
| 52 | + self.repo.config()?.open_level(ConfigLevel::Local) |
| 53 | + } |
| 54 | + |
| 55 | + pub fn get_git_config(&self, key: &str) -> Result<Option<String>, Error> { |
| 56 | + let local_config = self.get_local_git_config()?; |
| 57 | + match local_config.get_string(key) { |
| 58 | + Ok(value) => Ok(Some(value)), |
| 59 | + Err(ref e) if e.code() == ErrorCode::NotFound => Ok(None), |
| 60 | + Err(e) => Err(e), |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + pub fn get_git_configs_matching_key( |
| 65 | + &self, |
| 66 | + regexp: &Regex, |
| 67 | + ) -> Result<Vec<(String, String)>, Error> { |
| 68 | + let local_config = self.get_local_git_config()?; |
| 69 | + let mut entries = vec![]; |
| 70 | + |
| 71 | + local_config.entries(None)?.for_each(|entry| { |
| 72 | + if let Some(key) = entry.name() { |
| 73 | + if regexp.is_match(key) && entry.has_value() { |
| 74 | + let key = key.to_string(); |
| 75 | + let value = entry.value().unwrap().to_string(); |
| 76 | + entries.push((key, value)); |
| 77 | + } |
| 78 | + } |
| 79 | + })?; |
| 80 | + |
| 81 | + Ok(entries) |
| 82 | + } |
| 83 | + |
| 84 | + pub fn set_git_config(&self, key: &str, value: &str) -> Result<(), Error> { |
| 85 | + let mut local_config = self.get_local_git_config()?; |
| 86 | + local_config.set_str(key, value)?; |
| 87 | + Ok(()) |
| 88 | + } |
| 89 | + |
| 90 | + pub fn delete_git_config(&self, key: &str) -> Result<(), Error> { |
| 91 | + let mut local_config = self.get_local_git_config()?; |
| 92 | + match local_config.remove(key) { |
| 93 | + Ok(()) => Ok(()), |
| 94 | + Err(ref e) if e.code() == ErrorCode::NotFound => Ok(()), |
| 95 | + Err(e) => Err(e), |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + pub fn checkout_branch(&self, branch_name: &str) -> Result<(), Error> { |
| 100 | + let (object, reference) = self.repo.revparse_ext(branch_name)?; |
| 101 | + |
| 102 | + // set working directory |
| 103 | + self.repo.checkout_tree(&object, None)?; |
| 104 | + |
| 105 | + // set HEAD to branch_name |
| 106 | + match reference { |
| 107 | + // ref_name is an actual reference like branches or tags |
| 108 | + Some(ref_name) => self.repo.set_head(ref_name.name().unwrap()), |
| 109 | + // this is a commit, not a reference |
| 110 | + None => self.repo.set_head_detached(object.id()), |
| 111 | + } |
| 112 | + .unwrap_or_else(|_| panic!("Failed to set HEAD to branch {}", branch_name)); |
| 113 | + |
| 114 | + Ok(()) |
| 115 | + } |
| 116 | + |
| 117 | + pub fn git_branch_exists(&self, branch_name: &str) -> Result<bool, Error> { |
| 118 | + Ok(self.git_local_branch_exists(branch_name)? |
| 119 | + || self.git_remote_branch_exists(branch_name)?) |
| 120 | + } |
| 121 | + |
| 122 | + pub fn git_local_branch_exists(&self, branch_name: &str) -> Result<bool, Error> { |
| 123 | + match self.repo.find_branch(branch_name, BranchType::Local) { |
| 124 | + Ok(_branch) => Ok(true), |
| 125 | + Err(ref e) if e.code() == ErrorCode::NotFound => Ok(false), |
| 126 | + Err(e) => Err(e), |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + pub fn git_remote_branch_exists(&self, branch_name: &str) -> Result<bool, Error> { |
| 131 | + match self.repo.find_branch(branch_name, BranchType::Remote) { |
| 132 | + Ok(_branch) => Ok(true), |
| 133 | + Err(ref e) if e.code() == ErrorCode::NotFound => Ok(false), |
| 134 | + Err(e) => Err(e), |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + pub fn display_branch_not_part_of_chain_error(&self, branch_name: &str) { |
| 139 | + eprintln!("❌ Branch is not part of any chain: {}", branch_name.bold()); |
| 140 | + eprintln!( |
| 141 | + "To initialize a chain for this branch, run {} init <chain_name> <root_branch>", |
| 142 | + self.executable_name |
| 143 | + ); |
| 144 | + } |
| 145 | + |
| 146 | + pub fn run_status(&self, show_prs: bool) -> Result<(), Error> { |
| 147 | + let branch_name = self.get_current_branch_name()?; |
| 148 | + println!("On branch: {}", branch_name.bold()); |
| 149 | + println!(); |
| 150 | + |
| 151 | + let results = Branch::get_branch_with_chain(self, &branch_name)?; |
| 152 | + |
| 153 | + match results { |
| 154 | + BranchSearchResult::NotPartOfAnyChain => { |
| 155 | + self.display_branch_not_part_of_chain_error(&branch_name); |
| 156 | + process::exit(1); |
| 157 | + } |
| 158 | + BranchSearchResult::Branch(branch) => { |
| 159 | + branch.display_status(self, show_prs)?; |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + Ok(()) |
| 164 | + } |
| 165 | + |
| 166 | + pub fn init_chain( |
| 167 | + &self, |
| 168 | + chain_name: &str, |
| 169 | + root_branch: &str, |
| 170 | + branch_name: &str, |
| 171 | + sort_option: SortBranch, |
| 172 | + ) -> Result<(), Error> { |
| 173 | + let results = Branch::get_branch_with_chain(self, branch_name)?; |
| 174 | + |
| 175 | + match results { |
| 176 | + BranchSearchResult::NotPartOfAnyChain => { |
| 177 | + Branch::setup_branch(self, chain_name, root_branch, branch_name, &sort_option)?; |
| 178 | + |
| 179 | + match Branch::get_branch_with_chain(self, branch_name)? { |
| 180 | + BranchSearchResult::NotPartOfAnyChain => { |
| 181 | + eprintln!("Unable to set up chain for branch: {}", branch_name.bold()); |
| 182 | + process::exit(1); |
| 183 | + } |
| 184 | + BranchSearchResult::Branch(branch) => { |
| 185 | + println!("🔗 Succesfully set up branch: {}", branch_name.bold()); |
| 186 | + println!(); |
| 187 | + branch.display_status(self, false)?; |
| 188 | + } |
| 189 | + }; |
| 190 | + } |
| 191 | + BranchSearchResult::Branch(branch) => { |
| 192 | + eprintln!("❌ Unable to initialize branch to a chain.",); |
| 193 | + eprintln!(); |
| 194 | + eprintln!("Branch already part of a chain: {}", branch_name.bold()); |
| 195 | + eprintln!("It is part of the chain: {}", branch.chain_name.bold()); |
| 196 | + eprintln!("With root branch: {}", branch.root_branch.bold()); |
| 197 | + process::exit(1); |
| 198 | + } |
| 199 | + }; |
| 200 | + |
| 201 | + Ok(()) |
| 202 | + } |
| 203 | + |
| 204 | + pub fn remove_branch_from_chain(&self, branch_name: String) -> Result<(), Error> { |
| 205 | + let results = Branch::get_branch_with_chain(self, &branch_name)?; |
| 206 | + |
| 207 | + match results { |
| 208 | + BranchSearchResult::NotPartOfAnyChain => { |
| 209 | + Branch::delete_all_configs(self, &branch_name)?; |
| 210 | + |
| 211 | + println!( |
| 212 | + "Unable to remove branch from its chain: {}", |
| 213 | + branch_name.bold() |
| 214 | + ); |
| 215 | + println!("It is not part of any chain. Nothing to do."); |
| 216 | + } |
| 217 | + BranchSearchResult::Branch(branch) => { |
| 218 | + let chain_name = branch.chain_name.clone(); |
| 219 | + let root_branch = branch.root_branch.clone(); |
| 220 | + branch.remove_from_chain(self)?; |
| 221 | + |
| 222 | + println!( |
| 223 | + "Removed branch {} from chain {}", |
| 224 | + branch_name.bold(), |
| 225 | + chain_name.bold() |
| 226 | + ); |
| 227 | + println!("Its root branch was: {}", root_branch.bold()); |
| 228 | + } |
| 229 | + }; |
| 230 | + Ok(()) |
| 231 | + } |
| 232 | + |
| 233 | + pub fn list_chains(&self, current_branch: &str, show_prs: bool) -> Result<(), Error> { |
| 234 | + let list = Chain::get_all_chains(self)?; |
| 235 | + |
| 236 | + if list.is_empty() { |
| 237 | + println!("No chains to list."); |
| 238 | + println!( |
| 239 | + "To initialize a chain for this branch, run {} init <chain_name> <root_branch>", |
| 240 | + self.executable_name |
| 241 | + ); |
| 242 | + return Ok(()); |
| 243 | + } |
| 244 | + |
| 245 | + for (index, chain) in list.iter().enumerate() { |
| 246 | + chain.display_list(self, current_branch, show_prs)?; |
| 247 | + |
| 248 | + if index != list.len() - 1 { |
| 249 | + println!(); |
| 250 | + } |
| 251 | + } |
| 252 | + |
| 253 | + Ok(()) |
| 254 | + } |
| 255 | + |
| 256 | + pub fn move_branch( |
| 257 | + &self, |
| 258 | + chain_name: &str, |
| 259 | + branch_name: &str, |
| 260 | + sort_option: &SortBranch, |
| 261 | + ) -> Result<(), Error> { |
| 262 | + match Branch::get_branch_with_chain(self, branch_name)? { |
| 263 | + BranchSearchResult::NotPartOfAnyChain => { |
| 264 | + self.display_branch_not_part_of_chain_error(branch_name); |
| 265 | + process::exit(1); |
| 266 | + } |
| 267 | + BranchSearchResult::Branch(branch) => { |
| 268 | + branch.move_branch(self, chain_name, sort_option)?; |
| 269 | + |
| 270 | + match Branch::get_branch_with_chain(self, &branch.branch_name)? { |
| 271 | + BranchSearchResult::NotPartOfAnyChain => { |
| 272 | + eprintln!("Unable to move branch: {}", branch.branch_name.bold()); |
| 273 | + process::exit(1); |
| 274 | + } |
| 275 | + BranchSearchResult::Branch(branch) => { |
| 276 | + println!("🔗 Succesfully moved branch: {}", branch.branch_name.bold()); |
| 277 | + println!(); |
| 278 | + branch.display_status(self, false)?; |
| 279 | + } |
| 280 | + }; |
| 281 | + } |
| 282 | + }; |
| 283 | + |
| 284 | + Ok(()) |
| 285 | + } |
| 286 | + |
| 287 | + pub fn get_commit_hash_of_head(&self) -> Result<String, Error> { |
| 288 | + let head = self.repo.head()?; |
| 289 | + let oid = head.target().unwrap(); |
| 290 | + let commit = self.repo.find_commit(oid).unwrap(); |
| 291 | + Ok(commit.id().to_string()) |
| 292 | + } |
| 293 | + |
| 294 | + pub fn get_tree_id_from_branch_name(&self, branch_name: &str) -> Result<String, Error> { |
| 295 | + match self |
| 296 | + .repo |
| 297 | + .revparse_single(&format!("{}^{{tree}}", branch_name)) |
| 298 | + { |
| 299 | + Ok(tree_object) => { |
| 300 | + assert_eq!(tree_object.kind().unwrap(), ObjectType::Tree); |
| 301 | + Ok(tree_object.id().to_string()) |
| 302 | + } |
| 303 | + Err(_err) => Err(Error::from_str(&format!( |
| 304 | + "Unable to get tree id of branch {}", |
| 305 | + branch_name.bold() |
| 306 | + ))), |
| 307 | + } |
| 308 | + } |
| 309 | + |
| 310 | + pub fn dirty_working_directory(&self) -> Result<bool, Error> { |
| 311 | + // perform equivalent to git diff-index HEAD |
| 312 | + let obj = self.repo.revparse_single("HEAD")?; |
| 313 | + let tree = obj.peel(ObjectType::Tree)?; |
| 314 | + |
| 315 | + let diff = self |
| 316 | + .repo |
| 317 | + .diff_tree_to_workdir_with_index(tree.as_tree(), None)?; |
| 318 | + |
| 319 | + let diff_stats = diff.stats()?; |
| 320 | + let has_changes = diff_stats.files_changed() > 0 |
| 321 | + || diff_stats.insertions() > 0 |
| 322 | + || diff_stats.deletions() > 0; |
| 323 | + |
| 324 | + Ok(has_changes) |
| 325 | + } |
| 326 | +} |
0 commit comments