Skip to content

Merge Conflict Resolution Guide

How to resolve merge conflicts when bringing main into a long-running feature branch. This guide provides general conflict resolution principles (§1–§2), a catalog of known restructuring PRs with their specific conflict patterns (§3), and universal verification gates (§4).

When a new restructuring PR lands in main, add an entry to §3 following the template in §6.


1. General principles

1.1 Diagnose before acting

Every conflict traces back to one of: - Files moved or renamed on one branch, modified on the other. - Files added on one branch at paths that don't exist on the other. - Files deleted on one branch, modified on the other. - Source lists or config blocks replaced wholesale.

Identify which pattern applies before choosing a resolution strategy.

1.2 Resolve toward the forward-looking layout

When one side represents a restructuring and the other contains incremental changes, resolve toward the restructured layout and transplant the incremental changes into it. Do not revert restructuring to avoid conflicts.

1.3 Verification is part of resolution

Git reporting zero unmerged files is not enough. Run the universal verification gates in §4 after every merge. Add PR-specific gates when adding a new entry to §3.


2. Quick diagnosis

# What conflicted and what type?
git status --short | grep -E '^UU|^UA|^AU|^DU|^UD|^AA|^DD'

# Inspect the three merge stages (1=base, 2=ours, 3=theirs)
git ls-files -u

# Diff our side vs theirs for a specific file
git diff HEAD...MERGE_HEAD -- <file>

Stage codes:

Code Meaning Typical action
UU Both modified Merge contents manually
UA Added by main, absent here git add after checking for path moves (§3)
UD Deleted by main, modified here git rm if the deletion is legitimate

3. Known restructuring PRs

Each entry documents the layout changes, conflict patterns, and verification gates specific to one restructuring PR. When a new restructuring PR lands, add a new subsection here.

Template: see §6.

3.1 PR #88 — Buildsystem Maintenance

What changed:

Change Before After
C++ tests moved python/tests/cpp/test_*.cpp novaphy/tests/test_*.cpp
C++ test build config moved python/tests/CMakeLists.txt novaphy/tests/CMakeLists.txt
Python tests flattened python/tests/python/test_*.py python/tests/test_*.py
CMake compat moved compat/ cmake/compat/
Demos installed as subpackage (no __init__.py) python/demos/__init__.pynovaphy.demos

Conflict patterns:

Pattern A: CMakeLists.txt source list replaced

Cause: main replaced VBD sources (cpu/vbd_*.cpp + vbd_cuda/) with a new structure (solver_vbd.cpp + rigid/ + soft/).

Symptom: UU conflict in novaphy/src/dynamics/vbd/CMakeLists.txt with two completely different source lists.

Fix: Take main's version in full (it's a replacement, not an addition).

git checkout --theirs novaphy/src/dynamics/vbd/CMakeLists.txt
git add novaphy/src/dynamics/vbd/CMakeLists.txt

Pattern B: File renamed here + modified on main

Cause: We moved a file (e.g. python/tests/CMakeLists.txtnovaphy/tests/CMakeLists.txt) and main modified the original.

Symptom: UU conflict with Git markers referencing two different file paths and different internal conventions (e.g. cpp/ prefix).

Fix: Keep our file location. Port main's logical changes (new files, new link deps) into our path convention.

Pattern C: Test path depth wrong (parents[N] off by one)

Cause: Main's tests compute REPO_ROOT via Path(__file__).resolve().parents[3] — correct for the old python/tests/python/ depth, overshooting after flattening.

Symptom: FileNotFoundError with paths like /home/user/src/python/... (repo name missing).

Fix: Reduce parents[N] by 1 for every test file that was moved:

Move Old New
python/tests/python/python/tests/ (REPO_ROOT) parents[3] parents[2]
python/tests/python/python/tests/ (module loader) parents[2] parents[1]
grep -rn '\.parents\[' python/tests/

Pattern D: Main added files at old paths

Cause: main added files under python/tests/cpp/ or compat/ — directories we moved or deleted.

