No more version history, now we have just the contents of the current version.
Editing a definition no longer changes the order in which definitions load.
This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example:
using driver.love, define Foo = 3 in a definition
define Bar = Foo + 1
edit and redefine Foo = 4
Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading.
After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly.
Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git.
I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
EZHO4TSWIYYUE73S6XQWIEF3HA3H7MKCNJOT27NTWTVSPVS2SL5QC ZTOLDEC3ST5AJE2J7BQSNFD6I5NC5CEJLZ7W3J2VE3HKZWTPUPBQC TXNEYIBKZILXF2A4BZHMASP4I6X5VIQWWBGGEGZGOS7MO5HFVHYQC Y5BXW7FXLHJJN7IAQV5G45VPQBKLWK6G4Q6R3I7Z4FD7MS3447PQC D43U7GQ46MR4O6C3C6VHSU2A5LCKUBKGO7Y32KMYFGMMUDJVAL7QC NMZSTB75TP6BNKRIHSBBRJ2HTRFSIOD35H2QJHETUXVIEZJ3BSJAC CJNKA73FDAYU7A7A2S7K6DPWR2RKVQTSQBZ6AEAWNKZK63HFA53AC D6WPPROPNXZ4WTZ6Y7UVXEL77BB2IN3B23RCQ3EEC5UZ5OEIIOBAC 57HKHZ7Z4QSCS6X35H5WZ5Y4MALGLYAPMUTP25BTU2MYTO6HOLXAC LQGK4PX65UVHK5556477BZWYNX57IXKGVXLEYZQL7Z5EBDZOWQ2AC R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC 5OVKHVY6TJK53NCEUGSFBMHDENBJ25IEZNBWCI6QRCRLRKG5K7NAC CYEH4AXBCTDLTBMWC3THTJHT6KBRNSGQBFQPWTEYRRM3LI2XGKRQC 2DVVKKVA6PJ7VKYLGPQ22AXUB6ZWFMPWB445PRDZJDNLURUFDNDQC LRDM35CEK3OHXOTB7TEFJRL7P6PQWO5ZG3F2BVA7DIDFHBPJQ7KAC BSDXVB3HU5Y5FZ244FU2F577RTM6SWEHAZX3IELBVUP52CRFVDSAC QFURHRTPVQM7FXYJCYJEKQ7ZB7JPFSBEM4GM3SJIEU6MHVYRQ3EQC WNHI74P7U7VMNAQDGPFI7ZFI57HWA7CPUSIVZKCAIQMAIX5PFEYQC UY647VAQW72BNAUPRRREATG54F44WAXAY3SXZVWZSDVHFU4OZJOAC GXE3ESLGGSXI45XXDOBZLAPT6DR2J7Q7LBMSHHYVOPHK3WAALZPQC H3RX6UWRIBSSGIKCHCYBW2UMW63HLV77BULUXS3BIBKKBEOJ64EQC WYKKFV2GP7JRPN4SCWTHECFCQCHCIOMUP2TNNX5YACQAEKJ5QP5QC 3BRGOF7NV52C3CY6HLGH53TDW2OHRQYQHWBEA3P6BKCUTN5DVHQQC FFFJ54GJ3A2HNKZEHO7RHUZ2YWT67EK4B3Y2YJVYCEUANGY6BQQAC FS2ITYYHBLFT66YUC3ENPFYI2HOYHOVEPQIN7NQR6KF5MEK4NKZAC D4FEFHQCSILZFQ5VLWNXAIRZNUMCDNGJSM4UJ6T6FDMMIWYRYILQC json = require 'json'function main(args)local infile = io.open(args[1])local manifest_s = infile:read('*a')infile:close()local manifest = json.decode(manifest_s)local core_filenames = {}for k,v in pairs(manifest) doif not starts_with(k, 'fw_') thentable.insert(core_filenames, k)endendtable.sort(core_filenames)for _,core in ipairs(core_filenames) dolocal filename = ('%04d'):format(manifest[core])..'-'..corelocal f = io.open(filename)if f thenprint(f:read('*a'))print('')endendendfunction starts_with(s, prefix)if #s < #prefix thenreturn falseendfor i=1,#prefix doif s:sub(i,i) ~= prefix:sub(i,i) thenreturn falseendendreturn trueendmain(arg)
# The on-disk representation of freewheeling appsWhen you start up a freewheeling app, you'll see a directory printed out inthe parent terminal (always launch it from a terminal window):```new edits will go to /home/...```When editing such an app using the driver (see [README.md](README.md)), newdefinitions will go into this directory. Let's call it `$SAVE_DIR` in the restof this doc.It is always safe to move such definitions into this repo. (We'll call it `.`in the rest of this doc.) You'll want to do this if you're sharing them withothers, and it's also helpful if the driver crashes on your app. Movingdefinitions will never, ever change app behavior.```sh$ mv -i $SAVE_DIR/[0-9]* . # should never clobber any existing files$ mv $SAVE_DIR/head . # expected to clobber the existing file```Try looking inside the `head` file with a text editor. It'll contain a number,the current version of the _manifest_ for this app. For example:```478```If you moved the files you should see such a file in `.`. If you open thisfile, you'll see a JSON table from definition names to version ids. Forexample:```{ "a": 273, "b": 478}```This means the current definition of `a` is in `0273-a` and of `b` in`0478-b`.Poking around these files gets repetitive, so there's a tool to streamlinethings:```````stitch-live.lua` takes a manifest file as its argument, and prints out allthe definitions that make up the app at that version.To compare two versions of the app, use `stitch-live.lua` to copy thedefinitions in each into a separate file, and use a file comparison tool (e.g.`diff`) to compare the two files.# Scenarios considered in designing this representation* Capture history of changes.- Though it is perhaps too fine-grained and noisy.* Merge changes from non-live forks to live ones.- New files in repo can't hide changes in save dir, because filenames arealways disjoint between the two.- This doesn't apply yet to live updates. Two forks of a single live appwill likely have unnecessary merge conflicts.* No special tools required to publish changes to others.- Just move files from save dir to repo.# Scenarios I _would_ like to take into consideration in the future* Cleaner commits; it's clear what changed.* merge changes between live forks.lua tools/stitch-live.lua 0478-fwmanifestThis means the current state of the app is in a file called `0478-fwmanifest`.
-- on incoming messages to a specific file, however, the app must:-- save the message's value to a new, smallest unused numeric prefix-- execute the value-- if there's an error, go back to the previous value of the same-- definition if one exists
-- on incoming messages to a specific file, the app must:-- determine the definition name from the first word-- execute the value, returning any errors-- look up the filename for the definition or define a new filename for it-- save the message's value to the filename
Live.head = 0Live.next_version = 1Live.history = {} -- array of filename roots corresponding to each numeric prefixLive.manifest = {} -- mapping from roots to numeric prefixes as of version Live.head
Live.filenames_to_load = {} -- filenames in order of numeric prefixLive.filename = {} -- map from definition name to filename (including numeric prefix)Live.final_prefix = 0
local files = {}live.append_files_with_numeric_prefix('', files)table.sort(files)live.check_integrity(files)live.append_files_with_numeric_prefix(love.filesystem.getSaveDirectory(), files)table.sort(files)live.check_integrity(files)Live.history = live.load_history(files)Live.next_version = #Live.history + 1local head_string = love.filesystem.read('head')Live.head = tonumber(head_string)if Live.head > 0 thenLive.manifest = json.decode(love.filesystem.read(live.versioned_manifest(Live.head)))endlive.load_everything_in_manifest()endfunction live.append_files_with_numeric_prefix(dir, files)for _,file in ipairs(love.filesystem.getDirectoryItems(dir)) doif file:match('^%d') thentable.insert(files, file)endendendfunction live.check_integrity(files)local manifest_found, file_found = false, falselocal expected_index = 1for _,file in ipairs(files) dofor numeric_prefix, root in file:gmatch('(%d+)-(.+)') do-- only runs oncelocal index = tonumber(numeric_prefix)-- skip files without numeric prefixesif index ~= nil thenif index < expected_index thenprint(index, expected_index)endassert(index >= expected_index)if index > expected_index thenassert(index == expected_index+1)assert(manifest_found and file_found)expected_index = indexmanifest_found, file_found = false, falseendassert(index == expected_index)if root == 'fwmanifest' thenassert(not manifest_found)manifest_found = trueelseassert(not file_found)file_found = trueend
-- if necessary, copy files from repo to save dirif io.open(love.filesystem.getSaveDirectory()..'/0000-freewheeling-start') == nil thenprint('copying all definitions from repo to save dir')for _,filename in ipairs(love.filesystem.getDirectoryItems('')) dofor numeric_prefix, root in filename:gmatch('(%d+)-(.+)') do-- only runs oncelocal buf = love.filesystem.read(filename)print('copying', filename)love.filesystem.write(filename, buf)
endfunction live.load_history(files)local result = {}for _,file in ipairs(files) dofor numeric_prefix, root in file:gmatch('(%d+)-(.+)') do
-- load files from save dirfor _,filename in ipairs(love.filesystem.getDirectoryItems('')) dofor numeric_prefix, root in filename:gmatch('(%d+)-(.+)') do
local index = tonumber(numeric_prefix)-- skipif index ~= nil thenif root ~= 'fwmanifest' thenassert(index == #result+1)table.insert(result, root)end
if tonumber(numeric_prefix) > 0 then -- skip 0000Live.filename[root] = filenametable.insert(Live.filenames_to_load, filename)Live.final_prefix = math.max(Live.final_prefix, tonumber(numeric_prefix))
function live.load_everything_in_manifest()local files_to_load = {}for k,v in pairs(Live.manifest) do-- Most keys in the manifest are definitions. If we need to store any-- metadata we'll do it in keys starting with a specific prefix.if not starts_with(k, 'fw_') thenlocal root, index = k, vlocal filename = live.versioned_filename(index, root)table.insert(files_to_load, filename)endendtable.sort(files_to_load)for _,filename in ipairs(files_to_load) do
function live.load_all()for _,filename in ipairs(Live.filenames_to_load) do--? print('loading', filename)
Live.manifest[APP] = love.filesystem.getIdentity() -- doesn't need to be persisted, but no harm if it does..live.send_to_driver(json.encode(Live.manifest))
Live.filename[APP] = love.filesystem.getIdentity()live.send_to_driver(json.encode(Live.filename))
local definition_name = buf:match('^%S+%s+(%S+)')Live.manifest[definition_name] = nillive.eval(definition_name..' = nil') -- ignore errors which will likely be from keywords like `function = nil`local next_filename = live.versioned_filename(Live.next_version, definition_name)love.filesystem.write(next_filename, '')table.insert(Live.history, definition_name)live.roll_forward()
local definition_name = buf:match('^%s*%S+%s+(%S+)')if Live.filename[definition_name] thenlocal index = table.find(Live.filenames_to_load, Live.filename[definition_name])table.remove(Live.filenames_to_load, index)live.eval(definition_name..' = nil') -- ignore errors which will likely be from keywords like `function = nil`love.filesystem.remove(Live.filename[definition_name])Live.filename[definition_name] = nilend
local next_filename = live.versioned_filename(Live.next_version, definition_name)love.filesystem.write(next_filename, buf)table.insert(Live.history, definition_name)Live.manifest[definition_name] = Live.next_versionlive.roll_forward()
-- eval succeeded without errors; persist the definitionlocal filename = Live.filename[definition_name]if filename == nil thenLive.final_prefix = Live.final_prefix+1filename = ('%04d-%s'):format(Live.final_prefix, definition_name)table.insert(Live.filenames_to_load, filename)Live.filename[definition_name] = filenameendlove.filesystem.write(filename, buf)
end-- update Live.Head and record the new Live.Manifest (which caller has already modified)function live.roll_forward()Live.manifest[PARENT] = Live.headlocal manifest_filename = live.versioned_manifest(Live.next_version)love.filesystem.write(manifest_filename, json.encode(Live.manifest))Live.head = Live.next_versionlove.filesystem.write('head', tostring(Live.head))Live.next_version = Live.next_version + 1end-- update app.Head and reload app.Manifest appropriatelyfunction live.roll_back()Live.head = Live.manifest[PARENT]love.filesystem.write('head', tostring(Live.head))local previous_manifest_filename = live.versioned_manifest(Live.head)Live.manifest = json.decode(love.filesystem.read(previous_manifest_filename))
This file contains no definition, but is used as a marker in the save dir toindicate all definitions have been copied from the repo to the save dir.