Skip to content
Merged
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
7 changes: 7 additions & 0 deletions crates/aube-codes/src/warnings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub const WARN_AUBE_PEER_DEDUPE_COLLISION: &str = "WARN_AUBE_PEER_DEDUPE_COLLISI
// ── lockfile ────────────────────────────────────────────────────────
pub const WARN_AUBE_LOCKFILE_MERGE_CONFLICT: &str = "WARN_AUBE_LOCKFILE_MERGE_CONFLICT";
pub const WARN_AUBE_LOCKFILE_MERGE_CLEANUP_FAILED: &str = "WARN_AUBE_LOCKFILE_MERGE_CLEANUP_FAILED";
pub const WARN_AUBE_LOCKFILE_CONFLICT_MARKERS: &str = "WARN_AUBE_LOCKFILE_CONFLICT_MARKERS";
pub const WARN_AUBE_YARN_BERRY_UNSUPPORTED: &str = "WARN_AUBE_YARN_BERRY_UNSUPPORTED";

// ── progress UI ─────────────────────────────────────────────────────
Expand Down Expand Up @@ -449,6 +450,12 @@ pub const ALL: &[CodeMeta] = &[
description: "After a successful merge, removing one of the merged branch lockfiles failed.",
exit_code: None,
},
CodeMeta {
name: WARN_AUBE_LOCKFILE_CONFLICT_MARKERS,
category: category::LOCKFILE,
description: "The active lockfile contains Git conflict markers; aube is regenerating it from package.json.",
exit_code: None,
},
CodeMeta {
name: WARN_AUBE_YARN_BERRY_UNSUPPORTED,
category: category::LOCKFILE,
Expand Down
34 changes: 17 additions & 17 deletions crates/aube-linker/src/materialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,23 @@ pub(crate) fn validate_package_link_name(name: &str) -> Result<(), Error> {
}
}

fn is_safe_package_component(component: &str) -> bool {
if component.is_empty() || matches!(component, "." | "..") {
return false;
}
if component.len() >= 2 && component.as_bytes()[1] == b':' {
return false;
}
!std::path::Path::new(component).components().any(|c| {
matches!(
c,
std::path::Component::ParentDir
| std::path::Component::RootDir
| std::path::Component::Prefix(_)
)
})
}

#[cfg(test)]
mod package_name_tests {
use super::*;
Expand Down Expand Up @@ -742,20 +759,3 @@ mod package_name_tests {
}
}
}

fn is_safe_package_component(component: &str) -> bool {
if component.is_empty() || matches!(component, "." | "..") {
return false;
}
if component.len() >= 2 && component.as_bytes()[1] == b':' {
return false;
}
!std::path::Path::new(component).components().any(|c| {
matches!(
c,
std::path::Component::ParentDir
| std::path::Component::RootDir
| std::path::Component::Prefix(_)
)
})
}
26 changes: 26 additions & 0 deletions crates/aube-lockfile/src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,32 @@ pub fn detect_existing_lockfile_kind(project_dir: &Path) -> Option<LockfileKind>
None
}

/// Return true when the active lockfile contains Git conflict markers.
///
/// Used by install's prefer-frozen path to distinguish a merge/rebase
/// artifact from an arbitrary parse error: conflict markers can be
/// repaired by regenerating from the already-resolved `package.json`,
/// while other parse failures should stay loud.
pub fn active_lockfile_has_conflict_markers(project_dir: &Path) -> bool {
for (path, _) in lockfile_candidates(project_dir, /*include_aube=*/ true) {
if !path.exists() {
continue;
}
return read_lockfile(&path)
.map(|content| has_conflict_markers(&content))
.unwrap_or(false);
}
false
}

fn has_conflict_markers(content: &str) -> bool {
content.lines().any(|line| {
line.starts_with("<<<<<<< ")
|| line.trim_end_matches('\r') == "======="
|| line.starts_with(">>>>>>> ")
})
}

/// Resolve the canonical lockfile filename for `project_dir` (aube's own).
///
/// Returns `aube-lock.<branch>.yaml` when `gitBranchLockfile: true` is
Expand Down
7 changes: 4 additions & 3 deletions crates/aube-lockfile/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ pub mod yarn;

