git: Use branch names for resolve conflict buttons (#44421)

This makes merge conflict resolution clearer because we're now parsing
the branch names from the conflict region instead of hardcoding HEAD and
ORIGIN.

### Before
<img width="1157" height="1308" alt="image"
src="https://github.com/user-attachments/assets/1fd72823-4650-48dd-b26a-77c66d21614d"
/>

### After
<img width="1440" height="1249" alt="Screenshot 2025-12-08 at 2 17
12 PM"
src="https://github.com/user-attachments/assets/d23c219a-6128-4e2d-a8bc-3f128aa55272"
/>

Release Notes:

- git: Use branch names for git conflict buttons instead of HEAD and
ORIGIN
This commit is contained in:
Anthony Eid
2025-12-08 14:49:06 -05:00
committed by GitHub
parent f4892559f0
commit 464d4f72eb
2 changed files with 71 additions and 5 deletions
+2 -2
View File
@@ -372,7 +372,7 @@ fn render_conflict_buttons(
.gap_1()
.bg(cx.theme().colors().editor_background)
.child(
Button::new("head", "Use HEAD")
Button::new("head", format!("Use {}", conflict.ours_branch_name))
.label_size(LabelSize::Small)
.on_click({
let editor = editor.clone();
@@ -392,7 +392,7 @@ fn render_conflict_buttons(
}),
)
.child(
Button::new("origin", "Use Origin")
Button::new("origin", format!("Use {}", conflict.theirs_branch_name))
.label_size(LabelSize::Small)
.on_click({
let editor = editor.clone();
+69 -3
View File
@@ -1,4 +1,4 @@
use gpui::{App, Context, Entity, EventEmitter};
use gpui::{App, Context, Entity, EventEmitter, SharedString};
use std::{cmp::Ordering, ops::Range, sync::Arc};
use text::{Anchor, BufferId, OffsetRangeExt as _};
@@ -92,6 +92,8 @@ impl ConflictSetSnapshot {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConflictRegion {
pub ours_branch_name: SharedString,
pub theirs_branch_name: SharedString,
pub range: Range<Anchor>,
pub ours: Range<Anchor>,
pub theirs: Range<Anchor>,
@@ -179,18 +181,25 @@ impl ConflictSet {
let mut conflict_start: Option<usize> = None;
let mut ours_start: Option<usize> = None;
let mut ours_end: Option<usize> = None;
let mut ours_branch_name: Option<SharedString> = None;
let mut base_start: Option<usize> = None;
let mut base_end: Option<usize> = None;
let mut theirs_start: Option<usize> = None;
let mut theirs_branch_name: Option<SharedString> = None;
while let Some(line) = lines.next() {
let line_end = line_pos + line.len();
if line.starts_with("<<<<<<< ") {
if let Some(branch_name) = line.strip_prefix("<<<<<<< ") {
// If we see a new conflict marker while already parsing one,
// abandon the previous one and start a new one
conflict_start = Some(line_pos);
ours_start = Some(line_end + 1);
let branch_name = branch_name.trim();
if !branch_name.is_empty() {
ours_branch_name = Some(SharedString::new(branch_name));
}
} else if line.starts_with("||||||| ")
&& conflict_start.is_some()
&& ours_start.is_some()
@@ -208,12 +217,17 @@ impl ConflictSet {
base_end = Some(line_pos);
}
theirs_start = Some(line_end + 1);
} else if line.starts_with(">>>>>>> ")
} else if let Some(branch_name) = line.strip_prefix(">>>>>>> ")
&& conflict_start.is_some()
&& ours_start.is_some()
&& ours_end.is_some()
&& theirs_start.is_some()
{
let branch_name = branch_name.trim();
if !branch_name.is_empty() {
theirs_branch_name = Some(SharedString::new(branch_name));
}
let theirs_end = line_pos;
let conflict_end = (line_end + 1).min(buffer_len);
@@ -229,6 +243,12 @@ impl ConflictSet {
.map(|(start, end)| buffer.anchor_after(start)..buffer.anchor_before(end));
conflicts.push(ConflictRegion {
ours_branch_name: ours_branch_name
.take()
.unwrap_or_else(|| SharedString::new_static("HEAD")),
theirs_branch_name: theirs_branch_name
.take()
.unwrap_or_else(|| SharedString::new_static("Origin")),
range,
ours,
theirs,
@@ -304,6 +324,8 @@ mod tests {
let first = &conflict_snapshot.conflicts[0];
assert!(first.base.is_none());
assert_eq!(first.ours_branch_name.as_ref(), "HEAD");
assert_eq!(first.theirs_branch_name.as_ref(), "branch-name");
let our_text = snapshot
.text_for_range(first.ours.clone())
.collect::<String>();
@@ -315,6 +337,8 @@ mod tests {
let second = &conflict_snapshot.conflicts[1];
assert!(second.base.is_some());
assert_eq!(second.ours_branch_name.as_ref(), "HEAD");
assert_eq!(second.theirs_branch_name.as_ref(), "branch-name");
let our_text = snapshot
.text_for_range(second.ours.clone())
.collect::<String>();
@@ -381,6 +405,8 @@ mod tests {
// The conflict should have our version, their version, but no base
let conflict = &conflict_snapshot.conflicts[0];
assert!(conflict.base.is_none());
assert_eq!(conflict.ours_branch_name.as_ref(), "HEAD");
assert_eq!(conflict.theirs_branch_name.as_ref(), "branch-nested");
// Check that the nested conflict was detected correctly
let our_text = snapshot
@@ -407,6 +433,14 @@ mod tests {
let conflict_snapshot = ConflictSet::parse(&snapshot);
assert_eq!(conflict_snapshot.conflicts.len(), 1);
assert_eq!(
conflict_snapshot.conflicts[0].ours_branch_name.as_ref(),
"ours"
);
assert_eq!(
conflict_snapshot.conflicts[0].theirs_branch_name.as_ref(),
"Origin" // default branch name if there is none
);
}
#[test]
@@ -449,6 +483,38 @@ mod tests {
let conflict_snapshot = ConflictSet::parse(&snapshot);
assert_eq!(conflict_snapshot.conflicts.len(), 4);
assert_eq!(
conflict_snapshot.conflicts[0].ours_branch_name.as_ref(),
"HEAD1"
);
assert_eq!(
conflict_snapshot.conflicts[0].theirs_branch_name.as_ref(),
"branch1"
);
assert_eq!(
conflict_snapshot.conflicts[1].ours_branch_name.as_ref(),
"HEAD2"
);
assert_eq!(
conflict_snapshot.conflicts[1].theirs_branch_name.as_ref(),
"branch2"
);
assert_eq!(
conflict_snapshot.conflicts[2].ours_branch_name.as_ref(),
"HEAD3"
);
assert_eq!(
conflict_snapshot.conflicts[2].theirs_branch_name.as_ref(),
"branch3"
);
assert_eq!(
conflict_snapshot.conflicts[3].ours_branch_name.as_ref(),
"HEAD4"
);
assert_eq!(
conflict_snapshot.conflicts[3].theirs_branch_name.as_ref(),
"branch4"
);
let range = test_content.find("seven").unwrap()..test_content.find("eleven").unwrap();
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);