on master help sign in
v2 of `ob upgrade` based on Nix's hashing algorithm

Squashed commit of the following: commit 75a74933673f892fb7363197834ad0da6e8448a5 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 22 11:19:19 2018 -0400 drop migration before merge commit ca03bd342f4b8043ffe0acc74d38a8bedeab09df Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 22 11:19:06 2018 -0400 commentrs commit a92ebcc5a41bce01d70b0a5475e080e7f2e53a3e Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 22 11:10:18 2018 -0400 Toggle handoff here commit 8bf48682ae4552a2ed0610551385436688e866cb Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 22 11:09:46 2018 -0400 Fix broken graph reading - default.nix: include migration dir close to ob command - Resolve symlink when reading ob path - Fix broken graph edge filename parsing: commit 4befbacbd519b422dc43d6e55dba417e0e9a796e Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 22 09:45:14 2018 -0400 create-migration: fail if HEAD is not in graph commit c6c40800e791a45240a7e0e23317ca36b764d8fc Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 19:58:15 2018 -0400 Test change commit 98d9101f69925fd7c6fad79b2323c8454cd2e13f Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 19:57:53 2018 -0400 Backfill for real commit bc85fcecf5f107640177b4722a45eb6865bbfe61 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 19:56:18 2018 -0400 Command to create new commit migration commit 44c1e441ce0d54415361ca3363861008e1d41746 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 19:35:05 2018 -0400 tweak migration msg per ryan commit eb2f0c52c91d33074ff05a092a6d1c601d0fd56a Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 17:48:59 2018 -0400 Avoid duplicate vertices when backfilling commit 36caeec315e2b3e0d704c7a973c0934e8fcdcfbf Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 17:30:21 2018 -0400 Squashed commit of the following: commit 3210f5ee72d37aebe9af391d9b8aaad46c4f80df Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 17:29:48 2018 -0400 drop it commit 1e86f6f4639915fe750768667f3e8377473d14fe Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 17:27:28 2018 -0400 blew commit 1659aeeb9cd0dc6d881fd1387eb431d55671d872 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 17:25:58 2018 -0400 Fail if creating a loop graph commit 6adda5544b97197321417fb338be30d5364bb7d0 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 17:24:02 2018 -0400 Migration for last commit commit c268907003b0c429cba979f18c0a28cebbd8adb7 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 17:23:10 2018 -0400 Initial migrationsa commit 0f366017a739b32065be9a7324cce61b5596da31 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 17:19:54 2018 -0400 add migration, with trivial change to change hash commit 46b4fbf66b2891d1d344fb4704a60f963d65c042 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 17:04:23 2018 -0400 Compute vertex hash using new algo and Backfill migrations for entire git history commit 3757aabd3012897fd2e0951116d8f3779616e30a Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 17:02:51 2018 -0400 a silly bug, that lead to including migration in hashing commit 15b07deeb324b9d68773b29234e14af7b30e4e02 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 17:01:16 2018 -0400 delete migration; still broken commit 236abccd1a386fbec111db987e008f43c7a4cc5f Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 16:59:44 2018 -0400 Fix backfill not actually computing hash for each rev; and add migrations commit f7dd195136dcae3836f2d7f045c7e4c94915bc47 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 16:44:51 2018 -0400 Allow `ob internal hash` to take in an optional revision Need this to debug stuff commit 6e8da9a0fa4fbbbdba4c6ab1de0f3bc8f8298350 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 16:27:16 2018 -0400 Add `ob internal hash` commit 451e78224b44c37505bc9b0ede98e40c3e617e12 Merge: 0650d6f be97ccc Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 16:22:37 2018 -0400 Merge remote-tracking branch 'origin/master' into ob-upgrade-pr--hashLikeNix commit 0650d6fa02ff241dd5c6ebe5809145f5ec3d3fa6 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 16:22:16 2018 -0400 Backfill initial graph, implementation not using yet commit a45000e02e2bb68f9f345dba9138e82e94f471c3 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 15:38:38 2018 -0400 hashing implementation for particular git revision commit fd25f7b9276a68e00c94d40742310fd74d1ceb82 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 14:45:58 2018 -0400 Start working on hashing at rev commit 02b88075ae06cda63a0ab0db968b99da48593771 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 14:35:40 2018 -0400 Initial version of the new hashing algorithm commit 56eebf9064f8b12816a592e3dc636198fe40fffe Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 12:50:04 2018 -0400 Add runCommand helper for nice experience in repls commit d4afde6518d198508f2d851556ff2f1540b6d1c5 Merge: 6e51eee 491fc94 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 12:16:24 2018 -0400 Merge remote-tracking branch 'origin/master' into ob-upgrade-pr--hashLikeNix Incorporate changes from PR #84 commit 6e51eee107fd6aba81dad3995569ef4ac85b0f2d Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 12:01:06 2018 -0400 Add the function spec commit 59451bcd21dbed50e46180c9359360843f284eec Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 21 11:52:09 2018 -0400 Delete current migration graph and scripts commit ab4aa96a6ab77ff781560ebf9a7445a49d16967b Author: Sridhar Ratnakumar <srid@srid.ca> Date: Wed Jun 20 12:36:26 2018 -0400 ob upgrade: use thunk's git branch by default commit 7b3c7c8c1ee32384582dd2978b163485eb68c621 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Tue Jun 19 11:05:22 2018 -0400 Fix: use the cached version commit c827f20ffe8fe80305f63476cb0637f7741f3903 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Tue Jun 19 11:00:29 2018 -0400 Simplify caching and implementation of graph traversal Also remove the test migration content commit fe0828d443e51c9d11dceacd33fbd2c0a92cac58 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 19:52:23 2018 -0400 Memoize graph traversal for better performance commit 7963fbcee1d4d852e2904b679ab6ac40f18f6c51 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 18:53:04 2018 -0400 Refactor migration so that command determines the monoid instances Also have the path finding algorithm directly returning the mappend'ed action. This change is first needed in order to memoize the algorithm commit e6bc5a43d8b437ada2cea7073c3ae189ec05db6f Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 17:11:24 2018 -0400 Simplify migrations output commit 9bc105c7c72e7584312a120d9c2bc15fd6554fba Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 16:50:50 2018 -0400 Revert the upgraded thunk upon errors in 2nd stage commit 28f2d91656be06fb845d4cf1ce1b5d823157ba43 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 16:28:30 2018 -0400 Fix: `ob upgrade` should not try to handoff outside of projects commit 453f6f4fa19b1698a841af5a18665b7d5437f5f2 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 16:19:23 2018 -0400 Handle graph errors using MonadThrow instead of calling fail/error commit 4fc2b7f3ffa6e2a5fbd331d8435bfae4f73e96fa Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 15:23:53 2018 -0400 Don't invoke cli parser before handoff decision commit 79773105392a4b84fd19001584e63f90c1580e8b Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 14:56:09 2018 -0400 Resurrect old handoff logic so bash-completion hopefully starts to work again commit 6c09b99aac9bd840686fe52cd2d3c35cb968326c Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 14:16:17 2018 -0400 Sub-spinner should respect no-trail property of parent spinners Also improve the API of these functions. commit 2e81b1152d10aac76be1828d60ca0f0d35521a53 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 13:00:38 2018 -0400 Allow 1-vertex path Also delete old invocations of findPath commit 259b8538a9c4d39b2db1a819fa9f905b966b6cc5 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 12:51:20 2018 -0400 Add a check to ensure that all paths in DAG sum to the same value All paths in the DAG from `a` to `b` must sum to the same `action` value, otherwise the migration is invalid. commit 5509571917cca90df95a6c704d5461f8ad1953e3 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 11:22:22 2018 -0400 Toggling handoff migration in this commit commit 402ed23ab5924db1c0d1d4b6785c78b78f181e77 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 11:14:05 2018 -0400 Do no check project ob graph during handoff decision Use ambient ob graph here. commit 9ac7615657fa15c29c12da47473341363908fc82 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Mon Jun 18 10:57:25 2018 -0400 Eliminate migrate-only-from-hash and switch over to internal command commit 87f2d8a3f61a26b2d8420719a1bedf0a3eee0a50 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 15:56:34 2018 -0400 Backfill migrations commit b0dcd0ceb02792dff16bbe6d016d88d447df264c Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 15:56:02 2018 -0400 Add `ob upgrade` Squashed commit of the following: commit 4f24bfc962c06da0df202cee9003da93882fb432 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 15:53:01 2018 -0400 update migration commit c6c111e914ca2a54ab44b63f6d0e0122dfc745bc Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 15:52:49 2018 -0400 s commit 2e86ff9af3e7d33d3c7ba02a9f368a4e548966c9 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 15:46:07 2018 -0400 ensure clean repo unless when resuming upgrade commit 07c9d7a78e34ba112d775b00af19f78def6786f8 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 15:40:39 2018 -0400 Ensure that git repo is clean before upgrade amidst other cleanups commit 1bf94695ae4904ea7182443f5af9e01ef2ce051f Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 14:01:08 2018 -0400 more stuff commit fd358e6cca80d0a41723bc8589171a96985de77e Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 13:50:09 2018 -0400 slap a spinner during project ob finding commit ad24ad427175b247ee9404d5b68f2b76903e8cf1 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 13:42:33 2018 -0400 minimize annoying ctrl+e tip commit 7b4d6cd5a1b46d3a56b7513b64a82a91087477f1 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 13:40:47 2018 -0400 fix another spinner bug commit 99a82b6ee878e8665fcb1b327d3cfa92be804b67 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 13:24:26 2018 -0400 fix something in spinner commit 3a10e53ef20f6644b1806eafbddff42870ad0bdd Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 13:17:52 2018 -0400 Nice output commit 626bc685fb44eacde9f87adcd6f9b7e1184eb3d6 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 12:54:44 2018 -0400 reduce verbose thunk spinner commit 8993e26f573c3bcd2f0904c81647d89f33f4e879 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Sat Jun 16 12:37:37 2018 -0400 simplify withSpinner commit 170c2e4aaa81c3451b43ba7bf92eb1d8b1effae7 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 20:07:58 2018 -0400 up migration commit 18f9acb6ce6f0f04d094512e2724ca1941117dd8 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 20:07:38 2018 -0400 Advanced spinner commit 06937f4befc2aa1bd0676c59feb503cdc26f0b99 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 19:54:30 2018 -0400 up migration commit f08b3fa9a0a235542889f5f72964ecc2d99a84ba Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 19:54:06 2018 -0400 fix a spinenr bug commit c370122a086f68f469529a28d817ab78b670227d Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 19:49:32 2018 -0400 update migration commit 41442f43580b1ce855b41b99318af4ba052a0332 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 19:49:21 2018 -0400 spinner final status commit b03a603a7395f24de0e7f1a951dd533ff2092d90 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 19:21:22 2018 -0400 update migration commit b79987b55123cdbc550ad7636a7dfd1458ff61a1 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 19:21:06 2018 -0400 handle nil upgrade and migrations commit c4dba1bdf0f56950457cc2d0aa458493a4958728 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 19:17:10 2018 -0400 update migration commit 227c20654be6535e21b428fe5e2e61b02ec53930 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 19:16:53 2018 -0400 tweak commit 221c517445270a25d50ea8eba7338b29665ab86d Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 19:09:33 2018 -0400 Updated migration commit 8bd414a541d27de67d5b1c3fd7e62f4e5847e838 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 19:08:46 2018 -0400 Don't mess with user's project directory Extract thunk in temporary directory and update only on success commit 3ee98c7fabd46b3a832d75c495b6c086f7342896 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 17:51:08 2018 -0400 update migration commit fe4b8f067d494a0235e7bf834844e297bae203ce Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 17:50:50 2018 -0400 Potential fix for one-vertex bug commit c877787c92ac20afef1a398b8fb255c5df3c9b4b Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 17:32:50 2018 -0400 Run migration/backfill.sh commit c7542aa1a0f7f2c0aeaa3ce45dd4ededf2cc2aac Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 17:32:22 2018 -0400 Add backfill script, and add a TODO about bash completion breakage commit 1e63ee9f3b98bf370131af30cee3dbc2a3815082 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 17:21:00 2018 -0400 Migration for parent commit commit e9d45341d224a7abd2472597e49ab8b8e09d0170 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 17:20:22 2018 -0400 Stop hardcoding my stuff commit bf04f2cf9c6c370b2482b33b9ee7f55673055a58 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 17:12:06 2018 -0400 Update migration commit bc01193162ac282f4e3cda3b5d0c0ba30f9cd2ac Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 17:11:26 2018 -0400 Update migration script to account for new name commit ee1c6741f3fda0fc0d9b7e5fcc96bb087ca42309 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 17:06:21 2018 -0400 Bail out early if hashes are missing commit ed5a25651517117ae4e03f1f65c54f6d240eed91 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 17:01:36 2018 -0400 Include migration directory in nix derivation for command commit cef94c387b56a27e7c0565f0427047b27a7daf34 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 17:01:11 2018 -0400 migration directory is no longer invisible This messes up with nix commit 029fd09f331942f3ec355a97cb109c677663d3ab Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 16:18:14 2018 -0400 Migration for recent commits commit f04084edb580d0dd2fcf71ad88296579880de021 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 16:10:18 2018 -0400 lol optparse-applicative commit 61271fe3416e3d2a62ee54d4684ea9642abf62fe Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 16:07:46 2018 -0400 gr was using old script commit 3301e0cd07e283432a1b344df2bb1abc6d7c5c4b Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 16:00:09 2018 -0400 grr bash commit 6971b251e41dea4d6b30abf81fe5c14b533db2df Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 15:52:43 2018 -0400 more hash computation adjustements commit 328272075a71c8536e8d2817b65d8857589602b8 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 15:40:12 2018 -0400 compute hash instead of reusing vertex commit da1cd1d0d946a9c1d012736ff3a6a8ff18a668e1 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 15:26:00 2018 -0400 Re-enable cycle checking commit b1fea8cad932c68e60a0669bbf0a9eae98ff50a7 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 15:21:56 2018 -0400 Fix a bug in path finding commit 3ab6e4d89df8493f7a14e074b2b34e7c2a3cbca9 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 15:12:05 2018 -0400 Fix initial migration graph: remove duplicate vertices Also add README.md detailing how we generate it commit 50406e2b1c4e512a50bca4ef5bbda3463f6f5859 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 14:55:37 2018 -0400 runMigration should use the path finding algo commit 2e618265ced6fd4d51d0adca9e3b60a42ae68e79 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 13:40:53 2018 -0400 try finding reverse path commit c8a2a3bac161d603be39124187e970e545b09082 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 13:32:41 2018 -0400 display multi adjacements if error commit 62a02aeda56dd15e2f9966a811de8dbd2cbf34bd Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 13:22:49 2018 -0400 disable cycle detection temporarily commit a2d43b11e78ad91c70c30e7e134ef7cb2d0f9ba0 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 13:20:54 2018 -0400 show graph to use better representation commit b29b0b5382e8048a966dd348fa6563b0def683fc Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 13:06:51 2018 -0400 bug fixes commit 81f8a80022fdc87100cb0eb42c63f0bf4158bbb8 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 12:53:26 2018 -0400 Handoff decision implementation commit b1c4da25566cc7c5358c1cd2f97f18076f6975bf Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 11:37:22 2018 -0400 Refactor handing off code so it is easier to modify commit 371055a05d1a3dcac7ca3067f819e5432775c63b Author: Sridhar Ratnakumar <srid@srid.ca> Date: Fri Jun 15 10:59:50 2018 -0400 Refactor so yesterday's code is readable commit eb2daf03cce4b584018145f51c6cccedf6486210 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 14 17:49:15 2018 -0400 Initial handoff flow WIP prototype A bunch of undefined for future work commit cd5c85cbadc4c346be8ea81afd98ae7f2ca4b3d8 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 14 16:10:48 2018 -0400 migration: Falsify all handoff edges before upgrade We'll make the merge edge True commit ffb2bbef31acb5b539a36e267bd96113338190ea Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 14 15:16:44 2018 -0400 readGraph: read from new edge-directory format Also add `ob internal showgraph` for diagnosis commit f8a075c8726cc72423d1c35ba952d6cdfec3af0a Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 14 14:19:12 2018 -0400 ob upgrade: Switch over the new hash script commit ff06d1e83017cd97da9759b5df115e6a553f343a Merge: 721051a 1a0ef14 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 14 14:04:18 2018 -0400 Merge branch 'initial-migrations' into ob-upgrade commit 721051aaebe1907a690be04459873f561475b6a5 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Wed Jun 13 16:19:39 2018 -0400 Workaround https://github.com/obsidiansystems/obelisk/issues/78 commit ddfc7f52e1dad5365346c6f3873e737c0196f5b3 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Wed Jun 13 16:14:12 2018 -0400 ob upgrade should handoff before migrating Initial implementation, not cleaned up commit 1e6ee6351d0d10a0932007d2b9144759960e2a0a Author: Sridhar Ratnakumar <srid@srid.ca> Date: Tue Jun 12 20:53:04 2018 -0400 Make obelisk-migration its own package under lib/ commit bff070506c6f46b9d9128e2ee18b0a7ac6fa69c6 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Tue Jun 12 17:49:58 2018 -0400 remove hardcoded path commit 8e4aff2afc2970a069ed012c26bec72a89f9e5ad Author: Sridhar Ratnakumar <srid@srid.ca> Date: Tue Jun 12 17:43:55 2018 -0400 Tweak logging commit dca43b8901d60a2c05c6d8512d22df80305677e3 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Tue Jun 12 17:32:55 2018 -0400 Finish (basically) working `ob upgrade` commit 39676037fc24efaf3d221ee4003c6061e86ed267 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Tue Jun 12 17:22:48 2018 -0400 Get the first and last vertex of graph commit 66c51486b338fd18816f11cdce346fc0a12ab5af Author: Sridhar Ratnakumar <srid@srid.ca> Date: Tue Jun 12 17:09:47 2018 -0400 Detect missing migration in obelisk commit 33254a2ce745e58c304f342ff7506528681ae32b Author: Sridhar Ratnakumar <srid@srid.ca> Date: Tue Jun 12 16:25:52 2018 -0400 Migration graph implementation, for use by ob upgrade Not yet tested commit c7d1322858aaad7b34b9fec546b57f0fcf1f62d7 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Tue Jun 12 13:30:27 2018 -0400 Start `ob upgrade` command Just updates the obelisk impl thunk to latest master for now. commit 1a0ef14c05ae274d559aa7fa62ae970a155403af Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 14 13:39:25 2018 -0400 Backfill handoff migration graph commit 1675813a29b08905359d89681ceb6eeb508ca62b Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 14 13:38:09 2018 -0400 Backfill obelisk upgrade migrations ... with empty file (representing zero actions required from user part). commit 72c1d6a1e4b2895a14a66a08ea30f75abf57ee40 Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 14 13:36:00 2018 -0400 Handoff uses the same hashing commit 7a73c9243be08ab579e9baa36bdd3d543044967a Author: Sridhar Ratnakumar <srid@srid.ca> Date: Thu Jun 14 13:35:11 2018 -0400 Add hashing script for obelisk

