Skip to content

Commit 8b6b03c

Browse files
authored
Merge pull request #13247 from gitbutlerapp/is-conflicted-refactor
Move conflicted commit detection from headers to commit message markers
2 parents e223291 + 79ac145 commit 8b6b03c

23 files changed

Lines changed: 727 additions & 175 deletions

File tree

crates/but-core/src/commit/conflict.rs

Lines changed: 537 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,8 +555,11 @@ impl<'repo> Commit<'repo> {
555555
}
556556

557557
/// Return `true` if this commit contains a tree that is conflicted.
558+
///
559+
/// Checks the commit message for conflict markers first (new style),
560+
/// then falls back to the `gitbutler-conflicted` header (legacy).
558561
pub fn is_conflicted(&self) -> bool {
559-
self.headers().is_some_and(|hdr| hdr.is_conflicted())
562+
is_conflicted(self.inner.message.as_ref(), self.headers().as_ref())
560563
}
561564

562565
/// If the commit is conflicted, then it returns the auto-resolution tree,
@@ -686,3 +689,9 @@ impl ConflictEntries {
686689
set.len()
687690
}
688691
}
692+
693+
mod conflict;
694+
pub use conflict::{
695+
add_conflict_markers, is_conflicted, message_is_conflicted,
696+
rewrite_conflict_markers_on_message_change, strip_conflict_markers,
697+
};

crates/but-core/tests/fixtures/scenario/multiple-remotes-with-tracking-branches.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ git remote add origin normal-remote
2929
git remote add nested/remote nested-remote
3030
git remote add nested/remote-b nested-remote-b
3131

32+
# NOTE: `git fetch` automatically creates remote HEAD refs (e.g.
33+
# refs/remotes/origin/HEAD) since git 2.48. Tests relying on these
34+
# refs require at least that version.
3235
git fetch origin
3336
git fetch nested/remote
3437
git fetch nested/remote-b

crates/but-rebase/src/cherry_pick.rs

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub enum EmptyCommit {
2323
}
2424