Symptom: UA conflicts at old paths.

Fix: Move them:

Main added at Move to
python/tests/cpp/test_new.cpp novaphy/tests/test_new.cpp
python/tests/python/test_new.py python/tests/test_new.py
compat/new_file.cmake cmake/compat/new_file.cmake
git mv <old-path> <new-path>
git add <new-path>

PR-specific verification

In addition to the universal gates in §4, verify:

# All demos imports use the subpackage
grep -rn 'from demos\.' python/
# Expected: zero results

3.2 PR #143 — Demo and pytest cleanup

What changed:

Change Before After
LBM robot demos renamed python/demos/demo_lbm_robot_pyvista.py, python/demos/demo_lbm_robot_gripper_pyvista.py python/demos/demo_lbm_robot.py, python/demos/demo_lbm_robot_gripper.py
LBM examples consolidated python/demos/demo_lbm_immersed_bodies.py, python/demos/demo_lbm_pyvista_volume.py python/demos/_lbm_scene.py, python/demos/demo_lbm_volume.py
Featherstone rope consolidated python/demos/featherstone/demo_fs_rope.py python/demos/featherstone/_rope_scene.py, python/demos/featherstone/demo_fs_pgs_rope.py
Legacy Polyscope fluid demos removed python/demos/demo_ball_in_water.py, python/demos/demo_fluid_box.py python/demos/demo_fluid_coupling.py, python/demos/demo_dam_break.py
Legacy Polyscope stress demo removed python/demos/demo_pyramids_numerous.py python/demos/featherstone/demo_fs_pgs_pyramids_numerous.py
Legacy PPO pendulum demo removed python/demos/demoSim_ppo_inverted_pendulum.py Physics-only supported-viewer example: python/demos/xpbd/demo_xpbd_pendulum.py
Redundant XPBD demo removed python/demos/demo_newton_xpbd_pyramid.py python/demos/demo_pyramid_ball.py and python/tests/test_xpbd_demos.py
Redundant IK utilities removed python/demos/ik_arm_demo/benchmark.py, python/demos/ik_arm_demo/self_check.py python/demos/ik_arm_demo/benchmark_compare.py and python/tests/test_ik_demo.py
Standalone viewers retired python/novaphy/viz_moderngl.py, python/novaphy/viz_pyvista.py python/novaphy/viewer/ and its ViewerGL/ViewerNull interfaces
Ad-hoc benchmark removed python/tests/benchmark_rigid_1000.py python/demos/demo_performance_monitor.py and benchmark skill
Demo runtime tests migrated python/tests/test_demo_simulate_loop_alignment.py python/tests/test_demo_runtime.py
Viewer implementation tests migrated python/tests/test_viz_moderngl.py Split python/tests/test_viewer_* modules
Viewer tests split python/tests/test_viewer_api.py python/tests/_viewer_test_support.py plus ten python/tests/test_viewer_* domain modules
Container tests split python/tests/test_newton_container_contract.py python/tests/_newton_container_support.py plus eight python/tests/test_newton_* contract modules
Negative API contracts migrated python/tests/test_no_compat_layer.py python/tests/test_newton_model_contract.py
URDF audit migrated python/tests/test_newton_basic_urdf_audit.py python/tests/test_urdf_import_metadata.py
Solver config tests migrated python/tests/test_solver_config_api.py python/tests/test_newton_signature_alignment.py plus solver-specific suites
Featherstone direct tests migrated python/tests/test_solver_featherstone_direct.py python/tests/test_solver_featherstone_newton.py
XPBD demo tests migrated python/tests/test_newton_xpbd_demo.py python/tests/test_xpbd_demos.py
VBD tests split python/tests/test_solver_vbd.py, python/tests/test_solver_vbd_cuda.py Two support modules plus six CPU and four CUDA domain modules
MuJoCo tests split python/tests/test_solver_mujoco_native.py python/tests/_solver_mujoco_support.py, python/tests/test_solver_mujoco_config.py, python/tests/test_solver_mujoco_dynamics.py
Joint tests split python/tests/test_joint_unification.py python/tests/test_joint_types.py, python/tests/test_joint_metadata.py, python/tests/test_joint_builder_api.py