commitd108104914330dd96571d28ea6ba4597c2f1e559 authorSridhar Ratnakumar <srid@srid.ca> dateFri 22 Jun 2018, 11:20 (8 years ago) parentbe97ccc
17 files changed+889−136
default.nix+16−3 lib/cliapp/src/Obelisk/CliApp.hs+2 lib/cliapp/src/Obelisk/CliApp/Demo.hs+9−8 lib/cliapp/src/Obelisk/CliApp/Logging.hs+2−2 lib/cliapp/src/Obelisk/CliApp/Process.hs+2−2 lib/cliapp/src/Obelisk/CliApp/Spinner.hs+78−24 lib/cliapp/src/Obelisk/CliApp/Types.hs+1−1 lib/command/obelisk-command.cabal+2 lib/command/src/Obelisk/Command.hs+115−48 lib/command/src/Obelisk/Command/Deploy.hs+2−2 lib/command/src/Obelisk/Command/Nix.hs+3−3 lib/command/src/Obelisk/Command/Project.hs+2−2 lib/command/src/Obelisk/Command/Thunk.hs+62−36 lib/command/src/Obelisk/Command/Upgrade.hs+366 lib/command/src/Obelisk/Command/Utils.hs+51−5 lib/migration/obelisk-migration.cabal+24 lib/migration/src/Obelisk/Migration.hs+152
default.nix+16−3
@@ -90,6 +90,14 @@ let
9090 --replace " arbitrary = (:|) <$> arbitrary <*> arbitrary" ""
9191 '';
9292 })).override { megaparsec = super.megaparsec_6_1_1; };
93+ algebraic-graphs =
94+ let src = pkgs.fetchFromGitHub {
95+ owner = "snowleopard";
96+ repo = "alga";
97+ rev = "480a73137e9b38ad3f1bc2c628847953d2fb3e25";
98+ sha256 = "0dpwi5ffs88brl3lz51bwb004c6zm8ds8pkw1vzsg2a6aaiyhlzl";
99+ };
100+ in pkgs.haskell.lib.dontCheck (self.callCabal2nix "algebraic-graphs" src {});
93101 network-transport = self.callHackage "network-transport" "0.5.2" {};
94102 network-transport-tcp = self.callHackage "network-transport-tcp" "0.6.0" {};
95103 };
@@ -104,11 +112,12 @@ let
104112 addLibs = self: super: {
105113 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {};
106114 obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (hackGet ./lib/asset + "/serve-snap") {};
107− obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
108115 obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
116+ obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
109117 obelisk-command = (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}).override { Cabal = super.Cabal_2_0_0_2; };
110118 obelisk-executable-config = executableConfig.haskellPackage self;
111119 obelisk-executable-config-inject = executableConfig.platforms.web.inject self; # TODO handle platforms.{ios,android}
120+ obelisk-migration = self.callCabal2nix "obelisk-migration" (cleanSource ./lib/migration) {};
112121 obelisk-run = self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {};
113122 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
114123 obelisk-snap = self.callCabal2nix "obelisk-snap" (cleanSource ./lib/snap) {};
@@ -123,9 +132,13 @@ rec {
123132 inherit (reflex-platform) nixpkgs pinBuildInputs;
124133 path = reflex-platform.filterGit ./.;
125134 obelisk = ghcObelisk;
126− command = pkgs.runCommand ghcObelisk.obelisk-command.name { nativeBuildInputs = [pkgs.makeWrapper]; } ''
135+ commandWithMigration = ghcObelisk.obelisk-command.overrideAttrs (drv: {
136+ postInstall = (drv.postInstall or "") +
137+ ''cp -r ${./migration} $out/migration;'';
138+ });
139+ command = pkgs.runCommand commandWithMigration.name { nativeBuildInputs = [pkgs.makeWrapper]; } ''
127140 mkdir -p "$out/bin"
128− ln -s '${ghcObelisk.obelisk-command}/bin/ob' "$out/bin/ob"
141+ ln -s '${commandWithMigration}/bin/ob' "$out/bin/ob"
129142 wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
130143 '';
131144 commandEnv = ghcObelisk.obelisk-command.env;
lib/cliapp/src/Obelisk/CliApp.hs+2
@@ -15,6 +15,8 @@ module Obelisk.CliApp
1515
1616 -- .Spinner
1717 , withSpinner
18+ , withSpinnerNoTrail
19+ , withSpinner'
1820
1921 -- .Logging
2022 , newCliConfig
lib/cliapp/src/Obelisk/CliApp/Demo.hs+9−8
@@ -1,11 +1,12 @@
11{-# LANGUAGE FlexibleContexts #-}
22{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE ScopedTypeVariables #-}
34module Obelisk.CliApp.Demo where
45
56import Control.Concurrent (threadDelay)
67import Control.Monad.IO.Class (liftIO)
78import Control.Monad.Reader (MonadIO)
8−import Data.Monoid ((<>))
9+import Data.Semigroup ((<>))
910import qualified Data.Text as T
1011import System.Process (proc)
1112
@@ -13,20 +14,19 @@ import Control.Monad.Catch (MonadMask)
1314
1415import Obelisk.CliApp
1516
16−cliDemo
17− :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m)
18− => m ()
17+cliDemo :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m) => m ()
1918cliDemo = withSpinner "CLI Demo" $ do
2019 putLog Notice "This demo will showcase the CLI library functionality"
21− withSpinner "A long-running task" $ do
20+ _ <- withSpinner' "Searching long for something" (Just $ \c -> "Discovered " <> T.pack (show c) <> " objects") $ do
2221 delay
2322 withSpinner "Nested task" $ do
2423 delay
2524 putLog Notice "In nested task"
26− withSpinner "Inbetween" $ do
27− withSpinner "Runnning something specific" $ do
25+ -- This is a 'no trail' spinner that won't leave a trail (even its sub spinners)
26+ withSpinnerNoTrail "In between, doing something temporary" $ do
27+ withSpinner "Runnning something deep inside" $ do
2828 delay
29− putLog Notice "This task has the same name and still works"
29+ putLog Notice "You can still log from inside no-trail spinners"
3030 delay
3131 delay
3232 putLog Error "Top nested task finished"
@@ -36,6 +36,7 @@ cliDemo = withSpinner "CLI Demo" $ do
3636 putLog Notice "This is some info mesage"
3737 putLog Warning "And now a warning as well"
3838 delay
39+ return (42 :: Integer)
3940 putLog Notice "Now we start a 2nd spinner, run a couple of process, the last of which fails:"
4041 withSpinner "Looking around" $ do
4142 delay
lib/cliapp/src/Obelisk/CliApp/Logging.hs+2−2
@@ -36,7 +36,7 @@ newCliConfig sev noColor noSpinner = do
3636 level <- newIORef sev
3737 lock <- newMVar False
3838 tipDisplayed <- newIORef False
39− stack <- newIORef []
39+ stack <- newIORef ([], [])
4040 return $ CliConfig level noColor noSpinner lock tipDisplayed stack
4141
4242runCli :: MonadIO m => CliConfig -> CliT m a -> m a
@@ -163,7 +163,7 @@ allowUserToMakeLoggingVerbose keyCode = bracket showTip (liftIO . killThread) $
163163 where
164164 showTip = fork $ unlessVerbose $ do
165165 conf <- getCliConfig
166− liftIO $ threadDelay $ 3*1000000 -- Only show tip for actions taking too long (3 seconds or more)
166+ liftIO $ threadDelay $ 10*1000000 -- Only show tip for actions taking too long (10 seconds or more)
167167 tipDisplayed <- liftIO $ atomicModifyIORef' (_cliConfig_tipDisplayed conf) $ (,) True
168168 unless tipDisplayed $ unlessVerbose $ do -- Check again in case the user had pressed Ctrl+e recently
169169 putLog Notice "Tip: Press Ctrl+e to display full output"
lib/cliapp/src/Obelisk/CliApp/Process.hs+2−2
@@ -73,7 +73,7 @@ createProcess
7373 :: (MonadIO m, Cli m)
7474 => CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
7575createProcess p = do
76− putLog Debug $ "Creating process: " <> T.pack (show p)
76+ putLog Debug $ "Creating process: " <> T.pack (show $ cmdspec p)
7777 liftIO $ Process.createProcess p
7878
7979-- | Like `System.Process.createProcess_` but also logs (debug) the process being run
@@ -81,7 +81,7 @@ createProcess_
8181 :: (MonadIO m, Cli m)
8282 => String -> CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
8383createProcess_ name p = do
84− putLog Debug $ "Creating process " <> T.pack name <> ": " <> T.pack (show p)
84+ putLog Debug $ "Creating process " <> T.pack name <> ": " <> T.pack (show $ cmdspec p)
8585 liftIO $ Process.createProcess p
8686
8787-- | Like `System.Process.callProcess` but also logs (debug) the process being run
lib/cliapp/src/Obelisk/CliApp/Spinner.hs+78−24
@@ -1,8 +1,15 @@
11{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE LambdaCase #-}
23{-# LANGUAGE MultiParamTypeClasses #-}
34{-# LANGUAGE OverloadedStrings #-}
5+{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TupleSections #-}
47-- | Provides a simple CLI spinner that interoperates cleanly with the rest of the logging output.
5−module Obelisk.CliApp.Spinner (withSpinner) where
8+module Obelisk.CliApp.Spinner
9+ ( withSpinner
10+ , withSpinnerNoTrail
11+ , withSpinner'
12+ ) where
613
714import Control.Concurrent (killThread, threadDelay)
815import Control.Monad (forM_, (>=>))
@@ -12,6 +19,7 @@ import Control.Monad.Log (Severity (..), logMessage)
1219import Control.Monad.Reader (MonadIO)
1320import Data.IORef
1421import qualified Data.List as L
22+import Data.Maybe (isNothing)
1523import Data.Text (Text)
1624import System.Console.ANSI (Color (Blue, Cyan, Green, Red))
1725
@@ -23,39 +31,85 @@ import Obelisk.CliApp.Types (Cli, CliConfig (..), HasCliConfig, Output (..), get
2331withSpinner
2432 :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m)
2533 => Text -> m a -> m a
26−withSpinner s f = do
34+withSpinner s = withSpinner' s $ Just $ const s
35+
36+-- | A spinner that leaves no trail after a successful run.
37+--
38+-- Use if you wish the spinner to be 'temporary' visually to the user.
39+--
40+-- The 'no trail' property automatically carries over to sub-spinners (in that they won't
41+-- leave a trail either).
42+withSpinnerNoTrail
43+ :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m)
44+ => Text -> m a -> m a
45+withSpinnerNoTrail s = withSpinner' s Nothing
46+
47+-- | Advanced version that controls the display and content of the trail message.
48+withSpinner'
49+ :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m)
50+ => Text
51+ -> Maybe (a -> Text) -- ^ Leave an optional trail with the given message creator
52+ -> m a
53+ -> m a
54+withSpinner' msg mkTrail action = do
2755 noSpinner <- _cliConfig_noSpinner <$> getCliConfig
2856 if noSpinner
29− then putLog Notice s >> f
30− else bracket' run cleanup $ const f
57+ then putLog Notice msg >> action
58+ else bracket' run cleanup $ const action
3159 where
3260 run = do
33− -- Add this log to the spinner stack
34− stack <- _cliConfig_spinnerStack <$> getCliConfig
35− wasEmpty <- liftIO $ atomicModifyIORef' stack $ \old -> (TerminalString_Normal s : old, null old)
36− if wasEmpty
37− then do -- Fork a thread to manage output of anything on the stack
61+ -- Add this log to the spinner stack, and start a spinner if it is top-level.
62+ modifyStack pushSpinner >>= \case
63+ True -> do -- Top-level spinner; fork a thread to manage output of anything on the stack
3864 ctrleThread <- fork $ allowUserToMakeLoggingVerbose enquiryCode
3965 spinnerThread <- fork $ runSpinner spinner $ \c -> do
40− logs <- liftIO $ concatStack c <$> readIORef stack
66+ logs <- renderSpinnerStack c . snd <$> readStack
4167 logMessage $ Output_Overwrite logs
4268 pure [ctrleThread, spinnerThread]
43− else pure []
44− cleanup failed tids = do
45− stack <- _cliConfig_spinnerStack <$> getCliConfig
69+ False -> -- Sub-spinner; nothing to do.
70+ pure []
71+ cleanup tids resultM = do
4672 liftIO $ mapM_ killThread tids
4773 logMessage Output_ClearLine
48− let mark = if failed
49− then TerminalString_Colorized Red "✖"
50− else TerminalString_Colorized Green "✔"
51− -- Delete this log from the spinner stack
52− logs <- liftIO $ atomicModifyIORef' stack $ \old -> (L.delete (TerminalString_Normal s) old, concatStack mark old)
53− logMessage $ Output_Write logs
74+ logsM <- modifyStack $ popSpinner $ case resultM of
75+ Nothing ->
76+ ( TerminalString_Colorized Red "✖"
77+ , Just msg -- Always display final message if there was an exception.
78+ )
79+ Just result ->
80+ ( TerminalString_Colorized Green "✔"
81+ , mkTrail <*> pure result
82+ )
83+ forM_ logsM $ logMessage . Output_Write -- Last message, finish off with newline.
84+ pushSpinner (flag, old) =
85+ ( (isTemporary : flag, TerminalString_Normal msg : old)
86+ , null old -- Is empty?
87+ )
88+ where
89+ isTemporary = isNothing mkTrail
90+ popSpinner (mark, trailMsgM) (flag, old) =
91+ ( (newFlag, new)
92+ -- With final trail spinner message to render
93+ , renderSpinnerStack mark . (: new) . TerminalString_Normal <$> (
94+ if inTemporarySpinner then Nothing else trailMsgM
95+ )
96+ )
97+ where
98+ inTemporarySpinner = or newFlag -- One of our parent spinners is temporary
99+ newFlag = drop 1 flag
100+ new = L.delete (TerminalString_Normal msg) old
101+ readStack = liftIO . readIORef
102+ =<< fmap _cliConfig_spinnerStack getCliConfig
103+ modifyStack f = liftIO . flip atomicModifyIORef' f
104+ =<< fmap _cliConfig_spinnerStack getCliConfig
54105 spinner = coloredSpinner defaultSpinnerTheme
55106
56107-- | How nested spinner logs should be displayed
57−concatStack :: TerminalString -> [TerminalString] -> [TerminalString]
58−concatStack mark = L.intersperse space . go . L.reverse
108+renderSpinnerStack
109+ :: TerminalString -- ^ That which comes before the final element in stack
110+ -> [TerminalString] -- ^ Spinner elements in reverse order
111+ -> [TerminalString]
112+renderSpinnerStack mark = L.intersperse space . go . L.reverse
59113 where
60114 go [] = []
61115 go (x:[]) = mark : [x]
@@ -86,9 +140,9 @@ defaultSpinnerTheme :: SpinnerTheme
86140defaultSpinnerTheme = spinnerCircleHalves
87141
88142-- | Like `bracket` but the `release` function can know whether an exception was raised
89−bracket' :: MonadMask m => m a -> (Bool -> a -> m b) -> (a -> m c) -> m c
143+bracket' :: MonadMask m => m a -> (a -> Maybe c -> m b) -> (a -> m c) -> m c
90144bracket' acquire release use = mask $ \unmasked -> do
91145 resource <- acquire
92− result <- unmasked (use resource) `onException` release True resource
93− _ <- release False resource
146+ result <- unmasked (use resource) `onException` release resource Nothing
147+ _ <- release resource $ Just result
94148 return result
lib/cliapp/src/Obelisk/CliApp/Types.hs+1−1
@@ -20,7 +20,7 @@ data CliConfig = CliConfig
2020 , _cliConfig_noSpinner :: Bool -- Disallow spinners
2121 , _cliConfig_lock :: MVar Bool -- Whether the last message was an Overwrite output
2222 , _cliConfig_tipDisplayed :: IORef Bool -- Whether the user tip (to make verbose) was already displayed
23− , _cliConfig_spinnerStack :: IORef [TerminalString] -- Stack of logs from nested spinners
23+ , _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString]) -- Stack of logs from nested spinners
2424 }
2525
2626data Output
lib/command/obelisk-command.cabal+2
@@ -15,6 +15,7 @@ library
1515 , binary
1616 , bytestring
1717 , obelisk-cliapp
18+ , obelisk-migration
1819 , containers
1920 , data-default
2021 , directory
@@ -48,6 +49,7 @@ library
4849 Obelisk.Command.Project
4950 Obelisk.Command.Run
5051 Obelisk.Command.Thunk
52+ Obelisk.Command.Upgrade
5153 Obelisk.Command.Utils
5254 Obelisk.Command.VmBuilder
5355 -- -fobject-code is so that the StaticPointers extension can work in ghci
lib/command/src/Obelisk/Command.hs+115−48
@@ -14,8 +14,9 @@ import Data.Bool (bool)
1414import qualified Data.ByteString.Base16 as Base16
1515import qualified Data.ByteString.Lazy as LBS
1616import Data.List
17−import Data.Maybe (catMaybes)
17+import Data.Maybe (catMaybes, listToMaybe)
1818import Data.Monoid
19+import Data.Text (Text)
1920import qualified Data.Text as T
2021import Data.Text.Encoding
2122import GHC.StaticPtr
@@ -28,13 +29,16 @@ import System.IO (hIsTerminalDevice, stdout)
2829import System.Posix.Process (executeFile)
2930
3031import Obelisk.App
31−import Obelisk.CliApp (Severity (..), failWith, getLogLevel, newCliConfig, putLog)
32+import Obelisk.CliApp
3233import Obelisk.CliApp.Demo (cliDemo)
3334import Obelisk.Command.Deploy
3435import Obelisk.Command.Project
35−import qualified Obelisk.Command.VmBuilder as VmBuilder
3636import Obelisk.Command.Run
3737import Obelisk.Command.Thunk
38+import Obelisk.Command.Upgrade
39+import Obelisk.Command.Utils (getObeliskExe)
40+import qualified Obelisk.Command.VmBuilder as VmBuilder
41+import Obelisk.Migration (Hash)
3842
3943
4044data Args = Args
@@ -87,11 +91,15 @@ data ObCommand
8791 | ObCommand_Run
8892 | ObCommand_Thunk ThunkCommand
8993 | ObCommand_Repl
94+ | ObCommand_Upgrade (Maybe Text)
9095 | ObCommand_Internal ObInternal
9196 deriving Show
9297
9398data ObInternal
9499 = ObInternal_RunStaticIO StaticKey
100+ | ObInternal_Hash (Maybe Text)
101+ | ObInternal_CreateMigration
102+ | ObInternal_Migrate Hash
95103 | ObInternal_CLIDemo
96104 deriving Show
97105
@@ -101,8 +109,10 @@ inNixShell' p = withProjectRoot "." $ \root -> do
101109 projectShell root False "ghc" cmd
102110 where
103111 mkCmd = do
104− obArgs <- getObArgs =<< getArgsConfig
105− progName <- getExecutablePath
112+ argsCfg <- getArgsConfig
113+ myArgs <- getArgs
114+ obArgs <- parseCLIArgs argsCfg myArgs
115+ progName <- getObeliskExe
106116 return $ progName : catMaybes
107117 [ Just "--no-handoff"
108118 , bool Nothing (Just "--verbose") $ _args_verbose obArgs
@@ -119,6 +129,7 @@ obCommand cfg = hsubparser
119129 , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
120130 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
121131 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
132+ , command "upgrade" $ info (ObCommand_Upgrade <$> argument (maybeReader $ Just . Just . T.pack) (action "branch" <> metavar "GITBRANCH" <> value Nothing <> help "Git branch of obelisk to update to (defaults to the thunk's branch)")) $ progDesc "Upgrade Obelisk in the project"
122133 ])
123134 <|> subparser
124135 (mconcat
@@ -188,6 +199,9 @@ data DeployInitOpts = DeployInitOpts
188199internalCommand :: Parser ObInternal
189200internalCommand = subparser $ mconcat
190201 [ command "run-static-io" $ info (ObInternal_RunStaticIO <$> argument (eitherReader decodeStaticKey) (action "static-key")) mempty
202+ , command "hash" $ info (ObInternal_Hash <$> argument (maybeReader $ Just . Just . T.pack) (action "rev" <> metavar "GITREVISION" <> value Nothing <> help "Calculate for the specified git revision (defaults to working copy")) $ progDesc "Computes hash of working directory (or git revision)"
203+ , command "create-migration" $ info (pure ObInternal_CreateMigration) mempty
204+ , command "migrate" $ info (ObInternal_Migrate <$> strArgument (action "fromHash" <> metavar "FROMHASH" <> help "Migrate from this hash")) $ progDesc "Perform a migrate from the given hash to HEAD of obelisk thunk"
191205 , command "clidemo" $ info (pure ObInternal_CLIDemo) mempty
192206 ]
193207
@@ -232,66 +246,106 @@ parserPrefs = defaultPrefs
232246 { prefShowHelpOnEmpty = True
233247 }
234248
235−-- | Are we on a terminal with active user interaction?
236−isInteractiveTerm :: IO Bool
237−isInteractiveTerm = do
238− isTerm <- hIsTerminalDevice stdout
239− -- Running in bash/fish/zsh completion
240− inShellCompletion <- liftIO $ isInfixOf "completion" . unwords <$> getArgs
241− return $ isTerm && not inShellCompletion
249+parseCLIArgs :: ArgsConfig -> [String] -> IO Args
250+parseCLIArgs cfg as = pure as >>= handleParseResult . execParserPure parserPrefs (argsInfo cfg)
242251
243−mkObeliskConfig :: ArgsConfig -> IO Obelisk
244−mkObeliskConfig argsCfg = do
252+-- | Create an Obelisk config for this process.
253+mkObeliskConfig :: IO Obelisk
254+mkObeliskConfig = do
255+ cliArgs <- getArgs
256+ -- This function should not use argument parser (full argument parsing happens post handoff)
257+ let logLevel = toLogLevel $ "-v" `elem` cliArgs
245258 notInteractive <- not <$> isInteractiveTerm
246− logLevel <- toLogLevel <$> getObArgs argsCfg
247259 cliConf <- newCliConfig logLevel notInteractive notInteractive
248260 return $ Obelisk cliConf
249261 where
250− toLogLevel = bool Notice Debug . _args_verbose
251−
252−getObArgs :: ArgsConfig -> IO Args
253−getObArgs cfg = getArgs >>= handleParseResult . execParserPure parserPrefs (argsInfo cfg)
262+ toLogLevel = bool Notice Debug
263+ isInteractiveTerm = do
264+ isTerm <- hIsTerminalDevice stdout
265+ -- Running in bash/fish/zsh completion
266+ inShellCompletion <- liftIO $ isInfixOf "completion" . unwords <$> getArgs
267+ return $ isTerm && not inShellCompletion
268+
269+-- For use from development obelisk repls
270+--
271+-- Example:
272+-- > runCommand $ someFuncInMonadObelisk ...
273+runCommand :: ObeliskT IO a -> IO a
274+runCommand f = flip runObelisk f =<< mkObeliskConfig
254275
255276main :: IO ()
256−main = do
257− argsCfg <- getArgsConfig
258− mkObeliskConfig argsCfg >>= (`runObelisk` ObeliskT (main' argsCfg))
277+main = runCommand . main' =<< getArgsConfig
259278
260279main' :: MonadObelisk m => ArgsConfig -> m ()
261280main' argsCfg = do
262281 obPath <- liftIO getExecutablePath
263− obArgs <- liftIO $ getObArgs argsCfg
282+ myArgs <- liftIO getArgs
264283 logLevel <- getLogLevel
265284 putLog Debug $ T.pack $ unwords
266285 [ "Starting Obelisk <" <> obPath <> ">"
267− , "args=" <> show obArgs
286+ , "args=" <> show myArgs
268287 , "logging-level=" <> show logLevel
269288 ]
270289
271− --TODO: We'd like to actually use the parser to determine whether to hand off,
272− --but in the case where this implementation of 'ob' doesn't support all
273− --arguments being passed along, this could fail. For now, we don't bother
274− --with optparse-applicative until we've done the handoff.
275− myArgs <- liftIO getArgs
276− let go as = do
277− args' <- liftIO $ handleParseResult (execParserPure parserPrefs (argsInfo argsCfg) as)
278− case _args_noHandOffPassed args' of
279− False -> return ()
280− True -> putLog Warning "--no-handoff should only be passed once and as the first argument; ignoring"
281− ob $ _args_command args'
282− handoffAndGo as = findProjectObeliskCommand "." >>= \case
283− Nothing -> go as -- If not in a project, just run ourselves
284− Just impl -> do
285− -- Invoke the real implementation, using --no-handoff to prevent infinite recursion
286− putLog Debug $ "Handing off to " <> T.pack impl
287− liftIO $ executeFile impl False ("--no-handoff" : myArgs) Nothing
288− case myArgs of
289− "--no-handoff" : as -> go as -- If we've been told not to hand off, don't hand off
290− a:as -- Otherwise bash completion would always hand-off even if the user isn't trying to
291− | "--bash-completion" `isPrefixOf` a
292− && "--no-handoff" `elem` as -> go (a:as)
293− | otherwise -> handoffAndGo (a:as)
294− as -> handoffAndGo as
290+ (mainWithHandOff argsCfg <=< parseHandoff) =<< liftIO getArgs
291+
292+-- Type representing the result of a handoff calculation.
293+data HandOff m
294+ = HandOff_Yes FilePath [String] -- Handoff immediately to the given impl with the given args
295+ | HandOff_Decide (m Bool) FilePath [String] -- Handoff only if the action returns True
296+ | HandOff_No [String] -- Do not handoff, and continue with the given args
297+
298+mainWithHandOff :: MonadObelisk m => ArgsConfig -> HandOff m -> m ()
299+mainWithHandOff argsCfg = \case
300+ HandOff_Yes impl as -> do
301+ putLog Debug $ "Handing off to project obelisk " <> T.pack impl
302+ liftIO $ executeFile impl False ("--no-handoff" : as) Nothing
303+ HandOff_Decide f impl as -> do
304+ withSpinner' "Deciding whether to handoff"
305+ (Just $ bool
306+ "Decided /not/ to handoff to project obelisk"
307+ "Decided to handoff to project obelisk") f >>= \case
308+ True -> mainWithHandOff argsCfg $ HandOff_Yes impl as
309+ False -> mainWithHandOff argsCfg $ HandOff_No as
310+ HandOff_No as -> do
311+ putLog Debug $ "Not handing off"
312+ args' <- liftIO $ parseCLIArgs argsCfg as
313+ warnIfExtraneousFlag args'
314+ ob $ _args_command args'
315+ where
316+ warnIfExtraneousFlag a = case _args_noHandOffPassed a of
317+ False -> return ()
318+ True -> putLog Warning $
319+ "Ignoring unexpected --no-handoff (should only be passed once and as the first argument)"
320+
321+-- Handoff logic
322+parseHandoff :: MonadObelisk m => [String] -> m (HandOff m)
323+parseHandoff as' = case hasNoHandoff as' of
324+ (True, as) ->
325+ pure $ HandOff_No as
326+ (False, as) -> findProjectObeliskCommand "." >>= \case
327+ Nothing -> do
328+ putLog Debug "Not in a project; no need to hand off"
329+ pure $ HandOff_No as
330+ Just impl -> case getSubCommand as of
331+ Just "upgrade" ->
332+ pure $ HandOff_Decide (decideHandOffToProjectOb ".") impl as
333+ _ ->
334+ pure $ HandOff_Yes impl as
335+ where
336+ getSubCommand = listToMaybe . filter (not . isPrefixOf "-")
337+ hasNoHandoff = \case
338+ "--no-handoff" : xs ->
339+ -- If we've been told not to hand off, don't hand off
340+ (True, xs)
341+ x:xs
342+ -- Otherwise bash completion would always hand-off even if the user isn't trying to
343+ | "--bash-completion" `isPrefixOf` x && "--no-handoff" `elem` xs ->
344+ (True, x:xs)
345+ | otherwise ->
346+ (False, x:xs)
347+ xs ->
348+ (False, xs)
295349
296350ob :: MonadObelisk m => ObCommand -> m ()
297351ob = \case
@@ -330,12 +384,25 @@ ob = \case
330384 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
331385 ThunkCommand_Pack thunks -> forM_ thunks $ \(ThunkPackOpts dir upstream) -> packThunk dir (T.pack upstream)
332386 ObCommand_Repl -> runRepl
387+ ObCommand_Upgrade branch -> do
388+ upgradeObelisk "." branch
333389 ObCommand_Internal icmd -> case icmd of
334390 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
335391 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
336392 Just p -> do
337393 c <- getObelisk
338394 liftIO $ runObelisk c $ deRefStaticPtr p
395+ ObInternal_Hash revM -> case revM of
396+ Nothing -> do
397+ hash <- getDirectoryHash [migrationDirName] "."
398+ putLog Notice $ "Hash: " <> hash
399+ Just rev -> do
400+ [hash] <- getHashAtGitRevision [rev] [migrationDirName] "."
401+ putLog Notice $ "Hash for " <> rev <> ": " <> hash
402+ ObInternal_CreateMigration -> do
403+ createMigrationEdgeFromHEAD "."
404+ ObInternal_Migrate fromHash -> do
405+ migrateObelisk "." fromHash
339406 ObInternal_CLIDemo -> cliDemo
340407
341408--TODO: Clean up all the magic strings throughout this codebase
lib/command/src/Obelisk/Command/Deploy.hs+2−2
@@ -78,7 +78,7 @@ deployPush deployPath getNixBuilders = do
7878 Right (ThunkData_Packed _) ->
7979 build
8080 Right (ThunkData_Checkout _) -> do
81− checkGitCleanStatus srcPath >>= \case
81+ checkGitCleanStatus srcPath True >>= \case
8282 True -> do
8383 result <- build
8484 packThunk srcPath "origin" -- get upstream
@@ -107,7 +107,7 @@ deployPush deployPath getNixBuilders = do
107107 ]
108108 ]
109109 deployAndSwitch res
110− isClean <- checkGitCleanStatus deployPath
110+ isClean <- checkGitCleanStatus deployPath True
111111 when (not isClean) $ do
112112 withSpinner "Commiting changes to Git" $ do
113113 callProcessAndLogOutput (Debug, Error) $ proc "git"
lib/command/src/Obelisk/Command/Nix.hs+3−3
@@ -17,7 +17,7 @@ import qualified Data.Text as T
1717import System.Process (proc)
1818
1919import Obelisk.App (MonadObelisk)
20−import Obelisk.CliApp (Severity (..), readProcessAndLogStderr, withSpinner)
20+import Obelisk.CliApp
2121
2222-- | Where to put nix-build output
2323data OutLink
@@ -56,7 +56,7 @@ instance Default NixBuildConfig where
5656 def = NixBuildConfig def def mempty mempty
5757
5858nixBuild :: MonadObelisk m => NixBuildConfig -> m FilePath
59−nixBuild cfg = withSpinner msg $ do
59+nixBuild cfg = withSpinner' ("Running nix-build on " <> desc) (Just $ const $ "Built " <> desc) $ do
6060 readProcessAndLogStderr Debug $ proc "nix-build" $ mconcat
6161 [[path], attrArg, args, outLink, buildersArg]
6262 where
@@ -70,7 +70,7 @@ nixBuild cfg = withSpinner msg $ do
7070 OutLink_Default -> []
7171 OutLink_None -> ["--no-out-link"]
7272 OutLink_IndirectRoot l -> ["--out-link", l]
73− msg = T.pack $ "Running nix-build on " <> path <> maybe "" (\a -> " [" <> a <> "]") attr
73+ desc = T.pack $ path <> maybe "" (\a -> " [" <> a <> "]") attr
7474 buildersArg = case _nixBuildConfig_builders cfg of
7575 [] -> []
7676 builders -> ["--builders", intercalate ";" builders]
lib/command/src/Obelisk/Command/Project.hs+2−2
@@ -33,7 +33,7 @@ import GitHub.Data.GitData (Branch)
3333import GitHub.Data.Name (Name)
3434
3535import Obelisk.App (MonadObelisk)
36−import Obelisk.CliApp (Severity (..), callProcessAndLogOutput, createProcess_, failWith, putLog, withSpinner)
36+import Obelisk.CliApp
3737import Obelisk.Command.Thunk
3838--TODO: Make this module resilient to random exceptions
3939
@@ -110,7 +110,7 @@ initProject source = withSystemTempDirectory "ob-init" $ \tmpDir -> do
110110--TODO: Allow the user to ignore our security concerns
111111-- | Find the Obelisk implementation for the project at the given path
112112findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
113−findProjectObeliskCommand target = do
113+findProjectObeliskCommand target = withSpinnerNoTrail "Locating project obelisk" $ do
114114 myUid <- liftIO getRealUserID
115115 targetStat <- liftIO $ getFileStatus target
116116 (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat myUid >>= \case
lib/command/src/Obelisk/Command/Thunk.hs+62−36
@@ -14,6 +14,7 @@ module Obelisk.Command.Thunk
1414 , ThunkData (..)
1515 , ReadThunkError (..)
1616 , GitHubSource (..)
17+ , getThunkGitBranch
1718 , getLatestRev
1819 , updateThunkToLatest
1920 , createThunk
@@ -23,6 +24,7 @@ module Obelisk.Command.Thunk
2324 , unpackThunk
2425 , packThunk
2526 , readThunk
27+ , updateThunk
2628 , getThunkPtr
2729 , getThunkPtr'
2830 ) where
@@ -71,9 +73,8 @@ import System.Posix (getSymbolicLinkStatus, modificationTime)
7173import System.Process (proc)
7274
7375import Obelisk.App (MonadObelisk)
74−import Obelisk.CliApp (Severity (..), callProcessAndLogOutput, failWith, putLog, readProcessAndLogStderr,
75− withExitFailMessage, withSpinner)
76−import Obelisk.Command.Utils (checkGitCleanStatus, gitProc)
76+import Obelisk.CliApp
77+import Obelisk.Command.Utils
7778
7879--TODO: Support symlinked thunk data
7980data ThunkData
@@ -122,6 +123,11 @@ data GitSource = GitSource
122123 }
123124 deriving (Show, Eq, Ord)
124125
126+getThunkGitBranch :: ThunkPtr -> Maybe Text
127+getThunkGitBranch (ThunkPtr _ src) = fmap untagName $ case src of
128+ ThunkSource_GitHub s -> _gitHubSource_branch s
129+ ThunkSource_Git s -> Just $ _gitSource_branch s
130+
125131commitNameToRef :: Name Commit -> Ref
126132commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
127133
@@ -405,7 +411,7 @@ createThunkWithLatest target s = do
405411 }
406412
407413updateThunkToLatest :: MonadObelisk m => FilePath -> m ()
408−updateThunkToLatest target = withSpinner ("Updating thunk " <> T.pack target <> " to latest") $ do
414+updateThunkToLatest target = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $ do
409415 (overwrite, ptr) <- readThunk target >>= \case
410416 Left err -> failWith $ T.pack $ "thunk update: " <> show err
411417 Right c -> case c of
@@ -572,11 +578,40 @@ nixBuildAttrWithCache exprPath attr = do
572578 , _nixBuildConfig_outLink = OutLink_None
573579 }
574580
575−unpackThunk
576− :: MonadObelisk m
577− => FilePath
578− -> m ()
579−unpackThunk thunkDir = readThunk thunkDir >>= \case
581+-- | Safely update thunk using a custom action
582+--
583+-- A temporary working space is used to do any update. When the custom
584+-- action successfully completes, the resulting (packed) thunk is copied
585+-- back to the original location.
586+updateThunk :: MonadObelisk m => FilePath -> (FilePath -> m a) -> m a
587+updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
588+ p' <- copyThunkToTmp tmpDir p
589+ unpackThunk' True p'
590+ result <- f p'
591+ updateThunkFromTmp p' "origin" -- TODO: stop hardcoding remote
592+ return result
593+ where
594+ copyThunkToTmp tmpDir thunkDir = readThunk thunkDir >>= \case
595+ Left err -> failWith $ "withThunkUnpacked: " <> T.pack (show err)
596+ Right (ThunkData_Packed _) -> do
597+ let tmpThunk = tmpDir </> "thunk"
598+ callProcessAndLogOutput (Notice, Error) $
599+ proc "cp" ["-r", "-T", thunkDir, tmpThunk]
600+ return tmpThunk
601+ Right _ -> failWith $ "Thunk is not packed"
602+ updateThunkFromTmp p' remote = do
603+ packThunk' True p' remote
604+ callProcessAndLogOutput (Notice, Error) $
605+ proc "cp" ["-r", "-T", p', p]
606+
607+finalMsg :: Bool -> (a -> Text) -> Maybe (a -> Text)
608+finalMsg noTrail s = if noTrail then Nothing else Just s
609+
610+unpackThunk :: MonadObelisk m => FilePath -> m ()
611+unpackThunk = unpackThunk' False
612+
613+unpackThunk' :: MonadObelisk m => Bool -> FilePath -> m ()
614+unpackThunk' noTrail thunkDir = readThunk thunkDir >>= \case
580615 Left err -> failWith $ "thunk unpack: " <> T.pack (show err)
581616 --TODO: Overwrite option that rechecks out thunk; force option to do so even if working directory is dirty
582617 Right (ThunkData_Checkout _) -> failWith "thunk unpack: thunk is already unpacked"
@@ -592,18 +627,18 @@ unpackThunk thunkDir = readThunk thunkDir >>= \case
592627 Nothing -> failWith "Cannot determine clone URI for thunk source"
593628 Just c -> return $ T.unpack $ getUrl c
594629
595− withSpinner ("Retrieving thunk " <> T.pack thunkName <> " from GitHub") $ do
596− callProcessAndLogOutput (Notice, Notice) $
630+ withSpinner' ("Fetching thunk " <> T.pack thunkDir)
631+ ( finalMsg noTrail $ const $ "Fetched thunk " <> T.pack thunkDir) $ do
632+ callProcessAndLogOutput (Debug, Debug) $
597633 proc "hub" ["clone", "-n", githubURI, tmpRepo]
598634 --If this directory already exists then something is weird and we should fail
599635 liftIO $ createDirectory obGitDir
600− callProcessAndLogOutput (Notice, Error) $
636+ callProcessAndLogOutput (Debug, Error) $
601637 proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
602638
603− withSpinner ("Preparing thunk in " <> T.pack thunkDir) $ do
604639 -- Checkout
605− putLog Notice $ "Checking out " <> T.pack (show $ maybe "" untagName $ _gitHubSource_branch s)
606− callProcessAndLogOutput (Notice, Notice) $
640+ putLog Debug $ "Checking out " <> T.pack (show $ maybe "" untagName $ _gitHubSource_branch s)
641+ callProcessAndLogOutput (Debug, Debug) $
607642 proc "hub" $ concat
608643 [ ["-C", tmpRepo]
609644 , pure "checkout"
@@ -612,13 +647,14 @@ unpackThunk thunkDir = readThunk thunkDir >>= \case
612647 ]
613648 -- Set upstream branch
614649 forM_ (_gitHubSource_branch s) $ \branch ->
615− callProcessAndLogOutput (Notice, Error) $
650+ callProcessAndLogOutput (Debug, Error) $
616651 proc "hub" ["-C", tmpRepo, "branch", "-u", "origin/" <> T.unpack (untagName branch)]
617652 callProcessAndLogOutput (Notice, Error) $ proc "rm" ["-r", thunkDir]
618653 callProcessAndLogOutput (Notice, Error) $ proc "mv" ["-T", tmpRepo, thunkDir]
619654
620655 ThunkSource_Git s -> do
621− withSpinner ("Retreiving thunk " <> T.pack thunkName <> " from Git repo") $ do
656+ withSpinner' ("Fetching thunk " <> T.pack thunkName)
657+ (finalMsg noTrail $ const $ "Fetched thunk " <> T.pack thunkName) $ do
622658 let git = callProcessAndLogOutput (Notice, Notice) . gitProc tmpRepo
623659 git ["clone", if _gitSource_fetchSubmodules s then "--recursive" else "", show (_gitSource_url s)]
624660 git ["reset", "--hard", Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr]
@@ -626,7 +662,6 @@ unpackThunk thunkDir = readThunk thunkDir >>= \case
626662 git ["submodule", "update", "--recursive", "--init"]
627663 git ["branch", "-u", "origin/" <> T.unpack (untagName $ _gitSource_branch s)]
628664
629− withSpinner ("Preparing thunk in " <> T.pack thunkDir) $ do
630665 liftIO $ createDirectory obGitDir
631666 callProcessAndLogOutput (Notice, Error) $
632667 proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
@@ -637,16 +672,16 @@ unpackThunk thunkDir = readThunk thunkDir >>= \case
637672
638673--TODO: add force mode to pack even if changes are present
639674--TODO: add a rollback mode to pack to the original thunk
640−packThunk
641− :: MonadObelisk m
642− => FilePath
643− -> Text
644− -> m ()
645−packThunk thunkDir upstream = readThunk thunkDir >>= \case
675+packThunk :: MonadObelisk m => FilePath -> Text -> m ()
676+packThunk = packThunk' False
677+
678+packThunk' :: MonadObelisk m => Bool -> FilePath -> Text -> m ()
679+packThunk' noTrail thunkDir upstream = readThunk thunkDir >>= \case
646680 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
647681 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
648682 Right (ThunkData_Checkout _) -> do
649− withSpinner ("Packing thunk " <> T.pack thunkDir) $ do
683+ withSpinner' ("Packing thunk " <> T.pack thunkDir)
684+ (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
650685 thunkPtr <- getThunkPtr thunkDir upstream
651686 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
652687 liftIO $ createThunk thunkDir thunkPtr
@@ -656,13 +691,8 @@ getThunkPtr = getThunkPtr' True
656691
657692getThunkPtr' :: MonadObelisk m => Bool -> FilePath -> Text -> m ThunkPtr
658693getThunkPtr' checkClean thunkDir upstream = do
659− when checkClean $ checkGitCleanStatus thunkDir >>= \case
660− False -> do
661− statusDebug <- readGitProcess thunkDir ["status", "--ignored"]
662− putLog Warning "cannot proceed with unsaved working copy (git status):"
663− putLog Notice statusDebug
664− failWith "thunk pack: thunk checkout contains unsaved modifications"
665− True -> return ()
694+ when checkClean $ ensureCleanGitRepo thunkDir True $
695+ "thunk pack: thunk checkout contains unsaved modifications"
666696
667697 -- Check whether there are any stashes
668698 stashOutput <- readGitProcess thunkDir ["stash", "list"]
@@ -788,7 +818,3 @@ parseSshShorthand uri = do
788818 guard $ isNothing (T.findIndex (=='/') authAndHostname)
789819 && not (T.null colonAndPath)
790820 parseURI $ T.unpack properUri
791−
792−
793−readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
794−readGitProcess repo = fmap T.pack . readProcessAndLogStderr Notice . gitProc repo
lib/command/src/Obelisk/Command/Upgrade.hsnew file+366
@@ -0,0 +1,366 @@
1+{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3+{-# LANGUAGE LambdaCase #-}
4+{-# LANGUAGE MultiParamTypeClasses #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE ScopedTypeVariables #-}
7+module Obelisk.Command.Upgrade where
8+
9+import Control.Monad (forM, forM_, unless, void)
10+import Control.Monad.Catch (onException)
11+import Control.Monad.IO.Class (liftIO)
12+import Data.Bool (bool)
13+import Data.Maybe (catMaybes)
14+import Data.Monoid (Any (..), getAny)
15+import Data.Semigroup ((<>))
16+import qualified Data.Set as S
17+import Data.Text (Text)
18+import qualified Data.Text as T
19+import System.Directory
20+import System.FilePath
21+import System.IO.Temp
22+import System.Posix.Process (executeFile)
23+import System.Process (cwd, proc)
24+
25+import Obelisk.App (MonadObelisk)
26+import Obelisk.CliApp
27+import Obelisk.Command.Utils
28+
29+import Obelisk.Command.Project (toImplDir)
30+import Obelisk.Command.Project (findProjectObeliskCommand)
31+import Obelisk.Command.Thunk (ThunkData (..), getThunkGitBranch, readThunk, updateThunk)
32+
33+import Obelisk.Migration
34+
35+newtype HandOffAction = HandoffAction Any
36+ deriving (Monoid, Ord, Eq, Show)
37+
38+instance Action HandOffAction where
39+ parseEdgeMeta = HandoffAction . Any . (== "True")
40+
41+data MigrationGraph
42+ = MigrationGraph_ObeliskUpgrade
43+ | MigrationGraph_ObeliskHandoff
44+ deriving (Eq, Show)
45+
46+graphName :: MigrationGraph -> Text
47+graphName = \case
48+ MigrationGraph_ObeliskHandoff -> "obelisk-handoff"
49+ MigrationGraph_ObeliskUpgrade -> "obelisk-upgrade"
50+
51+fromGraphName :: Text -> MigrationGraph
52+fromGraphName = \case
53+ "obelisk-handoff" -> MigrationGraph_ObeliskHandoff
54+ "obelisk-upgrade" -> MigrationGraph_ObeliskUpgrade
55+ _ -> error "Invalid graph name specified"
56+
57+ensureCleanProject :: MonadObelisk m => FilePath -> m ()
58+ensureCleanProject project =
59+ ensureCleanGitRepo project False "Cannot upgrade with uncommited changes"
60+
61+-- | Decide whether we (ambient ob) should handoff to project obelisk before performing upgrade
62+decideHandOffToProjectOb :: MonadObelisk m => FilePath -> m Bool
63+decideHandOffToProjectOb project = do
64+ ensureCleanProject project
65+ updateThunk (toImplDir project) $ \projectOb -> do
66+ (ambientGraph, ambientHash) <- getAmbientObInfo
67+ projectHash <- computeVertexHash projectOb
68+ case hasVertex projectHash ambientGraph of
69+ False -> do
70+ putLog Warning "Project ob not found in ambient ob's migration graph; handing off anyway"
71+ return True
72+ True -> findPathAction ambientGraph projectHash ambientHash >>= \case
73+ Nothing -> do
74+ putLog Warning "No migration path between project and ambient ob; handing off anyway"
75+ return True
76+ Just (HandoffAction dontHandoff) -> do
77+ return $ not $ getAny dontHandoff
78+ where
79+ getAmbientObInfo = do
80+ ambientOb <- getAmbientOb
81+ getMigrationGraph' ambientOb MigrationGraph_ObeliskHandoff >>= \case
82+ Nothing -> do
83+ failWith "Ambient ob has no migration (this can't be possible)"
84+ Just m -> do
85+ -- We don't have ambient's ob source code, so locate its hash from the
86+ -- graph. The last vertex should be it.
87+ ambientHash <- getLast (_migration_graph m) >>= \case
88+ Nothing -> failWith "Ambient ob has no (last) vertex"
89+ Just v -> pure v
90+ unless (hasVertex ambientHash m) $
91+ failWith "Ambient ob's hash is not in its own graph"
92+ return (m, ambientHash)
93+
94+-- | Return the path to the current ('ambient') obelisk process Nix directory
95+getAmbientOb :: MonadObelisk m => m FilePath
96+getAmbientOb = takeDirectory . takeDirectory <$> liftIO getObeliskExe
97+
98+upgradeObelisk :: MonadObelisk m => FilePath -> Maybe Text -> m ()
99+upgradeObelisk project gitBranchM = do
100+ ensureCleanProject project
101+ gitBranch <- maybe (getObeliskBranch project) pure gitBranchM
102+ updateObelisk project gitBranch >>= handOffToNewOb project
103+
104+getObeliskBranch :: MonadObelisk m => FilePath -> m Text
105+getObeliskBranch project = readThunk (toImplDir project) >>= \case
106+ Left e -> failWith $ T.pack $ show e
107+ Right (ThunkData_Checkout _) -> failWith "obelisk thunk must be packed"
108+ Right (ThunkData_Packed tptr) -> case getThunkGitBranch tptr of
109+ Just v -> pure v
110+ Nothing ->
111+ failWith "You must specify a git branch to `ob upgrade` as obelisk thunk does not specify any."
112+
113+updateObelisk :: MonadObelisk m => FilePath -> Text -> m Hash
114+updateObelisk project gitBranch =
115+ withSpinner ("Fetching new Obelisk [" <> gitBranch <> "]") $
116+ updateThunk (toImplDir project) $ \obImpl -> do
117+ fromHash <- computeVertexHash obImpl
118+ callProcessAndLogOutput (Debug, Debug) $
119+ gitProc obImpl ["checkout", T.unpack gitBranch]
120+ callProcessAndLogOutput (Debug, Debug) $
121+ gitProc obImpl ["pull"]
122+ return fromHash
123+
124+handOffToNewOb :: MonadObelisk m => FilePath -> Hash -> m ()
125+handOffToNewOb project fromHash = do
126+ impl <- withSpinner' "Preparing for handoff" (Just $ ("Handed off to new Obelisk " <>) . T.pack) $
127+ findProjectObeliskCommand project >>= \case
128+ Nothing -> failWith "Not an Obelisk project"
129+ Just impl -> pure impl
130+ let opts = ["internal", "migrate", T.unpack fromHash]
131+ liftIO $ executeFile impl False ("--no-handoff" : opts) Nothing
132+
133+migrateObelisk :: MonadObelisk m => FilePath -> Hash -> m ()
134+migrateObelisk project fromHash = void $ withSpinner' "Migrating to new Obelisk" (Just id) $ do
135+ updateThunk (toImplDir project) $ \obImpl -> revertObImplOnFail obImpl $ do
136+ toHash <- computeVertexHash obImpl
137+ g <- getMigrationGraph' obImpl MigrationGraph_ObeliskUpgrade >>= \case
138+ Nothing -> failWith "New obelisk has no migration metadata"
139+ Just m -> pure m
140+
141+ unless (hasVertex fromHash g) $ do
142+ failWith $ "Current obelisk hash " <> fromHash <> " missing in migration graph of new obelisk"
143+ unless (hasVertex toHash g) $ do
144+ -- This usually means that the target obelisk branch does not have
145+ -- migration vertex for its latest commit; typically due to developer
146+ -- negligence.
147+ failWith $ "New obelisk hash " <> toHash <> " missing in its migration graph"
148+
149+ if fromHash == toHash
150+ then do
151+ pure $ "No upgrade available (new Obelisk is the same)"
152+ else do
153+ putLog Debug $ "Migrating from " <> fromHash <> " to " <> toHash
154+ findPathAction g fromHash toHash >>= \case
155+ Nothing -> do
156+ failWith "Unable to find migration path"
157+ Just action -> do
158+ unless (action == mempty) $ do
159+ putLog Notice "To upgrade your project to the new version of obelisk, please follow these instructions:\n"
160+ putLog Notice action
161+ pure $ "Migrated from " <> fromHash <> " to " <> toHash
162+ where
163+ revertObImplOnFail impl f = f `onException` do
164+ putLog Notice $ T.pack $ "Reverting changes to " <> impl
165+ callProcessAndLogOutput (Notice, Notice) $ gitProc project ["checkout", impl]
166+
167+-- | Get the migration graph for project
168+getMigrationGraph'
169+ :: (Action action, MonadObelisk m)
170+ => FilePath -> MigrationGraph -> m (Maybe (Migration action))
171+getMigrationGraph' obDir graph = do
172+ let name = graphName graph
173+ dir = migrationDir obDir
174+ putLog Debug $ "Reading migration graph " <> name <> " from " <> T.pack dir
175+ liftIO $ readGraph dir name
176+
177+computeVertexHash :: MonadObelisk m => FilePath -> m Hash
178+computeVertexHash = getDirectoryHash [migrationDirName]
179+
180+migrationDir :: FilePath -> FilePath
181+migrationDir project = project </> migrationDirName
182+
183+migrationIgnore :: [FilePath]
184+migrationIgnore = [migrationDirName]
185+
186+migrationDirName :: FilePath
187+migrationDirName = "migration"
188+
189+-- TODO: Move this to migration library? (but we rely on wrapProgram exes)
190+
191+-- | Get the unique hash of the given directory
192+--
193+-- Excludes the following before computing the hash:
194+-- * the specified top-level files/ directories.
195+-- * .git directory
196+-- * untracked Git files
197+-- * ignored Git files
198+--
199+-- Uses the same predictive algorithm that Nix (`nix hash-path`).
200+--
201+-- This function will do a full copy of the directory to a temporary location before
202+-- computing the hash. Because it will be deleting the files in exclude list, and
203+-- other files if the directory is a git repo. This needs to be done as `nix hash-path`
204+-- doesn't support taking an excludes list.
205+getDirectoryHash :: MonadObelisk m => [FilePath] -> FilePath -> m Hash
206+getDirectoryHash excludes dir = withSystemTempDirectory "obelisk-hash-" $ \tmpDir -> do
207+ withSpinnerNoTrail (T.pack $ "Copying " <> dir <> " to " <> tmpDir) $ do
208+ runProc $ copyDir dir tmpDir
209+ getDirectoryHashDestructive excludes tmpDir
210+
211+-- Do /not/ call this directly! Call `getDirectoryHash` instead.
212+getDirectoryHashDestructive :: MonadObelisk m => [FilePath] -> FilePath -> m Hash
213+getDirectoryHashDestructive excludes dir = do
214+ liftIO (doesDirectoryExist $ dir </> ".git") >>= \case
215+ True -> do
216+ tidyUpGitWorkingCopy dir
217+ withSpinnerNoTrail "Removing .git directory" $
218+ liftIO $ removePathForcibly $ dir </> ".git"
219+ False -> pure ()
220+ withSpinnerNoTrail "Removing excluded paths" $ do
221+ forM_ (fmap (dir </>) excludes) $
222+ liftIO . removePathForcibly
223+ nixHash dir
224+
225+createMigrationEdgeFromHEAD :: MonadObelisk m => FilePath -> m ()
226+createMigrationEdgeFromHEAD obDir = do
227+ headHash <- getHeadVertex obDir
228+ wcHash <- getDirectoryHash migrationIgnore obDir
229+ if (headHash == wcHash)
230+ then
231+ putLog Warning "No migration necessary (working copy has not changed from HEAD)"
232+ else do
233+ -- TODO: Add `ob internal create-migration --backfill=n` do it for all revisions.
234+ -- See the failure condition in getHeadVertex
235+ written <- writeEdge (migrationDir obDir) headHash wcHash
236+ unless written $
237+ putLog Warning "No migration was created"
238+
239+-- | Return the hash corresponding to HEAD
240+--
241+-- Fail if the hash does not exist in project's migration graph.
242+getHeadVertex :: MonadObelisk m => FilePath -> m Hash
243+getHeadVertex obDir = do
244+ projectGraph :: Migration Text <- getMigrationGraph' obDir MigrationGraph_ObeliskUpgrade >>= \case
245+ Nothing -> failWith "No migration graph found"
246+ Just g -> pure g
247+ [headHash] <- getHashAtGitRevision ["HEAD"] migrationIgnore obDir
248+ unless (hasVertex headHash projectGraph) $ do
249+ -- This means that the HEAD commit has no vertex in the graph,
250+ -- possible due to developer negligence when commiting it.
251+ -- Perhaps we should use a post-commit hook or some such thing
252+ -- to reject such commits in the first place? For now, just
253+ -- error out.
254+ failWith $ "No vertex found for HEAD (" <> headHash <> ")"
255+ return headHash
256+
257+-- | TODO: Verify the integrity of the migration graph in relation to the Git repo.
258+verifyGraph :: MonadObelisk m => FilePath -> m ()
259+verifyGraph = undefined
260+
261+-- | Create, or update, the migration graph with new edges and new vertices
262+-- corresponding to every commit in the Git history from HEAD.
263+--
264+-- NOTE: This creates a linear graph, and doesn't follow the Git graph
265+-- structure.
266+backfillGraph :: MonadObelisk m => Maybe Int -> FilePath -> m ()
267+backfillGraph lastN project = do
268+ revs <- fmap (takeM lastN . fmap (fst . T.breakOn " ") . T.lines) $
269+ readProc $ gitProc project ["log", "--pretty=oneline"]
270+ -- Note: we need to take unique hashes only; this is fine for backfilling.
271+ -- But future migrations should ensure that there are no duplicate hashes
272+ -- (which would cause cycles) such as those introduced by revert commits.
273+ vertices :: [Hash] <- withSpinnerNoTrail "Computing hash for git history" $
274+ fmap (unique . reverse) $ getHashAtGitRevision revs [migrationDirName] project
275+ void $ withSpinner'
276+ ("Backfilling with " <> tshow (length vertices) <> " vertices")
277+ (Just $ \n -> "Backfilled " <> tshow n <> " edges.") $ do
278+ let vertexPairs = zip vertices $ drop 1 vertices
279+ let edgesDir = migrationDir project
280+ liftIO $ createDirectoryIfMissing False edgesDir
281+ fmap (length . filter (== True)) $ forM vertexPairs $ \(v1, v2) -> do
282+ writeEdge edgesDir v1 v2
283+ where
284+ -- Return unique items in the list /while/ preserving order
285+ unique = loop mempty
286+ where
287+ loop _ [] = []
288+ loop s (x : xs)
289+ | S.member x s = loop s xs
290+ | otherwise = x : loop (S.insert x s) xs
291+ takeM n' xs = case n' of
292+ Just n -> take n xs
293+ Nothing -> xs
294+
295+-- | Write the edge to filesystem.
296+--
297+-- Return True if the edge was created, False if already exists.
298+writeEdge :: MonadObelisk m => FilePath -> Hash -> Hash -> m Bool
299+writeEdge dir v1 v2 = do
300+ unless (v1 /= v2) $
301+ failWith $ "Cannot create self loop with: " <> v1
302+ let edgeDir = dir </> (T.unpack $ v1 <> "-" <> v2)
303+ liftIO (doesDirectoryExist edgeDir) >>= \case
304+ True -> pure False
305+ False -> do
306+ putLog Notice $ T.pack $ "Creating edge " <> edgeDir
307+ liftIO $ createDirectory edgeDir
308+ forM_ actionFiles $ \fp -> liftIO $
309+ writeFile (edgeDir </> fp) ""
310+ pure True
311+ where
312+ actionFiles = ["obelisk-handoff", "obelisk-upgrade"]
313+
314+getHashAtGitRevision :: MonadObelisk m => [Text] -> [FilePath] -> FilePath -> m [Hash]
315+getHashAtGitRevision revs excludes dir = withSystemTempDirectory "obelisk-hashrev-" $ \tmpDir -> do
316+ withSpinner (T.pack $ "Copying " <> dir <> " to " <> tmpDir) $ do
317+ runProc $ copyDir dir tmpDir
318+ tidyUpGitWorkingCopy tmpDir
319+ -- Discard changes to tracked files
320+ runProcSilently $ gitProc tmpDir ["reset", "--hard"]
321+ forM revs $ \rev -> do
322+ runProcSilently $ gitProc tmpDir ["checkout", T.unpack rev]
323+ -- Checking out an arbitrary revision can leave untracked files (from
324+ -- previous revison) around, so tidy them up.
325+ tidyUpGitWorkingCopy tmpDir
326+ withFilesStashed tmpDir (excludes <> [".git"]) $
327+ nixHash tmpDir
328+ where
329+ withFilesStashed base fs m = withSystemTempDirectory "obelisk-hashrev-stash-" $ \stashDir -> do
330+ existingPaths <- fmap catMaybes $ forM fs $ \p -> do
331+ liftIO (doesPathExist $ base </> p) >>= pure . bool Nothing (Just p)
332+ forM_ existingPaths $ \p ->
333+ liftIO $ renamePath (base </> p) (stashDir </> p)
334+ result <- m
335+ forM_ existingPaths $ \p ->
336+ liftIO $ renamePath (stashDir </> p) (base </> p)
337+ return result
338+
339+nixHash :: MonadObelisk m => FilePath -> m Hash
340+nixHash dir = withSpinnerNoTrail "Running `nix hash-path`" $
341+ readProc $ proc "nix" ["hash-path", "--type", "md5", dir]
342+
343+-- | Clean up the following files in the git working copy
344+--
345+-- * Paths ignored by .gitignored, but still present in the filesystem
346+-- * Untracked files (not added to git index)
347+-- * Any empty directories (these are not tracked by git)
348+--
349+-- Note that this leaves modified (staged or unstaged) files as they are.
350+tidyUpGitWorkingCopy :: MonadObelisk m => FilePath -> m ()
351+tidyUpGitWorkingCopy dir = withSpinnerNoTrail "Tidying up git working copy" $ do
352+ ignored <- gitLsFiles dir ["--ignored", "--exclude-standard", "--others"]
353+ untracked <- gitLsFiles dir ["--exclude-standard", "--others"]
354+ putLog Debug $ T.pack $ "Found " <> show (length ignored) <> " ignored files."
355+ putLog Debug $ T.pack $ "Untracked:\n" <> unlines untracked
356+ putLog Debug $ T.pack $ "Ignored:\n" <> unlines ignored
357+ withSpinnerNoTrail "Removing untracked and ignored files" $ do
358+ forM_ (fmap (dir </>) $ ignored <> untracked) $
359+ liftIO . removePathForcibly
360+ -- Empty directories won't be included in these lists. Git doesn't track them
361+ -- So we must delete these separately.
362+ runProc $ proc "find" [dir, "-depth", "-empty", "-type", "d", "-delete"]
363+ where
364+ gitLsFiles pwd opts = fmap lines $ readProcessAndLogStderr Error $
365+ (proc "git" $ ["ls-files", "."] <> opts) { cwd = Just pwd }
366+
lib/command/src/Obelisk/Command/Utils.hs+51−5
@@ -1,20 +1,43 @@
11{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE LambdaCase #-}
23{-# LANGUAGE OverloadedStrings #-}
34module Obelisk.Command.Utils where
45
56import Control.Applicative (liftA2)
7+import Data.Bool (bool)
68import Data.Semigroup ((<>))
9+import Data.Text (Text)
710import qualified Data.Text as T
11+import System.Directory (canonicalizePath)
12+import System.Environment (getExecutablePath)
813import qualified System.Process as P
914
1015import Obelisk.App (MonadObelisk)
11−import Obelisk.CliApp (Severity (..), callProcessAndLogOutput, readProcessAndLogStderr)
16+import Obelisk.CliApp
17+
18+getObeliskExe :: IO FilePath
19+getObeliskExe = getExecutablePath >>= canonicalizePath
1220
1321-- Check whether the working directory is clean
14−checkGitCleanStatus :: MonadObelisk m => FilePath -> m Bool
15−checkGitCleanStatus repo = do
16− let git = readProcessAndLogStderr Debug . gitProc repo
17− null <$> liftA2 (<>) (git ["status", "--porcelain", "--ignored"]) (git ["diff"])
22+checkGitCleanStatus :: MonadObelisk m => FilePath -> Bool -> m Bool
23+checkGitCleanStatus repo withIgnored = do
24+ let
25+ runGit = readProcessAndLogStderr Debug . gitProc repo
26+ gitStatus = runGit $ ["status", "--porcelain"] <> bool [] ["--ignored"] withIgnored
27+ gitDiff = runGit ["diff"]
28+ null <$> liftA2 (<>) gitStatus gitDiff
29+
30+-- | Ensure that git repo is clean
31+ensureCleanGitRepo :: MonadObelisk m => FilePath -> Bool -> Text -> m ()
32+ensureCleanGitRepo path withIgnored s =
33+ withSpinnerNoTrail ("Ensuring clean git repo at " <> T.pack path) $ do
34+ checkGitCleanStatus path withIgnored >>= \case
35+ False -> do
36+ statusDebug <- readGitProcess path $ ["status"] <> bool [] ["--ignored"] withIgnored
37+ putLog Warning "Working copy is unsaved; git status:"
38+ putLog Notice statusDebug
39+ failWith s
40+ True -> pure ()
1841
1942initGit :: MonadObelisk m => FilePath -> m ()
2043initGit repo = do
@@ -31,6 +54,29 @@ gitProc repo argsRaw =
3154 args@("clone":_) -> args <> [repo]
3255 args -> ["-C", repo] <> args
3356
57+-- | Recursively copy a directory using `cp -a`
58+copyDir :: FilePath -> FilePath -> P.CreateProcess
59+copyDir src dest =
60+ (P.proc "cp" ["-a", ".", dest]) { P.cwd = Just src }
61+
62+readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
63+readGitProcess repo = fmap T.pack . readProcessAndLogStderr Notice . gitProc repo
64+
3465processToShellString :: FilePath -> [String] -> String
3566processToShellString cmd args = unwords $ map quoteAndEscape (cmd : args)
3667 where quoteAndEscape x = T.unpack $ "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
68+
69+-- | A simpler wrapper for CliApp's most used process function with sensible defaults.
70+runProc :: MonadObelisk m => P.CreateProcess -> m ()
71+runProc = callProcessAndLogOutput (Notice, Error)
72+
73+-- | Like runProc, but all output goes to Debug logging level
74+runProcSilently :: MonadObelisk m => P.CreateProcess -> m ()
75+runProcSilently = callProcessAndLogOutput (Debug, Debug)
76+
77+-- | A simpler wrapper for CliApp's readProcessAndLogStderr with sensible defaults.
78+readProc :: MonadObelisk m => P.CreateProcess -> m Text
79+readProc = fmap T.pack . readProcessAndLogStderr Error
80+
81+tshow :: Show a => a -> Text
82+tshow = T.pack . show
lib/migration/obelisk-migration.cabalnew file+24
@@ -0,0 +1,24 @@
1+name: obelisk-migration
2+version: 0.1
3+license: BSD3
4+build-type: Simple
5+cabal-version: >=1.2
6+
7+library
8+ hs-source-dirs: src
9+ build-depends:
10+ algebraic-graphs >= 0.2
11+ , base
12+ , containers
13+ , directory
14+ , exceptions
15+ , filepath
16+ , mtl
17+ , obelisk-cliapp
18+ , process
19+ , text
20+ , transformers
21+ exposed-modules:
22+ Obelisk.Migration
23+ other-modules:
24+ ghc-options: -Wall -fobject-code
lib/migration/src/Obelisk/Migration.hsnew file+152
@@ -0,0 +1,152 @@
1+{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE LambdaCase #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
5+{-# LANGUAGE TupleSections #-}
6+module Obelisk.Migration where
7+
8+import Control.Monad (forM)
9+import Control.Monad.Catch (Exception, MonadThrow, throwM)
10+import Control.Monad.State.Strict (evalStateT, get, put)
11+import Data.Map (Map)
12+import qualified Data.Map as Map
13+import Data.Maybe (catMaybes)
14+import Data.Semigroup ((<>))
15+import qualified Data.Set as Set
16+import Data.Text (Text)
17+import qualified Data.Text as T
18+import System.Directory (doesDirectoryExist, doesFileExist, listDirectory)
19+import System.FilePath
20+
21+import Algebra.Graph.AdjacencyMap
22+
23+type Hash = Text
24+
25+-- Migration graph with edge metadata
26+data Migration action = Migration
27+ { _migration_graph :: AdjacencyMap Hash
28+ , _migration_actions :: Map (Hash, Hash) action -- Edge metadata
29+ }
30+ deriving (Eq, Show)
31+
32+-- A path is a list of edges
33+type Path = [(Hash, Hash)]
34+
35+
36+data CyclicGraphError = CyclicGraphError
37+ deriving Show
38+
39+data MultipleTerminalVertices = MultipleTerminalVertices
40+ deriving Show
41+
42+data NonEquivalentPaths = NonEquivalentPaths Hash Hash
43+ deriving Show
44+
45+data MissingEdgeInActionMap = MissingEdgeInActionMap (Hash, Hash)
46+ deriving Show
47+
48+instance Exception CyclicGraphError
49+instance Exception MultipleTerminalVertices
50+instance Exception NonEquivalentPaths
51+instance Exception MissingEdgeInActionMap
52+
53+class Action action where
54+ parseEdgeMeta :: String -> action
55+
56+instance Action Text where
57+ parseEdgeMeta = T.pack
58+
59+-- | Get the directed-acylic graph of migration
60+getDag :: (MonadThrow m, Ord a) => AdjacencyMap a -> m (AdjacencyMap a)
61+getDag g = case topSort g of
62+ Just _ -> pure g
63+ Nothing -> throwM CyclicGraphError
64+
65+-- | Read the graph from a directory of files.
66+readGraph :: Action action => FilePath -> Text -> IO (Maybe (Migration action))
67+readGraph root name = doesDirectoryExist root >>= \case
68+ False -> return Nothing
69+ True -> do
70+ allVertexPairs <- listDirectory root >>= return . catMaybes . fmap getEdgeVertices
71+ edgesInfo <- fmap catMaybes $ forM allVertexPairs $ \vs -> do
72+ getEdgeFor name vs >>= return . fmap (vs, )
73+ return $ pure $ Migration (edges $ fmap fst edgesInfo) (Map.fromList edgesInfo)
74+ where
75+ getEdgeVertices p = case T.splitOn "-" $ T.pack p of
76+ [v1, v2] -> Just (v1, v2)
77+ _ -> Nothing
78+ getEdgeFor graph (v1, v2) = do
79+ let f = root </> (T.unpack $ v1 <> "-" <> v2) </> T.unpack graph
80+ doesFileExist f >>= \case
81+ True -> do
82+ c <- readFile f >>= pure . parseEdgeMeta
83+ return $ Just c
84+ False -> do
85+ return Nothing
86+
87+-- | Find the concataneted actions between two vertices
88+--
89+-- All paths between the vertices must have the same mappend'ed action;
90+-- otherwise this will throw.
91+findPathAction
92+ :: (MonadThrow m, Action action, Monoid action, Ord action)
93+ => Migration action -> Hash -> Hash -> m (Maybe action)
94+findPathAction m start end = flip evalStateT Map.empty $ do
95+ graph <- fmap adjacencyMap $ getDag $ _migration_graph m
96+ let
97+ findPathFromCached start' = do
98+ -- TODO: refactor the caching part into a separate `memoize` function
99+ cache <- get
100+ case Map.lookup start' cache of
101+ Just result ->
102+ pure result
103+ Nothing -> do
104+ result <- findPathFrom start'
105+ put $ Map.insert start' result cache
106+ pure result
107+ findPathFrom start' = if start' == end
108+ then
109+ pure $ Just mempty
110+ else case Map.lookup start' graph of
111+ Just adjs -> do
112+ actions <- fmap catMaybes $ flip traverse (Set.toList adjs) $ \adj -> do
113+ action0 <- getAction m (start', adj)
114+ actionM <- findPathFromCached adj
115+ pure $ fmap (mappend action0) $ actionM
116+ case uniqs actions of
117+ [] -> pure Nothing
118+ [v] -> pure $ Just v -- Exactly one monoidal value; accept.
119+ _ -> throwM $ NonEquivalentPaths start' end
120+ Nothing ->
121+ pure Nothing
122+ uniqs = Set.toList . Set.fromList
123+ findPathFromCached start
124+
125+hasVertex :: Hash -> Migration action -> Bool
126+hasVertex h (Migration g _) = Set.member h $ vertexSet g
127+
128+-- | Get the action for the given edge (which must exist)
129+getAction :: MonadThrow m => Migration action -> (Hash, Hash) -> m action
130+getAction (Migration _ h) e = case Map.lookup e h of
131+ Just v -> pure v
132+ Nothing -> throwM $ MissingEdgeInActionMap e
133+
134+-- | Get the last vertex of the given DAG. Nothing if empty graph.
135+--
136+-- Assumes that the graph is fully connected.
137+getLast :: (MonadThrow m, Ord a, Eq a) => AdjacencyMap a -> m (Maybe a)
138+getLast g = lastVertices >>= \case
139+ [] -> pure $ Nothing
140+ [x] -> pure $ Just x
141+ _ -> throwM MultipleTerminalVertices
142+ where
143+ lastVertices
144+ = pure
145+ . fmap fst
146+ . filter (\(_, nexts) -> nexts == [])
147+ . adjacencyList
148+ =<< getDag g
149+
150+-- | Get the first vertex of the given DAG. Nothing if empty graph.
151+getFirst :: (MonadThrow m, Ord a, Eq a) => AdjacencyMap a -> m (Maybe a)
152+getFirst = getLast . transpose