From dfd48fa9710871eac03cec6180e36596d9dcfd5f Mon Sep 17 00:00:00 2001 From: Koya Masuda <71430401+koxya@users.noreply.github.com> Date: Mon, 18 May 2026 22:13:41 +0900 Subject: [PATCH] git_ui: Count commit title length by characters instead of bytes (#57025) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What - Replace `title.len()` with `title.chars().count()` in the commit title length check, so the limit is measured in Unicode characters instead of UTF-8 bytes. - Applied in two places that share the same logic: - `crates/git_ui/src/git_panel.rs` — Git panel's inline warning - `crates/git_ui/src/commit_modal.rs` — commit modal's inline warning # Why The commit title length check used str::len(), which returns UTF-8 byte length rather than character count. As a result, titles containing multi-byte characters (Japanese, Chinese, emoji, etc.) triggered the warning far below the configured commit_title_max_length — around 24 characters instead of the default 72. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - Before https://github.com/user-attachments/assets/a895530c-2f73-470c-97fa-29d9467c14e1 - After https://github.com/user-attachments/assets/ffbe1ba2-0ccc-4b02-87f5-836da7841dd9 - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #ISSUE Release Notes: - Fixed commit title length check miscounting multi-byte characters as multiple characters. --- crates/git_ui/src/commit_modal.rs | 6 +++-- crates/git_ui/src/git_panel.rs | 43 ++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 3ec5453f9b..5da2f670f3 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -1,5 +1,7 @@ use crate::branch_picker::{self, BranchList}; -use crate::git_panel::{GitPanel, commit_message_editor, panel_editor_style}; +use crate::git_panel::{ + GitPanel, commit_message_editor, commit_title_exceeds_limit, panel_editor_style, +}; use crate::git_panel_settings::GitPanelSettings; use git::repository::CommitOptions; use git::{Amend, Commit, GenerateCommitMessage, Signoff}; @@ -548,7 +550,7 @@ impl Render for CommitModal { .text(cx) .lines() .next() - .is_some_and(|title| title.len() > max_title_length) + .is_some_and(|title| commit_title_exceeds_limit(title, max_title_length)) } else { false }; diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 9bf5d047c4..04246fcd64 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -4566,7 +4566,7 @@ impl GitPanel { .text(cx) .lines() .next() - .is_some_and(|title| title.len() > max_title_length) + .is_some_and(|title| commit_title_exceeds_limit(title, max_title_length)) } else { false }; @@ -7438,6 +7438,10 @@ fn format_git_error_toast_message(error: &anyhow::Error) -> String { } } +pub(crate) fn commit_title_exceeds_limit(title: &str, max_length: usize) -> bool { + max_length > 0 && title.chars().count() > max_length +} + #[cfg(test)] mod tests { use git::{ @@ -8916,6 +8920,43 @@ mod tests { } } + #[test] + fn test_commit_title_exceeds_limit() { + // ASCII only + let within_ascii = "abcde"; + let exceeds_ascii = "abcdef"; + assert!(!commit_title_exceeds_limit(within_ascii, 5)); + assert!(commit_title_exceeds_limit(exceeds_ascii, 5)); + + // Multi-byte characters are counted as grapheme clusters + let within_japanese = "あいうえお"; // 5 chars, 15 bytes + let exceeds_japanese = "あいうえおか"; // 6 chars, 18 bytes + assert!(!commit_title_exceeds_limit(within_japanese, 5)); + assert!(commit_title_exceeds_limit(exceeds_japanese, 5)); + + // Mixed ASCII + multi-byte + let within_mixed = "abcあ"; + let exceeds_mixed = "abcああ"; + assert!(!commit_title_exceeds_limit(within_mixed, 4)); + assert!(commit_title_exceeds_limit(exceeds_mixed, 4)); + + // Emoji counts as one character each + let within_emoji = "🚀"; + let exceeds_emoji = "🚀🚀"; + assert!(!commit_title_exceeds_limit(within_emoji, 1)); + assert!(commit_title_exceeds_limit(exceeds_emoji, 1)); + + // A max_length of 0 disables the limit check + assert!(!commit_title_exceeds_limit( + "anything goes when disabled", + 0 + )); + assert!(!commit_title_exceeds_limit("", 0)); + + // Empty title never exceeds a positive limit + assert!(!commit_title_exceeds_limit("", 72)); + } + #[gpui::test] async fn test_dispatch_context_with_focus_states(cx: &mut TestAppContext) { init_test(cx);