Tutorials › Pharmacometric Workflows and Reproducible Modeling › Reading Diffs and Resolving Merge Conflicts

Project Structure, Version Control and Documentation · Tutorial 58 of 399

Reading Diffs and Resolving Merge Conflicts

Learn to interpret Git diffs, combine conflicting changes carefully, and check the resulting files and repository state before completing a merge.

Intermediate 11 min read

What You'll Learn

  • Read unified diff headers, context lines, additions, deletions, and file metadata.
  • Distinguish changes to the working tree, index, and committed versions.
  • Explain how a three-way merge uses a common ancestor and why conflicts arise.
  • Resolve conflicts by comparing both edits with the base version and preserving compatible intent.
  • Verify resolved scripts and configuration files before staging and completing a merge.
  • Recognize when a textual diff is insufficient to assess a change.

Why Diffs and Conflicts Need Careful Review

A branch may change a model script while another branch changes the same script or a related configuration file. Git can combine many edits automatically, but a successful automatic merge does not establish that the resulting project is correct. Likewise, a conflict marker is not a verdict about which contributor is right: it identifies a section that requires a deliberate decision.

Reading diffs and resolving conflicts are linked tasks. A diff shows how recorded text differs between two states; understanding it helps you determine what each branch intended. After resolving a conflict, review the final file as a whole and check that the intended changes remain, that no conflict artifacts are left, and that relevant project checks pass.

Definition: A diff is a line-oriented representation of differences between two file states. In a unified diff, lines beginning with a minus sign are present in the comparison's old state but absent from its new state; lines beginning with a plus sign are present in the new state but absent from the old state. Unprefixed context lines are shown to locate the change. A merge conflict occurs when Git cannot combine changes to a shared base automatically, so a person must decide what the resulting content should be.

Reading a Unified Diff

Before interpreting a diff, identify which two states are being compared. In Git, git diff ordinarily compares the working tree with the index, showing unstaged changes. git diff --cached compares the index with the current commit, showing staged changes. A comparison between commits instead describes changes from one committed state to another. The same plus and minus signs have the same meaning in each case, but the old and new states differ.

A unified diff usually begins with file names and a hunk header. For example, @@ -10,3 +10,4 @@ indicates that the hunk represents three old lines beginning at line 10 and four new lines beginning at line 10. A hunk contains context lines as well as changed lines. Counts can differ because lines have been added or removed. The header locates the change; it does not explain its purpose.

A minus line is not automatically a mistake, and a plus line is not automatically an improvement. Read changed lines together with their context, then ask what behavior or project convention may change. For a script, that might mean checking which input path or output location it now uses. For configuration, it may mean checking whether a changed value is intended to apply to all runs or only one run. Diffs can also show file creation or deletion, renames, and changes to file mode; review those changes rather than limiting attention to the visible line edits.

Worked Example: Interpreting a Script Diff

Suppose a hypothetical project has a script that reads a prepared analysis table and writes results to a run folder. A diff shows: minus line, input_path <- "data/analysis.csv"; plus line, input_path <- config$input_path; context line, data <- read.csv(input_path). The diff compares the earlier committed file with the proposed new file.

The old version sets the input path directly to a particular file. The new version gets the path from configuration. The unchanged context line still passes input_path to the data-reading function. Thus the visible edit changes where the path value comes from; it does not, by itself, show that the file named in the configuration exists or that the configuration is loaded before this code runs.

A careful review therefore checks the surrounding script and the relevant configuration entry. If the configuration is read earlier and its input path points to the intended prepared table, the edit may support configurable runs. If not, the script may fail or read an unintended file. The diff identifies the textual change, while project context is needed to judge its consequence.

How a Three-Way Merge Produces a Conflict

When merging branches, Git generally compares their changes with a common ancestor, often called the merge base. It can combine changes automatically when they are compatible. For example, if each branch adds text in separate, nonoverlapping locations, Git can often retain both. If both branches alter the same lines in incompatible ways, Git cannot infer which result is intended and marks the file as conflicted.

This is a line-based decision, not a judgment about meaning. Two changes that are semantically incompatible may merge automatically if they affect separate lines. Conversely, two edits to the same line can conflict even when a person can readily combine them. Therefore, the absence of conflicts does not prove that the merge preserves the project's intended behavior.

In a typical merge conflict, the working-tree file contains markers such as <<<<<<< HEAD, =======, and >>>>>>> branch-name. The section above the separator represents the current branch's version; the section below it represents the version being merged in. The labels depend on the operation: in a rebase, their practical meanings can differ from what a reader might infer from the words “ours” and “theirs.” Inspect the actual operation and compare the versions rather than choosing by label alone.

Git also stores the common ancestor and the two branch versions in separate index entries while a file is unresolved. Those versions can help when the conflict markers alone do not explain the competing edits. The essential task is to understand each edit relative to the shared base, decide what content should result, and replace the markers with that content.

Worked Example: Combining Compatible Configuration Changes

Assume a hypothetical configuration file has this base line: run_label = "base". One branch changes it to run_label = "screening" and also adds output_dir = "runs/screening". The other changes the label to run_label = "final" and adds seed = 2718. Git reports a conflict on the label.

