Changes and Dependencies
What a change holds
classDiagram
class Change {
version
change_id
deps: [ChangeHash]
author: KeyId
hlc: hybrid logical clock
message
tree_ops: [TreeOp]
edits: [Edit]
kind: normal, resolution or working-copy
}
class TreeOp {
AddNode, Move, Delete, SetMode
}
class Edit {
Insert lines between two atoms
Delete a range of atoms
Replace a whole opaque file
}
Change *-- TreeOp
Change *-- Edit
- Tree operations add, move, delete and change the mode of files and directories, each identified by a NodeId.
- Edits change file content. A text file is edited line by line; an opaque file (binary) is replaced whole.
A file is a graph of lines
Each line is an atom, named by the change that introduced it and an index. A file's content is a graph: an edge from atom A to atom B means "B comes right after A". Deleting a line marks the atom deleted; it stays in the graph as a tombstone.
flowchart LR
START((START)) --> a1["c1:0 fn main() {"]
a1 --> a2["c1:1 println!(hi)"]
a2 --> a3["c1:2 }"]
a3 --> END((END))
a1 --> b1["c2:0 let x = 1;"]
b1 --> a2
a2 -.-> d1["c3 deletes c1:1"]
Change c1 wrote three lines. Change c2 inserted a line between c1:0 and c1:1. Change c3 deleted c1:1. The graph is the union of what every change added, so it does not matter in which order the changes arrive: the result is the same.
Dependencies
A change depends on the changes that introduced anything it touches: every line it deletes, every line it inserts next to, every file it edits or moves.
flowchart BT
c1["c1: create main.rs"]
c2["c2: insert a line in main.rs"] --> c1
c3["c3: delete a line of c1"] --> c1
c4["c4: create README"]
c5["c5: edit a line c2 wrote"] --> c2
c2andc3both depend onc1, but not on each other.c4depends on nothing.c5depends onc2, and so, through it, onc1.
Commutation
Two changes commute when neither depends on the other, directly or through others. Commuting changes can be applied in either order and give exactly the same result.
flowchart LR
S[State with c1] -->|apply c2, then c3| R[Same result]
S -->|apply c3, then c2| R
This is why Oxygen needs no merge commits. Pulling someone's work adds their changes to the set; anything that did not touch your work simply sits beside it.
Explicit dependencies
Changes can be logically coupled without sharing any lines: an API change in one file and the update of its caller in another. oxy commit --depends-on CHANGE adds a dependency so the two can never be separated. Use it sparingly: every extra dependency turns history back towards a straight line.
Rewriting
Amending, rebasing, splitting or squashing makes a new change with the same ChangeId. Changes that depended on the old one are rewritten automatically, their references translated to the new change. Where a line they referred to no longer exists, the reference moves to the nearest surviving line and oxy tells you.
flowchart BT
subgraph before[Before amending C]
C1[C #aaa]
D1[D #ddd] --> C1
end
subgraph after[After]
C2[C #bbb same ChangeId]
D2[D #eee same ChangeId] --> C2
end
before ==> after