Skip to content
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
157 changes: 138 additions & 19 deletions compiler/rustc_parse/src/parser/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3052,6 +3052,126 @@ impl<'a> Parser<'a> {
None
}

/// Try to identify if a `git rebase` is in progress to provide more accurate labels.
fn vcs_conflict_marker_labels(&self, is_middlediff3: bool) -> ConflictMarkerLabels {
let mut msg_start: String = "being merged into".into();
let mut msg_middle: String = "incoming code".into();

// Ideally, we'd execute `git rev-parse --git-path rebase-merge`, in order to get the git
// metadata for the rebase currently happening, but instead we only customize the output if
// invoking cargo from the project root. If this directory doesn't exist, then the merge
// conflict might not have been caused by a git rebase, and we'll fall-back on the default
// diagnostic.
let git = |path: &str| {
std::fs::read_to_string(format!(".git/{path}"))
.map(|content| content.trim().to_string())
};
let mut local_branch = None;
let mut local_sha = None;
let mut onto_descr = None;
let mut merge_to = None;
let mut onto_sha = None;
let mut is_git_pull = false;
if let Ok(onto) = git("rebase-merge/onto") {
let onto = onto.trim();
onto_sha = Some(onto.to_string());
if let Ok(fetch) = git("FETCH_HEAD")
&& let Some(parts) = fetch
.lines()
.map(|line| line.split('\t').collect::<Vec<_>>())
.filter(|parts| parts[1] == "")
.next()
&& let [fetch_head, "", branch_descr] = &parts[..]
&& &onto == fetch_head
{
is_git_pull = true;
onto_descr = Some(branch_descr.trim().to_string());
}
}
if let Ok(head_name) = git("rebase-merge/head-name")
&& let head_name = head_name.trim()
&& let Some(branch_name) = head_name.strip_prefix("refs/heads/")
{
local_branch = Some(branch_name.to_string());
}
if let Ok(local) = git("rebase-merge/stopped-sha") {
local_sha = Some(local.trim().to_string());
}
if let Ok(head) = git("HEAD")
&& let Some(merging_to) = head.trim().strip_prefix("ref: refs/heads/")
&& let Ok(msg) = git("MERGE_MSG")
{
merge_to = Some(merging_to.to_string());
if let Some(line) = msg.lines().next()
&& let Some(msg) = line.strip_prefix("Merge branch ")
{
let split: Vec<_> = msg.split(" into ").collect();
onto_descr = Some(if let [msg, _branch] = &split[..] {
// `git merge`
// Merge branch 'main' into branch-name
format!("branch {msg}")
} else {
// `git pull --no-rebase`
// Merge branch `main` of github.com:user/repo
format!("remote branch {msg}")
});
}
}
match (local_branch, local_sha, onto_descr, onto_sha, merge_to) {
(Some(branch_name), _, Some(onto_descr), _, None) => {
// `git pull`, branch_descr (from `.git/FETCH_HEAD`) has "branch 'name' of <remote>"
msg_start = format!("from {}", onto_descr.trim());
msg_middle = if is_git_pull {
format!("code from local branch '{branch_name}' before `git pull`")
} else {
format!("code from branch '{branch_name}'")
};
}
(None, Some(from_sha), Some(onto_descr), _, None) => {
// `git pull`, branch_descr (from `.git/FETCH_HEAD`) has "branch 'name' of <remote>"
msg_start = format!("from {}", onto_descr.trim());
msg_middle = format!("code you had in local commit `{from_sha}` before `git pull`");
}
(Some(branch_name), _, None, Some(_), None) => {
// `git rebase`, but we don't have the branch name for the target.
// We could do `git branch --points-at onto_sha` to get a list of branch names, but
// that would necessitate to call into `git` *and* would be a linear scan of every
// branch, which can be expensive in repos with lots of branches.
msg_start = "that you're rebasing onto".to_string();
msg_middle = format!("code from branch '{branch_name}' that you are rebasing");
}
(None, Some(from_sha), None, Some(onto_sha), None) => {
// `git rebase`, but we don't have the branch name for the source nor the target.
msg_start = format!("you had in commit `{onto_sha}` that you are rebasing onto");
msg_middle = format!("code from commit `{from_sha}` that you are rebasing");
}
(None, None, None, None, Some(merge_to)) => {
// `git merge from-branch`
msg_start = format!("from branch '{merge_to}'");
msg_middle = format!("code that you're merging");
}
(None, None, Some(onto_descr), None, Some(merge_to)) => {
// `git merge from-branch`
msg_start = format!("from branch '{merge_to}'");
msg_middle = format!("code from {onto_descr}");
}
_ => {
// We're not in a `git merge`, `git rebase` or `git pull`.
}
}

ConflictMarkerLabels {
start: format!(
"between this marker and `{}` is the code {msg_start}",
if is_middlediff3 { "|||||||" } else { "=======" }
),
middle: format!("between this marker and `>>>>>>>` is the {msg_middle}"),
middlediff3: "between this marker and `=======` is the base code (what the two refs \
diverged from)",
end: "this marker concludes the conflict region",
}
}