The two labels cannot both be the single value of run_label, so the conflict requires a decision about which run this configuration should describe. The added output directory and seed are different settings. If the intended result is a final run that retains the explicit output location and seed, the resolved content is: run_label = "final"; output_dir = "runs/screening"; seed = 2718.

This resolution preserves both additions but does not automatically make the directory name consistent with the label. If project conventions require matching names, the reviewer should decide whether the output directory should instead become runs/final. That decision depends on the intended run configuration, not on Git's conflict markers. Once decided, remove all marker lines and verify the complete configuration, including any related settings.

A Deliberate Conflict-Resolution Workflow

A reliable resolution begins before editing the conflicted file. Confirm which branch operation is underway, inspect the repository status, and identify every conflicted path. Then compare the competing versions with their common base. Resolving one file does not resolve the other conflicted files, and a file that Git merged automatically can still contain an unintended combination.

1
Establish the operation and scope.
Check the repository status and list the paths marked as unresolved. Note whether this is a merge or a rebase; do not infer the meaning of side labels from habit.
2
Inspect the competing edits.
Read the conflict region in context and, when useful, compare the base and both versions. Ask what each change was intended to accomplish, not just which lines differ.
3
Edit for intended behavior.
Retain one side, combine parts, or write a different resolution when justified. Remove every conflict marker and preserve unrelated valid content.
4
Review and verify.
Read the entire resolved file, inspect the diff, and run appropriate checks for the affected script or configuration. Check that the repository reports no unresolved paths.
5
Stage and complete only after review.
Stage the resolved files once satisfied, inspect the staged diff, and complete the merge or continue the rebase. If a check fails, revise the resolution rather than treating staging as evidence of correctness.

This order matters because staging records a resolution in the index; it does not validate its contents. Likewise, finishing a merge records a combined history, but does not establish that the project still runs as intended. The staged diff is an important final opportunity to notice accidental deletions, unrelated edits, or leftover markers before recording the result.

Worked Example: Resolving and Verifying a Script Conflict

In a hypothetical project, the base script selects a configuration file with config_file <- "config/default.yml". The current branch changes this to config_file <- "config/analysis.yml". The incoming branch changes it to config_file <- "config/sensitivity.yml". Both branches edited the same line, and the merge reports a conflict.

First, identify which run the merged script should execute. Suppose the intended result is the analysis run, while the sensitivity configuration remains available for a separate script. The resolved line is therefore config_file <- "config/analysis.yml"; the sensitivity file is not deleted or renamed merely because its path did not win this conflict. If the intended merged script instead needs a selectable configuration, a deliberate alternative could read the path from an existing run configuration mechanism, provided that mechanism is already set up correctly.

Next, review the whole script to confirm that it loads the selected file and uses the configuration in the expected order. Check for all three marker strings, review the diff against the current commit, and inspect the repository status. A syntax or project-level check appropriate to the script can catch errors that a line diff cannot. Only after the content and checks are satisfactory should the file be staged and the merge completed.

Diagnostics, Alternatives, and Limits

Useful warning signs include unexpected large deletions, changes outside the files or sections under review, leftover conflict markers, a configuration value that no longer matches its documented purpose, and a script that references a path not supplied by the project. Search for marker strings and inspect both unstaged and staged diffs. A clean status means Git has no unrecorded changes to report; it is not proof that a model workflow or its outputs are correct.

For conflicts, the main alternatives are to accept one complete side, combine selected edits, or abort the operation and reconsider the work. Accepting one side is appropriate only when its content fully reflects the intended result; doing so blindly can discard valid changes. Combining edits is appropriate when the intentions are compatible, but may require revising related values elsewhere. Aborting can be safer when the merge context is misunderstood or the resolution would require decisions that have not been agreed. An automatic merge tool can help display or combine text, but its output still requires review.

Text diffs have important limits. They show textual differences, not whether the code is scientifically appropriate, whether a configuration value is valid, or whether the resulting workflow is reproducible. A change in one file may depend on a corresponding change elsewhere. Review scope should therefore include related files and relevant checks, without treating a successful text merge as a substitute for project-level verification.

Common pitfall: “The conflict is resolved if I keep my side and Git lets me commit.” This reasoning is wrong because the branch label does not establish which edit is correct, and Git accepting a staged file checks neither scientific intent nor workflow behavior. Compare both changes with the base, preserve compatible valid edits, inspect the resulting file, and run relevant checks before completing the operation.

Check Your Understanding

Answer each question, then compare your reasoning with the solutions below.

  1. In a unified diff, what do a minus-prefixed line, a plus-prefixed line, and an unprefixed context line represent?
  2. Two branches edit the same configuration line to different values. What information should guide the resolution?
  3. Why does the absence of merge conflicts not guarantee that a merged script is correct?
  4. What should be checked after editing a conflicted file but before completing the merge?

Solutions. (1) A minus-prefixed line is in the old comparison state but not the new one; a plus-prefixed line is in the new state but not the old one; an unprefixed line is context shown in both. (2) Compare each edit with the common base and determine the intended configuration, using project context and the purpose of the run rather than choosing by branch label alone. (3) Git's automatic combination is based on textual changes; edits to separate lines can still produce an incoherent or unintended result. (4) Read the whole resolved file, confirm markers are gone, inspect the diff and repository status, and run relevant script or configuration checks before staging and completing the operation.

Clinical Biostats