git_eq/
lib.rs

1use std::{
2    path::Path,
3    process::{Command, Output},
4    time::{SystemTime, SystemTimeError},
5};
6
7use anyhow::Result;
8use config::Config;
9
10pub mod config;
11mod file_watcher;
12
13/// To run the earthquake procedure
14pub fn earthquake_procedure(config: Config) -> Result<()> {
15    let current_branch = current_branch()?;
16    let remote = remote(&current_branch)?;
17    let user_email = user_email()?;
18    let elapsed = current_unix_epoch()?;
19    let branch_name = format!("earthquake/{current_branch}-{user_email}-{elapsed}");
20
21    checkout(&branch_name)?;
22    if any_uncommited_changes()? {
23        add()?;
24        commit(&config.commit_message)?;
25    }
26    push(&branch_name, &remote)?;
27
28    Ok(())
29}
30
31fn any_uncommited_changes() -> Result<bool> {
32    let output = output_git_command(&["status", "--porcelain"])?;
33    Ok(!output.is_empty())
34}
35
36fn current_branch() -> Result<String> {
37    output_git_command(&["branch", "--show-current"])
38}
39
40fn user_email() -> Result<String> {
41    let output = output_git_command(&["config", "--get", "user.email"])?;
42    if output.is_empty() {
43        return Err(anyhow::Error::msg("No user email is configured"));
44    }
45    Ok(output)
46}
47
48fn remote(branch_name: &str) -> Result<String> {
49    let config_path = format!("branch.{branch_name}.remote");
50    let output = output_git_command(&["config", "--get", &config_path])?;
51    if output.is_empty() {
52        return Err(anyhow::Error::msg("No remote branch is configured"));
53    }
54    Ok(output)
55}
56
57fn current_unix_epoch() -> Result<u64, SystemTimeError> {
58    let current_unix_epoch = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
59    Ok(current_unix_epoch.as_secs())
60}
61
62fn checkout(branch_name: &str) -> Result<()> {
63    spawn_git_command(&["checkout", "-b", branch_name])
64}
65
66fn add() -> Result<()> {
67    // Add all files even when the current directory is not the root directory of the git repository
68    spawn_git_command(&["add", "--all"])
69}
70
71fn commit(message: &str) -> Result<()> {
72    // Bypass GPG-sign and pre-commit hook
73    spawn_git_command(&["commit", "--no-gpg-sign", "--no-verify", "-m", message])
74}
75
76fn push(branch_name: &str, remote: &str) -> Result<()> {
77    spawn_git_command(&["push", "-u", remote, branch_name])
78}
79
80fn spawn_git_command(args: &[&str]) -> Result<()> {
81    wait_git_lock_released()?;
82    let mut child = Command::new("git").args(args).spawn()?;
83    child.wait()?;
84    Ok(())
85}
86
87fn output_git_command(args: &[&str]) -> Result<String> {
88    let output = Command::new("git").args(args).output()?;
89    read_git_info(output)
90}
91
92fn wait_git_lock_released() -> Result<()> {
93    let git_root_directory = output_git_command(&["rev-parse", "--show-toplevel"])?;
94    let lock_path = Path::new(&git_root_directory).join(".git/index.lock");
95
96    if lock_path.exists() {
97        file_watcher::wait_until_deleted(&lock_path);
98    }
99    Ok(())
100}
101
102fn read_git_info(output: Output) -> Result<String> {
103    let info = String::from_utf8(output.stdout)?;
104    Ok(info.trim_end().to_string())
105}