2525
pub(crate) mod function {
26-
use std::{collections::HashSet, path::PathBuf};
26+
use std::path::PathBuf;
2727

2828
use anyhow::{Context as _, bail};
2929
use bstr::BString;
@@ -168,18 +168,16 @@ pub(crate) mod function {
168168
}
169169

170170
let headers = to_rebase.headers();
171-
let to_rebase_is_conflicted = headers.as_ref().is_some_and(|hdr| hdr.is_conflicted());
172171
let mut new_commit = to_rebase.inner;
173172
new_commit.tree = resolved_tree_id.detach();
174173

175174
// Assure the commit isn't thinking it's conflicted.
176-
if to_rebase_is_conflicted {
177-
if let Some(pos) = new_commit
178-
.extra_headers()
179-
.find_pos(HEADERS_CONFLICTED_FIELD)
180-
{
181-
new_commit.extra_headers.remove(pos);
182-
}
175+
new_commit.message = but_core::commit::strip_conflict_markers(new_commit.message.as_ref());
176+
if let Some(pos) = new_commit
177+
.extra_headers()
178+
.find_pos(HEADERS_CONFLICTED_FIELD)
179+
{
180+
new_commit.extra_headers.remove(pos);
183181
} else if headers.is_none() {
184182
let headers = Headers::from_config(&repo.config_snapshot());
185183
new_commit
@@ -204,10 +202,6 @@ pub(crate) mod function {
204202
treat_as_unresolved: gix::merge::tree::TreatAsUnresolved,
205203
) -> anyhow::Result<gix::Id<'repo>> {
206204
let repo = resolved_tree_id.repo;
207-
// in case someone checks this out with vanilla Git, we should warn why it looks like this
208-
let readme_content =
209-
b"You have checked out a GitButler Conflicted commit. You probably didn't mean to do this.";
210-
let readme_blob = repo.write_blob(readme_content)?;
211205

212206
let conflicted_files =
213207
extract_conflicted_files(resolved_tree_id, cherry_pick, treat_as_unresolved)?;
@@ -242,15 +236,18 @@ pub(crate) mod function {
242236
resolved_tree_id,
243237
)?;
244238
tree.upsert(".conflict-files", EntryKind::Blob, conflicted_files_blob)?;
245-
tree.upsert("CONFLICT-README.txt", EntryKind::Blob, readme_blob)?;
246239

247240
let mut headers = to_rebase
248241
.headers()
249242
.unwrap_or_else(|| Headers::from_config(&repo.config_snapshot()));
250-
headers.conflicted = conflicted_files.conflicted_header_field();
243+
headers.conflicted = None;
251244
to_rebase.tree = tree.write().context("failed to write tree")?.detach();
252245
set_parent(&mut to_rebase, head.id.detach())?;
253246

247+
// Add conflict markers to the commit message
248+
to_rebase.inner.message =
249+
but_core::commit::add_conflict_markers(to_rebase.inner.message.as_ref());
250+
254251
to_rebase.set_headers(&headers);
255252
Ok(crate::commit::create(
256253
repo,
@@ -341,22 +338,5 @@ pub(crate) mod function {
341338
|| !self.our_entries.is_empty()
342339
|| !self.their_entries.is_empty()
343340
}
344-
345-
fn total_entries(&self) -> usize {
346-
let set = self
347-
.ancestor_entries
348-
.iter()
349-
.chain(self.our_entries.iter())
350-
.chain(self.their_entries.iter())
351-
.collect::<HashSet<_>>();
352-
353-
set.len()
354-
}
355-
356-
/// Return the `conflicted` header field value.
357-
pub(crate) fn conflicted_header_field(&self) -> Option<u64> {
358-
let entries = self.total_entries();
359-
Some(if entries > 0 { entries as u64 } else { 1 })
360-
}
361341
}
362342
}

crates/but-rebase/src/graph_rebase/cherry_pick.rs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -305,18 +305,16 @@ fn commit_from_unconflicted_tree<'repo>(
305305
let repo = to_rebase.id.repo;
306306

307307
let headers = to_rebase.headers();
308-
let to_rebase_is_conflicted = headers.as_ref().is_some_and(|hdr| hdr.is_conflicted());
309308
let mut new_commit = to_rebase.inner;
310309
new_commit.tree = resolved_tree_id.detach();
311310

312311
// Ensure the commit isn't thinking it's conflicted.
313-
if to_rebase_is_conflicted {
314-
if let Some(pos) = new_commit
315-
.extra_headers()
316-
.find_pos(HEADERS_CONFLICTED_FIELD)
317-
{
318-
new_commit.extra_headers.remove(pos);
319-
}
312+
new_commit.message = but_core::commit::strip_conflict_markers(new_commit.message.as_ref());
313+
if let Some(pos) = new_commit
314+
.extra_headers()
315+
.find_pos(HEADERS_CONFLICTED_FIELD)
316+
{
317+
new_commit.extra_headers.remove(pos);
320318
} else if headers.is_none() {
321319
let headers = Headers::from_config(&repo.config_snapshot());
322320
new_commit
@@ -347,10 +345,6 @@ fn commit_from_conflicted_tree<'repo>(
347345
sign_commit: SignCommit,
348346
) -> anyhow::Result<gix::Id<'repo>> {
349347
let repo = resolved_tree_id.repo;
350-
// in case someone checks this out with vanilla Git, we should warn why it looks like this
351-
let readme_content =
352-
b"You have checked out a GitButler Conflicted commit. You probably didn't mean to do this.";
353-
let readme_blob = repo.write_blob(readme_content)?;
354348

355349
let conflicted_files =
356350
extract_conflicted_files(resolved_tree_id, cherry_pick, treat_as_unresolved)?;
@@ -382,15 +376,18 @@ fn commit_from_conflicted_tree<'repo>(
382376
resolved_tree_id,
383377
)?;
384378
tree.upsert(".conflict-files", EntryKind::Blob, conflicted_files_blob)?;
385-
tree.upsert("CONFLICT-README.txt", EntryKind::Blob, readme_blob)?;
386379

387380
let mut headers = to_rebase
388381
.headers()
389382
.unwrap_or_else(|| Headers::from_config(&repo.config_snapshot()));
390-
headers.conflicted = conflicted_files.conflicted_header_field();
383+
headers.conflicted = None;
391384
to_rebase.tree = tree.write().context("failed to write tree")?.detach();
392385
to_rebase.parents = parents.into();
393386

387+
// Add conflict markers to the commit message
388+
to_rebase.inner.message =
389+
but_core::commit::add_conflict_markers(to_rebase.inner.message.as_ref());
390+
394391
to_rebase.set_headers(&headers);
395392
Ok(crate::commit::create(
396393
repo,

crates/but-rebase/src/lib.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,11 @@ fn rebase(
286286
None if commit.parents.is_empty() => {
287287
let mut new_commit = commit;
288288
if let Some(new_message) = new_message {
289-
new_commit.message = new_message;
289+
new_commit.message =
290+
but_core::commit::rewrite_conflict_markers_on_message_change(
291+
new_commit.message.as_ref(),
292+
new_message,
293+
);
290294
}
291295
cursor = Some(commit::create(
292296
repo,
@@ -325,7 +329,11 @@ fn rebase(
325329
let mut new_commit = repo.find_commit(new_commit)?.decode()?.to_owned()?;
326330
new_commit.parents = base_commit.parent_ids().map(|id| id.detach()).collect();
327331
if let Some(new_message) = new_message {
328-
new_commit.message = new_message;
332+
new_commit.message =
333+
but_core::commit::rewrite_conflict_markers_on_message_change(
334+
new_commit.message.as_ref(),
335+
new_message,
336+
);
329337
}
330338
*cursor = commit::create(
331339
repo,
@@ -371,7 +379,10 @@ fn reword_commit(
371379
new_message: BString,
372380
) -> Result<gix::ObjectId> {
373381
let mut new_commit = repo.find_commit(oid)?.decode()?.to_owned()?;
374-
new_commit.message = new_message;
382+
new_commit.message = but_core::commit::rewrite_conflict_markers_on_message_change(
383+
new_commit.message.as_ref(),
384+
new_message,
385+
);
375386
Ok(commit::create(
376387
repo,
377388
new_commit,

crates/but-rebase/tests/rebase/graph_rebase/cherry_pick.rs

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ fn basic_cherry_pick_cp_conflicts() -> Result<()> {
6969

7070
insta::assert_debug_snapshot!(result, @"
7171
ConflictedCommit(
72-
Sha1(e9ee7b59aff786fc970c30f6965d1de1913c7ec4),
72+
Sha1(9661555c611ff64e8c597f7f5a0575f211eb2e12),
7373
)
7474
");
7575

@@ -80,7 +80,7 @@ fn basic_cherry_pick_cp_conflicts() -> Result<()> {
8080
assert_eq!(&get_parents(&id.attach(&repo))?, &[onto]);
8181

8282
insta::assert_snapshot!(visualize_tree(id.attach(&repo)), @r#"
83-
0367fb7
83+
1090f8a
8484
├── .auto-resolution:aa3d213
8585
│ ├── base-f:100644:7898192 "a\n"
8686
│ └── target-f:100644:eb5a316 "target\n"
@@ -95,7 +95,6 @@ fn basic_cherry_pick_cp_conflicts() -> Result<()> {
9595
│ ├── base-f:100644:7898192 "a\n"
9696
│ ├── clean-f:100644:8312630 "clean\n"
9797
│ └── target-f:100644:9b1719f "conflict\n"
98-
├── CONFLICT-README.txt:100644:2af04b7 "You have checked out a GitButler Conflicted commit. You probably didn\'t mean to do this."
9998
├── base-f:100644:7898192 "a\n"
10099
└── target-f:100644:eb5a316 "target\n"
101100
"#);
@@ -184,7 +183,7 @@ fn single_parent_to_multiple_parents_cp_conflicts() -> Result<()> {
184183

185184
insta::assert_debug_snapshot!(result, @"
186185
ConflictedCommit(
187-
Sha1(0fcbe01202743fa55f1a1e07342ad26f2e7a0abe),
186+
Sha1(c8779c405b92941176effe9eb7a72c0dd410c3fd),
188187
)
189188
");
190189

@@ -195,7 +194,7 @@ fn single_parent_to_multiple_parents_cp_conflicts() -> Result<()> {
195194
assert_eq!(&get_parents(&id.attach(&repo))?, &[onto, onto2]);
196195

197196
insta::assert_snapshot!(visualize_tree(id.attach(&repo)), @r#"
198-
1804f3d
197+
4c6dc70
199198
├── .auto-resolution:744efa9
200199
│ ├── base-f:100644:7898192 "a\n"
201200
│ ├── target-2-f:100644:caac8f9 "target 2\n"
@@ -212,7 +211,6 @@ fn single_parent_to_multiple_parents_cp_conflicts() -> Result<()> {
212211
│ ├── base-f:100644:7898192 "a\n"
213212
│ ├── clean-f:100644:8312630 "clean\n"
214213
│ └── target-f:100644:9b1719f "conflict\n"
215-
├── CONFLICT-README.txt:100644:2af04b7 "You have checked out a GitButler Conflicted commit. You probably didn\'t mean to do this."
216214
├── base-f:100644:7898192 "a\n"
217215
├── target-2-f:100644:caac8f9 "target 2\n"
218216
└── target-f:100644:eb5a316 "target\n"
@@ -311,7 +309,7 @@ fn multiple_parents_to_single_parent_cp_conflicts() -> Result<()> {
311309

312310
insta::assert_debug_snapshot!(result, @"
313311
ConflictedCommit(
314-
Sha1(28fa7c91af8652f4e69c1e3184f92569a3468a34),
312+
Sha1(2d4dcd916020924f2412642b842a791dfea74571),
315313
)
316314
");
317315

@@ -322,7 +320,7 @@ fn multiple_parents_to_single_parent_cp_conflicts() -> Result<()> {
322320
assert_eq!(&get_parents(&id.attach(&repo))?, &[onto]);
323321

324322
insta::assert_snapshot!(visualize_tree(id.attach(&repo)), @r#"
325-
91fe014
323+
8c4acd1
326324
├── .auto-resolution:aa3d213
327325
│ ├── base-f:100644:7898192 "a\n"
328326
│ └── target-f:100644:eb5a316 "target\n"
@@ -339,7 +337,6 @@ fn multiple_parents_to_single_parent_cp_conflicts() -> Result<()> {
339337
│ ├── clean-2-f:100644:13e9394 "clean 2\n"
340338
│ ├── clean-f:100644:8312630 "clean\n"
341339
│ └── target-f:100644:9b1719f "conflict\n"
342-
├── CONFLICT-README.txt:100644:2af04b7 "You have checked out a GitButler Conflicted commit. You probably didn\'t mean to do this."
343340
├── base-f:100644:7898192 "a\n"
344341
└── target-f:100644:eb5a316 "target\n"
345342
"#);
@@ -441,7 +438,7 @@ fn multiple_parents_to_multiple_parents_cp_conflicts() -> Result<()> {
441438

442439
insta::assert_debug_snapshot!(result, @"
443440
ConflictedCommit(
444-
Sha1(2e6cb06fe98780bb8c7a301a522edd98805d1499),
441+
Sha1(bf66deb17cc4e84faa063a3253566004aabaf7fe),
445442
)
446443
");
447444

@@ -452,7 +449,7 @@ fn multiple_parents_to_multiple_parents_cp_conflicts() -> Result<()> {
452449
assert_eq!(&get_parents(&id.attach(&repo))?, &[onto, onto2]);
453450

454451
insta::assert_snapshot!(visualize_tree(id.attach(&repo)), @r#"
455-
0aeaf79
452+
1620d95
456453
├── .auto-resolution:744efa9
457454
│ ├── base-f:100644:7898192 "a\n"
458455
│ ├── target-2-f:100644:caac8f9 "target 2\n"
@@ -471,7 +468,6 @@ fn multiple_parents_to_multiple_parents_cp_conflicts() -> Result<()> {
471468
│ ├── clean-2-f:100644:13e9394 "clean 2\n"
472469
│ ├── clean-f:100644:8312630 "clean\n"
473470
│ └── target-f:100644:9b1719f "conflict\n"
474-
├── CONFLICT-README.txt:100644:2af04b7 "You have checked out a GitButler Conflicted commit. You probably didn\'t mean to do this."
475471
├── base-f:100644:7898192 "a\n"
476472
├── target-2-f:100644:caac8f9 "target 2\n"
477473
└── target-f:100644:eb5a316 "target\n"
@@ -688,7 +684,7 @@ fn no_parents_to_single_parent_cp_conflicts() -> Result<()> {
688684

689685
insta::assert_debug_snapshot!(result, @"
690686
ConflictedCommit(
691-
Sha1(28f862257bff139659b763a2c873b8d3f0f780b0),
687+
Sha1(2ee47b5cbe2705d5d81f4b4067ef4753d87b3b02),
692688
)
693689
");
694690

@@ -699,7 +695,7 @@ fn no_parents_to_single_parent_cp_conflicts() -> Result<()> {
699695
assert_eq!(&get_parents(&id.attach(&repo))?, &[onto]);
700696

701697
insta::assert_snapshot!(visualize_tree(id.attach(&repo)), @r#"
702-
1267a55
698+
38edd44
703699
├── .auto-resolution:aa3d213
704700
│ ├── base-f:100644:7898192 "a\n"
705701
│ └── target-f:100644:eb5a316 "target\n"
@@ -711,7 +707,6 @@ fn no_parents_to_single_parent_cp_conflicts() -> Result<()> {
711707
├── .conflict-side-1:144e5f5
712708
│ ├── base-f:100644:7898192 "a\n"
713709
│ └── target-f:100644:9b1719f "conflict\n"
714-
├── CONFLICT-README.txt:100644:2af04b7 "You have checked out a GitButler Conflicted commit. You probably didn\'t mean to do this."
715710
├── base-f:100644:7898192 "a\n"
716711
└── target-f:100644:eb5a316 "target\n"
717712
"#);
@@ -738,7 +733,7 @@ fn cherry_pick_back_to_original_parents_unconflicts() -> Result<()> {
738733

739734
insta::assert_debug_snapshot!(result, @"
740735
ConflictedCommit(
741-
Sha1(2e6cb06fe98780bb8c7a301a522edd98805d1499),
736+
Sha1(bf66deb17cc4e84faa063a3253566004aabaf7fe),
742737
)
743738
");
744739

@@ -758,7 +753,7 @@ fn cherry_pick_back_to_original_parents_unconflicts() -> Result<()> {
758753

759754
insta::assert_debug_snapshot!(result, @"
760755
Commit(
761-
Sha1(3d7dfa09a071658d3b84eb1ee195ea0ebfeb601f),
756+
Sha1(2a307db18bf263e3a802ac71282c5d8016ea75a1),
762757
)
763758
");
764759

0 commit comments

Comments
 (0)