pub(super) fn recover_vcs_conflict_marker(&mut self) {
// <<<<<<<
let Some(start) = self.conflict_marker(&TokenKind::Shl, &TokenKind::Lt) else {
Expand Down Expand Up @@ -3083,44 +3203,32 @@ impl<'a> Parser<'a> {
self.bump();
}

let labels = self.vcs_conflict_marker_labels(middlediff3.is_some());

let mut err = self.dcx().struct_span_fatal(spans, "encountered diff marker");
match middlediff3 {
// We're using diff3
Some(middlediff3) => {
err.span_label(
start,
"between this marker and `|||||||` is the code that we're merging into",
);
err.span_label(middlediff3, "between this marker and `=======` is the base code (what the two refs diverged from)");
err.span_label(start, labels.start);
err.span_label(middlediff3, labels.middlediff3);
}
None => {
err.span_label(
start,
"between this marker and `=======` is the code that we're merging into",
);
err.span_label(start, labels.start);
}
};

if let Some(middle) = middle {
err.span_label(middle, "between this marker and `>>>>>>>` is the incoming code");
err.span_label(middle, labels.middle);
}
if let Some(end) = end {
err.span_label(end, "this marker concludes the conflict region");
err.span_label(end, labels.end);
}
err.note(
"conflict markers indicate that a merge was started but could not be completed due \
to merge conflicts\n\
to resolve a conflict, keep only the code you want and then delete the lines \
containing conflict markers",
);
err.help(
"if you're having merge conflicts after pulling new code:\n\
the top section is the code you already had and the bottom section is the remote code\n\
if you're in the middle of a rebase:\n\
the top section is the code being rebased onto and the bottom section is the code \
coming from the current commit being rebased",
);

err.note(
"for an explanation on these markers from the `git` documentation:\n\
visit <https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging#_checking_out_conflicts>",
Expand All @@ -3141,3 +3249,14 @@ impl<'a> Parser<'a> {
Ok(())
}
}

struct ConflictMarkerLabels {
/// The label for the `<<<<<<<` marker.
start: String,
/// The label for the `|||||||` or `=======` marker.
middle: String,
/// The label for the `=======` marker.
middlediff3: &'static str,
/// The label for the `>>>>>>>` marker.
end: &'static str,
}
19 changes: 19 additions & 0 deletions tests/run-make/git-merge-conflict/file.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
error: encountered diff marker
--> main.rust:2:1
|
2 | <<<<<<< HEAD //~ ERROR encountered diff marker
| ^^^^^^^ between this marker and `=======` is the code from branch 'current-branch'
3 | Foo(u8),
4 | =======
| ------- between this marker and `>>>>>>>` is the code from remote branch 'main' of github.com:user/repo
5 | Bar(i8),
6 | >>>>>>> branch
| ^^^^^^^ this marker concludes the conflict region
|
= note: conflict markers indicate that a merge was started but could not be completed due to merge conflicts
to resolve a conflict, keep only the code you want and then delete the lines containing conflict markers
= note: for an explanation on these markers from the `git` documentation:
visit <https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging#_checking_out_conflicts>

error: aborting due to 1 previous error

1 change: 1 addition & 0 deletions tests/run-make/git-merge-conflict/git/HEAD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ref: refs/heads/current-branch
1 change: 1 addition & 0 deletions tests/run-make/git-merge-conflict/git/MERGE_HEAD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SHA_FOR_THE_COMMIT_WE_ARE_MERGING
4 changes: 4 additions & 0 deletions tests/run-make/git-merge-conflict/git/MERGE_MSG
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Merge branch 'main' of github.com:user/repo

# Conflicts:
# src/main.rs
7 changes: 7 additions & 0 deletions tests/run-make/git-merge-conflict/main.rust
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enum E {
<<<<<<< HEAD //~ ERROR encountered diff marker
Foo(u8),
=======
Bar(i8),
>>>>>>> branch
}
10 changes: 10 additions & 0 deletions tests/run-make/git-merge-conflict/rmake.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Checks the output in cases where a manual `git merge` had conflicts.

use run_make_support::{diff, rfs, rustc};

fn main() {
// We use `.rust` purely to avoid `fmt` check on a file with conflict markers
rfs::copy_dir_all("git", ".git");
let file_out = rustc().input("main.rust").run_fail().stderr_utf8();
diff().expected_file("file.stderr").actual_text("actual-file-stderr", file_out).run();
}
19 changes: 19 additions & 0 deletions tests/run-make/git-pull-merge-conflict/file.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
error: encountered diff marker
--> main.rust:2:1
|
2 | <<<<<<< HEAD //~ ERROR encountered diff marker
| ^^^^^^^ between this marker and `=======` is the code from branch 'current-branch'
3 | Foo(u8),
4 | =======
| ------- between this marker and `>>>>>>>` is the code from branch 'main'
5 | Bar(i8),
6 | >>>>>>> branch
| ^^^^^^^ this marker concludes the conflict region
|
= note: conflict markers indicate that a merge was started but could not be completed due to merge conflicts
to resolve a conflict, keep only the code you want and then delete the lines containing conflict markers
= note: for an explanation on these markers from the `git` documentation:
visit <https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging#_checking_out_conflicts>

error: aborting due to 1 previous error

1 change: 1 addition & 0 deletions tests/run-make/git-pull-merge-conflict/git/HEAD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ref: refs/heads/current-branch
1 change: 1 addition & 0 deletions tests/run-make/git-pull-merge-conflict/git/MERGE_HEAD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SHA_FOR_THE_COMMIT_WE_ARE_MERGING
4 changes: 4 additions & 0 deletions tests/run-make/git-pull-merge-conflict/git/MERGE_MSG
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Merge branch 'main' into branch-name

# Conflicts:
# src/main.rs
7 changes: 7 additions & 0 deletions tests/run-make/git-pull-merge-conflict/main.rust
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enum E {
<<<<<<< HEAD //~ ERROR encountered diff marker
Foo(u8),
=======
Bar(i8),
>>>>>>> branch
}
10 changes: 10 additions & 0 deletions tests/run-make/git-pull-merge-conflict/rmake.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Checks the output in cases where a manual `git merge` had conflicts.

use run_make_support::{diff, rfs, rustc};

fn main() {
// We use `.rust` purely to avoid `fmt` check on a file with conflict markers
rfs::copy_dir_all("git", ".git");
let file_out = rustc().input("main.rust").run_fail().stderr_utf8();
diff().expected_file("file.stderr").actual_text("actual-file-stderr", file_out).run();
}
19 changes: 19 additions & 0 deletions tests/run-make/git-pull-rebase-conflict/file.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
error: encountered diff marker
--> main.rust:2:1
|
2 | <<<<<<< HEAD //~ ERROR encountered diff marker
| ^^^^^^^ between this marker and `=======` is the code from branch 'main' of github.com:user/repo
3 | Foo(u8),
4 | =======
| ------- between this marker and `>>>>>>>` is the code from local branch 'branch-name' before `git pull`
5 | Bar(i8),
6 | >>>>>>> branch
| ^^^^^^^ this marker concludes the conflict region
|
= note: conflict markers indicate that a merge was started but could not be completed due to merge conflicts
to resolve a conflict, keep only the code you want and then delete the lines containing conflict markers
= note: for an explanation on these markers from the `git` documentation:
visit <https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging#_checking_out_conflicts>

error: aborting due to 1 previous error

1 change: 1 addition & 0 deletions tests/run-make/git-pull-rebase-conflict/git/FETCH_HEAD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REBASING_ON_TOP_OF_THIS_COMMIT_SHA_VALUE branch 'main' of github.com:user/repo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
refs/heads/branch-name
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REBASING_ON_TOP_OF_THIS_COMMIT_SHA_VALUE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
COMMIT_SHA_OF_THE_BRANCH_WE_ARE_REBASING
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A_COMMIT_SHA_VALUE_GIT_REBASE_STOPPED_AT
7 changes: 7 additions & 0 deletions tests/run-make/git-pull-rebase-conflict/main.rust
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enum E {
<<<<<<< HEAD //~ ERROR encountered diff marker
Foo(u8),
=======
Bar(i8),
>>>>>>> branch
}
10 changes: 10 additions & 0 deletions tests/run-make/git-pull-rebase-conflict/rmake.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Checks the output in cases where a `git pull` configured to rebase had conflicts.

use run_make_support::{diff, rfs, rustc};

fn main() {
// We use `.rust` purely to avoid `fmt` check on a file with conflict markers
rfs::copy_dir_all("git", ".git");
let file_out = rustc().input("main.rust").run_fail().stderr_utf8();
diff().expected_file("file.stderr").actual_text("actual-file-stderr", file_out).run();
}
19 changes: 19 additions & 0 deletions tests/run-make/git-rebase-conflict/file.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
error: encountered diff marker
--> main.rust:2:1
|
2 | <<<<<<< HEAD //~ ERROR encountered diff marker
| ^^^^^^^ between this marker and `=======` is the code that you're rebasing onto
3 | Foo(u8),
4 | =======
| ------- between this marker and `>>>>>>>` is the code from branch 'branch-name' that you are rebasing
5 | Bar(i8),
6 | >>>>>>> branch
| ^^^^^^^ this marker concludes the conflict region
|
= note: conflict markers indicate that a merge was started but could not be completed due to merge conflicts
to resolve a conflict, keep only the code you want and then delete the lines containing conflict markers
= note: for an explanation on these markers from the `git` documentation:
visit <https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging#_checking_out_conflicts>

error: aborting due to 1 previous error

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
refs/heads/branch-name
Empty file.
1 change: 1 addition & 0 deletions tests/run-make/git-rebase-conflict/git/rebase-merge/onto
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REBASING_ON_TOP_OF_THIS_COMMIT_SHA_VALUE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
COMMIT_SHA_OF_THE_BRANCH_WE_ARE_REBASING
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A_COMMIT_SHA_VALUE_GIT_REBASE_STOPPED_AT
7 changes: 7 additions & 0 deletions tests/run-make/git-rebase-conflict/main.rust
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enum E {
<<<<<<< HEAD //~ ERROR encountered diff marker
Foo(u8),
=======
Bar(i8),
>>>>>>> branch
}
10 changes: 10 additions & 0 deletions tests/run-make/git-rebase-conflict/rmake.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Checks the output in cases where a manual `git rebase` was stopped due to conflicts.

use run_make_support::{diff, rfs, rustc};

fn main() {
// We use `.rust` purely to avoid `fmt` check on a file with conflict markers
rfs::copy_dir_all("git", ".git");
let file_out = rustc().input("main.rust").run_fail().stderr_utf8();
diff().expected_file("file.stderr").actual_text("actual-file-stderr", file_out).run();
}
Loading