Skip to contents

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      0

One 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      1

A 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 conflicts

Nothing was left behind to inspect:

nrow(dolt_conflicts(con))
#> [1] 0
DBI::dbGetQuery(con, "SELECT region FROM sites WHERE id = 1")
#>   region
#> 1   CORK

To 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             1

The 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:0

Resolve 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   CORK

Note 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 connection

A 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 679154d526afc82fa6a59afa8f3e24f4cfce701e

They 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] 4

dolt_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)