+
Skip to content

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ indicatif = "0.17"
jiff = { version = "0.2.15", features = ["serde"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls-native-roots"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.140"
Copy link
Owner

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

tokio = { version = "1.45", features = [
"process",
"fs",
Expand Down
52 changes: 44 additions & 8 deletions src/cmd/mod.rs
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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i might rename the file to recovery.rs and call recovery::ask etc instead of doing this

Copy link
Owner

Choose a reason for hiding this comment

The 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;
Expand Down Expand Up @@ -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))]
Expand Down Expand Up @@ -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()) {
Copy link
Owner

Choose a reason for hiding this comment

The 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

Copy link
Author

@gwennlbh gwennlbh Jun 10, 2025

Choose a reason for hiding this comment

The 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) {
Copy link
Owner

Choose a reason for hiding this comment

The 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
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod cmd;
mod error;
mod git;
mod model;
mod recover;

use std::io::stdout;

Expand Down
113 changes: 113 additions & 0 deletions src/recover.rs
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 {
Copy link
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the action field has just a few constant string values, i think it could be turned into a enum with just source inside, what do you think? Also, will the annotations work with enums? I'm not familiar with that way of organizing errors (i... Usually just slap anyhow lol)

// 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 {
Copy link
Owner

Choose a reason for hiding this comment

The 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(&params).map_err(|source| RecoveryError {
Copy link
Owner

@ilaborie ilaborie Jun 9, 2025

Choose a reason for hiding this comment

The 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 {
Copy link
Owner

@ilaborie ilaborie Jun 9, 2025

Choose a reason for hiding this comment

The 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(())
}
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载