pub use drift::DriftStatus;
pub use io::{
Error, LockfileKind, aube_lock_filename, build_canonical_map, detect_existing_lockfile_kind,
parse_for_import, parse_json, parse_lockfile, parse_lockfile_with_kind, pnpm_lock_filename,
read_lockfile, write_lockfile, write_lockfile_as, write_lockfile_preserving_existing,
Error, LockfileKind, active_lockfile_has_conflict_markers, aube_lock_filename,
build_canonical_map, detect_existing_lockfile_kind, parse_for_import, parse_json,
parse_lockfile, parse_lockfile_with_kind, pnpm_lock_filename, read_lockfile, write_lockfile,
write_lockfile_as, write_lockfile_preserving_existing,
};
pub(crate) use io::{atomic_write_lockfile, current_git_branch};
pub use merge::{MergeReport, merge_branch_lockfiles};
Expand Down
5 changes: 5 additions & 0 deletions crates/aube/src/commands/install/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,10 @@ pub async fn run(opts: InstallOptions) -> miette::Result<()> {
&lockfile_importer_key,
&manifest,
)?;
let lockfile_conflict_marker_warning_emitted = lockfile_pre_parse.is_none()
&& lockfile_enabled
&& matches!(mode, FrozenMode::Fix | FrozenMode::Prefer)
&& aube_lockfile::active_lockfile_has_conflict_markers(&lockfile_dir);
let existing_for_resolver: Option<&aube_lockfile::LockfileGraph> =
lockfile_pre_parse.as_ref().map(|(g, _)| g);

