vignette("doltliter") covers a single pass through the
commit loop. This article is about working on more than one line of
history at once: branching, merging, what happens when two branches
disagree, and how to undo things.
library(doltliter)
path <- tempfile(fileext = ".db")
con <- DBI::dbConnect(Doltlite(), path)
dolt_config(con, user.name = "Ada Lovelace", user.email = "ada@example.com")We will curate a small reference table — the kind of data that gets corrected over time, which is where version control earns its keep.
sites <- data.frame(
id = 1:4,
name = c("Ballycotton", "Roches Point", "Mizen Head", "Loop Head"),
region = c("Cork", "Cork", "Cork", "Clare"),
active = c(1L, 1L, 0L, 1L),
stringsAsFactors = FALSE
)
DBI::dbWriteTable(con, "sites", sites,
field.types = c(id = "INTEGER PRIMARY KEY"))
dolt_commit(con, "Initial site list")Branches are per connection
Each connection holds its own active branch. Creating one does not move you onto it:
dolt_branch(con, "review")
dolt_branches(con)[, c("name", "dirty")]
#> name dirty
#> 1 main 0
#> 2 review 0
active_branch(con)
#> [1] "main"dolt_checkout() switches; create = TRUE
does both at once.
dolt_checkout(con, "review")
active_branch(con)
#> [1] "review"Work done here does not touch main:
DBI::dbExecute(con, "UPDATE sites SET active = 1 WHERE name = 'Mizen Head'")
#> [1] 1
dolt_commit(con, "Reactivate Mizen Head")
dolt_checkout(con, "main")
DBI::dbGetQuery(con, "SELECT name, active FROM sites WHERE name = 'Mizen Head'")
#> name active
#> 1 Mizen Head 0One thing to be aware of: uncommitted work belongs to the branch, not the connection. There is no stash — checking out does not shelve changes, and another connection that checks out the same branch will see the same working set.
A clean merge
When the two sides touch different rows, the merge just happens and returns the new commit hash:
dolt_merge(con, "review")
#> [1] "c70730f72cbfd753c0bb05f8f12d30cc35553b04"
DBI::dbGetQuery(con, "SELECT name, active FROM sites ORDER BY id")
#> name active
#> 1 Ballycotton 1
#> 2 Roches Point 1
#> 3 Mizen Head 1
#> 4 Loop Head 1A conflicting merge
Now the interesting case. Two branches edit the same row:
dolt_checkout(con, "regional", create = TRUE)
DBI::dbExecute(con, "UPDATE sites SET region = 'County Cork' WHERE id = 1")
#> [1] 1
dolt_commit(con, "Use full county names")
dolt_checkout(con, "main")
DBI::dbExecute(con, "UPDATE sites SET region = 'CORK' WHERE id = 1")
#> [1] 1
dolt_commit(con, "Normalise regions to upper case")Here is the part that catches people out. Conflicts in DoltLite are never durable — they exist only inside the transaction that produced them. A merge run in autocommit mode, which is what you get by default, is rolled back in full the moment it conflicts:
dolt_merge(con, "regional")
#> Error in `dbBind()`:
#> ! cannot merge: conflicts detected, autocommit transaction rolled back. Run the merge inside BEGIN/COMMIT to inspect dolt_conflicts and dolt_schema_conflicts, resolve with dolt_conflicts_resolve(), then commit with dolt_commit(). Conflicts are never committed as conflictsNothing was left behind to inspect:
nrow(dolt_conflicts(con))
#> [1] 0
DBI::dbGetQuery(con, "SELECT region FROM sites WHERE id = 1")
#> region
#> 1 CORKTo actually work on the conflict, put the merge in a transaction:
DBI::dbBegin(con)
dolt_merge(con, "regional")
#> [1] "Merge has 1 conflict(s). Resolve and then commit with dolt_commit."
dolt_conflicts(con)
#> table num_conflicts
#> 1 sites 1The per-table view carries base_, our_ and
their_ columns, so you can see all three sides at once:
detail <- dolt_conflicts_table(con, "sites")
detail[, grep("region|dolt_conflict_id", names(detail), value = TRUE)]
#> base_region our_region their_region dolt_conflict_id
#> 1 Cork CORK County Cork 1:0Resolve by taking one side wholesale, or by editing the rows directly and then resolving:
dolt_conflicts_resolve(con, "ours")
nrow(dolt_conflicts(con))
#> [1] 0
dolt_commit(con, "Merge regional, keeping upper-case regions")
DBI::dbGetQuery(con, "SELECT region FROM sites WHERE id = 1")
#> region
#> 1 CORKNote that dolt_commit() also ended the SQL transaction,
so there is nothing left for DBI::dbCommit() to do:
DBI::dbCommit(con)
#> Error:
#> ! no transaction is open on this connectionA commit is refused while any conflict remains, so you cannot accidentally record a half-merged state.
Tags
Tags name a commit so you can come back to it:
dolt_tag(con, "v1.0")
dolt_tags(con)[, 1:2]
#> tag_name tag_hash
#> 1 v1.0 679154d526afc82fa6a59afa8f3e24f4cfce701eThey work anywhere a ref does —
dolt_at(con, "sites", "v1.0"), or as a diff endpoint.
Undoing things
dolt_reset() throws away uncommitted work;
"soft" unstages but keeps it, "hard" discards
it:
DBI::dbExecute(con, "DELETE FROM sites WHERE id = 4")
#> [1] 1
DBI::dbGetQuery(con, "SELECT count(*) AS n FROM sites")$n
#> [1] 3
dolt_reset(con, "hard")
DBI::dbGetQuery(con, "SELECT count(*) AS n FROM sites")$n
#> [1] 4dolt_revert() is the one to reach for when the change is
already committed. It does not rewrite history — it writes a
new commit applying the inverse:
DBI::dbExecute(con, "INSERT INTO sites VALUES (5, 'Blackrock', 'CORK', 1)")
#> [1] 1
oops <- dolt_commit(con, "Add Blackrock")
dolt_revert(con, oops)
#> [1] "27433109c45380af53c66c70ff15460c1dc58bd2"
DBI::dbGetQuery(con, "SELECT count(*) AS n FROM sites")$n
#> [1] 4
dolt_log(con)$message[1:2]
#> [1] "Revert \"Add Blackrock\"" "Add Blackrock"dolt_cherry_pick() applies a single commit from
elsewhere onto the current branch, and dolt_rebase()
replays a whole branch onto another. Both resolve conflicts the same way
dolt_merge() does, so the transaction advice above applies
to them too.
DBI::dbDisconnect(con)