git_ui: Count commit title length by characters instead of bytes (#57025)

# 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.
This commit is contained in:
Koya Masuda authored and GitHub committed 2026-05-18 13:13:41 +00:00
1 parent 0c10bbbc81
commit dfd48fa971
2 files changed
+46 -3

No files matched your search

+4 -2
View File
@@ -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
};
+42 -1
View File
@@ -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);