Expand All @@ -405,6 +409,7 @@ pub async fn run(opts: InstallOptions) -> miette::Result<()> {
workspace_catalogs: &workspace_catalogs,
settings_ctx: &settings_ctx,
lockfile_pre_parse: lockfile_pre_parse.as_ref(),
lockfile_conflict_marker_warning_emitted,
existing_for_resolver,
source_kind_before,
lockfile_enabled,
Expand Down
58 changes: 43 additions & 15 deletions crates/aube/src/commands/install/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ pub(super) fn pre_parse_lockfile(
match parse_lockfile_dir_remapped_with_kind(lockfile_dir, lockfile_importer_key, manifest) {
Ok(parsed) => Ok(Some(parsed)),
Err(aube_lockfile::Error::NotFound(_)) => Ok(None),
Err(e) if active_lockfile_has_conflict_markers(lockfile_dir) => {
warn_lockfile_conflict_markers(lockfile_dir, &e);
Ok(None)
}
Err(e) => Err(miette::Report::new(e)).wrap_err("failed to parse lockfile"),
}
}
Expand All @@ -39,6 +43,7 @@ pub(super) struct LockfileOnlyInput<'a> {
pub workspace_catalogs: &'a crate::commands::CatalogMap,
pub settings_ctx: &'a aube_settings::ResolveCtx<'a>,
pub lockfile_pre_parse: Option<&'a (LockfileGraph, LockfileKind)>,
pub lockfile_conflict_marker_warning_emitted: bool,
pub existing_for_resolver: Option<&'a LockfileGraph>,
pub source_kind_before: Option<LockfileKind>,
pub lockfile_enabled: bool,
Expand Down Expand Up @@ -68,6 +73,7 @@ pub(super) async fn run_lockfile_only(input: LockfileOnlyInput<'_>) -> miette::R
workspace_catalogs,
settings_ctx,
lockfile_pre_parse,
lockfile_conflict_marker_warning_emitted,
existing_for_resolver,
source_kind_before,
lockfile_enabled,
Expand Down Expand Up @@ -113,22 +119,32 @@ pub(super) async fn run_lockfile_only(input: LockfileOnlyInput<'_>) -> miette::R
if let Err(e) = parsed
&& !matches!(e, aube_lockfile::Error::NotFound(_))
{
// Can't hand &Error to miette::Report since Diagnostic
// is only implemented on owned Error. Re-parse once to
// get an owned Error value for the Diagnostic path.
// Slightly wasteful on the error branch, install is
// about to abort anyway so speed does not matter here.
// What matters: keeping the source span and miette help
// text instead of flattening to one line via `{e}`.
match parse_lockfile_dir_remapped_with_kind(lockfile_dir, lockfile_importer_key, manifest) {
Ok(_) => {
// Race: second parse succeeded while first failed.
// Surface the observed error text as a best
// effort flat message. Extremely unlikely.
return Err(miette!("failed to parse lockfile: {e}"));
if active_lockfile_has_conflict_markers(lockfile_dir) {
if !lockfile_conflict_marker_warning_emitted {
warn_lockfile_conflict_markers(lockfile_dir, e);
}
Err(owned) => {
return Err(miette::Report::new(owned)).wrap_err("failed to parse lockfile");
} else {
// Can't hand &Error to miette::Report since Diagnostic
// is only implemented on owned Error. Re-parse once to
// get an owned Error value for the Diagnostic path.
// Slightly wasteful on the error branch, install is
// about to abort anyway so speed does not matter here.
// What matters: keeping the source span and miette help
// text instead of flattening to one line via `{e}`.
match parse_lockfile_dir_remapped_with_kind(
lockfile_dir,
lockfile_importer_key,
manifest,
) {
Ok(_) => {
// Race: second parse succeeded while first failed.
// Surface the observed error text as a best
// effort flat message. Extremely unlikely.
return Err(miette!("failed to parse lockfile: {e}"));
}
Err(owned) => {
return Err(miette::Report::new(owned)).wrap_err("failed to parse lockfile");
}
}
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
Expand Down Expand Up @@ -392,6 +408,18 @@ pub(super) fn select_lockfile_result(
}
}

fn active_lockfile_has_conflict_markers(lockfile_dir: &Path) -> bool {
aube_lockfile::active_lockfile_has_conflict_markers(lockfile_dir)
}

fn warn_lockfile_conflict_markers(lockfile_dir: &Path, err: &aube_lockfile::Error) {
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_LOCKFILE_CONFLICT_MARKERS,
"lockfile in {} contains Git conflict markers; regenerating from package.json ({err})",
lockfile_dir.display()
);
}

pub(super) fn apply_lockfile_graph_platform_rules(
mut graph: LockfileGraph,
kind: LockfileKind,
Expand Down
6 changes: 6 additions & 0 deletions docs/error-codes.data.json
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,12 @@
"description": "After a successful merge, removing one of the merged branch lockfiles failed.",
"exit_code": null
},
{
"name": "WARN_AUBE_LOCKFILE_CONFLICT_MARKERS",
"category": "Lockfile",
"description": "The active lockfile contains Git conflict markers; aube is regenerating it from package.json.",
"exit_code": null
},
{
"name": "WARN_AUBE_YARN_BERRY_UNSUPPORTED",
"category": "Lockfile",
Expand Down
53 changes: 53 additions & 0 deletions test/install.bats
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@ teardown() {
_common_teardown
}

_write_conflicted_basic_lockfile() {
cat >aube-lock.yaml <<'YAML'
<<<<<<< HEAD
lockfileVersion: '9.0'
importers:
.:
dependencies:
is-odd:
specifier: 3.0.1
version: 3.0.1
=======
lockfileVersion: '9.0'
importers:
.:
dependencies:
is-odd:
specifier: 3.0.1
version: 3.0.1
>>>>>>> main
YAML
}

@test "aube install creates node_modules" {
_setup_basic_fixture
run aube install
Expand All @@ -30,6 +52,37 @@ teardown() {
assert_output --partial "Already up to date"
}

@test "aube install regenerates lockfile with merge conflict markers" {
_setup_basic_fixture
run aube install
assert_success

_write_conflicted_basic_lockfile

run aube install
assert_success
assert_output --partial "conflict markers"
run grep -E '^(<<<<<<<|=======|>>>>>>>)' aube-lock.yaml
assert_failure
run grep 'is-odd@3.0.1' aube-lock.yaml
assert_success
}

@test "aube install --lockfile-only warns once for merge conflict markers" {
_setup_basic_fixture
run aube install
assert_success

_write_conflicted_basic_lockfile

run aube install --lockfile-only
assert_success
count="$(printf '%s\n' "$output" | grep -c 'conflict markers')"
assert_equal "$count" 1
run grep -E '^(<<<<<<<|=======|>>>>>>>)' aube-lock.yaml
assert_failure
}

@test "aube install warm path works in CI frozen mode" {
_setup_basic_fixture
export CI=1
Expand Down
Loading