Conflict patterns:

Pattern A: Renamed LBM demo modified on another branch

Cause: The cleanup renamed the two ViewerGL robot demos to remove the obsolete _pyvista suffix while another branch edited or imported the old path.

Symptom: A modify/delete conflict at an old demo path, or a runtime ModuleNotFoundError for a module ending in _pyvista.

Fix: Keep the new filename, transplant the other branch's logical change into it, and update imports to novaphy.demos.demo_lbm_robot or novaphy.demos.demo_lbm_robot_gripper. Do not recreate a compatibility module at the old path.

Pattern B: Consolidated demo deleted on one side and modified on the other

Cause: Reusable scene construction moved out of the retired LBM immersed and Featherstone rope entry points before those duplicate entry points were deleted.

Symptom: A UD conflict in demo_lbm_immersed_bodies.py or featherstone/demo_fs_rope.py.

Fix: Port renderer-independent LBM changes into _lbm_scene.py and rope topology changes into featherstone/_rope_scene.py. Port user-facing runtime changes into demo_lbm_volume.py or demo_fs_pgs_rope.py, then keep the old entry point deleted.

Pattern C: Monolithic pytest file modified after it was split

Cause: Another branch added a regression to one of the removed aggregate test modules.

Symptom: A modify/delete conflict, or a newly resurrected copy of test_viewer_api.py, test_newton_container_contract.py, test_solver_vbd.py, test_solver_vbd_cuda.py, test_solver_mujoco_native.py, or test_joint_unification.py.

Fix: Move the individual test and any narrowly shared helper into the matching domain module. Use the test name and behavior, not its former line number, to choose among viewer lifecycle/backend/geometry/picking, container model/state/control/contact, VBD runtime/config/joint/particle/contact, MuJoCo config/dynamics, or joint type/metadata/builder modules. Keep the aggregate file deleted and confirm the test is collected exactly once.

Pattern D: Viewer extras conflict in pyproject.toml

Cause: This cleanup retired the PyVista volume extra, made the PyImgui GLFW integration explicit, and made the demo/development extras include the common viewer. Another branch may edit the same dependency table.

Symptom: A UU conflict in [project.optional-dependencies], a missing imgui.integrations.glfw.GlfwRenderer, or an accidentally restored viz-volume extra.

Fix: Preserve viz because the out-of-scope IPC demos still use it. Preserve viewer with imgui[glfw] and pycollada for Collada/DAE mesh loading, keep test with both trimesh and pycollada, keep examples and dev dependent on viewer, and do not restore viz-volume or PyVista.

Pattern E: Retired demo or viewer modified on another branch

Cause: Another branch changed one of the legacy Polyscope demos, the standalone ModernGL/PyVista viewers, or the ad-hoc rigid benchmark after this cleanup selected a supported replacement.

Symptom: A UD conflict at one of the fully qualified demo, viewer, or benchmark paths in the table above. Accepting the modified side resurrects a retired dependency or duplicates a retained example.

Fix: Keep the old path deleted. Port physics behavior to the replacement listed in the table and port rendering behavior to python/novaphy/viewer/. The PPO pendulum has no feature-equivalent replacement; if its RL behavior is still required, transplant that behavior into a new supported-viewer demo instead of restoring its Polyscope entry point.

Pattern F: Migrated regression test modified on another branch

Cause: Another branch added assertions to a deleted API, solver, URDF, demo-runtime, XPBD-demo, or viewer-implementation test after its behavior was moved into a retained suite.

Symptom: A UD conflict in test_no_compat_layer.py, test_solver_config_api.py, test_solver_featherstone_direct.py, test_newton_basic_urdf_audit.py, test_demo_simulate_loop_alignment.py, test_newton_xpbd_demo.py, or test_viz_moderngl.py.

