-
Notifications
You must be signed in to change notification settings - Fork 4
feat: recover message of last failed commit #424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,14 @@ | ||
use std::process::exit; | ||
|
||
use console::Term; | ||
use serde::{Deserialize, Serialize}; | ||
use tracing::{info, warn}; | ||
use url::Url; | ||
|
||
use crate::git::has_staged_changes; | ||
use crate::recover::{ | ||
ask_for_recovery, clear_recovery_file, read_recovery_file, write_recovery_file, | ||
}; | ||
Comment on lines
+9
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i might rename the file to recovery.rs and call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea |
||
use crate::{git, EmojiFormat, Error, GitmojiConfig, Result, EXIT_CANNOT_UPDATE, EXIT_NO_CONFIG}; | ||
|
||
mod commit; | ||
|
@@ -50,10 +54,10 @@ async fn update_config_or_stop(config: GitmojiConfig) -> GitmojiConfig { | |
} | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
struct CommitTitleDescription { | ||
title: String, | ||
description: Option<String>, | ||
#[derive(Debug, Clone, Serialize, Deserialize)] | ||
pub struct CommitTitleDescription { | ||
pub title: String, | ||
pub description: Option<String>, | ||
} | ||
|
||
#[tracing::instrument(skip(term))] | ||
|
@@ -92,15 +96,47 @@ pub async fn commit(all: bool, amend: bool, term: &Term) -> Result<()> { | |
return Ok(()); | ||
} | ||
|
||
let CommitTitleDescription { title, description } = | ||
ask_commit_title_description(&config, term).await?; | ||
let commit = match read_recovery_file() { | ||
Ok(Some(commit)) => match ask_for_recovery(term, commit.clone()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💭 thought: Maybe we could improve the UX if we use the recovery value as default value for title & description. It's nearly as fast for the user, and allow update value if necessary There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about it, but since the function that asks for gitmoji inputs is called through another one that only returns title+description, we can't really get out just the emoji Do we make different prompts (title + description) when recovering? I do like the idea of limiting the number of key presses needed to just "retry" a commit, clicking enter like 4 times would be a bit much imo |
||
Ok(true) => commit, | ||
_ => ask_commit_title_description(&config, term).await?, | ||
}, | ||
Ok(None) => ask_commit_title_description(&config, term).await?, | ||
Err(err) => { | ||
warn!("Cannot read recovery file: {err}"); | ||
ask_commit_title_description(&config, term).await? | ||
} | ||
}; | ||
|
||
// Add before commit | ||
let all = all || config.auto_add(); | ||
|
||
// Commit | ||
let status = git::commit(all, amend, config.signed(), &title, description.as_deref()).await?; | ||
status.success().then_some(()).ok_or(Error::FailToCommit) | ||
let status = git::commit( | ||
all, | ||
amend, | ||
config.signed(), | ||
&commit.title, | ||
commit.description.as_deref(), | ||
) | ||
.await?; | ||
|
||
match status.success().then_some(()).ok_or(Error::FailToCommit) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔨 todo: should be simplify, why not having a simple if-then-else here ? |
||
Ok(()) => { | ||
if let Err(recovery_err) = clear_recovery_file() { | ||
warn!("{recovery_err}"); | ||
} | ||
|
||
Ok(()) | ||
} | ||
Err(err) => { | ||
if let Err(recovery_err) = write_recovery_file(commit) { | ||
warn!("{recovery_err}"); | ||
} | ||
|
||
Err(err) | ||
} | ||
} | ||
} | ||
|
||
/// Configure Gitmoji | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ mod cmd; | |
mod error; | ||
mod git; | ||
mod model; | ||
mod recover; | ||
|
||
use std::io::stdout; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
use console::Term; | ||
use dialoguer::{theme::ColorfulTheme, Confirm}; | ||
|
||
use crate::cmd::CommitTitleDescription; | ||
use std::{ | ||
fs, | ||
io::{Error, ErrorKind::NotFound}, | ||
}; | ||
|
||
#[derive(Debug, derive_more::Error, derive_more::Display)] | ||
#[display("Could not {action} recovery file: {source}")] | ||
pub struct RecoveryError { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 suggestion: turn it into an enum with a single variant. It's better if later we want to add more error like splitting read and write error. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the |
||
// The source error | ||
#[error(source)] | ||
source: Error, | ||
|
||
action: String, | ||
} | ||
|
||
type Result<T> = std::result::Result<T, RecoveryError>; | ||
|
||
pub fn ask_for_recovery( | ||
term: &Term, | ||
recovered: CommitTitleDescription, | ||
) -> std::result::Result<bool, dialoguer::Error> { | ||
Ok(Confirm::with_theme(&ColorfulTheme::default()) | ||
.with_prompt(format!( | ||
"Last commit failed. Reuse recovered commit message “{}” ?", | ||
recovered.title | ||
)) | ||
.default(true) | ||
.interact_on(term)?) | ||
} | ||
|
||
pub fn write_recovery_file(params: CommitTitleDescription) -> Result<()> { | ||
let path = directories::BaseDirs::new() | ||
.ok_or(RecoveryError { | ||
source: Error::new(NotFound, "Base directories not found"), | ||
action: "write".to_string(), | ||
})? | ||
.cache_dir() | ||
.join("gitmoji-rs") | ||
.join("lastmessage.json"); | ||
|
||
if !path.exists() { | ||
let dir = path.parent().ok_or(RecoveryError { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔨 todo: create a private function that return the file to avoid duplication |
||
source: Error::new(NotFound, "path to file is has no parent"), | ||
action: "create directory for".to_string(), | ||
})?; | ||
|
||
fs::create_dir_all(dir).expect("Failed to create recovery directory"); | ||
} | ||
|
||
let content = serde_json::to_string(¶ms).map_err(|source| RecoveryError { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔨 todo: write it in TOML (see config module that already read/write data in toml) |
||
source: source.into(), | ||
action: "serialize".to_string(), | ||
})?; | ||
|
||
fs::write(path, content).map_err(|source| RecoveryError { | ||
source, | ||
action: "write".to_string(), | ||
})?; | ||
|
||
Ok(()) | ||
} | ||
|
||
pub fn read_recovery_file() -> Result<Option<CommitTitleDescription>> { | ||
let path = directories::BaseDirs::new() | ||
.ok_or(RecoveryError { | ||
source: Error::new(NotFound, "Base directories not found"), | ||
action: "read".to_string(), | ||
})? | ||
.cache_dir() | ||
.join("gitmoji-rs") | ||
.join("lastmessage.json"); | ||
|
||
if !path.exists() { | ||
return Ok(None); | ||
} | ||
|
||
let content = fs::read_to_string(&path).map_err(|source| RecoveryError { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔨 todo: write it in TOML (see config module that already read/write data in toml) |
||
source, | ||
action: "read".to_string(), | ||
})?; | ||
|
||
let params: CommitTitleDescription = | ||
serde_json::from_str(&content).map_err(|source| RecoveryError { | ||
source: source.into(), | ||
action: "deserialize".to_string(), | ||
})?; | ||
|
||
Ok(Some(params)) | ||
} | ||
|
||
pub fn clear_recovery_file() -> Result<()> { | ||
let path = directories::BaseDirs::new() | ||
.ok_or(RecoveryError { | ||
source: Error::new(NotFound, "Base directories not found"), | ||
action: "clear".to_string(), | ||
})? | ||
.cache_dir() | ||
.join("gitmoji-rs") | ||
.join("lastmessage.json"); | ||
|
||
if path.exists() { | ||
fs::remove_file(path).map_err(|source| RecoveryError { | ||
source, | ||
action: "remove".to_string(), | ||
})?; | ||
} | ||
|
||
Ok(()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 thought: we could use TOML to store the data instead, the dependency is already present