Fix: Find the exact forward destination in the table, port the assertion there, and keep the old path deleted. Before resolving, search by the original test function name to avoid collecting the same regression twice.

PR-specific verification:

# Every deleted path in this restructuring must be cataloged above.
git diff --diff-filter=D --name-only main |
  while IFS= read -r path; do
    rg -F -q "\`$path\`" docs/guide/merge-conflict-resolution.md ||
      { echo "Undocumented deleted path: $path"; exit 1; }
  done

# Removed entry points and aggregate tests must not return. This derives the
# complete list from the PR rather than maintaining a second partial list.
git diff --diff-filter=D --name-only main |
  while IFS= read -r path; do test ! -e "$path" || exit 1; done

# All retained non-IPC demos use the common viewer path.
rg -n -i 'polyscope|SceneVisualizer|novaphy\.viz' \
  python/demos --glob '!demo_ipc_*.py'
# Expected: zero results

# Retired PyVista surface stays absent.
rg -n -i 'pyvista|viz_pyvista|viz-volume' python pyproject.toml
# Expected: zero results

# Split suites and behavior migrations remain healthy.
pytest python/tests/ -v

4. Universal verification gates

After resolving all git conflicts, run these checks. All must pass before committing. These gates apply regardless of which PR caused the conflicts.

Gate 1: Zero source tree injection in sys.path

novaphy._core is a compiled C extension that only exists at the install location. Adding the source python/ directory to sys.path shadows the installed package.

grep -rn 'sys\.path\.insert\|sys\.path\.append' python/

Expected: zero results, except python/novaphy/viewer/gl_backend.py under if __name__ == "__main__" (acceptable).

Gate 2: Zero sys.modules replacement

grep -rn 'sys\.modules\[.novaphy.\]' python/

Expected: zero results (same exception as Gate 1).

Gate 3: No unresolved git conflicts

git diff --name-only --diff-filter=U

Expected: no output.


5. Standard merge procedure

# 1. Start the merge
git checkout <your-branch>
git fetch origin main
git merge origin/main

# 2. Diagnose (see §2)
git status --short | grep -E '^[UAD]'

# 3. Identify which restructuring PR(s) caused the conflicts (see §3)
#    Resolve each conflict using the patterns documented there.

# 4. Universal verification gates (see §4) — DO NOT SKIP
grep -rn 'sys\.path\.insert\|sys\.path\.append' python/
grep -rn 'sys\.modules\[.novaphy.\]' python/
git diff --name-only --diff-filter=U

# 5. PR-specific verification (see the PR entry in §3)

# 6. Commit
git add <resolved-files>
git commit -m "Merge branch 'main' into <your-branch>"

# 7. Verify (if environment supports it)
pytest python/tests/ -v

6. How to extend this document

When a new restructuring PR lands in main, add a subsection under §3 using this template:

### 3.X PR #NNN — <short title>

**What changed:**

| Change | Before | After |
|--------|--------|-------|
| <description> | `<old-path>` | `<new-path>` |

**Conflict patterns:**

#### Pattern A: <name>

**Cause:** <why this conflict happens due to this PR's changes>

**Symptom:** <what error or conflict marker the agent will see>

**Fix:** <concrete steps, with copyable commands>

#### Pattern B: ...

**PR-specific verification:**

```bash
<grep command>
# Expected: <what should happen>
```

Keep each pattern self-contained: an agent encountering it for the first time should be able to diagnose and fix it from this entry alone.

Prevention tips

  1. Alphabetize source lists in CMakeLists.txt — reduces spurious conflicts when both sides append to the end.
  2. Merge main before starting a large refactor; land it quickly.
  3. Use git mv for renames — merge algorithms handle it better.
  4. Call out moves in the PR description — a sentence like "this PR moves C++ tests to novaphy/tests/" saves the next merger significant diagnosis time.
  5. Never use sys.path to make the source tree importable. If a demo script needs novaphy, install the package and import normally.