Merge branch 'master' into overridable-static
commit1040ba1644772133b3a8faff964d904a8e87329a
authorLuigy Leon <luichi.19@gmail.com>
dateThu 11 Oct 2018, 00:14 (8 years ago)
parent7fd639f7c547d4
1105 files changed+3470−616
.gitattributesnew file+1
@@ -0,0 +1 @@
1+migration/* linguist-vendored linguist-generated=true
README.md+53−21
@@ -5,6 +5,8 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
55- [Installing Obelisk](#installing-obelisk)
66- [Developing an Obelisk project](#developing-an-obelisk-project)
77- [Deploying](#deploying)
8+ - [Locally](#locally)
9+ - [EC2](#ec2)
810- [Mobile](#mobile)
911 - [iOS](#ios)
1012 - [Android](#android)
@@ -20,15 +22,22 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
2022 ```
2123 1. If you are using another operating system or linux distribution, ensure that these lines are present in `/etc/nix/nix.conf`:
2224 ```
23− sandbox = true
2425 substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
2526 trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
2627 ```
27− * If you are on MacOS, restart the nix daemon
28− ```
29− sudo launchctl stop org.nixos.nix-daemon
30− sudo launchctl start org.nixos.nix-daemon
31− ```
28+ * other Linux: enable sandboxing (see https://github.com/obsidiansystems/obelisk/issues/6)
29+ ```
30+ sandbox = true
31+ ```
32+ * MacOS: disable sandboxing (there are still some impure dependencies for now)
33+ ```
34+ sandbox = false
35+ ```
36+ then restart the nix daemon
37+ ```
38+ sudo launchctl stop org.nixos.nix-daemon
39+ sudo launchctl start org.nixos.nix-daemon
40+ ```
32411. Install obelisk: `nix-env -f https://github.com/obsidiansystems/obelisk/archive/master.tar.gz -iA command`
3342
3443### Contributing to Obelisk
@@ -90,29 +99,52 @@ Every time you change the Haskell source files in frontend, common or backend, `
9099
91100## Deploying
92101
102+### Locally
103+
104+Build everything:
105+
106+```
107+nix-build -A exe -o result-exe
108+```
109+
110+Run the server:
111+
112+```
113+cd result-exe
114+./backend
115+```
116+
117+### EC2
118+
93119In this section we will demonstrate how to deploy your Obelisk app to an Amazon EC2 instance.
94120
95121First create a new EC2 instance:
96122
97−1. Launch a NixOS 17.09 EC2 instance (we recommend [this AMI](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-40bee63a))
123+1. Launch a NixOS 17.09 EC2 instance (we recommend [this AMI](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-40bee63a))
981241. In the instance configuration wizard ensure that your instance has at least 1GB RAM and 10GB disk space.
991251. When prompted save your AWS private key (`~/myaws.pem`) somewhere safe. We'll need it later during deployment.
100−1. Go to "Security Groups", select your instance's security group and under "Inbound" tab add a new rule for HTTP port 80.
126+1. Go to "Security Groups", select your instance's security group and under "Inbound" tab add a new rule for HTTP port 80 and 443.
101127
102−At this stage your instance should be booting and become accessible shortly. Note down the hostname of your instance. It should look like this:
103−
104−```
105−INSTANCE_HOSTNAME=ec2-??-??-??-??.ca-central-1.compute.amazonaws.com
106−```
128+At this stage your instance should be booting and become accessible shortly. Note down the hostname of your EC2 instance.
107129
108130Now go to your Obelisk project directory (`~/code/myapp`), and initialize a deployment config (`~/code/myapp-deploy`):
109131Your project directory must be "thunkable", i.e. something on which `ob thunk pack` can be called. Usually it will be a git repository whose current revision has been pushed upstream.
110132
111133```
112134cd ~/code/myapp
113−ob deploy init --ssh-key ~/myaws.pem --hostname ${INSTANCE_HOSTNAME} ~/code/myapp-deploy
135+SERVER=ec2-35-183-22-197.ca-central-1.compute.amazonaws.com
136+ROUTE=https://myapp.com # Publicly accessible route to your app
137+EMAIL=myname@myapp.com
138+ob deploy init \
139+ --ssh-key ~/myaws.pem \
140+ --hostname $SERVER \
141+ --route $ROUTE \
142+ --admin-email $EMAIL \
143+ ~/code/myapp-deploy
114144```
115145
146+NOTE: HTTPS is enabled by default; to disable https, pass `--disable-https` to the `ob deploy init` command above.
147+
116148Then go to that created deployment configuration directory, and initiate the deployment:
117149
118150```
@@ -120,9 +152,9 @@ cd ~/code/myapp-deploy
120152ob deploy push
121153```
122154
123−`ob deploy push` will locally build your app and then transfer it, along with all the Nix package dependencies, via ssh to the EC2 instance. It will also configure Nginx so that the public port 80 proxies to the running app.
155+`ob deploy push` will locally build your app and then transfer it, along with all the Nix package dependencies, via ssh to the EC2 instance. The backend will live in `/var/lib/backend`.
124156
125−At this point you are done. Your app will be accessible at `http://${HOSTNAME}`!
157+At this point you are done. Your app will be accessible at `${ROUTE}`.
126158
127159### Deploying an updated version
128160
@@ -153,7 +185,7 @@ Install Xcode 8.2 (contains iOS SDK 10.2) and open it so that it runs its post i
153185These versions will work out of the box but iOS SDKs prior to 11.3 should also work. You can choose another installed version in `default.nix`
154186
155187More recent Xcodes should also work, as long as one of the SDKs mentioned above has been used.
156−To add another SDK to your current Xcode, [download](https://developer.apple.com/download/more/) the corresponding Xcode, extract it and copy its SDK folder next to the installed one, e.g.
188+To add another SDK to your current Xcode, [download](https://developer.apple.com/download/more/) the corresponding Xcode, extract it and copy its SDK folder next to the installed one, e.g.
157189```
158190open -W Xcode_9.2.xip
159191sudo cp -R Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.2.sdk
@@ -167,7 +199,7 @@ xcodebuild -showsdks
167199
168200##### Certificates
169201To deploy and/or package apps, you'll need to inform Apple of your development devices and permissions by
170−adding credentials to the correct provisioning profile via the Apple Developer portal.
202+adding credentials to the correct provisioning profile via the Apple Developer portal.
171203
1722041. Open up XCode and go to Preferences - Accounts. Select the organization
173205Member role, click Manage Certificates, and add an iOS Development
@@ -234,7 +266,7 @@ First, if you do not already have a keystore, create it as follows (for more inf
234266nix-shell -p androidenv.platformTools --run "keytool -genkey -v -keystore myandroidkey.jks -keyalg RSA -keysize 2048 -validity 10000 -alias myandroidalias"
235267```
236268
237−(Besure to give an appropriate keystore filename and key alias string above.)
269+(Be sure to give an appropriate keystore filename and key alias string above.)
238270
239271The `keytool` command will ask you for some details, including a keystore password and a key password (we will use these passwords further below). It will now have created a `myandroidkey.jks` file under the current directory. Move that to somewhere safe, and note down its full path.
240272
@@ -244,8 +276,8 @@ Now edit your project's `default.nix` and tell Obelisk of your app's keystore fi
244276 ...
245277 android.applicationId = "com.example.myapp";
246278 android.displayName = "My App";
247− android.releaseKey =
248− { storeFile = /path/to/myandroidkey.jks;
279+ android.releaseKey =
280+ { storeFile = /path/to/myandroidkey.jks;
249281 storePassword = "abcd1234";
250282 keyAlias = "myandroidalias";
251283 keyPassword = "abcd1234";
default.nix+162−103
@@ -5,8 +5,8 @@
55let
66 getReflexPlatform = sys: import ./dep/reflex-platform { inherit iosSdkVersion; system = sys; };
77 reflex-platform = getReflexPlatform system;
8− inherit (reflex-platform) hackGet;
9− pkgs = reflex-platform.nixpkgs;
8+ inherit (reflex-platform) hackGet nixpkgs;
9+ pkgs = nixpkgs;
1010in with pkgs.haskell.lib; with pkgs.lib;
1111let
1212 # TODO: Remove this after updating nixpkgs: https://github.com/NixOS/nixpkgs/issues/37750
@@ -16,6 +16,14 @@ let
1616 then removeConfigureFlag drv' "--ghc-option=-optl=-dead_strip"
1717 else drv';
1818
19+ commandRuntimeDeps = pkgs: with pkgs; [
20+ coreutils
21+ git
22+ gitAndTools.hub
23+ nix-prefetch-git
24+ openssh
25+ ];
26+
1927 #TODO: Upstream
2028 # Modify a Haskell package to add completion scripts for the given
2129 # executable produced by it. These completion scripts will be picked up
@@ -44,45 +52,28 @@ let
4452 enableLibraryProfiling = profiling;
4553 });
4654
47− #TODO: Eliminate this when https://github.com/phadej/github/pull/307 makes its way to reflex-platform
48− github = overrideCabal super.github (drv: {
49− src = pkgs.fetchFromGitHub {
50− owner = "ryantrinkle";
51− repo = "github";
52− rev = "8f543cdc07876bfb7b924d3722e3dbc1df4b02ca";
53− sha256 = "0vcnx9cxqd821kmjx1r4cvj95zs742qm1pwqnb52vw3djplbqd86";
54− };
55− sha256 = null;
56− revision = null;
57− editedCabalFile = null;
58− });
59−
6055 # Dynamic linking with split objects dramatically increases startup time (about 0.5 seconds on a decent machine with SSD)
6156 obelisk-command = addOptparseApplicativeCompletionScripts "ob" (justStaticExecutables' super.obelisk-command);
62−
63− optparse-applicative = self.callHackage "optparse-applicative" "0.14.0.0" {};
6457 });
6558 };
6659
6760 fixUpstreamPkgs = self: super: {
68− heist = doJailbreak super.heist; #TODO: Move up to reflex-platform; create tests for r-p supported packages
69− modern-uri =
70− let src = pkgs.fetchFromGitHub {
71− owner = "mrkkrp";
72− repo = "modern-uri";
73− rev = "21064285deb284cb3328094c69c34f9f67919cc9";
74− sha256 = "0vddw8r9sb31h1fz1anzxrs9p3a3p8ygpxlj398z5j47wmr86cmi";
75− };
76− in (overrideCabal (self.callCabal2nix "modern-uri" src {}) (drv: {
77− doCheck = false;
78− postPatch = (drv.postPatch or "") + ''
79− substituteInPlace Text/URI/Types.hs \
80− --replace "instance Arbitrary (NonEmpty (RText 'PathPiece)) where" "" \
81− --replace " arbitrary = (:|) <$> arbitrary <*> arbitrary" ""
82− '';
83− })).override { megaparsec = super.megaparsec_6_1_1; };
84− network-transport = self.callHackage "network-transport" "0.5.2" {};
85− network-transport-tcp = self.callHackage "network-transport-tcp" "0.6.0" {};
61+ algebraic-graphs = pkgs.haskell.lib.doJailbreak
62+ (self.callCabal2nix "algebraic-graphs" (pkgs.fetchFromGitHub {
63+ owner = "snowleopard";
64+ repo = "alga";
65+ rev = "480a73137e9b38ad3f1bc2c628847953d2fb3e25";
66+ sha256 = "0dpwi5ffs88brl3lz51bwb004c6zm8ds8pkw1vzsg2a6aaiyhlzl";
67+ }) {});
68+
69+ # Need deriveSomeUniverse
70+ # PR: https://github.com/dmwit/universe/pull/32
71+ universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
72+ owner = "obsidiansystems";
73+ repo = "universe";
74+ rev = "5a2fc823caa4163411d7e41aa80e67cefb15944a";
75+ sha256 = "0ll2z0fh18z6x8jl8kbp7ldagwccz3wjmvrw1gw752z058n82yfa";
76+ } + /template) {};
8677 };
8778
8879 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
@@ -95,37 +86,52 @@ let
9586 addLibs = self: super: {
9687 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {};
9788 obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (hackGet ./lib/asset + "/serve-snap") {};
98− obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
9989 obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
100− obelisk-command = (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}).override { Cabal = super.Cabal_2_0_0_2; };
90+ obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
91+ obelisk-command = (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}).overrideAttrs
92+ (drv: {
93+ buildInputs = drv.buildInputs ++ [ pkgs.makeWrapper ];
94+ postInstall = ''
95+ ${drv.postInstall or ""}
96+ # Install migrations
97+ cp -r ${./migration} $out/migration;
98+ '';
99+ postFixup = ''
100+ ${drv.postFixup or ""}
101+ # Make `ob` reference its runtime dependencies.
102+ wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
103+ '';
104+ });
101105 obelisk-executable-config = executableConfig.haskellPackage self;
102− obelisk-executable-config-inject = executableConfig.platforms.web.inject self; # TODO handle platforms.{ios,android}
106+ obelisk-executable-config-inject = executableConfig.platforms.web.inject self;
107+ obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
108+ obelisk-migration = self.callCabal2nix "obelisk-migration" (cleanSource ./lib/migration) {};
103109 obelisk-run = self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {};
110+ obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
104111 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
105112 obelisk-snap = self.callCabal2nix "obelisk-snap" (cleanSource ./lib/snap) {};
106113 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
107114 };
108115
116+ inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets;
117+
109118 defaultHaskellOverrides = composeExtensions fixUpstreamPkgs addLibs;
110119in
111120with pkgs.lib;
112121rec {
113122 inherit reflex-platform;
114123 inherit (reflex-platform) nixpkgs pinBuildInputs;
124+ pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
115125 path = reflex-platform.filterGit ./.;
116126 obelisk = ghcObelisk;
117127 command = ghcObelisk.obelisk-command;
118− shell = pinBuildInputs "obelisk-shell" ([
119− command
120− pkgs.openssh
121− pkgs.gitAndTools.hub
122− ]) [];
128+ shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs) [];
123129
124130 selftest = pkgs.writeScript "selftest" ''
125131 #!/usr/bin/env bash
126132 set -euo pipefail
127133
128− PATH="${ghcObelisk.obelisk-command}/bin:$PATH"
134+ PATH="${command}/bin:$PATH"
129135 export OBELISK_IMPL="${hackGet ./.}"
130136 "${justStaticExecutables' ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" "$@"
131137 '';
@@ -151,67 +157,100 @@ rec {
151157 '';
152158
153159 compressedJs = frontend: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
154− mkdir $out
155− cd $out
156− ln -s "${justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
157− closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ADVANCED --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
158− echo "//# sourceMappingURL=all.js.map" >> all.js
160+ mkdir $out
161+ cd $out
162+ ln -s "${justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
163+ closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ADVANCED --jscomp_warning=checkVars --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
164+ echo "//# sourceMappingURL=all.js.map" >> all.js
159165 '';
160166
167+ serverModules = {
168+ mkBaseEc2 = { hostName, routeHost, enableHttps, adminEmail, ... }: {...}: {
169+ imports = [
170+ (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
171+ ];
172+ networking = {
173+ inherit hostName;
174+ firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
175+ };
176+ security.acme.certs = if enableHttps then {
177+ "${routeHost}".email = adminEmail;
178+ } else {};
179+ ec2.hvm = true;
180+ };
181+
182+ mkObeliskApp =
183+ { exe
184+ , routeHost
185+ , enableHttps
186+ , name ? "backend"
187+ , user ? name
188+ , group ? user
189+ , baseUrl ? "/"
190+ , internalPort ? 8000
191+ , backendArgs ? "--port=${toString internalPort}"
192+ , ...
193+ }: {...}: {
194+ services.nginx = {
195+ enable = true;
196+ virtualHosts."${routeHost}" = {
197+ enableACME = enableHttps;
198+ forceSSL = enableHttps;
199+ locations.${baseUrl} = {
200+ proxyPass = "http://localhost:" + toString internalPort;
201+ };
202+ };
203+ };
204+ systemd.services.${name} = {
205+ wantedBy = [ "multi-user.target" ];
206+ after = [ "network.target" ];
207+ restartIfChanged = true;
208+ script = ''
209+ ln -sft . '${exe}'/*
210+ mkdir -p log
211+ exec ./backend ${backendArgs} >>backend.out 2>>backend.err </dev/null
212+ '';
213+ serviceConfig = {
214+ User = user;
215+ KillMode = "process";
216+ WorkingDirectory = "~";
217+ Restart = "always";
218+ RestartSec = 5;
219+ };
220+ };
221+ users = {
222+ users.${user} = {
223+ description = "${user} service";
224+ home = "/var/lib/${user}";
225+ createHome = true;
226+ isSystemUser = true;
227+ group = group;
228+ };
229+ groups.${group} = {};
230+ };
231+ };
232+ };
233+
161234 serverExe = backend: frontend: assets: config:
162235 pkgs.runCommand "serverExe" {} ''
163236 mkdir $out
164237 set -eux
165− ln -s "${justStaticExecutables backend}"/bin/backend $out/backend
166− ln -s "${assets}" $out/static
167− ln -s "${config}" $out/config
168− ln -s ${compressedJs frontend} $out/frontend.jsexe
169− ''; #TODO: run frontend.jsexe through the asset processing pipeline
238+ ln -s "${justStaticExecutables backend}"/bin/* $out/
239+ ln -s "${mkAssets assets}" $out/static.assets
240+ cp -r ${config} $out/config
241+ ln -s ${mkAssets (compressedJs frontend)} $out/frontend.jsexe.assets
242+ '';
170243
171− server = exe: hostName:
172− let system = "x86_64-linux";
173− nixos = import (pkgs.path + /nixos);
174− https = (import lib/https {}).module {
175− backendPort = 8000; # TODO read from config
176− # sslConfig = {
177− # hostName = "example.com";
178− # adminEmail = "webmaster@example.com";
179− # subdomains = [ "www" ];
180− # };
181− };
244+ server = { exe, hostName, adminEmail, routeHost, enableHttps, config }@args:
245+ let
246+ nixos = import (pkgs.path + /nixos);
182247 in nixos {
183− inherit system;
184− configuration = args: {
248+ system = "x86_64-linux";
249+ configuration = {
185250 imports = [
186− (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
187− https
251+ (serverModules.mkBaseEc2 args)
252+ (serverModules.mkObeliskApp args)
188253 ];
189− networking = { inherit hostName; };
190− systemd.services.backend = {
191− wantedBy = [ "multi-user.target" ];
192− after = [ "network.target" ];
193− restartIfChanged = true;
194− script = ''
195− ln -sft . "${exe}"/*
196− mkdir -p log
197− exec ./backend >>backend.out 2>>backend.err </dev/null
198− '';
199− serviceConfig = {
200− User = "backend";
201− KillMode = "process";
202− WorkingDirectory = "~";
203− Restart = "always";
204− RestartSec = 5;
205− };
206− };
207− users.extraUsers.backend = {
208− description = "backend service";
209− home = "/var/lib/backend";
210− createHome = true;
211− isSystemUser = true;
212− group = "backend";
213− };
214− ec2.hvm = true;
215254 };
216255 };
217256
@@ -225,12 +264,15 @@ rec {
225264 , packages ? {}
226265 , overrides ? _: _: {}
227266 , static ? base + /static
267+ , tools ? _: []
268+ , shellToolOverrides ? _: _: {}
269+ , withHoogle ? false # Setting this to `true` makes shell reloading far slower
228270 }:
229271 let frontendName = "frontend";
230272 backendName = "backend";
231273 commonName = "common";
232274 staticName = "static";
233− assets = processAssets { src = static; };
275+ processedStatic = processAssets { src = static; };
234276 # The packages whose names and roles are defined by this package
235277 predefinedPackages = filterAttrs (_: x: x != null) {
236278 ${frontendName} = nullIfAbsent (base + "/frontend");
@@ -239,14 +281,25 @@ rec {
239281 };
240282 combinedPackages = predefinedPackages // packages;
241283 projectOverrides = self: super: {
242− ${staticName} = dontHaddock (self.callCabal2nix "static" assets.haskellManifest {});
284+ ${staticName} = dontHaddock (self.callCabal2nix "static" processedStatic.haskellManifest {});
243285 ${backendName} = addBuildDepend super.${backendName} self.obelisk-run;
244286 };
245287 totalOverrides = composeExtensions (composeExtensions defaultHaskellOverrides projectOverrides) overrides;
288+ inherit (nixpkgs) lib;
289+ inherit (lib.strings) hasPrefix;
290+ privateConfigDirs = ["config/backend"];
291+ injectableConfig = builtins.filterSource (path: _:
292+ !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
293+ ) configPath;
246294 in {
295+ inherit shellToolOverrides tools withHoogle;
247296 overrides = totalOverrides;
248297 packages = combinedPackages;
249298 shells = {
299+ ghcSavedSplices = (filter (x: hasAttr x combinedPackages) [
300+ commonName
301+ frontendName
302+ ]);
250303 ghc = (filter (x: hasAttr x combinedPackages) [
251304 backendName
252305 commonName
@@ -257,25 +310,31 @@ rec {
257310 commonName
258311 ];
259312 };
260− withHoogle = false; # Setting this to `true` makes shell reloading far slower
261313 android = {
262314 ${if android == null then null else frontendName} = {
263315 executableName = "frontend";
264− ${if builtins.pathExists static then "assets" else null} = assets.symlinked;
316+ ${if builtins.pathExists static then "assets" else null} =
317+ executableConfig.platforms.android.inject injectableConfig processedStatic.symlinked;
265318 } // android;
266319 };
267320 ios = {
268321 ${if ios == null then null else frontendName} = {
269322 executableName = "frontend";
270− ${if builtins.pathExists static then "staticSrc" else null} = assets.symlinked;
323+ ${if builtins.pathExists static then "staticSrc" else null} =
324+ executableConfig.platforms.ios.inject injectableConfig processedStatic.symlinked;
271325 } // ios;
272326 };
273327 };
274328 in mkProject (projectDefinition args));
275− exe = serverExe (projectOut "x86_64-linux").ghc.backend (projectOut system).ghcjs.frontend assets.symlinked configPath;
329+ serverOn = sys: config: serverExe (projectOut sys).ghc.backend (projectOut system).ghcjs.frontend static config;
330+ # `exe` is project's backend executable, with frontend assets, config, etc.
331+ # `linuxExe` is the same but built for x86_64-linux.
332+ exe = serverOn system configPath;
333+ linuxExe = serverOn "x86_64-linux" configPath;
276334 in projectOut system // {
277− inherit exe;
278− server = { hostName }: server exe hostName;
335+ inherit exe linuxExe;
336+ server = args@{ hostName, adminEmail, routeHost, enableHttps, config }:
337+ server (args // { exe = serverOn "x86_64-linux" config;});
279338 obelisk = import (base + "/.obelisk/impl") {};
280339 };
281340 haskellPackageSets = {
dep/.gitignorenew file+4
@@ -0,0 +1,4 @@
1+*
2+!.gitignore
3+!*/*.json
4+!*/default.nix
dep/reflex-platform/github.json+2−2
@@ -2,6 +2,6 @@
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
44 "branch": "develop",
5− "rev": "a4d679629b52f80ecae857864591d79d902a5c5d",
6− "sha256": "1p34qc3ryar81jh01g4jm0svr05yri83vlswm2jbxiabigy47652"
5+ "rev": "2f09cea8aea453969172fe1f499905af5afb4719",
6+ "sha256": "1v7v2fwahq4mcywjvg1i44jyqimpxd4kgn4x4a24lcdgw9yll9wg"
77}
lib/asset/manifest/src/Obelisk/Asset/Gather.hs−1
@@ -13,7 +13,6 @@ import Data.Char
1313import Data.Digest.Pure.SHA
1414import Data.Map (Map)
1515import qualified Data.Map as Map
16−import Data.Monoid
1716import qualified Data.Text as T
1817import Data.Text.Encoding
1918import qualified Data.Vector.Unboxed as UV
lib/asset/manifest/src/Obelisk/Asset/Promoted.hs+8−3
@@ -18,7 +18,6 @@ import qualified Data.Sequence as Seq
1818import Control.Monad.Trans.Writer
1919import System.FilePath
2020import System.Directory
21−import Data.Monoid
2221import Data.Text (Text)
2322import qualified Data.Text as T
2423import qualified Data.Text.IO as T
@@ -63,11 +62,17 @@ staticModuleFile moduleName paths = do
6362 , "{-# LANGUAGE FlexibleInstances #-}"
6463 , "{-# LANGUAGE KindSignatures #-}"
6564 , "{-# LANGUAGE OverloadedStrings #-}"
65+ , "{-# LANGUAGE ScopedTypeVariables #-}"
66+ , "{-# LANGUAGE TypeApplications #-}"
6667 , "module " <> moduleName <> " where"
6768 , ""
6869 , "import qualified GHC.Types"
69− , "import Data.Text ()"
70+ , "import Data.Text (Text)"
7071 , "import qualified Data.Text.Internal"
72+ , "import Data.Monoid ((<>))"
73+ , ""
74+ , "static :: forall a. StaticFile a => Text"
75+ , "static = \"static/\" <> hashedPath @a" --TODO: Use obelisk-route to generate this in a more consistent way
7176 , ""
7277 , T.pack $ pprint decs
7378 ]
@@ -96,7 +101,7 @@ staticClass :: WriterT (Seq Dec) Q StaticContext
96101staticClass = do
97102 let n x = Name (OccName x) NameS
98103 className = n "StaticFile"
99− methodName = n "static"
104+ methodName = n "hashedPath"
100105 cls = ClassD [] className [KindedTV (n "s") (ConT ''Symbol)] [] [SigD methodName (ConT ''Text)]
101106 tell $ Seq.singleton cls
102107 return $ StaticContext
lib/backend/obelisk-backend.cabal+11−1
@@ -7,11 +7,21 @@ library
77 hs-source-dirs: src
88 build-depends: base,
99 bytestring,
10+ categories,
1011 data-default,
12+ dependent-sum,
13+ dependent-sum-template,
1114 lens,
15+ mtl,
1216 obelisk-asset-serve-snap,
17+ obelisk-executable-config-inject,
18+ obelisk-frontend,
19+ obelisk-route,
1320 obelisk-snap,
1421 reflex-dom,
1522 snap,
16− snap-server
23+ snap-server,
24+ text,
25+ universe
1726 exposed-modules: Obelisk.Backend
27+ ghc-options: -Wall
lib/backend/src/Obelisk/Backend.hs+130−24
@@ -1,49 +1,155 @@
1+{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE GADTs #-}
4+{-# LANGUAGE KindSignatures #-}
5+{-# LANGUAGE LambdaCase #-}
6+{-# LANGUAGE MultiParamTypeClasses #-}
17{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE TemplateHaskell #-}
29module Obelisk.Backend
3− ( backend
4− , BackendConfig (..)
10+ ( Backend (..)
511 -- * Re-exports
612 , Default (def)
13+ , getPageName
14+ , getRouteWith
15+ , runSnapWithCommandLineArgs
16+ , serveDefaultObeliskApp
17+ , prettifyOutput
18+ , runBackend
719 ) where
820
9−import Control.Lens
21+import Prelude hiding (id, (.))
22+import Control.Category
23+import Control.Monad
24+import Control.Monad.Except
25+import Control.Categorical.Bifunctor
26+import Data.ByteString (ByteString)
1027import qualified Data.ByteString.Char8 as BSC8
1128import Data.Default (Default (..))
12−import Obelisk.Asset.Serve.Snap (serveAssets)
13−import Obelisk.Snap
29+import Data.Dependent.Sum
30+import Data.Functor.Sum
31+import Data.Functor.Identity
32+import Data.Text (Text)
33+import qualified Data.Text as T
34+import Data.Text.Encoding
35+import Obelisk.Asset.Serve.Snap (serveAsset)
36+import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
37+import Obelisk.Frontend
38+import Obelisk.Route
1439import Reflex.Dom
15−import System.IO (hSetBuffering, stderr, BufferMode (..))
16−import Snap (httpServe, defaultConfig, commandLineConfig, route)
40+import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, rqPathInfo,
41+ rqQueryString, writeBS, writeText)
1742import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
43+import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
1844
19−--TODO: Add a link to a large explanation of the idea of using 'def'
20−-- | Configure the operation of the Obelisk backend. For reasonable defaults,
21−-- use 'def'.
22−data BackendConfig = BackendConfig
23− { _backendConfig_head :: StaticWidget () ()
45+data Backend backendRoute frontendRoute = Backend
46+ { _backend_routeEncoder :: Encoder (Either Text) Identity (R (Sum backendRoute (ObeliskRoute frontendRoute))) PageName
47+ , _backend_run :: ((R backendRoute -> Snap ()) -> IO ()) -> IO ()
2448 }
2549
26−instance Default BackendConfig where
27− def = BackendConfig (return ())
50+-- | The static assets provided must contain a compiled GHCJS app that corresponds exactly to the Frontend provided
51+data GhcjsApp route = GhcjsApp
52+ { _ghcjsApp_compiled :: !StaticAssets
53+ , _ghcjsApp_value :: !(Frontend route)
54+ }
55+
56+-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
57+--TODO: The frontend should be provided together with the asset paths so that this isn't so easily breakable; that will probably make this function obsolete
58+serveDefaultObeliskApp :: MonadSnap m => Frontend (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
59+serveDefaultObeliskApp frontend = serveObeliskApp defaultStaticAssets frontendApp
60+ where frontendApp = GhcjsApp
61+ { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
62+ , _ghcjsApp_value = frontend
63+ }
2864
29−-- | Start an Obelisk backend
30−backend :: BackendConfig -> IO ()
31−backend cfg = do
65+prettifyOutput :: IO ()
66+prettifyOutput = do
3267 -- Make output more legible by decreasing the likelihood of output from
3368 -- multiple threads being interleaved
69+ hSetBuffering stdout LineBuffering
3470 hSetBuffering stderr LineBuffering
3571
72+defaultStaticAssets :: StaticAssets
73+defaultStaticAssets = StaticAssets
74+ { _staticAssets_processed = "static.assets"
75+ , _staticAssets_unprocessed = "static"
76+ }
77+
78+defaultFrontendGhcjsAssets :: StaticAssets
79+defaultFrontendGhcjsAssets = StaticAssets
80+ { _staticAssets_processed = "frontend.jsexe.assets"
81+ , _staticAssets_unprocessed = "frontend.jsexe"
82+ }
83+
84+runSnapWithCommandLineArgs :: Snap () -> IO ()
85+runSnapWithCommandLineArgs a = do
3686 -- Get the web server configuration from the command line
3787 cmdLineConf <- commandLineConfig defaultConfig
38− headHtml <- fmap snd $ renderStatic $ _backendConfig_head cfg
3988 let httpConf = cmdLineConf
4089 { accessLog = Just $ ConfigIoLog BSC8.putStrLn
4190 , errorLog = Just $ ConfigIoLog BSC8.putStrLn
4291 }
43− appCfg = def & appConfig_initialHead .~ headHtml
4492 -- Start the web server
45− httpServe httpConf $ route
46− [ ("", serveApp "" appCfg)
47− , ("", serveAssets "frontend.jsexe.assets" "frontend.jsexe") --TODO: Can we prevent naming conflicts between frontend.jsexe and static?
48− , ("", serveAssets "static.assets" "static")
49− ]
93+ httpServe httpConf a
94+
95+getPageName :: (MonadSnap m) => m PageName
96+getPageName = do
97+ p <- getsRequest rqPathInfo
98+ q <- getsRequest rqQueryString
99+ let pageNameEncoder' :: Encoder Identity Identity PageName (String, String)
100+ pageNameEncoder' = bimap
101+ (unpackTextEncoder . pathSegmentsTextEncoder . listToNonEmptyEncoder)
102+ (unpackTextEncoder . queryParametersTextEncoder . toListMapEncoder)
103+ return $ decode pageNameEncoder' (T.unpack (decodeUtf8 p), T.unpack (decodeUtf8 q))
104+
105+getRouteWith :: (MonadSnap m) => Encoder Identity parse route PageName -> m (parse route)
106+getRouteWith e = do
107+ pageName <- getPageName
108+ return $ tryDecode e pageName
109+
110+serveObeliskApp :: MonadSnap m => StaticAssets -> GhcjsApp (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
111+serveObeliskApp staticAssets frontendApp = \case
112+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
113+ ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
114+ ResourceRoute_Static :=> Identity pathSegments -> serveStaticAssets staticAssets pathSegments
115+ ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp frontendApp $ GhcjsAppRoute_Resource :/ pathSegments
116+ ResourceRoute_JSaddleWarp :=> Identity _ -> do
117+ let msg = "Error: Obelisk.Backend received jsaddle request"
118+ liftIO $ putStrLn $ T.unpack msg
119+ writeText msg
120+
121+serveStaticAssets :: MonadSnap m => StaticAssets -> [Text] -> m ()
122+serveStaticAssets assets pathSegments = serveAsset (_staticAssets_processed assets) (_staticAssets_unprocessed assets) $ T.unpack $ T.intercalate "/" pathSegments
123+
124+data StaticAssets = StaticAssets
125+ { _staticAssets_processed :: !FilePath
126+ , _staticAssets_unprocessed :: !FilePath
127+ }
128+ deriving (Show, Read, Eq, Ord)
129+
130+data GhcjsAppRoute :: (* -> *) -> * -> * where
131+ GhcjsAppRoute_App :: appRouteComponent a -> GhcjsAppRoute appRouteComponent a
132+ GhcjsAppRoute_Resource :: GhcjsAppRoute appRouteComponent [Text]
133+
134+--TODO: Don't assume we're being served at "/"
135+serveGhcjsApp :: MonadSnap m => GhcjsApp (R appRouteComponent) -> R (GhcjsAppRoute appRouteComponent) -> m ()
136+serveGhcjsApp app = \case
137+ GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest ->
138+ writeBS <=< liftIO $ renderGhcjsFrontend (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
139+ GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
140+
141+runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
142+runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
143+ Left e -> fail $ "backend error:\n" <> T.unpack e
144+ Right validFullEncoder -> _backend_run backend $ \serveRoute -> do
145+ runSnapWithCommandLineArgs $ do
146+ getRouteWith validFullEncoder >>= \case
147+ Identity r -> case r of
148+ InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
149+ InR obeliskRoute :=> Identity a -> serveDefaultObeliskApp frontend $ obeliskRoute :/ a
150+
151+renderGhcjsFrontend :: route -> Frontend route -> IO ByteString
152+renderGhcjsFrontend route f = do
153+ let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
154+ ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
155+ renderFrontendHtml route (_frontend_head f >> injectExecutableConfigs >> baseTag) (_frontend_body f >> ghcjsScript)
lib/cliapp/src/Obelisk/CliApp.hs+4
@@ -12,9 +12,12 @@ module Obelisk.CliApp
1212 , CliConfig
1313 , HasCliConfig
1414 , getCliConfig
15+ , Output
1516
1617 -- .Spinner
1718 , withSpinner
19+ , withSpinnerNoTrail
20+ , withSpinner'
1821
1922 -- .Logging
2023 , newCliConfig
@@ -27,6 +30,7 @@ module Obelisk.CliApp
2730 , Severity (..)
2831
2932 -- .Process
33+ , ProcessFailed (..)
3034 , readProcessAndLogStderr
3135 , callProcessAndLogOutput
3236 , createProcess_
lib/cliapp/src/Obelisk/CliApp/Demo.hs+10−10
@@ -1,11 +1,11 @@
11{-# LANGUAGE FlexibleContexts #-}
22{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE ScopedTypeVariables #-}
34module Obelisk.CliApp.Demo where
45
56import Control.Concurrent (threadDelay)
6−import Control.Monad.IO.Class (liftIO)
7−import Control.Monad.Reader (MonadIO)
8−import Data.Monoid ((<>))
7+import Control.Monad.IO.Class
8+import Data.Semigroup ((<>))
99import qualified Data.Text as T
1010import System.Process (proc)
1111
@@ -13,20 +13,19 @@ import Control.Monad.Catch (MonadMask)
1313
1414import Obelisk.CliApp
1515
16−cliDemo
17− :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m)
18− => m ()
16+cliDemo :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m) => m ()
1917cliDemo = withSpinner "CLI Demo" $ do
2018 putLog Notice "This demo will showcase the CLI library functionality"
21− withSpinner "A long-running task" $ do
19+ _ <- withSpinner' "Searching long for something" (Just $ \c -> "Discovered " <> T.pack (show c) <> " objects") $ do
2220 delay
2321 withSpinner "Nested task" $ do
2422 delay
2523 putLog Notice "In nested task"
26− withSpinner "Inbetween" $ do
27− withSpinner "Runnning something specific" $ do
24+ -- This is a 'no trail' spinner that won't leave a trail (even its sub spinners)
25+ withSpinnerNoTrail "In between, doing something temporary" $ do
26+ withSpinner "Runnning something deep inside" $ do
2827 delay
29− putLog Notice "This task has the same name and still works"
28+ putLog Notice "You can still log from inside no-trail spinners"
3029 delay
3130 delay
3231 putLog Error "Top nested task finished"
@@ -36,6 +35,7 @@ cliDemo = withSpinner "CLI Demo" $ do
3635 putLog Notice "This is some info mesage"
3736 putLog Warning "And now a warning as well"
3837 delay
38+ return (42 :: Integer)
3939 putLog Notice "Now we start a 2nd spinner, run a couple of process, the last of which fails:"
4040 withSpinner "Looking around" $ do
4141 delay
lib/cliapp/src/Obelisk/CliApp/Logging.hs+10−9
@@ -22,11 +22,11 @@ import Data.Semigroup ((<>))
2222import Data.Text (Text)
2323import qualified Data.Text as T
2424import qualified Data.Text.IO as T
25−import System.Console.ANSI (Color (Red, White, Yellow), ColorIntensity (Vivid),
25+import System.Console.ANSI (Color (Red, Yellow), ColorIntensity (Vivid),
2626 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
2727 SGR (SetColor, SetConsoleIntensity), clearLine)
2828import System.Exit (ExitCode (..), exitWith)
29−import System.IO (BufferMode (NoBuffering), hFlush, hReady, hSetBuffering, stdin, stdout)
29+import System.IO (BufferMode (NoBuffering), hFlush, hReady, hSetBuffering, stderr, stdin, stdout)
3030
3131import qualified Obelisk.CliApp.TerminalString as TS
3232import Obelisk.CliApp.Types
@@ -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
@@ -137,15 +137,16 @@ withExitFailMessage msg f = f `catch` \(e :: ExitCode) -> do
137137writeLog:: (MonadIO m, MonadMask m) => Bool -> Bool -> WithSeverity Text -> m ()
138138writeLog withNewLine noColor (WithSeverity severity s)
139139 | noColor && severity <= Warning = liftIO $ putFn $ T.pack (show severity) <> ": " <> s
140− | not noColor && severity <= Error = TS.putStrWithSGR errorColors withNewLine s
141− | not noColor && severity <= Warning = TS.putStrWithSGR warningColors withNewLine s
142− | not noColor && severity >= Debug = TS.putStrWithSGR debugColors withNewLine s
140+ | not noColor && severity <= Error = TS.putStrWithSGR errorColors h withNewLine s
141+ | not noColor && severity <= Warning = TS.putStrWithSGR warningColors h withNewLine s
142+ | not noColor && severity >= Debug = TS.putStrWithSGR debugColors h withNewLine s
143143 | otherwise = liftIO $ putFn s
144144 where
145− putFn = if withNewLine then T.putStrLn else T.putStr
145+ putFn = if withNewLine then (T.hPutStrLn h) else (T.hPutStr h)
146+ h = if severity <= Error then stderr else stdout
146147 errorColors = [SetColor Foreground Vivid Red]
147148 warningColors = [SetColor Foreground Vivid Yellow]
148− debugColors = [SetColor Foreground Vivid White, SetConsoleIntensity FaintIntensity]
149+ debugColors = [SetConsoleIntensity FaintIntensity]
149150
150151-- | Allow the user to immediately switch to verbose logging upon pressing a particular key.
151152--
@@ -163,7 +164,7 @@ allowUserToMakeLoggingVerbose keyCode = bracket showTip (liftIO . killThread) $
163164 where
164165 showTip = fork $ unlessVerbose $ do
165166 conf <- getCliConfig
166− liftIO $ threadDelay $ 3*1000000 -- Only show tip for actions taking too long (3 seconds or more)
167+ liftIO $ threadDelay $ 10*1000000 -- Only show tip for actions taking too long (10 seconds or more)
167168 tipDisplayed <- liftIO $ atomicModifyIORef' (_cliConfig_tipDisplayed conf) $ (,) True
168169 unless tipDisplayed $ unlessVerbose $ do -- Check again in case the user had pressed Ctrl+e recently
169170 putLog Notice "Tip: Press Ctrl+e to display full output"
lib/cliapp/src/Obelisk/CliApp/Process.hs+14−15
@@ -9,7 +9,8 @@
99-- | An extension of `System.Process` that integrates with logging (`Obelisk.CLI.Logging`)
1010-- and is thus spinner friendly.
1111module Obelisk.CliApp.Process
12− ( readProcessAndLogStderr
12+ ( ProcessFailed (..)
13+ , readProcessAndLogStderr
1314 , callProcessAndLogOutput
1415 , createProcess
1516 , createProcess_
@@ -19,9 +20,8 @@ module Obelisk.CliApp.Process
1920
2021import Control.Applicative (liftA2)
2122import Control.Monad (void, (<=<))
22−import Control.Monad.Catch (MonadMask)
23−import Control.Monad.IO.Class (liftIO)
24−import Control.Monad.Reader (MonadIO)
23+import Control.Monad.Catch (Exception, MonadMask, throwM)
24+import Control.Monad.IO.Class (MonadIO, liftIO)
2525import qualified Data.ByteString.Char8 as BSC
2626import Data.Function (fix)
2727import Data.Monoid ((<>))
@@ -38,10 +38,14 @@ import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), cmd
3838import qualified System.Process as Process
3939
4040import Control.Monad.Log (Severity (..))
41−import Obelisk.CliApp.Logging (failWith, putLog, putLogRaw)
41+import Obelisk.CliApp.Logging (putLog, putLogRaw)
4242import Obelisk.CliApp.Types (Cli)
4343
44−-- | Like `System.Process.readProcess` but logs the stderr instead of letting the external process inherit it.
44+data ProcessFailed = ProcessFailed Process.CmdSpec Int -- exit code
45+ deriving Show
46+
47+instance Exception ProcessFailed
48+
4549readProcessAndLogStderr
4650 :: (MonadIO m, MonadMask m, Cli m)
4751 => Severity -> CreateProcess -> m String
@@ -73,7 +77,7 @@ createProcess
7377 :: (MonadIO m, Cli m)
7478 => CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
7579createProcess p = do
76− putLog Debug $ "Creating process: " <> T.pack (show p)
80+ putLog Debug $ "Creating process: " <> T.pack (show $ cmdspec p)
7781 liftIO $ Process.createProcess p
7882
7983-- | Like `System.Process.createProcess_` but also logs (debug) the process being run
@@ -81,7 +85,7 @@ createProcess_
8185 :: (MonadIO m, Cli m)
8286 => String -> CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
8387createProcess_ name p = do
84− putLog Debug $ "Creating process " <> T.pack name <> ": " <> T.pack (show p)
88+ putLog Debug $ "Creating process " <> T.pack name <> ": " <> T.pack (show $ cmdspec p)
8589 liftIO $ Process.createProcess p
8690
8791-- | Like `System.Process.callProcess` but also logs (debug) the process being run
@@ -104,7 +108,6 @@ withProcess
104108 :: (MonadIO m, MonadMask m, Cli m)
105109 => CreateProcess -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
106110withProcess process f = do -- TODO: Use bracket.
107− let procTitle = T.pack $ show $ cmdspec process
108111 -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
109112 (_, Just out, Just err, p) <- createProcess $ process
110113 { std_out = CreatePipe , std_err = CreatePipe }
@@ -112,12 +115,8 @@ withProcess process f = do -- TODO: Use bracket.
112115 f out err -- Pass the handles to the passed function
113116
114117 liftIO (waitForProcess p) >>= \case
115− ExitSuccess -> return ()
116− ExitFailure code -> do
117− -- Log an error. We also fail immediately; however we should probably let the caller control that.
118− failWith $ procTitle <> " failed with exit code: " <> T.pack (show code)
119− return (out, err) -- Return the handles
120−
118+ ExitSuccess -> return (out, err)
119+ ExitFailure code -> throwM $ ProcessFailed (cmdspec process) code
121120
122121-- Create an input stream from the file handle, associating each item with the given severity.
123122streamHandle :: Severity -> Handle -> IO (InputStream (Severity, BSC.ByteString))
lib/cliapp/src/Obelisk/CliApp/Spinner.hs+85−28
@@ -1,17 +1,24 @@
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_, (>=>))
916import Control.Monad.Catch (MonadMask, mask, onException)
1017import Control.Monad.IO.Class
1118import Control.Monad.Log (Severity (..), logMessage)
12−import Control.Monad.Reader (MonadIO)
1319import Data.IORef
1420import qualified Data.List as L
21+import Data.Maybe (isNothing)
1522import Data.Text (Text)
1623import System.Console.ANSI (Color (Blue, Cyan, Green, Red))
1724
@@ -23,39 +30,86 @@ import Obelisk.CliApp.Types (Cli, CliConfig (..), HasCliConfig, Output (..), get
2330withSpinner
2431 :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m)
2532 => Text -> m a -> m a
26−withSpinner s f = do
33+withSpinner s = withSpinner' s $ Just $ const s
34+
35+-- | A spinner that leaves no trail after a successful run.
36+--
37+-- Use if you wish the spinner to be ephemerally visible to the user.
38+--
39+-- The 'no trail' property automatically carries over to sub-spinners (in that
40+-- they won't leave a trail either).
41+withSpinnerNoTrail
42+ :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m)
43+ => Text -> m a -> m a
44+withSpinnerNoTrail s = withSpinner' s Nothing
45+
46+-- | Advanced version that controls the display and content of the trail message.
47+withSpinner'
48+ :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m)
49+ => Text
50+ -> Maybe (a -> Text) -- ^ Leave an optional trail with the given message creator
51+ -> m a
52+ -> m a
53+withSpinner' msg mkTrail action = do
2754 noSpinner <- _cliConfig_noSpinner <$> getCliConfig
2855 if noSpinner
29− then putLog Notice s >> f
30− else bracket' run cleanup $ const f
56+ then putLog Notice msg >> action
57+ else bracket' run cleanup $ const action
3158 where
3259 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
60+ -- Add this log to the spinner stack, and start a spinner if it is top-level.
61+ modifyStack pushSpinner >>= \case
62+ True -> do -- Top-level spinner; fork a thread to manage output of anything on the stack
3863 ctrleThread <- fork $ allowUserToMakeLoggingVerbose enquiryCode
3964 spinnerThread <- fork $ runSpinner spinner $ \c -> do
40− logs <- liftIO $ concatStack c <$> readIORef stack
65+ logs <- renderSpinnerStack c . snd <$> readStack
4166 logMessage $ Output_Overwrite logs
4267 pure [ctrleThread, spinnerThread]
43− else pure []
44− cleanup failed tids = do
45− stack <- _cliConfig_spinnerStack <$> getCliConfig
68+ False -> -- Sub-spinner; nothing to do.
69+ pure []
70+ cleanup tids resultM = do
4671 liftIO $ mapM_ killThread tids
4772 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
73+ logsM <- modifyStack $ popSpinner $ case resultM of
74+ Nothing ->
75+ ( TerminalString_Colorized Red "✖"
76+ , Just msg -- Always display final message if there was an exception.
77+ )
78+ Just result ->
79+ ( TerminalString_Colorized Green "✔"
80+ , mkTrail <*> pure result
81+ )
82+ -- Last message, finish off with newline.
83+ forM_ logsM $ logMessage . Output_Write
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]
@@ -79,16 +133,19 @@ runSpinner spinner f = forM_ spinner $ f >=> const delay
79133type SpinnerTheme = [Text]
80134
81135-- Find more spinners at https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json
82−spinnerCircleHalves :: SpinnerTheme
83−spinnerCircleHalves = ["◐", "◓", "◑", "◒"]
136+_spinnerCircleHalves :: SpinnerTheme
137+_spinnerCircleHalves = ["◐", "◓", "◑", "◒"]
138+
139+spinnerMoon :: SpinnerTheme
140+spinnerMoon = ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"]
84141
85142defaultSpinnerTheme :: SpinnerTheme
86−defaultSpinnerTheme = spinnerCircleHalves
143+defaultSpinnerTheme = spinnerMoon
87144
88145-- | 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
146+bracket' :: MonadMask m => m a -> (a -> Maybe c -> m b) -> (a -> m c) -> m c
90147bracket' acquire release use = mask $ \unmasked -> do
91148 resource <- acquire
92− result <- unmasked (use resource) `onException` release True resource
93− _ <- release False resource
149+ result <- unmasked (use resource) `onException` release resource Nothing
150+ _ <- release resource $ Just result
94151 return result
lib/cliapp/src/Obelisk/CliApp/TerminalString.hs+6−6
@@ -18,9 +18,9 @@ import Data.Semigroup ((<>))
1818import Data.Text (Text)
1919import qualified Data.Text as T
2020import qualified Data.Text.IO as T
21−import System.Console.ANSI (Color, ColorIntensity (Vivid), ConsoleLayer (Foreground), SGR (Reset, SetColor),
22− SGR, setSGR, setSGRCode)
21+import System.Console.ANSI
2322import qualified System.Console.Terminal.Size as TerminalSize
23+import System.IO (Handle)
2424
2525-- | Printable text on terminals
2626--
@@ -65,11 +65,11 @@ colorizeText color s = mconcat
6565 ]
6666
6767-- | Safely print the string with the given ANSI control codes, resetting in the end.
68−putStrWithSGR :: MonadIO m => [SGR] -> Bool -> Text -> m ()
69−putStrWithSGR sgr withNewLine s = liftIO $ bracket_ (setSGR sgr) reset $ T.putStr s
68+putStrWithSGR :: MonadIO m => [SGR] -> Handle -> Bool -> Text -> m ()
69+putStrWithSGR sgr h withNewLine s = liftIO $ bracket_ (hSetSGR h sgr) reset $ T.hPutStr h s
7070 where
71− reset = setSGR [Reset] >> newline -- New line should come *after* reset (to reset cursor color).
72− newline = when withNewLine $ T.putStrLn ""
71+ reset = hSetSGR h [Reset] >> newline -- New line should come *after* reset (to reset cursor color).
72+ newline = when withNewLine $ T.hPutStrLn h ""
7373
7474-- | Code for https://en.wikipedia.org/wiki/Enquiry_character
7575enquiryCode :: String
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+6
@@ -15,6 +15,7 @@ library
1515 , binary
1616 , bytestring
1717 , obelisk-cliapp
18+ , obelisk-migration
1819 , containers
1920 , data-default
2021 , directory
@@ -23,9 +24,12 @@ library
2324 , github
2425 , here
2526 , hit
27+ , hpack
2628 , io-streams
2729 , exceptions
2830 , logging-effect
31+ , lens
32+ , modern-uri
2933 , monad-loops
3034 , mtl
3135 , network
@@ -48,6 +52,8 @@ library
4852 Obelisk.Command.Project
4953 Obelisk.Command.Run
5054 Obelisk.Command.Thunk
55+ Obelisk.Command.Upgrade
56+ Obelisk.Command.Upgrade.Hash
5157 Obelisk.Command.Utils
5258 Obelisk.Command.VmBuilder
5359 -- -fobject-code is so that the StaticPointers extension can work in ghci
lib/command/src/Obelisk/App.hs+4−3
@@ -11,8 +11,9 @@ import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
1111import Control.Monad.Reader (MonadIO, ReaderT (..), ask, runReaderT)
1212import Control.Monad.Trans.Class (lift)
1313import System.Directory (XdgDirectory (XdgData), getXdgDirectory)
14+import Control.Monad.Log (MonadLog)
1415
15−import Obelisk.CliApp (Cli, CliConfig, CliT, HasCliConfig, getCliConfig, runCli)
16+import Obelisk.CliApp (CliConfig, CliT, HasCliConfig, getCliConfig, runCli, Output)
1617
1718newtype Obelisk = Obelisk
1819 { _obelisk_cliConfig :: CliConfig
@@ -25,7 +26,7 @@ newtype ObeliskT m a = ObeliskT
2526 ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask
2627 , HasObelisk, HasCliConfig)
2728
28−deriving instance Monad m => Cli (ObeliskT m)
29+deriving instance Monad m => MonadLog Output (ObeliskT m)
2930
3031class HasObelisk m where
3132 getObelisk :: m Obelisk
@@ -43,7 +44,7 @@ runObelisk c =
4344 . unObeliskT
4445
4546type MonadObelisk m =
46− ( Cli m
47+ ( MonadLog Output m
4748 , HasCliConfig m
4849 , HasObelisk m
4950 , MonadIO m
lib/command/src/Obelisk/Command.hs+142−50
@@ -8,14 +8,15 @@
88module Obelisk.Command where
99
1010import Control.Monad
11+import Control.Monad.Catch (catch)
1112import Control.Monad.IO.Class (liftIO)
1213import qualified Data.Binary as Binary
1314import Data.Bool (bool)
1415import qualified Data.ByteString.Base16 as Base16
1516import qualified Data.ByteString.Lazy as LBS
1617import Data.List
17−import Data.Maybe (catMaybes)
18−import Data.Monoid
18+import Data.Maybe (catMaybes, listToMaybe)
19+import Data.Text (Text)
1920import qualified Data.Text as T
2021import Data.Text.Encoding
2122import GHC.StaticPtr
@@ -28,13 +29,17 @@ 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.Upgrade.Hash (getDirectoryHash, getHashAtGitRevision)
40+import Obelisk.Command.Utils
41+import qualified Obelisk.Command.VmBuilder as VmBuilder
42+import Obelisk.Migration (Hash)
3843
3944
4045data Args = Args
@@ -87,11 +92,17 @@ data ObCommand
8792 | ObCommand_Run
8893 | ObCommand_Thunk ThunkCommand
8994 | ObCommand_Repl
95+ | ObCommand_Watch
96+ | ObCommand_Upgrade (Maybe Text)
9097 | ObCommand_Internal ObInternal
9198 deriving Show
9299
93100data ObInternal
94101 = ObInternal_RunStaticIO StaticKey
102+ | ObInternal_Hash (Maybe Text)
103+ | ObInternal_CreateMigration
104+ | ObInternal_VerifyMigration
105+ | ObInternal_Migrate Hash
95106 | ObInternal_CLIDemo
96107 deriving Show
97108
@@ -101,8 +112,10 @@ inNixShell' p = withProjectRoot "." $ \root -> do
101112 projectShell root False "ghc" cmd
102113 where
103114 mkCmd = do
104− obArgs <- getObArgs =<< getArgsConfig
105− progName <- getExecutablePath
115+ argsCfg <- getArgsConfig
116+ myArgs <- getArgs
117+ obArgs <- parseCLIArgs argsCfg myArgs
118+ progName <- getObeliskExe
106119 return $ progName : catMaybes
107120 [ Just "--no-handoff"
108121 , bool Nothing (Just "--verbose") $ _args_verbose obArgs
@@ -119,6 +132,8 @@ obCommand cfg = hsubparser
119132 , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
120133 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
121134 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
135+ , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
136+ , 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"
122137 ])
123138 <|> subparser
124139 (mconcat
@@ -161,6 +176,9 @@ deployInitOpts = DeployInitOpts
161176 <$> strArgument (action "directory" <> metavar "DEPLOYDIR" <> help "Path to a directory that it will create")
162177 <*> strOption (long "ssh-key" <> action "file" <> metavar "SSHKEY" <> help "Path to an ssh key that it will symlink to")
163178 <*> some (strOption (long "hostname" <> metavar "HOSTNAME" <> help "hostname of the deployment target"))
179+ <*> strOption (long "route" <> metavar "PUBLICROUTE" <> help "Publicly accessible URL of your app")
180+ <*> strOption (long "admin-email" <> metavar "ADMINEMAIL" <> help "Email address where administrative alerts will be sent")
181+ <*> flag True False (long "disable-https" <> help "Disable automatic https configuration for the backend")
164182 <*> strOption (long "upstream" <> value "origin" <> metavar "REMOTE" <> help "git remote to use for the src thunk" <> showDefault)
165183
166184type TeamID = String
@@ -181,6 +199,9 @@ data DeployInitOpts = DeployInitOpts
181199 { _deployInitOpts_outputDir :: FilePath
182200 , _deployInitOpts_sshKey :: FilePath
183201 , _deployInitOpts_hostname :: [String]
202+ , _deployInitOpts_route :: String
203+ , _deployInitOpts_adminEmail :: String
204+ , _deployInitOpts_enableHttps :: Bool
184205 , _deployInitOpts_remote :: String
185206 }
186207 deriving Show
@@ -188,6 +209,10 @@ data DeployInitOpts = DeployInitOpts
188209internalCommand :: Parser ObInternal
189210internalCommand = subparser $ mconcat
190211 [ command "run-static-io" $ info (ObInternal_RunStaticIO <$> argument (eitherReader decodeStaticKey) (action "static-key")) mempty
212+ , 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)"
213+ , command "create-migration" $ info (pure ObInternal_CreateMigration) mempty
214+ , command "verify-migration" $ info (pure ObInternal_VerifyMigration) mempty
215+ , 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"
191216 , command "clidemo" $ info (pure ObInternal_CLIDemo) mempty
192217 ]
193218
@@ -232,66 +257,115 @@ parserPrefs = defaultPrefs
232257 { prefShowHelpOnEmpty = True
233258 }
234259
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
260+parseCLIArgs :: ArgsConfig -> [String] -> IO Args
261+parseCLIArgs cfg as = pure as >>= handleParseResult . execParserPure parserPrefs (argsInfo cfg)
242262
243−mkObeliskConfig :: ArgsConfig -> IO Obelisk
244−mkObeliskConfig argsCfg = do
263+-- | Create an Obelisk config for the current process.
264+mkObeliskConfig :: IO Obelisk
265+mkObeliskConfig = do
266+ cliArgs <- getArgs
267+ -- This function should not use argument parser (full argument parsing happens post handoff)
268+ let logLevel = toLogLevel $ "-v" `elem` cliArgs
245269 notInteractive <- not <$> isInteractiveTerm
246− logLevel <- toLogLevel <$> getObArgs argsCfg
247270 cliConf <- newCliConfig logLevel notInteractive notInteractive
248271 return $ Obelisk cliConf
249272 where
250− toLogLevel = bool Notice Debug . _args_verbose
251−
252−getObArgs :: ArgsConfig -> IO Args
253−getObArgs cfg = getArgs >>= handleParseResult . execParserPure parserPrefs (argsInfo cfg)
273+ toLogLevel = bool Notice Debug
274+ isInteractiveTerm = do
275+ isTerm <- hIsTerminalDevice stdout
276+ -- Running in bash/fish/zsh completion
277+ inShellCompletion <- liftIO $ isInfixOf "completion" . unwords <$> getArgs
278+
279+ -- Respect the user’s TERM environment variable. Dumb terminals
280+ -- like Eshell cannot handle lots of control sequences that the
281+ -- spinner uses.
282+ termEnv <- lookupEnv "TERM"
283+ let isDumb = termEnv == Just "dumb"
284+
285+ return $ isTerm && not inShellCompletion && not isDumb
286+
287+-- | For use from development obelisk repls
288+--
289+-- Example:
290+-- > runCommand $ someFuncInMonadObelisk ...
291+runCommand :: ObeliskT IO a -> IO a
292+runCommand f = flip runObelisk f =<< mkObeliskConfig
254293
255294main :: IO ()
256−main = do
257− argsCfg <- getArgsConfig
258− mkObeliskConfig argsCfg >>= (`runObelisk` ObeliskT (main' argsCfg))
295+main = runCommand . main' =<< getArgsConfig
259296
260297main' :: MonadObelisk m => ArgsConfig -> m ()
261298main' argsCfg = do
262299 obPath <- liftIO getExecutablePath
263− obArgs <- liftIO $ getObArgs argsCfg
300+ myArgs <- liftIO getArgs
264301 logLevel <- getLogLevel
265302 putLog Debug $ T.pack $ unwords
266303 [ "Starting Obelisk <" <> obPath <> ">"
267− , "args=" <> show obArgs
304+ , "args=" <> show myArgs
268305 , "logging-level=" <> show logLevel
269306 ]
270307
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
308+ (mainWithHandOff argsCfg <=< parseHandoff) =<< liftIO getArgs
309+ `catch`
310+ \(ProcessFailed p code) -> failWith $ "Process exited with code " <> tshow code <> "; " <> tshow p
311+
312+-- Type representing the result of a handoff calculation.
313+data HandOff m
314+ = HandOff_Yes FilePath [String] -- Handoff immediately to the given impl with the given args
315+ | HandOff_Decide (m Bool) FilePath [String] -- Handoff only if the action returns True
316+ | HandOff_No [String] -- Do not handoff, and continue with the given args
317+
318+mainWithHandOff :: MonadObelisk m => ArgsConfig -> HandOff m -> m ()
319+mainWithHandOff argsCfg = \case
320+ HandOff_Yes impl as -> do
321+ putLog Debug $ "Handing off to project obelisk " <> T.pack impl
322+ liftIO $ executeFile impl False ("--no-handoff" : as) Nothing
323+ HandOff_Decide f impl as -> do
324+ withSpinner' "Deciding whether to handoff"
325+ (Just $ bool
326+ "Continuing in ambient obelisk without handing off"
327+ "Handed off to project obelisk") f >>= \case
328+ True -> mainWithHandOff argsCfg $ HandOff_Yes impl as
329+ False -> mainWithHandOff argsCfg $ HandOff_No as
330+ HandOff_No as -> do
331+ putLog Debug $ "Not handing off"
332+ args' <- liftIO $ parseCLIArgs argsCfg as
333+ warnIfExtraneousFlag args'
334+ ob $ _args_command args'
335+ where
336+ warnIfExtraneousFlag a = case _args_noHandOffPassed a of
337+ False -> return ()
338+ True -> putLog Warning $
339+ "Ignoring unexpected --no-handoff (should only be passed once and as the first argument)"
340+
341+-- Handoff logic
342+parseHandoff :: MonadObelisk m => [String] -> m (HandOff m)
343+parseHandoff as' = case hasNoHandoff as' of
344+ (True, as) ->
345+ pure $ HandOff_No as
346+ (False, as) -> findProjectObeliskCommand "." >>= \case
347+ Nothing -> do
348+ putLog Debug "Not in a project; no need to hand off"
349+ pure $ HandOff_No as
350+ Just impl -> case getSubCommand as of
351+ Just "upgrade" ->
352+ pure $ HandOff_Decide (decideHandOffToProjectOb ".") impl as
353+ _ ->
354+ pure $ HandOff_Yes impl as
355+ where
356+ getSubCommand = listToMaybe . filter (not . isPrefixOf "-")
357+ hasNoHandoff = \case
358+ "--no-handoff" : xs ->
359+ -- If we've been told not to hand off, don't hand off
360+ (True, xs)
361+ x:xs
362+ -- Otherwise bash completion would always hand-off even if the user isn't trying to
363+ | "--bash-completion" `isPrefixOf` x && "--no-handoff" `elem` xs ->
364+ (True, x:xs)
365+ | otherwise ->
366+ (False, x:xs)
367+ xs ->
368+ (False, xs)
295369
296370ob :: MonadObelisk m => ObCommand -> m ()
297371ob = \case
@@ -316,7 +390,11 @@ ob = \case
316390 getThunkPtr' False root (T.pack $ _deployInitOpts_remote deployOpts)
317391 let sshKeyPath = _deployInitOpts_sshKey deployOpts
318392 hostname = _deployInitOpts_hostname deployOpts
319− deployInit thunkPtr (root </> "config") deployDir sshKeyPath hostname
393+ route = _deployInitOpts_route deployOpts
394+ adminEmail = _deployInitOpts_adminEmail deployOpts
395+ enableHttps = _deployInitOpts_enableHttps deployOpts
396+ deployInit thunkPtr (root </> "config") deployDir
397+ sshKeyPath hostname route adminEmail enableHttps
320398 DeployCommand_Push remoteBuilder -> deployPush "." $ case remoteBuilder of
321399 Nothing -> pure []
322400 Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
@@ -330,12 +408,26 @@ ob = \case
330408 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
331409 ThunkCommand_Pack thunks -> forM_ thunks $ \(ThunkPackOpts dir upstream) -> packThunk dir (T.pack upstream)
332410 ObCommand_Repl -> runRepl
411+ ObCommand_Watch -> inNixShell' $ static runWatch
412+ ObCommand_Upgrade branch -> do
413+ upgradeObelisk "." branch
333414 ObCommand_Internal icmd -> case icmd of
334415 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
335416 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
336417 Just p -> do
337418 c <- getObelisk
338419 liftIO $ runObelisk c $ deRefStaticPtr p
420+ ObInternal_Hash revM -> do
421+ hash <- case revM of
422+ Nothing -> getDirectoryHash [migrationDirName] "."
423+ Just rev -> fmap head $ getHashAtGitRevision [rev] [migrationDirName] "."
424+ putLog Notice hash
425+ ObInternal_CreateMigration ->
426+ createMigrationEdgeFromHEAD "."
427+ ObInternal_VerifyMigration ->
428+ verifyMigration "."
429+ ObInternal_Migrate fromHash ->
430+ migrateObelisk "." fromHash
339431 ObInternal_CLIDemo -> cliDemo
340432
341433--TODO: Clean up all the magic strings throughout this codebase
lib/command/src/Obelisk/Command/Deploy.hs+124−29
@@ -2,15 +2,17 @@
22{-# LANGUAGE LambdaCase #-}
33{-# LANGUAGE OverloadedStrings #-}
44{-# LANGUAGE ScopedTypeVariables #-}
5+{-# LANGUAGE ViewPatterns #-}
56module Obelisk.Command.Deploy where
67
8+import Control.Lens
79import Control.Monad
10+import Control.Monad.Catch (Exception (displayException), MonadThrow, throwM, try)
811import Control.Monad.IO.Class (liftIO)
912import Data.Bits
1013import Data.Default
1114import qualified Data.Map as Map
1215import Data.Maybe
13−import Data.Monoid
1416import qualified Data.Text as T
1517import qualified Data.Text.IO as T
1618import System.Directory
@@ -18,6 +20,9 @@ import System.Environment (getEnvironment)
1820import System.FilePath
1921import System.Posix.Files
2022import System.Process (delegate_ctlc, env, proc)
23+import Text.URI (URI)
24+import qualified Text.URI as URI
25+import Text.URI.Lens
2126
2227import Obelisk.App (MonadObelisk)
2328import Obelisk.CliApp (Severity (..), callProcessAndLogOutput, failWith, putLog, withSpinner)
@@ -26,32 +31,48 @@ import Obelisk.Command.Project
2631import Obelisk.Command.Thunk
2732import Obelisk.Command.Utils
2833
29−deployInit :: MonadObelisk m => ThunkPtr -> FilePath -> FilePath -> FilePath -> [String] -> m ()
30−deployInit thunkPtr configDir deployDir sshKeyPath hostnames = do
31− hasConfigDir <- liftIO $ do
32− createDirectoryIfMissing True deployDir
33− doesDirectoryExist configDir
34− localKey <- liftIO (doesFileExist sshKeyPath) >>= \case
35− False -> failWith $ T.pack $ "ob deploy init: file does not exist: " <> sshKeyPath
36− True -> pure $ deployDir </> "ssh_key"
37− callProcessAndLogOutput (Notice, Error) $
38− cp [sshKeyPath, localKey]
39− liftIO $ setFileMode localKey $ ownerReadMode .|. ownerWriteMode
40− liftIO $ writeFile (deployDir </> "backend_hosts") $ unlines hostnames
34+deployInit
35+ :: MonadObelisk m
36+ => ThunkPtr
37+ -> FilePath
38+ -> FilePath
39+ -> FilePath
40+ -> [String] -- ^ hostnames
41+ -> String -- ^ route
42+ -> String -- ^ admin email
43+ -> Bool -- ^ enable https
44+ -> m ()
45+deployInit thunkPtr configDir deployDir sshKeyPath hostnames route adminEmail enableHttps = do
46+ (hasConfigDir, localKey) <- withSpinner ("Preparing " <> T.pack deployDir) $ do
47+ hasConfigDir <- liftIO $ do
48+ createDirectoryIfMissing True deployDir
49+ doesDirectoryExist configDir
50+ localKey <- liftIO (doesFileExist sshKeyPath) >>= \case
51+ False -> failWith $ T.pack $ "ob deploy init: file does not exist: " <> sshKeyPath
52+ True -> pure $ deployDir </> "ssh_key"
53+ callProcessAndLogOutput (Notice, Error) $
54+ proc "cp" [sshKeyPath, localKey]
55+ liftIO $ setFileMode localKey $ ownerReadMode .|. ownerWriteMode
56+ return $ (hasConfigDir, localKey)
57+ withSpinner "Validating configuration" $ do
58+ void $ getHostFromRoute enableHttps route -- make sure that hostname is present
4159 forM_ hostnames $ \hostname -> do
4260 putLog Notice $ "Verifying host keys (" <> T.pack hostname <> ")"
61+ -- Note: we can't use a spinner here as this function will prompt the user.
4362 verifyHostKey (deployDir </> "backend_known_hosts") localKey hostname
44− when hasConfigDir $ do
63+ when hasConfigDir $ withSpinner "Importing project configuration" $ do
4564 callProcessAndLogOutput (Notice, Error) $
46− cp
47− [ "-r"
48− , "-T"
49− , configDir
50− , deployDir </> "config"
51− ]
52− liftIO $ createThunk (deployDir </> "src") thunkPtr
53− liftIO $ setupObeliskImpl deployDir
54− initGit deployDir
65+ proc "cp" [ "-r" , "-T" , configDir , deployDir </> "config"]
66+ withSpinner "Writing deployment configuration" $ do
67+ writeDeployConfig deployDir "backend_hosts" $ unlines hostnames
68+ writeDeployConfig deployDir "enable_https" $ show enableHttps
69+ writeDeployConfig deployDir "admin_email" adminEmail
70+ writeDeployConfig deployDir ("config" </> "common" </> "route") $ route
71+ withSpinner "Creating source thunk (./src)" $ liftIO $ do
72+ createThunk (deployDir </> "src") thunkPtr
73+ setupObeliskImpl deployDir
74+ withSpinner ("Initializing git repository (" <> T.pack deployDir <> ")") $
75+ initGit deployDir
5576
5677setupObeliskImpl :: FilePath -> IO ()
5778setupObeliskImpl deployDir = do
@@ -61,18 +82,27 @@ setupObeliskImpl deployDir = do
6182
6283deployPush :: MonadObelisk m => FilePath -> m [String] -> m ()
6384deployPush deployPath getNixBuilders = do
64− let backendHosts = deployPath </> "backend_hosts"
65− host <- liftIO $ fmap (T.unpack . T.strip) $ T.readFile backendHosts
85+ host <- readDeployConfig deployPath "backend_hosts"
86+ adminEmail <- readDeployConfig deployPath "admin_email"
87+ enableHttps <- read <$> readDeployConfig deployPath "enable_https"
88+ route <- readDeployConfig deployPath $ "config" </> "common" </> "route"
89+ routeHost <- getHostFromRoute enableHttps route
6690 let srcPath = deployPath </> "src"
6791 build = do
6892 builders <- getNixBuilders
6993 buildOutput <- nixBuild $ def
7094 { _nixBuildConfig_target = Target
7195 { _target_path = srcPath
72− , _target_attr = Just "server"
96+ , _target_attr = Just "server.system"
7397 }
7498 , _nixBuildConfig_outLink = OutLink_None
75− , _nixBuildConfig_args = pure $ Arg "hostName" host
99+ , _nixBuildConfig_args =
100+ [ strArg "hostName" host
101+ , strArg "adminEmail" adminEmail
102+ , strArg "routeHost" routeHost
103+ , boolArg "enableHttps" enableHttps
104+ , rawArg "config" $ deployPath </> "config"
105+ ]
76106 , _nixBuildConfig_builders = builders
77107 }
78108 return $ listToMaybe $ lines buildOutput
@@ -80,7 +110,7 @@ deployPush deployPath getNixBuilders = do
80110 Right (ThunkData_Packed _) ->
81111 build
82112 Right (ThunkData_Checkout _) -> do
83− checkGitCleanStatus srcPath >>= \case
113+ checkGitCleanStatus srcPath True >>= \case
84114 True -> do
85115 result <- build
86116 packThunk srcPath "origin" -- get upstream
@@ -109,13 +139,14 @@ deployPush deployPath getNixBuilders = do
109139 ]
110140 ]
111141 deployAndSwitch res
112− isClean <- checkGitCleanStatus deployPath
142+ isClean <- checkGitCleanStatus deployPath True
113143 when (not isClean) $ do
114144 withSpinner "Commiting changes to Git" $ do
115145 callProcessAndLogOutput (Debug, Error) $ proc "git"
116146 ["-C", deployPath, "add", "."]
117147 callProcessAndLogOutput (Debug, Error) $ proc "git"
118148 ["-C", deployPath, "commit", "-m", "New deployment"]
149+ putLog Notice $ "Deployed => " <> T.pack route
119150 where
120151 callProcess' envMap cmd args = do
121152 processEnv <- Map.toList . (envMap <>) . Map.fromList <$> liftIO getEnvironment
@@ -133,6 +164,14 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
133164 result <- nixBuildAttrWithCache srcDir $ platform <> ".frontend"
134165 callProcessAndLogOutput (Notice, Error) $ proc (result </> "bin" </> "deploy") mobileArgs
135166
167+-- | Simplified deployment configuration mechanism. At one point we may revisit this.
168+writeDeployConfig :: MonadObelisk m => FilePath -> FilePath -> String -> m ()
169+writeDeployConfig deployDir fname = liftIO . writeFile (deployDir </> fname)
170+
171+readDeployConfig :: MonadObelisk m => FilePath -> FilePath -> m String
172+readDeployConfig deployDir fname = liftIO $ do
173+ fmap (T.unpack . T.strip) $ T.readFile $ deployDir </> fname
174+
136175verifyHostKey :: MonadObelisk m => FilePath -> FilePath -> String -> m ()
137176verifyHostKey knownHostsPath keyPath hostName =
138177 callProcessAndLogOutput (Notice, Warning) $ proc "ssh" $
@@ -147,3 +186,59 @@ sshArgs knownHostsPath keyPath askHostKeyCheck =
147186 , "-o", "StrictHostKeyChecking=" <> if askHostKeyCheck then "ask" else "yes"
148187 , "-i", keyPath
149188 ]
189+
190+-- common/route validation
191+-- TODO: move these to executable-config once the typed-config stuff is done.
192+
193+data InvalidRoute
194+ = InvalidRoute_NotHttps URI
195+ | InvalidRoute_MissingScheme URI
196+ | InvalidRoute_MissingHost URI
197+ | InvalidRoute_HasPort URI
198+ | InvalidRoute_HasPath URI
199+ deriving Show
200+
201+instance Exception InvalidRoute where
202+ displayException = \case
203+ InvalidRoute_MissingScheme uri -> route uri "must have an URI scheme"
204+ InvalidRoute_NotHttps uri -> route uri "must be HTTPS"
205+ InvalidRoute_MissingHost uri -> route uri "must contain a hostname"
206+ InvalidRoute_HasPort uri -> route uri "cannot specify port"
207+ InvalidRoute_HasPath uri -> route uri "cannot contain path"
208+ where
209+ route uri err = T.unpack $ "Route (" <> URI.render uri <> ") " <> err
210+
211+-- | Get the hostname from a https route
212+--
213+-- Fail if the route is invalid (i.e, no host present or scheme is not https)
214+getHostFromRoute
215+ :: MonadObelisk m
216+ => Bool -- ^ Ensure https?
217+ -> String
218+ -> m String
219+getHostFromRoute mustBeHttps route = do
220+ result :: Either InvalidRoute String <- try $ do
221+ validateCommonRouteAndGetHost mustBeHttps =<< URI.mkURI (T.strip $ T.pack route)
222+ either (failWith . T.pack . displayException) pure result
223+
224+validateCommonRouteAndGetHost
225+ :: (MonadThrow m, MonadObelisk m)
226+ => Bool -- ^ Ensure https?
227+ -> URI
228+ -> m String
229+validateCommonRouteAndGetHost mustBeHttps uri = do
230+ case uri ^? uriScheme of
231+ Just (Just (URI.unRText -> s)) -> case (mustBeHttps, s) of
232+ (False, _) -> pure ()
233+ (True, "https") -> pure ()
234+ _ -> throwM $ InvalidRoute_NotHttps uri
235+ _ -> throwM $ InvalidRoute_MissingScheme uri
236+ case uri ^. uriPath of
237+ [] -> pure ()
238+ _path -> throwM $ InvalidRoute_HasPath uri
239+ case uri ^? uriAuthority . _Right . authPort of
240+ Just (Just _port) -> throwM $ InvalidRoute_HasPort uri
241+ _ -> pure ()
242+ case uri ^? uriAuthority . _Right . authHost of
243+ Nothing -> throwM $ InvalidRoute_MissingHost uri
244+ Just sslHost -> return $ T.unpack $ URI.unRText sslHost
lib/command/src/Obelisk/Command/Nix.hs+54−9
@@ -1,23 +1,33 @@
11{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE LambdaCase #-}
23{-# LANGUAGE MultiParamTypeClasses #-}
34{-# LANGUAGE OverloadedStrings #-}
45{-# LANGUAGE ScopedTypeVariables #-}
56module Obelisk.Command.Nix
67 ( nixBuild
8+ , withNixRemoteCheck
79 , NixBuildConfig (..)
810 , Target (..)
911 , OutLink (..)
1012 , Arg (..)
13+ , boolArg
14+ , rawArg
15+ , strArg
1116 ) where
1217
18+import Control.Monad.Catch (catch, throwM)
19+import Control.Monad.IO.Class (liftIO)
20+import Data.Bool (bool)
1321import Data.Default
1422import Data.List (intercalate)
1523import Data.Monoid ((<>))
1624import qualified Data.Text as T
25+import System.Directory
26+import System.Environment
1727import System.Process (proc)
1828
1929import Obelisk.App (MonadObelisk)
20−import Obelisk.CliApp (Severity (..), readProcessAndLogStderr, withSpinner)
30+import Obelisk.CliApp
2131
2232-- | Where to put nix-build output
2333data OutLink
@@ -40,10 +50,24 @@ instance Default Target where
4050 , _target_attr = Nothing
4151 }
4252
43−data Arg = Arg
44− { _arg_key :: String
45− , _arg_value :: String
46− }
53+data Arg
54+ = Arg_Str String String
55+ | Arg_Expr String String
56+ deriving (Eq, Show)
57+
58+strArg :: String -> String -> Arg
59+strArg k = Arg_Str k
60+
61+rawArg :: String -> String -> Arg
62+rawArg k = Arg_Expr k
63+
64+boolArg :: String -> Bool -> Arg
65+boolArg k = Arg_Expr k . bool "false" "true"
66+
67+cliFromArgs :: [Arg] -> [String]
68+cliFromArgs = concatMap $ \case
69+ Arg_Str k v -> ["--argstr", k, v]
70+ Arg_Expr k v -> ["--arg", k, v]
4771
4872data NixBuildConfig = NixBuildConfig
4973 { _nixBuildConfig_target :: Target
@@ -56,8 +80,8 @@ instance Default NixBuildConfig where
5680 def = NixBuildConfig def def mempty mempty
5781
5882nixBuild :: MonadObelisk m => NixBuildConfig -> m FilePath
59−nixBuild cfg = withSpinner msg $ do
60− readProcessAndLogStderr Debug $ proc "nix-build" $ mconcat
83+nixBuild cfg = withSpinner' ("Running nix-build on " <> desc) (Just $ const $ "Built " <> desc) $ do
84+ withNixRemoteCheck $ readProcessAndLogStderr Debug $ proc "nix-build" $ mconcat
6185 [[path], attrArg, args, outLink, buildersArg]
6286 where
6387 path = _target_path $ _nixBuildConfig_target cfg
@@ -65,12 +89,33 @@ nixBuild cfg = withSpinner msg $ do
6589 attrArg = case attr of
6690 Nothing -> []
6791 Just a -> ["-A", a]
68− args = mconcat [["--argstr", k, v] | Arg k v <- _nixBuildConfig_args cfg]
92+ args = cliFromArgs $ _nixBuildConfig_args cfg
6993 outLink = case _nixBuildConfig_outLink cfg of
7094 OutLink_Default -> []
7195 OutLink_None -> ["--no-out-link"]
7296 OutLink_IndirectRoot l -> ["--out-link", l]
73− msg = T.pack $ "Running nix-build on " <> path <> maybe "" (\a -> " [" <> a <> "]") attr
97+ desc = T.pack $ path <> maybe "" (\a -> " [" <> a <> "]") attr
7498 buildersArg = case _nixBuildConfig_builders cfg of
7599 [] -> []
76100 builders -> ["--builders", intercalate ";" builders]
101+
102+-- | If a nix command fails, and this may be related to the NIX_REMOTE issue,
103+-- tell the user what to do.
104+--
105+-- Added: June, 2018. Consider removing this eventually.
106+withNixRemoteCheck :: MonadObelisk m => m a -> m a
107+withNixRemoteCheck f = f `catch` \e@(ProcessFailed _ _) -> do
108+ liftIO (lookupEnv "NIX_REMOTE") >>= \case
109+ Just _ -> throwM e
110+ Nothing -> liftIO (writable <$> getPermissions "/nix/var/nix/db") >>= \case
111+ True -> throwM e
112+ False -> do
113+ putLog Error "!!! "
114+ putLog Error "!!! A nix command failed to run. You might need to set the NIX_REMOTE environment variable"
115+ putLog Error "!!! to `daemon`. To do this, run the following before running obelisk:"
116+ putLog Error "!!! "
117+ putLog Error "!!! export NIX_REMOTE=daemon"
118+ putLog Error "!!! "
119+ putLog Error "!!! For details, see https://github.com/NixOS/nixpkgs/issues/5713"
120+ putLog Error "!!! "
121+ throwM e
lib/command/src/Obelisk/Command/Project.hs+5−6
@@ -20,7 +20,6 @@ import Control.Monad.IO.Class (liftIO)
2020import Control.Monad.State
2121import Data.Bits
2222import Data.Function (on)
23−import Data.Monoid
2423import qualified Data.Text as T
2524import System.Directory
2625import System.FilePath
@@ -33,9 +32,9 @@ import GitHub.Data.GitData (Branch)
3332import GitHub.Data.Name (Name)
3433
3534import Obelisk.App (MonadObelisk)
36−import Obelisk.CliApp (Severity (..), callProcessAndLogOutput, createProcess_, failWith, putLog, withSpinner)
35+import Obelisk.CliApp
3736import Obelisk.Command.Thunk
38−import Obelisk.Command.Utils (cp)
37+import Obelisk.Command.Nix (withNixRemoteCheck)
3938--TODO: Make this module resilient to random exceptions
4039
4140--TODO: Don't hardcode this
@@ -86,7 +85,7 @@ initProject source = withSystemTempDirectory "ob-init" $ \tmpDir -> do
8685 skel <- nixBuildAttrWithCache implDir "skeleton" --TODO: I don't think there's actually any reason to cache this
8786
8887 callProcessAndLogOutput (Notice, Error) $
89− cp
88+ proc "cp"
9089 [ "-r"
9190 , "--preserve=links"
9291 , obDir
@@ -96,7 +95,7 @@ initProject source = withSystemTempDirectory "ob-init" $ \tmpDir -> do
9695
9796 withSpinner "Copying project skeleton" $ do
9897 callProcessAndLogOutput (Notice, Error) $
99− cp --TODO: Make this package depend on nix-prefetch-url properly
98+ proc "cp"
10099 [ "-r"
101100 , "--no-preserve=mode"
102101 , "-T"
@@ -202,7 +201,7 @@ inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
202201 projectShell root False shellName command
203202
204203projectShell :: MonadObelisk m => FilePath -> Bool -> String -> String -> m ()
205−projectShell root isPure shellName command = do
204+projectShell root isPure shellName command = withNixRemoteCheck $ do
206205 (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCwd (Just root) $ proc "nix-shell" $
207206 [ "--pure" | isPure ] <>
208207 [ "-A"
lib/command/src/Obelisk/Command/Run.hs+86−32
@@ -13,31 +13,45 @@ import Data.Either
1313import Data.List
1414import Data.List.NonEmpty as NE
1515import Data.Maybe
16−import Data.Monoid
1716import qualified Data.Text as T
18−import Distribution.PackageDescription.Parse (ParseResult (..), parseGenericPackageDescription)
17+import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
18+import Distribution.Parsec.ParseResult (runParseResult)
19+import Distribution.Pretty
1920import Distribution.Types.BuildInfo
2021import Distribution.Types.CondTree
2122import Distribution.Types.GenericPackageDescription
2223import Distribution.Types.Library
23−import Distribution.Utils.Generic (withUTF8FileContents)
24+import Distribution.Utils.Generic
25+import Hpack.Config
26+import Hpack.Render
27+import Hpack.Yaml
28+import Language.Haskell.Extension
2429import Network.Socket
2530import System.Directory
2631import System.FilePath
2732import System.IO.Temp (withSystemTempDirectory)
33+import Data.ByteString (ByteString)
2834
2935import Obelisk.App (MonadObelisk, ObeliskT)
3036import Obelisk.CliApp (CliT (..), HasCliConfig, Severity (..), callCommand, failWith, getCliConfig, putLog,
3137 runCli)
3238import Obelisk.Command.Project (inProjectShell)
3339
40+data CabalPackageInfo = CabalPackageInfo
41+ { _cabalPackageInfo_packageRoot :: FilePath
42+ , _cabalPackageInfo_sourceDirs :: NE.NonEmpty FilePath
43+ -- ^ List of hs src dirs of the library component
44+ , _cabalPackageInfo_defaultExtensions :: [Extension]
45+ -- ^ List of globally enable extensions of the library component
46+ }
47+
3448-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
3549run :: ObeliskT IO ()
3650run = do
3751 pkgs <- getLocalPkgs
3852 withGhciScript pkgs $ \dotGhciPath -> do
3953 freePort <- getFreePort
40− runGhcid dotGhciPath $ Just $ unwords ["main", show freePort]
54+ runGhcid dotGhciPath $ Just $ unwords ["run", show freePort, "Backend.backend", "Frontend.frontend"]
4155
4256runRepl :: MonadObelisk m => m ()
4357runRepl = do
@@ -45,35 +59,63 @@ runRepl = do
4559 withGhciScript pkgs $ \dotGhciPath -> do
4660 runGhciRepl dotGhciPath
4761
62+runWatch :: MonadObelisk m => m ()
63+runWatch = do
64+ pkgs <- getLocalPkgs
65+ withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
66+
4867-- | Relative paths to local packages of an obelisk project
4968-- TODO a way to query this
5069getLocalPkgs :: Applicative f => f [FilePath]
5170getLocalPkgs = pure ["backend", "common", "frontend"]
5271
53−parseHsSrcDir
72+parseCabalPackage
5473 :: (MonadObelisk m)
55− => FilePath -- ^ package cabal file path
56− -> m (Maybe (NE.NonEmpty FilePath)) -- ^ List of hs src dirs of the library component
57−parseHsSrcDir cabalFp = do
58− exists <- liftIO $ doesFileExist cabalFp
59− if exists
60− then do
61− withUTF8FileContentsM cabalFp $ \cabal -> do
62− case parseGenericPackageDescription cabal of
63− ParseOk warnings gpkg -> do
64− mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
65− return $ do
66− (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
67− pure $ fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
68− ParseFailed e -> do
69− putLog Error $ T.pack $ "Failed to parse cabal file: " <> show e
74+ => FilePath -- ^ package directory
75+ -> m (Maybe CabalPackageInfo)
76+parseCabalPackage dir = do
77+ let cabalFp = dir </> (takeBaseName dir <> ".cabal")
78+ hpackFp = dir </> "package.yaml"
79+ hasCabal <- liftIO $ doesFileExist cabalFp
80+ hasHpack <- liftIO $ doesFileExist hpackFp
81+
82+ mCabalContents <- if hasCabal
83+ then Just <$> liftIO (readUTF8File cabalFp)
84+ else if hasHpack
85+ then do
86+ let decodeOptions = DecodeOptions hpackFp Nothing decodeYaml
87+ liftIO (readPackageConfig decodeOptions) >>= \case
88+ Left err -> do
89+ putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
7090 return Nothing
71− else return Nothing
91+ Right (DecodeResult hpackPackage _ _) -> do
92+ return $ Just $ renderPackage [] hpackPackage
93+ else return Nothing
7294
73−withUTF8FileContentsM :: (MonadIO m, HasCliConfig m) => FilePath -> (String -> CliT IO a) -> m a
95+ fmap join $ forM mCabalContents $ \cabalContents -> do
96+ let (warnings, result) = runParseResult $ parseGenericPackageDescription $
97+ toUTF8BS $ cabalContents
98+ mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
99+ case result of
100+ Right gpkg -> do
101+ return $ do
102+ (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
103+ pure $ CabalPackageInfo
104+ { _cabalPackageInfo_packageRoot = takeDirectory cabalFp
105+ , _cabalPackageInfo_sourceDirs =
106+ fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
107+ , _cabalPackageInfo_defaultExtensions =
108+ defaultExtensions $ libBuildInfo lib
109+ }
110+ Left (_, errors) -> do
111+ putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
112+ mapM_ (putLog Error) $ fmap (T.pack . show) errors
113+ return Nothing
114+
115+withUTF8FileContentsM :: (MonadIO m, HasCliConfig m) => FilePath -> (ByteString -> CliT IO a) -> m a
74116withUTF8FileContentsM fp f = do
75117 c <- getCliConfig
76− liftIO $ withUTF8FileContents fp $ runCli c . f
118+ liftIO $ withUTF8FileContents fp $ runCli c . f . toUTF8BS
77119
78120-- | Create ghci configuration to load the given packages
79121withGhciScript
@@ -82,26 +124,37 @@ withGhciScript
82124 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporory .ghci
83125 -> m ()
84126withGhciScript pkgs f = do
85− (pkgDirErrs, hsSrcDirs) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
86− let cabalFp = pkg </> pkg <.> "cabal"
87− flip fmap (parseHsSrcDir cabalFp) $ \case
127+ (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
128+ flip fmap (parseCabalPackage pkg) $ \case
88129 Nothing -> Left pkg
89− Just hsSrcDirs -> Right $ toList $ fmap (pkg </>) hsSrcDirs
130+ Just packageInfo -> Right packageInfo
90131
91− when (null hsSrcDirs) $
132+ when (null packageInfos) $
92133 failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " pkgs
93134 unless (null pkgDirErrs) $
94135 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
95136
96− let dotGhci = unlines
97− [ ":set -i" <> intercalate ":" (mconcat hsSrcDirs)
98− , ":load ./devel/Devel.hs" -- TODO more robust filepath
137+ let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
138+ extensionsLine = if extensions == mempty
139+ then ""
140+ else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
141+ dotGhci = unlines $
142+ [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
143+ , extensionsLine
144+ , ":load Backend Frontend"
145+ , "import Obelisk.Run"
146+ , "import qualified Frontend"
147+ , "import qualified Backend"
99148 ]
100149 withSystemTempDirectory "ob-ghci" $ \fp -> do
101150 let dotGhciPath = fp </> ".ghci"
102151 liftIO $ writeFile dotGhciPath dotGhci
103152 f dotGhciPath
104153
154+ where
155+ rootedSourceDirs pkg = NE.toList $
156+ (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
157+
105158-- | Run ghci repl
106159runGhciRepl
107160 :: MonadObelisk m
@@ -119,7 +172,8 @@ runGhcid dotGhci mcmd = callCommand $ unwords $ "ghcid" : opts
119172 where
120173 opts =
121174 [ "-W"
122− , "--command='ghci -ghci-script " <> dotGhci <> "' "
175+ --TODO: The decision of whether to use -fwarn-redundant-constraints should probably be made by the user
176+ , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints -ghci-script " <> dotGhci <> "' "
123177 , "--reload=config"
124178 , "--outputfile=ghcid-output.txt"
125179 ] <> testCmd
lib/command/src/Obelisk/Command/Thunk.hs+82−60
@@ -7,7 +7,6 @@
77{-# LANGUAGE PatternGuards #-}
88{-# LANGUAGE ScopedTypeVariables #-}
99{-# LANGUAGE TypeApplications #-}
10−
1110module Obelisk.Command.Thunk
1211 ( ThunkPtr (..)
1312 , ThunkRev (..)
@@ -15,6 +14,7 @@ module Obelisk.Command.Thunk
1514 , ThunkData (..)
1615 , ReadThunkError (..)
1716 , GitHubSource (..)
17+ , getThunkGitBranch
1818 , getLatestRev
1919 , updateThunkToLatest
2020 , createThunk
@@ -24,6 +24,7 @@ module Obelisk.Command.Thunk
2424 , unpackThunk
2525 , packThunk
2626 , readThunk
27+ , updateThunk
2728 , getThunkPtr
2829 , getThunkPtr'
2930 ) where
@@ -49,7 +50,6 @@ import Data.List.NonEmpty (NonEmpty (..))
4950import qualified Data.List.NonEmpty as NonEmpty
5051import qualified Data.Map as Map
5152import Data.Maybe
52−import Data.Monoid
5353import Data.Set (Set)
5454import qualified Data.Set as Set
5555import Data.Text (Text)
@@ -72,9 +72,9 @@ import System.Posix (getSymbolicLinkStatus, modificationTime)
7272import System.Process (proc)
7373
7474import Obelisk.App (MonadObelisk)
75−import Obelisk.CliApp (Severity (..), callProcessAndLogOutput, failWith, putLog, readProcessAndLogStderr,
76− withExitFailMessage, withSpinner)
77−import Obelisk.Command.Utils (checkGitCleanStatus, cp, git, procWithPackages)
75+import Obelisk.CliApp
76+import Obelisk.Command.Nix (withNixRemoteCheck)
77+import Obelisk.Command.Utils
7878
7979--TODO: Support symlinked thunk data
8080data ThunkData
@@ -123,6 +123,11 @@ data GitSource = GitSource
123123 }
124124 deriving (Show, Eq, Ord)
125125
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+
126131commitNameToRef :: Name Commit -> Ref
127132commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
128133
@@ -130,13 +135,13 @@ commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
130135getNixSha256ForUriUnpacked :: MonadObelisk m => URI -> m NixSha256
131136getNixSha256ForUriUnpacked uri =
132137 withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> T.pack (show uri)) $ do
133− fmap T.pack $ readProcessAndLogStderr Debug $
134− procWithPackages ["nix-prefetch-scripts"] "nix-prefetch-url" ["--unpack" , "--type" , "sha256" , show uri]
138+ fmap T.pack $ withNixRemoteCheck $ readProcessAndLogStderr Debug $
139+ proc "nix-prefetch-url" ["--unpack" , "--type" , "sha256" , show uri]
135140
136141nixPrefetchGit :: MonadObelisk m => URI -> Text -> Bool -> m NixSha256
137142nixPrefetchGit uri rev fetchSubmodules =
138143 withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> T.pack (show uri) <> " at " <> rev) $ do
139− out <- readProcessAndLogStderr Debug $
144+ out <- withNixRemoteCheck $ readProcessAndLogStderr Debug $
140145 proc "nix-prefetch-git" $ filter (/="")
141146 [ "--url", show uri
142147 , "--rev", T.unpack rev
@@ -151,10 +156,10 @@ nixPrefetchGit uri rev fetchSubmodules =
151156getLatestGitRev :: MonadObelisk m => GitSource -> m ThunkRev
152157getLatestGitRev s = withSystemTempDirectory "git-clone" $ \tmpDir -> do
153158 withExitFailMessage ("Unable to determine latest git revision for branch " <> branch) $ do
154− out <- fmap T.pack $ readProcessAndLogStderr Debug $ git tmpDir
155− [ ["clone", "--no-checkout", show $ _gitSource_url s] -- Might be able to use --bare here instead to go faster.
156− , ["log", T.unpack branch, "--max-count=1", "--format=%H"]
157− ]
159+ let git = fmap T.pack . readProcessAndLogStderr Debug . gitProc tmpDir
160+ out <- liftA2 (<>)
161+ (git ["clone", "--no-checkout", show $ _gitSource_url s]) -- Might be able to use --bare here instead to go faster.
162+ (git ["log", T.unpack branch, "--max-count=1", "--format=%H"])
158163
159164 case T.strip <$> safeLast (T.lines out) of
160165 Nothing -> failWith $ "Unable to parse git log: " <> out
@@ -406,7 +411,7 @@ createThunkWithLatest target s = do
406411 }
407412
408413updateThunkToLatest :: MonadObelisk m => FilePath -> m ()
409−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
410415 (overwrite, ptr) <- readThunk target >>= \case
411416 Left err -> failWith $ T.pack $ "thunk update: " <> show err
412417 Right c -> case c of
@@ -573,11 +578,40 @@ nixBuildAttrWithCache exprPath attr = do
573578 , _nixBuildConfig_outLink = OutLink_None
574579 }
575580
576−unpackThunk
577− :: MonadObelisk m
578− => FilePath
579− -> m ()
580−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
581615 Left err -> failWith $ "thunk unpack: " <> T.pack (show err)
582616 --TODO: Overwrite option that rechecks out thunk; force option to do so even if working directory is dirty
583617 Right (ThunkData_Checkout _) -> failWith "thunk unpack: thunk is already unpacked"
@@ -593,18 +627,18 @@ unpackThunk thunkDir = readThunk thunkDir >>= \case
593627 Nothing -> failWith "Cannot determine clone URI for thunk source"
594628 Just c -> return $ T.unpack $ getUrl c
595629
596− withSpinner ("Retrieving thunk " <> T.pack thunkName <> " from GitHub") $ do
597− callProcessAndLogOutput (Notice, Notice) $
630+ withSpinner' ("Fetching thunk " <> T.pack thunkDir)
631+ ( finalMsg noTrail $ const $ "Fetched thunk " <> T.pack thunkDir) $ do
632+ callProcessAndLogOutput (Debug, Debug) $
598633 proc "hub" ["clone", "-n", githubURI, tmpRepo]
599634 --If this directory already exists then something is weird and we should fail
600635 liftIO $ createDirectory obGitDir
601− callProcessAndLogOutput (Notice, Error) $
602− cp ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
636+ callProcessAndLogOutput (Debug, Error) $
637+ proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
603638
604− withSpinner ("Preparing thunk in " <> T.pack thunkDir) $ do
605639 -- Checkout
606− putLog Notice $ "Checking out " <> T.pack (show $ maybe "" untagName $ _gitHubSource_branch s)
607− callProcessAndLogOutput (Notice, Notice) $
640+ putLog Debug $ "Checking out " <> T.pack (show $ maybe "" untagName $ _gitHubSource_branch s)
641+ callProcessAndLogOutput (Debug, Debug) $
608642 proc "hub" $ concat
609643 [ ["-C", tmpRepo]
610644 , pure "checkout"
@@ -613,43 +647,41 @@ unpackThunk thunkDir = readThunk thunkDir >>= \case
613647 ]
614648 -- Set upstream branch
615649 forM_ (_gitHubSource_branch s) $ \branch ->
616− callProcessAndLogOutput (Notice, Error) $
650+ callProcessAndLogOutput (Debug, Error) $
617651 proc "hub" ["-C", tmpRepo, "branch", "-u", "origin/" <> T.unpack (untagName branch)]
618− callProcessAndLogOutput (Notice, Error) $
619− proc "rm" ["-r", thunkDir]
620− callProcessAndLogOutput (Notice, Error) $
621− procWithPackages ["coreutils"] "mv" ["-T", tmpRepo, thunkDir]
652+ callProcessAndLogOutput (Notice, Error) $ proc "rm" ["-r", thunkDir]
653+ callProcessAndLogOutput (Notice, Error) $ proc "mv" ["-T", tmpRepo, thunkDir]
622654
623655 ThunkSource_Git s -> do
624− withSpinner ("Retreiving thunk " <> T.pack thunkName <> " from Git repo") $
625− callProcessAndLogOutput (Notice, Notice) $ git tmpRepo
626− [ ["clone", if _gitSource_fetchSubmodules s then "--recursive" else "", show (_gitSource_url s)]
627− , ["reset", "--hard", Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr]
628− , if _gitSource_fetchSubmodules s then ["submodule", "update", "--recursive"] else []
629− , ["branch", "-u", "origin/" <> T.unpack (untagName $ _gitSource_branch s)]
630− ]
631−
632− withSpinner ("Preparing thunk in " <> T.pack thunkDir) $ do
656+ withSpinner' ("Fetching thunk " <> T.pack thunkName)
657+ (finalMsg noTrail $ const $ "Fetched thunk " <> T.pack thunkName) $ do
658+ let git = callProcessAndLogOutput (Notice, Notice) . gitProc tmpRepo
659+ git ["clone", if _gitSource_fetchSubmodules s then "--recursive" else "", show (_gitSource_url s)]
660+ git ["reset", "--hard", Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr]
661+ when (_gitSource_fetchSubmodules s) $
662+ git ["submodule", "update", "--recursive", "--init"]
663+ git ["branch", "-u", "origin/" <> T.unpack (untagName $ _gitSource_branch s)]
664+
633665 liftIO $ createDirectory obGitDir
634666 callProcessAndLogOutput (Notice, Error) $
635− cp ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
667+ proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
636668 callProcessAndLogOutput (Notice, Error) $
637669 proc "rm" ["-r", thunkDir]
638670 callProcessAndLogOutput (Notice, Error) $
639− procWithPackages ["coreutils"] "mv" ["-T", tmpRepo, thunkDir]
671+ proc "mv" ["-T", tmpRepo, thunkDir]
640672
641673--TODO: add force mode to pack even if changes are present
642674--TODO: add a rollback mode to pack to the original thunk
643−packThunk
644− :: MonadObelisk m
645− => FilePath
646− -> Text
647− -> m ()
648−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
649680 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
650681 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
651682 Right (ThunkData_Checkout _) -> do
652− withSpinner ("Packing thunk " <> T.pack thunkDir) $ do
683+ withSpinner' ("Packing thunk " <> T.pack thunkDir)
684+ (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
653685 thunkPtr <- getThunkPtr thunkDir upstream
654686 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
655687 liftIO $ createThunk thunkDir thunkPtr
@@ -659,14 +691,8 @@ getThunkPtr = getThunkPtr' True
659691
660692getThunkPtr' :: MonadObelisk m => Bool -> FilePath -> Text -> m ThunkPtr
661693getThunkPtr' checkClean thunkDir upstream = do
662− when checkClean $ checkGitCleanStatus thunkDir >>= \case
663− False -> do
664− statusDebug <- fmap T.pack $ readProcessAndLogStderr Notice $
665− git thunkDir [["status", "--ignored"]]
666− putLog Warning "cannot proceed with unsaved working copy (git status):"
667− putLog Notice statusDebug
668− failWith "thunk pack: thunk checkout contains unsaved modifications"
669− True -> return ()
694+ when checkClean $ ensureCleanGitRepo thunkDir True $
695+ "thunk pack: thunk checkout contains unsaved modifications"
670696
671697 -- Check whether there are any stashes
672698 stashOutput <- readGitProcess thunkDir ["stash", "list"]
@@ -792,7 +818,3 @@ parseSshShorthand uri = do
792818 guard $ isNothing (T.findIndex (=='/') authAndHostname)
793819 && not (T.null colonAndPath)
794820 parseURI $ T.unpack properUri
795−
796−
797−readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
798−readGitProcess repo args = fmap T.pack $ readProcessAndLogStderr Notice $ git repo [args]
lib/command/src/Obelisk/Command/Upgrade.hsnew file+293
@@ -0,0 +1,293 @@
1+{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3+{-# LANGUAGE LambdaCase #-}
4+{-# LANGUAGE MultiParamTypeClasses #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE ScopedTypeVariables #-}
7+{-# LANGUAGE TypeApplications #-}
8+module Obelisk.Command.Upgrade where
9+
10+import Control.Monad (forM, unless, void)
11+import Control.Monad.Catch (onException)
12+import Control.Monad.Except
13+import Control.Monad.IO.Class (liftIO)
14+import Data.Maybe (catMaybes)
15+import Data.Monoid (Any (..), getAny)
16+import Data.Semigroup ((<>))
17+import qualified Data.Set as S
18+import Data.Text (Text)
19+import qualified Data.Text as T
20+import System.Directory
21+import System.FilePath
22+import System.Posix.Process (executeFile)
23+
24+import Obelisk.App (MonadObelisk)
25+import Obelisk.CliApp
26+import Obelisk.Command.Utils
27+
28+import Obelisk.Command.Project (toImplDir)
29+import Obelisk.Command.Project (findProjectObeliskCommand)
30+import Obelisk.Command.Thunk (ThunkData (..), getThunkGitBranch, readThunk, updateThunk)
31+
32+import Obelisk.Command.Upgrade.Hash
33+import Obelisk.Migration
34+
35+newtype HandOffAction = HandoffAction Any
36+ deriving (Monoid, Semigroup, 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, Bounded, Enum)
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+graphNames :: [Text]
58+graphNames = fmap graphName $ [minBound .. maxBound]
59+
60+ensureCleanProject :: MonadObelisk m => FilePath -> m ()
61+ensureCleanProject project =
62+ ensureCleanGitRepo project False "Cannot upgrade with uncommited changes"
63+
64+-- | Decide whether we (ambient ob) should handoff to project obelisk before performing upgrade
65+decideHandOffToProjectOb :: MonadObelisk m => FilePath -> m Bool
66+decideHandOffToProjectOb project = do
67+ ensureCleanProject project
68+ updateThunk (toImplDir project) $ \projectOb -> do
69+ (ambientGraph, ambientHash) <- getAmbientObInfo
70+ projectHash <- computeVertexHash projectOb
71+ case hasVertex projectHash ambientGraph of
72+ False -> do
73+ putLog Warning "Project ob not found in ambient ob's migration graph; handing off anyway"
74+ return True
75+ True -> handleMigrationError (findPathAction ambientGraph projectHash ambientHash) >>= \case
76+ Nothing -> do
77+ putLog Warning "No migration path between project and ambient ob; handing off anyway"
78+ return True
79+ Just (HandoffAction dontHandoff) -> do
80+ return $ not $ getAny dontHandoff
81+ where
82+ getAmbientObInfo = do
83+ ambientOb <- getAmbientOb
84+ getMigrationGraph' ambientOb MigrationGraph_ObeliskHandoff >>= \case
85+ Nothing -> do
86+ failWith "Ambient ob has no migration (this can't be possible)"
87+ Just m -> do
88+ -- We don't have ambient's ob source code, so locate its hash from the
89+ -- graph. The last vertex should be it.
90+ ambientHash <- handleMigrationError $ getLast (_migration_graph m)
91+ return (m, ambientHash)
92+
93+-- | Return the path to the current ('ambient') obelisk process Nix directory
94+getAmbientOb :: MonadObelisk m => m FilePath
95+getAmbientOb = takeDirectory . takeDirectory <$> liftIO getObeliskExe
96+
97+upgradeObelisk :: MonadObelisk m => FilePath -> Maybe Text -> m ()
98+upgradeObelisk project gitBranchM = do
99+ ensureCleanProject project
100+ gitBranch <- maybe (getObeliskBranch project) pure gitBranchM
101+ updateObelisk project gitBranch >>= handOffToNewOb project
102+
103+getObeliskBranch :: MonadObelisk m => FilePath -> m Text
104+getObeliskBranch project = readThunk (toImplDir project) >>= \case
105+ Left e -> failWith $ T.pack $ show e
106+ Right (ThunkData_Checkout _) -> failWith "obelisk thunk must be packed"
107+ Right (ThunkData_Packed tptr) -> case getThunkGitBranch tptr of
108+ Just v -> pure v
109+ Nothing ->
110+ failWith "You must run `ob upgrade` with the Git branch specified explicitly, as the project's obelisk thunk does not specify any."
111+
112+updateObelisk :: MonadObelisk m => FilePath -> Text -> m Hash
113+updateObelisk project gitBranch =
114+ withSpinner ("Fetching new obelisk [" <> gitBranch <> "]") $
115+ updateThunk (toImplDir project) $ \obImpl -> do
116+ fromHash <- computeVertexHash obImpl
117+ callProcessAndLogOutput (Debug, Debug) $
118+ gitProc obImpl ["checkout", T.unpack gitBranch]
119+ callProcessAndLogOutput (Debug, Debug) $
120+ gitProc obImpl ["pull"]
121+ return fromHash
122+
123+handOffToNewOb :: MonadObelisk m => FilePath -> Hash -> m ()
124+handOffToNewOb project fromHash = do
125+ impl <- withSpinner' "Preparing for handoff" (Just $ ("Handed off to new obelisk " <>) . T.pack) $
126+ findProjectObeliskCommand project >>= \case
127+ Nothing -> failWith "Not an obelisk project"
128+ Just impl -> pure impl
129+ let opts = ["internal", "migrate", T.unpack fromHash]
130+ liftIO $ executeFile impl False ("--no-handoff" : opts) Nothing
131+
132+migrateObelisk :: MonadObelisk m => FilePath -> Hash -> m ()
133+migrateObelisk project fromHash = void $ withSpinner' "Migrating to new obelisk" (Just id) $ do
134+ updateThunk (toImplDir project) $ \obImpl -> revertObImplOnFail obImpl $ do
135+ toHash <- computeVertexHash obImpl
136+ g <- getMigrationGraph' obImpl MigrationGraph_ObeliskUpgrade >>= \case
137+ Nothing -> failWith "New obelisk has no migration metadata"
138+ Just m -> pure m
139+
140+ unless (hasVertex fromHash g) $ do
141+ failWith $ "Current obelisk hash " <> fromHash <> " missing in migration graph of new obelisk"
142+ unless (hasVertex toHash g) $ do
143+ -- This usually means that the target obelisk branch does not have
144+ -- migration vertex for its latest commit; typically due to developer
145+ -- negligence.
146+ failWith $ "New obelisk hash " <> toHash <> " missing in its migration graph"
147+
148+ if fromHash == toHash
149+ then do
150+ pure $ "No upgrade available (new Obelisk is the same)"
151+ else do
152+ putLog Debug $ "Migrating from " <> fromHash <> " to " <> toHash
153+ handleMigrationError (findPathAction g fromHash toHash) >>= \case
154+ Nothing -> do
155+ failWith "Unable to find migration path"
156+ Just action -> do
157+ unless (action == mempty) $ do
158+ putLog Notice "To upgrade your project to the new version of obelisk, please follow these instructions:\n"
159+ putLog Notice action
160+ pure $ "Migrated from " <> fromHash <> " to " <> toHash
161+ where
162+ revertObImplOnFail impl f = f `onException` do
163+ putLog Notice $ T.pack $ "Reverting changes to " <> impl
164+ callProcessAndLogOutput (Notice, Notice) $ gitProc project ["checkout", impl]
165+
166+-- | Get the migration graph for project
167+getMigrationGraph
168+ :: (Action action, MonadObelisk m)
169+ => FilePath -> MigrationGraph -> m (Migration action)
170+getMigrationGraph obDir graph = getMigrationGraph' obDir graph >>= \case
171+ Nothing -> failWith "Migration graph missing"
172+ Just m -> pure m
173+
174+getMigrationGraph'
175+ :: (Action action, MonadObelisk m)
176+ => FilePath -> MigrationGraph -> m (Maybe (Migration action))
177+getMigrationGraph' obDir graph = do
178+ let name = graphName graph
179+ dir = migrationDir obDir
180+ putLog Debug $ "Reading migration graph " <> name <> " from " <> T.pack dir
181+ liftIO $ readGraph dir name
182+
183+computeVertexHash :: MonadObelisk m => FilePath -> m Hash
184+computeVertexHash = getDirectoryHash [migrationDirName]
185+
186+-- | Verify the integrity of the migration graph in relation to the Git repo.
187+verifyMigration :: MonadObelisk m => FilePath -> m ()
188+verifyMigration obDir = do
189+ upgradeGraph <- withSpinner "Reading migration graph" $
190+ getMigrationGraph @Text obDir MigrationGraph_ObeliskUpgrade
191+ withSpinner "Checking graph integrity" $
192+ handleMigrationError $ ensureGraphIntegrity upgradeGraph
193+ headVertex <- liftIO (doesDirectoryExist $ obDir </> ".git") >>= \case
194+ True ->
195+ withSpinner "Checking existence of HEAD vertex" $
196+ getHeadVertex obDir
197+ False -> do
198+ hash <- computeVertexHash obDir
199+ unless (hasVertex hash upgradeGraph) $
200+ failWith $ "No vertex found for obelisk hash " <> hash
201+ pure hash
202+ withSpinner "Checking graph linearity" $
203+ -- NOTE: We don't support merge commits (i.e., diamonds in a graph) yet.
204+ -- Until we do, ensure that the graph is a fully connected linear list.
205+ runExceptT (ensureGraphLinearity upgradeGraph) >>= \case
206+ Left (NonLinearGraph_MultipleAdjacents vertex) ->
207+ failWith $ "Graph is not linear; branches at vertex: " <> tshow vertex
208+ Left (NonLinearGraph_NotConnected isolatedVertices) ->
209+ failWith $ "Graph is partly linear, with isolated vertices: " <> tshow isolatedVertices
210+ Left e ->
211+ failWith $ "Error in migration graph: " <> tshow e
212+ Right lastVertex -> unless (lastVertex == headVertex) $
213+ failWith $ "Graph is linear, but its last vertex (" <> lastVertex <> ") is different from head vertex (" <> headVertex <> ")"
214+
215+-- | Create an edge from HEAD vertex to the hash corresponding to the Git
216+-- working copy. The HEAD vertex must already exist.
217+createMigrationEdgeFromHEAD :: MonadObelisk m => FilePath -> m ()
218+createMigrationEdgeFromHEAD obDir = do
219+ headHash <- getHeadVertex obDir
220+ wcHash <- getDirectoryHash migrationIgnore obDir
221+ if (headHash == wcHash)
222+ then
223+ putLog Warning "No migration necessary (working copy has the same hash as that of HEAD)"
224+ else do
225+ writeEdge (migrationDir obDir) graphNames headHash wcHash >>= \case
226+ Nothing ->
227+ putLog Warning "No migration was created"
228+ Just edgeDir ->
229+ putLog Notice $ "Created edge: " <> T.pack edgeDir
230+
231+-- | Return the hash corresponding to HEAD
232+--
233+-- Fail if the hash does not exist in project's migration graph.
234+getHeadVertex :: MonadObelisk m => FilePath -> m Hash
235+getHeadVertex obDir = do
236+ projectGraph <- getMigrationGraph @Text obDir MigrationGraph_ObeliskUpgrade
237+ [headHash] <- getHashAtGitRevision ["HEAD"] migrationIgnore obDir
238+ unless (hasVertex headHash projectGraph) $ do
239+ -- This means that the HEAD commit has no vertex in the graph,
240+ -- possibly due to developer negligence when commiting it.
241+ -- Perhaps we should use a post-commit hook or some such thing
242+ -- to reject such commits in the first place? For now, just
243+ -- error out.
244+ failWith $ "No vertex found for HEAD (" <> headHash <> ")"
245+ return headHash
246+
247+-- | Create, or update, the migration graph with new edges and new vertices
248+-- corresponding to every commit in the Git history from HEAD.
249+--
250+-- NOTE: This creates a linear graph, and doesn't follow the Git graph
251+-- structure.
252+backfillGraph :: MonadObelisk m => Maybe Int -> FilePath -> m ()
253+backfillGraph lastN obDir = do
254+ revs <- fmap (takeM lastN . fmap (fst . T.breakOn " ") . T.lines) $
255+ readProc $ gitProc obDir ["log", "--pretty=oneline"]
256+ -- Note: we need to take unique hashes only; this is fine for backfilling.
257+ -- But future migrations should ensure that there are no duplicate hashes
258+ -- (which would cause cycles) such as those introduced by revert commits.
259+ vertices :: [Hash] <- withSpinnerNoTrail "Computing hash for git history" $
260+ fmap (unique . reverse) $ getHashAtGitRevision revs [migrationDirName] obDir
261+ void $ withSpinner'
262+ ("Backfilling with " <> tshow (length vertices) <> " vertices")
263+ (Just $ \n -> "Backfilled " <> tshow n <> " edges.") $ do
264+ let vertexPairs = zip vertices $ drop 1 vertices
265+ let edgesDir = migrationDir obDir
266+ liftIO $ createDirectoryIfMissing False edgesDir
267+ fmap (length . catMaybes) $ forM vertexPairs $ \(v1, v2) -> do
268+ writeEdge edgesDir graphNames v1 v2
269+ where
270+ -- Return unique items in the list /while/ preserving order
271+ unique = loop mempty
272+ where
273+ loop _ [] = []
274+ loop s (x : xs)
275+ | S.member x s = loop s xs
276+ | otherwise = x : loop (S.insert x s) xs
277+ takeM n' xs = case n' of
278+ Just n -> take n xs
279+ Nothing -> xs
280+
281+handleMigrationError :: MonadObelisk m => Except MigrationError a -> m a
282+handleMigrationError m = case runExcept m of
283+ Left e -> failWith $ "Error in migration graph: " <> tshow e
284+ Right a -> return a
285+
286+migrationDir :: FilePath -> FilePath
287+migrationDir project = project </> migrationDirName
288+
289+migrationIgnore :: [FilePath]
290+migrationIgnore = [migrationDirName]
291+
292+migrationDirName :: FilePath
293+migrationDirName = "migration"
lib/command/src/Obelisk/Command/Upgrade/Hash.hsnew file+116
@@ -0,0 +1,116 @@
1+{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE LambdaCase #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
5+{-# LANGUAGE TupleSections #-}
6+-- | Hash a git repo or arbitrary directory using nix-hash.
7+module Obelisk.Command.Upgrade.Hash
8+ ( getDirectoryHash
9+ , getHashAtGitRevision
10+ ) where
11+
12+import Control.Monad (forM, forM_)
13+import Control.Monad.IO.Class (liftIO)
14+import Data.Bool (bool)
15+import Data.Maybe (catMaybes)
16+import Data.Semigroup ((<>))
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.Process (cwd, proc)
23+
24+import Obelisk.App (MonadObelisk)
25+import Obelisk.CliApp
26+import Obelisk.Command.Nix (withNixRemoteCheck)
27+import Obelisk.Command.Utils
28+
29+import Obelisk.Migration
30+
31+-- | Get the unique hash of the given directory
32+--
33+-- Excludes the following before computing the hash:
34+-- * the specified top-level files/ directories.
35+-- * .git directory
36+-- * untracked Git files
37+-- * ignored Git files
38+-- * empty directories
39+--
40+-- Uses the same predictive algorithm as that of Nix (`nix-hash`).
41+--
42+-- This function will do a full copy of the directory to a temporary location before
43+-- computing the hash. Because it will be deleting the files in exclude list, and
44+-- other files if the directory is a git repo, which needs to be done as `nix-hash`
45+-- doesn't support taking an excludes list.
46+getDirectoryHash :: MonadObelisk m => [FilePath] -> FilePath -> m Hash
47+getDirectoryHash excludes dir = withSystemTempDirectory "obelisk-hash-" $ \tmpDir -> do
48+ withSpinnerNoTrail (T.pack $ "Copying " <> dir <> " to " <> tmpDir) $ do
49+ runProc $ copyDir dir tmpDir
50+ getDirectoryHashDestructive excludes tmpDir
51+
52+getDirectoryHashDestructive :: MonadObelisk m => [FilePath] -> FilePath -> m Hash
53+getDirectoryHashDestructive excludes dir = do
54+ liftIO (doesDirectoryExist $ dir </> ".git") >>= \case
55+ True -> do
56+ tidyUpGitWorkingCopy dir
57+ withSpinnerNoTrail "Removing .git directory" $
58+ liftIO $ removePathForcibly $ dir </> ".git"
59+ False -> pure ()
60+ withSpinnerNoTrail "Removing excluded paths" $ do
61+ forM_ (fmap (dir </>) excludes) $
62+ liftIO . removePathForcibly
63+ nixHash dir
64+
65+getHashAtGitRevision :: MonadObelisk m => [Text] -> [FilePath] -> FilePath -> m [Hash]
66+getHashAtGitRevision revs excludes dir = withSystemTempDirectory "obelisk-hashrev-" $ \tmpDir -> do
67+ withSpinnerNoTrail (T.pack $ "Copying " <> dir <> " to " <> tmpDir) $ do
68+ runProc $ copyDir dir tmpDir
69+ tidyUpGitWorkingCopy tmpDir
70+ -- Discard changes to tracked files
71+ runProcSilently $ gitProc tmpDir ["reset", "--hard"]
72+ forM revs $ \rev -> do
73+ runProcSilently $ gitProc tmpDir ["checkout", T.unpack rev]
74+ -- Checking out an arbitrary revision can leave untracked files (from
75+ -- previous revison) around, so tidy them up.
76+ tidyUpGitWorkingCopy tmpDir
77+ withFilesStashed tmpDir (excludes <> [".git"]) $
78+ nixHash tmpDir
79+ where
80+ withFilesStashed base fs m = withSystemTempDirectory "obelisk-hashrev-stash-" $ \stashDir -> do
81+ existingPaths <- fmap catMaybes $ forM fs $ \p -> do
82+ liftIO (doesPathExist $ base </> p) >>= pure . bool Nothing (Just p)
83+ forM_ existingPaths $ \p ->
84+ liftIO $ renamePath (base </> p) (stashDir </> p)
85+ result <- m
86+ forM_ existingPaths $ \p ->
87+ liftIO $ renamePath (stashDir </> p) (base </> p)
88+ return result
89+
90+nixHash :: MonadObelisk m => FilePath -> m Hash
91+nixHash dir = withSpinnerNoTrail "Running `nix-hash`" $ withNixRemoteCheck $
92+ readProc $ proc "nix-hash" [dir]
93+
94+-- | Clean up the following files in the git working copy
95+--
96+-- * Paths ignored by .gitignore, and still present
97+-- * Untracked files (not added to git index)
98+-- * Any empty directories (these are not tracked by git)
99+--
100+-- Note that this leaves modified (staged or unstaged) files as they are.
101+tidyUpGitWorkingCopy :: MonadObelisk m => FilePath -> m ()
102+tidyUpGitWorkingCopy dir = withSpinnerNoTrail "Tidying up git working copy" $ do
103+ ignored <- gitLsFiles dir ["--ignored", "--exclude-standard", "--others"]
104+ untracked <- gitLsFiles dir ["--exclude-standard", "--others"]
105+ putLog Debug $ T.pack $ "Ignored: " <> show (length ignored) <> " files."
106+ putLog Debug $ T.pack $ "Untracked files:\n" <> unlines untracked
107+ withSpinnerNoTrail "Removing untracked and ignored files" $ do
108+ forM_ (fmap (dir </>) $ ignored <> untracked) $
109+ liftIO . removePathForcibly
110+ -- Empty directories won't be included in these lists. Git doesn't track them
111+ -- So we must delete these separately.
112+ runProc $ proc "find" [dir, "-depth", "-empty", "-type", "d", "-delete"]
113+ where
114+ gitLsFiles pwd opts = fmap lines $ readProcessAndLogStderr Error $
115+ (proc "git" $ ["ls-files", "."] <> opts) { cwd = Just pwd }
116+
lib/command/src/Obelisk/Command/Utils.hs+58−24
@@ -1,48 +1,82 @@
11{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE LambdaCase #-}
23{-# LANGUAGE OverloadedStrings #-}
34module Obelisk.Command.Utils where
45
5−import qualified Data.List as L
6+import 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− out <- readProcessAndLogStderr Debug $ git repo [["status", "--porcelain", "--ignored"], ["diff"]]
17− pure $ null out
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 ()
20−initGit dir = callProcessAndLogOutput (Debug, Debug) $ git dir
21− [ ["init"]
22− , ["add", "."]
23− , ["commit", "-m", "Initial commit."]
24− ]
25−
26−git :: FilePath -> [[String]] -> P.CreateProcess
27−git repo argss =
28− inNixShell ["git"] $ L.intercalate "; " $
29− map (processToShellString "git" . runGitInDir) $ filter (not . null) argss
43+initGit repo = do
44+ let git = callProcessAndLogOutput (Debug, Debug) . gitProc repo
45+ git ["init"]
46+ git ["add", "."]
47+ git ["commit", "-m", "Initial commit."]
48+
49+gitProc :: FilePath -> [String] -> P.CreateProcess
50+gitProc repo argsRaw =
51+ P.proc "git" $ runGitInDir argsRaw
3052 where
3153 runGitInDir args' = case filter (not . null) args' of
3254 args@("clone":_) -> args <> [repo]
3355 args -> ["-C", repo] <> args
3456
35−-- | Like `System.Process.proc` but with the specified Nix packages installed
36−procWithPackages :: [String] -> FilePath -> [String] -> P.CreateProcess
37−procWithPackages pkgs cmd args = inNixShell pkgs $ processToShellString cmd args
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 }
3861
39−inNixShell :: [String] -> String -> P.CreateProcess
40−inNixShell pkgs cmd = P.proc "nix-shell" $ "-p" : pkgs <> ["--run", cmd]
62+readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
63+readGitProcess repo = fmap T.pack . readProcessAndLogStderr Notice . gitProc repo
4164
4265processToShellString :: FilePath -> [String] -> String
4366processToShellString cmd args = unwords $ map quoteAndEscape (cmd : args)
4467 where quoteAndEscape x = T.unpack $ "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
4568
46−-- | Portable `cp` command.
47−cp :: [String] -> P.CreateProcess
48−cp = procWithPackages ["coreutils"] "cp"
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/command/src/Obelisk/Command/VmBuilder.hs+4−5
@@ -1,7 +1,6 @@
11{-# LANGUAGE FlexibleContexts #-}
22{-# LANGUAGE OverloadedStrings #-}
33{-# LANGUAGE QuasiQuotes #-}
4−
54module Obelisk.Command.VmBuilder where
65
76import Control.Applicative (liftA2)
@@ -17,13 +16,13 @@ import qualified Data.Text as T
1716import qualified Data.Text.IO as T
1817import System.Directory (createDirectoryIfMissing)
1918import System.Exit (ExitCode)
20−import System.FilePath ((</>), (<.>))
19+import System.FilePath ((<.>), (</>))
2120import qualified System.Info
2221import System.Process (proc)
2322
2423import Obelisk.App (MonadObelisk, getObeliskUserStateDir)
25−import Obelisk.CliApp (Severity (..), callProcessAndLogOutput, failWith, readProcessAndLogStderr,
26− withExitFailMessage, withSpinner)
24+import Obelisk.CliApp
25+import Obelisk.Command.Nix (withNixRemoteCheck)
2726
2827-- | Generate the `--builders` argument string to enable the VM builder after ensuring it is available.
2928getNixBuildersArg :: MonadObelisk m => m String
@@ -132,7 +131,7 @@ getDockerBuilderStateDir = liftA2 (</>) getObeliskUserStateDir (pure "nix-docker
132131-- | Run a test build to see if a Linux build can finish successfully.
133132testLinuxBuild :: MonadObelisk m => FilePath -> m Bool
134133testLinuxBuild stateDir =
135− fmap (isRight :: Either ExitCode () -> Bool) $ try $
134+ fmap (isRight :: Either ExitCode () -> Bool) $ try $ withNixRemoteCheck $
136135 callProcessAndLogOutput (Debug, Debug) $ proc "nix-build"
137136 [ "-E", "(import <nixpkgs> { system = \"x86_64-linux\"; }).writeText \"test\" builtins.currentTime"
138137 , "--builders", nixBuildersArgString stateDir
lib/executable-config/README.md+2−7
@@ -7,10 +7,6 @@ Obelisk.ExecutableConfig serves two related purposes:
771. Injection: a cross-platform injection function, which can be used to put configuration files in a canonical location.
882. Retrieval: a cross-platform retrieval function that is aware of the canonical location on each platform and how to read files from that location.
99
10−## File Location
11−
12−ExecutableConfig expects your configuration files to be placed in `./config`.
13−
1410## Supported Platforms
1511
1612| Function | iOS | Android | Warp |Web backend + GHCJS |
@@ -28,7 +24,7 @@ ExecutableConfig expects your configuration files to be placed in `./config`.
2824
2925There is currently no `inject` function for `jsaddle-warp`-based frontends.
3026
31−`get` is a Haskell function that simply reads the configuration files from `./config`.
27+`get` is a Haskell function that simply reads the configuration files relative to the current directory.
3228
3329### Web backend + GHCJS
3430
@@ -36,9 +32,8 @@ There is currently no `inject` function for `jsaddle-warp`-based frontends.
3632
3733`inject` is a Haskell function that produces a snippet of HTML that must be appended to the `<head>` of the page that the backend serves (i.e., the entry point for the GHCJS application).
3834
39−`get` is a Haskell function that reads the configuration files from `./config`.
35+`get` is a Haskell function that reads the configuration files relative to the current directory.
4036
4137#### GHCJS
4238
4339`get` is a Haskell function that can be run by the GHCJS frontend to retrieve the injected configuration data from the served `<head>`. There is no `inject` that is run in GHCJS.
44−
lib/executable-config/default.nix+2−1
@@ -7,7 +7,8 @@
77}:
88let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} ''
99 set -x
10− cp -a "${assets}" $out
10+ mkdir -p $out
11+ cp --no-preserve=mode -Lr "${assets}" $out/static
1112 chmod +w "$out"
1213 if ! mkdir $out/config; then
1314 2>&1 echo config directory already exists or could not be created
lib/executable-config/inject/obelisk-executable-config-inject.cabal+5−4
@@ -7,11 +7,12 @@ library
77 hs-source-dirs: src
88
99 build-depends:
10− base,
10+ base,
1111 bytestring,
12+ directory,
1213 filepath,
13− reflex-dom,
14− text,
15− transformers
14+ reflex-dom-core,
15+ text
1616
1717 exposed-modules: Obelisk.ExecutableConfig.Inject
18+ ghc-options: -Wall
lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+43−9
@@ -1,17 +1,51 @@
11{-# LANGUAGE OverloadedStrings #-}
22module Obelisk.ExecutableConfig.Inject where
33
4−import Data.ByteString (ByteString)
5−import qualified Data.ByteString as BS
4+import Control.Monad (mapM_)
5+import Control.Monad.IO.Class (MonadIO, liftIO)
66import Data.Semigroup ((<>))
7+import Data.ByteString (ByteString)
8+import Data.Text (Text)
79import qualified Data.Text as T
8−import qualified Data.Text.Encoding as T
9−import Reflex.Dom
10+import qualified Data.Text.IO as T
11+import Reflex.Dom.Core hiding (value)
12+import System.Directory
1013import System.FilePath ((</>))
1114
15+-- | Produces a @<script>@ tag containing the contents of the config item,
16+-- rendered as a 'ByteString'.
1217inject :: FilePath -> IO ByteString
13−inject item = do
14− f <- BS.readFile $ "config" </> item
15− fmap snd $ renderStatic $
16− elAttr "script" ("type" =: "text/plain" <> "id" =: ("config-" <> T.pack item)) $
17− text $ T.decodeUtf8 f
18+inject key = do
19+ value <- T.readFile key
20+ fmap snd $ renderStatic $ injectPure (T.pack key) value
21+
22+-- | Produces a @<script>@ tag with an @id@ attribute containing the key and
23+-- whose value is the provided configuration
24+injectPure :: DomBuilder t m => Text -> Text -> m ()
25+injectPure key value =
26+ let attrs = ("type" =: "text/plain" <> "id" =: ("config-" <> key))
27+ in elAttr "script" attrs $ text value
28+
29+-- | Scans the "common" and "frontend" configuration folders and produces
30+-- injectable @<script>@ tags containing the configuration keys (filepaths) and
31+-- values.
32+injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => m ()
33+injectExecutableConfigs = do
34+ cfgC <- getConfigs "config/common"
35+ cfgF <- getConfigs "config/frontend"
36+ mapM_ (uncurry injectPure) (cfgC <> cfgF)
37+
38+getConfigs :: MonadIO m => FilePath -> m [(Text, Text)]
39+getConfigs fp = liftIO $ do
40+ dir <- doesDirectoryExist fp
41+ if dir
42+ then do
43+ ps <- listDirectory fp
44+ fmap concat $ mapM (\p -> getConfigs $ fp </> p) ps
45+ else do
46+ file <- doesFileExist fp
47+ if file
48+ then do
49+ f <- T.readFile fp
50+ return [(T.pack fp, f)]
51+ else return []
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig.hs+1−1
@@ -15,7 +15,7 @@ get :: Text -> IO (Maybe Text)
1515get name = bracket getAssets freeAssetManager $ \mgrObj -> do
1616 mgr <- assetManagerFromJava mgrObj
1717 let open = do
18− a <- withCString ("config" </> T.unpack name) $ \fn ->
18+ a <- withCString (T.unpack name) $ \fn ->
1919 assetManager_open mgr fn 3
2020 return $ if unAAsset a == nullPtr
2121 then Nothing
lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig.hs+1−1
@@ -14,7 +14,7 @@ import System.IO.Error
1414
1515get :: Text -> IO (Maybe Text)
1616get name = fmap join $ mainBundleResourcePath >>= \mp -> forM mp $ \p ->
17− catchDoesNotExist $ T.readFile $ T.unpack (T.decodeUtf8 p) </> "config" </> T.unpack name
17+ catchDoesNotExist $ T.readFile $ T.unpack (T.decodeUtf8 p) </> T.unpack name
1818
1919catchDoesNotExist :: IO a -> IO (Maybe a)
2020catchDoesNotExist f = catchJust doesNotExist (Just <$> f) $ const $ return Nothing
lib/executable-config/lookup/src/Obelisk/ExecutableConfig.hs+1−1
@@ -10,4 +10,4 @@ import System.IO.Error
1010get :: Text -> IO (Maybe Text)
1111get path = do
1212 let doesNotExist = \e -> if isDoesNotExistError e then Just () else Nothing
13− catchJust doesNotExist (fmap Just $ T.readFile $ "config" </> T.unpack path) (\_ -> pure Nothing)
13+ catchJust doesNotExist (fmap Just $ T.readFile $ T.unpack path) (\_ -> pure Nothing)
lib/frontend/obelisk-frontend.cabalnew file+22
@@ -0,0 +1,22 @@
1+name: obelisk-frontend
2+version: 0.1
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ hs-source-dirs: src
8+ build-depends: base,
9+ bytestring,
10+ dependent-sum,
11+ ghcjs-dom,
12+ jsaddle,
13+ lens,
14+ obelisk-route,
15+ primitive,
16+ ref-tf,
17+ reflex,
18+ reflex-dom-core,
19+ text,
20+ transformers
21+ exposed-modules: Obelisk.Frontend
22+ ghc-options: -Wall
lib/frontend/src/Obelisk/Frontend.hsnew file+165
@@ -0,0 +1,165 @@
1+{-# LANGUAGE ConstraintKinds #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE FlexibleInstances #-}
4+{-# LANGUAGE LambdaCase #-}
5+{-# LANGUAGE MultiParamTypeClasses #-}
6+{-# LANGUAGE OverloadedStrings #-}
7+{-# LANGUAGE RankNTypes #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE TemplateHaskell #-}
10+{-# LANGUAGE TypeFamilies #-}
11+{-# LANGUAGE UndecidableInstances #-}
12+module Obelisk.Frontend
13+ ( ObeliskWidget
14+ , Frontend (..)
15+ , runFrontend
16+ , renderFrontendHtml
17+ ) where
18+
19+import Prelude hiding ((.))
20+
21+import Control.Category
22+import Control.Concurrent
23+import Control.Lens
24+import Control.Monad
25+import Control.Monad.Fix
26+import Control.Monad.IO.Class
27+import Control.Monad.Primitive
28+import Control.Monad.Ref
29+import Control.Monad.Trans.Class
30+import Data.ByteString (ByteString)
31+import Data.Dependent.Sum (DSum (..))
32+import Data.Functor.Sum
33+import Data.IORef
34+import Data.Monoid hiding (Sum)
35+import GHCJS.DOM hiding (bracket, catch)
36+import GHCJS.DOM.Document
37+import GHCJS.DOM.Node
38+import qualified GHCJS.DOM.Types as DOM
39+import Language.Javascript.JSaddle (JSM)
40+import Obelisk.Route.Frontend
41+import Reflex.Dom.Core
42+import Reflex.Host.Class
43+import qualified Reflex.TriggerEvent.Base as TriggerEvent
44+
45+makePrisms ''Sum
46+
47+type ObeliskWidget t x route m =
48+ ( DomBuilder t m
49+ , MonadFix m
50+ , MonadHold t m
51+ , MonadSample t (Performable m)
52+ , MonadReflexCreateTrigger t m
53+ , PostBuild t m
54+ , PerformEvent t m
55+ , MonadIO m
56+ , MonadIO (Performable m)
57+ , TriggerEvent t m
58+ , HasDocument m
59+ , MonadRef m
60+ , Ref m ~ Ref IO
61+ , MonadRef (Performable m)
62+ , Ref (Performable m) ~ Ref IO
63+ , MonadFix (Performable m)
64+ , PrimMonad m
65+ , Prerender x m
66+ , SetRoute t route m
67+ )
68+
69+data Frontend route = Frontend
70+ { _frontend_head :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
71+ , _frontend_body :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
72+ }
73+
74+type Widget' x = ImmediateDomBuilderT DomTimeline (DomCoreWidget x)
75+
76+-- | A widget that isn't attached to any particular part of the DOM hierarchy
77+type FloatingWidget x = TriggerEventT DomTimeline (DomCoreWidget x)
78+
79+type DomCoreWidget x = PostBuildT DomTimeline (WithJSContextSingleton x (PerformEventT DomTimeline DomHost))
80+
81+--TODO: Rename
82+{-# INLINABLE attachWidget''' #-}
83+attachWidget'''
84+ :: (EventChannel -> PerformEventT DomTimeline (SpiderHost Global) (IORef (Maybe (EventTrigger DomTimeline ()))))
85+ -> IO ()
86+attachWidget''' w = runDomHost $ do
87+ events <- liftIO newChan
88+ (postBuildTriggerRef, fc@(FireCommand fire)) <- hostPerformEventT $ w events
89+ mPostBuildTrigger <- readRef postBuildTriggerRef
90+ forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()
91+ liftIO $ processAsyncEvents events fc
92+
93+--TODO: This is a collection of random stuff; we should make it make some more sense and then upstream to reflex-dom-core
94+runWithHeadAndBody
95+ :: ( (forall c. Widget' () c -> FloatingWidget () c) -- "Append to head"
96+ -> (forall c. Widget' () c -> FloatingWidget () c) -- "Append to body"
97+ -> FloatingWidget () ()
98+ )
99+ -> JSM ()
100+runWithHeadAndBody app = withJSContextSingletonMono $ \jsSing -> do
101+ globalDoc <- currentDocumentUnchecked
102+ headFragment <- createDocumentFragment globalDoc
103+ bodyFragment <- createDocumentFragment globalDoc
104+ unreadyChildren <- liftIO $ newIORef 0
105+ let commit = do
106+ headElement <- getHeadUnchecked globalDoc
107+ bodyElement <- getBodyUnchecked globalDoc
108+ void $ inAnimationFrame' $ \_ -> do
109+ replaceElementContents headElement headFragment
110+ replaceElementContents bodyElement bodyFragment
111+ liftIO $ attachWidget''' $ \events -> flip runWithJSContextSingleton jsSing $ do
112+ (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
113+ let appendImmediateDom :: DOM.DocumentFragment -> Widget' () c -> FloatingWidget () c
114+ appendImmediateDom df w = do
115+ events' <- TriggerEvent.askEvents
116+ lift $ do
117+ doc <- getOwnerDocumentUnchecked df
118+ let builderEnv = ImmediateDomBuilderEnv
119+ { _immediateDomBuilderEnv_document = doc
120+ , _immediateDomBuilderEnv_parent = toNode df
121+ , _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
122+ , _immediateDomBuilderEnv_commitAction = commit
123+ }
124+ runImmediateDomBuilderT w builderEnv events'
125+ flip runPostBuildT postBuild $ flip runTriggerEventT events $ app (appendImmediateDom headFragment) (appendImmediateDom bodyFragment)
126+ liftIO (readIORef unreadyChildren) >>= \case
127+ 0 -> DOM.liftJSM commit
128+ _ -> return ()
129+ return postBuildTriggerRef
130+
131+runFrontend :: forall backendRoute route. Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> Frontend (R route) -> JSM ()
132+runFrontend validFullEncoder frontend = do
133+ let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
134+ errorLeft = \case
135+ Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
136+ Right x -> Identity x
137+ runMyRouteViewT
138+ :: ( TriggerEvent t m
139+ , PerformEvent t m
140+ , MonadHold t m
141+ , DOM.MonadJSM m
142+ , DOM.MonadJSM (Performable m)
143+ , MonadFix m
144+ , MonadFix (Performable m)
145+ )
146+ => RoutedT t (R route) (SetRouteT t (R route) m) a
147+ -> m a
148+ runMyRouteViewT = runRouteViewT ve
149+ runWithHeadAndBody $ \appendHead appendBody -> runMyRouteViewT $ do
150+ mapRoutedT (mapSetRouteT appendHead) $ _frontend_head frontend
151+ mapRoutedT (mapSetRouteT appendBody) $ _frontend_body frontend
152+
153+renderFrontendHtml
154+ :: (t ~ SpiderTimeline Global)
155+ => r
156+ -> RoutedT t r (SetRouteT t r' (PostBuildT Spider (StaticDomBuilderT Spider (PerformEventT Spider (SpiderHost Global))))) ()
157+ -> RoutedT t r (SetRouteT t r' (PostBuildT Spider (StaticDomBuilderT Spider (PerformEventT Spider (SpiderHost Global))))) ()
158+ -> IO ByteString
159+renderFrontendHtml route headWidget bodyWidget = do
160+ --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
161+ html <- fmap snd $ renderStatic $ fmap fst $ runSetRouteT $ flip runRoutedT (pure route) $
162+ el "html" $ do
163+ el "head" headWidget
164+ el "body" bodyWidget
165+ return $ "<!DOCTYPE html>" <> html
lib/https/default.nixdeleted−7
@@ -1,7 +0,0 @@
1−# DO NOT HAND-EDIT THIS FILE
2−import ((import <nixpkgs> {}).fetchFromGitHub (
3− let json = builtins.fromJSON (builtins.readFile ./github.json);
4− in { inherit (json) owner repo rev sha256;
5− private = json.private or false;
6− }
7−))
lib/https/github.jsondeleted−6
@@ -1,6 +0,0 @@
1−{
2− "owner": "obsidiansystems",
3− "repo": "obelisk-https",
4− "rev": "5bc5cd81b669a0ea1b4415a1be8fd8c5a0680f0b",
5− "sha256": "105n7pxykz02n8ilgjznssmxa8hszclg21nqiyqdr64bcrxjyxf4"
6−}
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+223
@@ -0,0 +1,223 @@
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, forM_, unless, void)
9+import Control.Monad.Except
10+import Control.Monad.IO.Class (MonadIO, liftIO)
11+import Control.Monad.State.Strict (evalStateT, get, put)
12+import Data.Map (Map)
13+import qualified Data.Map as Map
14+import Data.Maybe (catMaybes)
15+import Data.Semigroup ((<>))
16+import Data.Set (Set)
17+import qualified Data.Set as Set
18+import Data.Text (Text)
19+import qualified Data.Text as T
20+import qualified Data.Text.IO as T
21+import System.Directory
22+import System.FilePath
23+
24+import Algebra.Graph.AdjacencyMap
25+
26+type Hash = Text
27+
28+-- Migration graph with edge metadata
29+data Migration action = Migration
30+ { _migration_graph :: AdjacencyMap Hash
31+ , _migration_actions :: Map (Hash, Hash) action -- Edge metadata
32+ }
33+ deriving (Eq, Show)
34+
35+-- A path is a list of edges
36+type Path = [(Hash, Hash)]
37+
38+data MigrationError
39+ = GraphInternalError Text
40+ | CyclicGraphError
41+ | MultipleTerminalVertices
42+ | NoTerminalVertex
43+ | NonEquivalentPaths Hash Hash
44+ | MissingEdgeInActionMap (Hash, Hash)
45+ | NonLinearGraph_MultipleAdjacents Hash
46+ | NonLinearGraph_NotConnected (Set Hash)
47+ deriving (Eq, Ord, Show)
48+
49+class Action action where
50+ parseEdgeMeta :: Text -> action
51+
52+instance Action Text where
53+ parseEdgeMeta = id
54+
55+-- | Get the directed-acylic graph of migration
56+-- TODO: Consider calling getDag only when reading the graph for efficiency.
57+getDag :: (MonadError MigrationError m, Ord a) => AdjacencyMap a -> m (AdjacencyMap a)
58+getDag g = case topSort g of
59+ Just _ -> pure g
60+ Nothing -> throwError CyclicGraphError
61+
62+-- | Read the graph from a directory of files.
63+readGraph :: Action action => FilePath -> Text -> IO (Maybe (Migration action))
64+readGraph root name = doesDirectoryExist root >>= \case
65+ False -> return Nothing
66+ True -> do
67+ allVertexPairs <- listDirectory root >>= return . catMaybes . fmap getEdgeVertices
68+ edgesInfo <- fmap catMaybes $ forM allVertexPairs $ \vs -> do
69+ getEdgeFor name vs >>= return . fmap (vs, )
70+ return $ pure $ Migration (edges $ fmap fst edgesInfo) (Map.fromList edgesInfo)
71+ where
72+ getEdgeVertices p = case T.splitOn "-" $ T.pack p of
73+ [v1, v2] -> Just (v1, v2)
74+ _ -> Nothing
75+ getEdgeFor graph (v1, v2) = do
76+ let f = root </> (T.unpack $ v1 <> "-" <> v2) </> T.unpack graph
77+ doesFileExist f >>= \case
78+ True -> do
79+ c <- parseEdgeMeta <$> T.readFile f
80+ return $ Just c
81+ False -> do
82+ return Nothing
83+
84+-- | Ensure that the migration graph is correct
85+--
86+-- In a correct migration graph: for all pairs of nodes, all paths between
87+-- those nodes have the same migration value
88+--
89+-- Throw the following if integrity check fails
90+-- * `CyclicGraphError`
91+-- * `NonEquivalentPaths`
92+--
93+-- Complexity: O(V+E)
94+ensureGraphIntegrity
95+ :: (MonadError MigrationError m, Monoid action, Ord action)
96+ => Migration action -> m ()
97+ensureGraphIntegrity m = do
98+ graph <- fmap adjacencyMap $ getDag $ _migration_graph m
99+ firstVertex <- getFirst $ _migration_graph m
100+ let
101+ visitFrom visited acc start = case Map.lookup start graph of
102+ Nothing -> pure ()
103+ Just adjs -> void $ flip traverse (Set.toList adjs) $ \adj -> do
104+ acc' <- fmap (mappend acc) $ getAction m (start, adj)
105+ visited' <- case Map.lookup adj visited of
106+ Nothing -> pure $ Map.insert adj acc' visited
107+ Just adjAcc -> if acc' == adjAcc
108+ then pure visited
109+ else throwError $ NonEquivalentPaths start adj
110+ visitFrom visited' acc' adj
111+ -- We only check all paths from the _first_ vertex; it is not necessary to
112+ -- check all paths between _all_ pairs of vertices as that can be proved by
113+ -- induction, as long as there is exactly one first vertex (which we check on
114+ -- further above).
115+ visitFrom mempty mempty firstVertex
116+
117+-- | Ensure that the graph is a fully connected linear list
118+--
119+-- Returns the last vertex in the linear list.
120+ensureGraphLinearity :: MonadError MigrationError m => Migration action -> m Hash
121+ensureGraphLinearity m = do
122+ graph <- fmap adjacencyMap $ getDag $ _migration_graph m
123+ firstVertex <- getFirst $ _migration_graph m
124+ let
125+ allVertices = vertexSet $ _migration_graph m
126+ visitFrom start = case Map.lookup start graph of
127+ Nothing -> throwError $ GraphInternalError $ "Vertex not found: " <> start
128+ Just adjs -> case (Set.toList adjs) of
129+ [] -> pure (start, [start])
130+ [adj] -> fmap (start :) <$> visitFrom adj
131+ _ -> throwError $ NonLinearGraph_MultipleAdjacents start
132+ (lastVertex, visitedVertices) <- visitFrom firstVertex
133+ let unvisitedVertices = Set.difference allVertices $ Set.fromList visitedVertices
134+ if Set.null unvisitedVertices
135+ then pure lastVertex
136+ else throwError $ NonLinearGraph_NotConnected unvisitedVertices
137+
138+-- | Find the concatenated actions between two vertices
139+--
140+-- All paths between the vertices must have the same mappend'ed action;
141+-- otherwise this will throw.
142+--
143+-- Complexity: O(V+E)
144+findPathAction
145+ :: (MonadError MigrationError m, Action action, Monoid action, Ord action)
146+ => Migration action -> Hash -> Hash -> m (Maybe action)
147+findPathAction m start end = flip evalStateT Map.empty $ do
148+ graph <- fmap adjacencyMap $ getDag $ _migration_graph m
149+ let
150+ findPathFromCached start' = do
151+ -- TODO: refactor the caching part into a separate `memoize` function
152+ cache <- get
153+ case Map.lookup start' cache of
154+ Just result ->
155+ pure result
156+ Nothing -> do
157+ result <- findPathFrom start'
158+ put $ Map.insert start' result cache
159+ pure result
160+ findPathFrom start' = if start' == end
161+ then
162+ pure $ Just mempty
163+ else case Map.lookup start' graph of
164+ Just adjs -> do
165+ actions <- fmap catMaybes $ flip traverse (Set.toList adjs) $ \adj -> do
166+ action0 <- getAction m (start', adj)
167+ actionM <- findPathFromCached adj
168+ pure $ fmap (mappend action0) $ actionM
169+ case uniqs actions of
170+ [] -> pure Nothing
171+ [v] -> pure $ Just v -- Exactly one monoidal value; accept.
172+ _ -> throwError $ NonEquivalentPaths start' end
173+ Nothing ->
174+ pure Nothing
175+ uniqs = Set.toList . Set.fromList
176+ findPathFromCached start
177+
178+hasVertex :: Hash -> Migration action -> Bool
179+hasVertex h (Migration g _) = Set.member h $ vertexSet g
180+
181+-- | Get the action for the given edge (which must exist)
182+getAction :: MonadError MigrationError m => Migration action -> (Hash, Hash) -> m action
183+getAction (Migration _ h) e = case Map.lookup e h of
184+ Just v -> pure v
185+ Nothing -> throwError $ MissingEdgeInActionMap e
186+
187+-- | Get the last vertex of the given DAG. Nothing if empty graph.
188+--
189+-- Assumes that the graph is fully connected.
190+getLast :: (MonadError MigrationError m, Ord a, Eq a) => AdjacencyMap a -> m a
191+getLast g = lastVertices >>= \case
192+ [] -> throwError NoTerminalVertex
193+ [x] -> pure x
194+ _ -> throwError MultipleTerminalVertices
195+ where
196+ lastVertices
197+ = pure
198+ . fmap fst
199+ . filter (\(_, nexts) -> nexts == [])
200+ . adjacencyList
201+ =<< getDag g
202+
203+-- | Get the first vertex of the given DAG. Nothing if empty graph.
204+getFirst :: (MonadError MigrationError m, Ord a, Eq a) => AdjacencyMap a -> m a
205+getFirst = getLast . transpose
206+
207+-- | Write an edge directly to the filesystem.
208+--
209+-- Actions are written with empty string.
210+--
211+-- Return the edge directory created.
212+writeEdge :: MonadIO m => FilePath -> [Text] -> Hash -> Hash -> m (Maybe FilePath)
213+writeEdge dir actionFiles v1 v2 = do
214+ unless (v1 /= v2) $
215+ fail $ "Cannot create self loop with: " <> T.unpack v1
216+ let edgeDir = dir </> (T.unpack $ v1 <> "-" <> v2)
217+ liftIO (doesDirectoryExist edgeDir) >>= \case
218+ True -> pure Nothing
219+ False -> do
220+ liftIO $ createDirectory edgeDir
221+ forM_ actionFiles $ \fp -> liftIO $
222+ writeFile (edgeDir </> T.unpack fp) ""
223+ pure $ Just edgeDir
lib/route/obelisk-route.cabalnew file+36
@@ -0,0 +1,36 @@
1+name: obelisk-route
2+version: 0.2
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ hs-source-dirs: src
8+ build-depends: base,
9+ categories,
10+ constraints,
11+ containers,
12+ dependent-map,
13+ dependent-sum,
14+ dependent-sum-universe-orphans,
15+ dependent-sum-template,
16+ either,
17+ ghcjs-dom,
18+ http-types,
19+ jsaddle,
20+ lens,
21+ monad-control,
22+ mtl,
23+ network-uri,
24+ primitive,
25+ ref-tf,
26+ reflex,
27+ reflex-dom-core,
28+ template-haskell,
29+ text,
30+ transformers,
31+ universe,
32+ universe-template
33+ exposed-modules: Obelisk.Route
34+ Obelisk.Route.TH
35+ Obelisk.Route.Frontend
36+ ghc-options: -Wall -fwarn-redundant-constraints
lib/route/src/Obelisk/Route.hsnew file+795
@@ -0,0 +1,795 @@
1+{-# LANGUAGE ApplicativeDo #-}
2+{-# LANGUAGE ConstraintKinds #-}
3+{-# LANGUAGE EmptyCase #-}
4+{-# LANGUAGE EmptyDataDecls #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GADTs #-}
8+{-# LANGUAGE KindSignatures #-}
9+{-# LANGUAGE LambdaCase #-}
10+{-# LANGUAGE MultiParamTypeClasses #-}
11+{-# LANGUAGE OverloadedStrings #-}
12+{-# LANGUAGE PatternSynonyms #-}
13+{-# LANGUAGE RankNTypes #-}
14+{-# LANGUAGE ScopedTypeVariables #-}
15+{-# LANGUAGE TemplateHaskell #-}
16+{-# LANGUAGE TypeFamilies #-}
17+{-# LANGUAGE TypeOperators #-}
18+{-# LANGUAGE ViewPatterns #-}
19+module Obelisk.Route
20+ ( R
21+ , pattern (:/)
22+ , PageName
23+ , PathQuery
24+ , Encoder
25+ , unsafeEncoder
26+ , checkEncoder
27+ , EncoderImpl (..)
28+ , EncoderFunc (..)
29+ , unsafeMkEncoder
30+ , encode
31+ , decode
32+ , tryDecode
33+ , hoistCheck
34+ , hoistParse
35+ , mapSome
36+ , SegmentResult (..)
37+ , pathComponentEncoder
38+ , enumEncoder
39+ , enum1Encoder
40+ , checkEnum1EncoderFunc
41+ , unitEncoder
42+ , pathOnlyEncoder
43+ , singletonListEncoder
44+ , unpackTextEncoder
45+ , prefixTextEncoder
46+ , unsafeTshowEncoder
47+ , someConstEncoder
48+ , singlePathSegmentEncoder
49+ , justEncoder
50+ , nothingEncoder
51+ , isoEncoder
52+ , wrappedEncoder
53+ , unwrappedEncoder
54+ , listToNonEmptyEncoder
55+ , prefixNonemptyTextEncoder
56+ , joinPairTextEncoder
57+ , toListMapEncoder
58+ , shadowEncoder
59+ , prismEncoder
60+ , rPrism
61+ , obeliskRouteEncoder
62+ , obeliskRouteSegment
63+ , pageNameEncoder
64+ , handleEncoder
65+ , ObeliskRoute (..)
66+ , _ObeliskRoute_App
67+ , _ObeliskRoute_Resource
68+ , ResourceRoute (..)
69+ , JSaddleWarpRoute (..)
70+ , jsaddleWarpRouteEncoder
71+ , IndexOnlyRoute (..)
72+ , indexOnlyRouteSegment
73+ , indexOnlyRouteEncoder
74+ , someSumEncoder
75+ , Void1
76+ , void1Encoder
77+ , pathSegmentsTextEncoder
78+ , queryParametersTextEncoder
79+ ) where
80+
81+import Prelude hiding ((.), id)
82+
83+import Control.Applicative
84+import Control.Category (Category (..))
85+import qualified Control.Categorical.Functor as Cat
86+import Control.Categorical.Bifunctor
87+import Control.Category.Associative
88+import Control.Category.Monoidal
89+import Control.Lens (Identity (..), Prism', makePrisms, itraverse, imap, prism, (^.), re, matching, (^?), _Just, _Nothing, Iso', from, view, Wrapped (..))
90+import Control.Monad.Except
91+import Data.Dependent.Sum (DSum (..))
92+import Data.Dependent.Map (DMap)
93+import qualified Data.Dependent.Map as DMap
94+import Data.Either.Validation (Validation (..))
95+import Data.Foldable
96+import Data.Functor.Sum
97+import Data.GADT.Compare
98+import Data.GADT.Compare.TH
99+import Data.GADT.Show
100+import Data.List.NonEmpty (NonEmpty (..))
101+import Data.Map (Map)
102+import qualified Data.Map as Map
103+import Data.Maybe
104+import Data.Set (Set)
105+import qualified Data.Set as Set
106+import Data.Some (Some)
107+import qualified Data.Some as Some
108+import Data.Some.Universe.Orphans ()
109+import Data.Text (Text)
110+import qualified Data.Text as T
111+import Data.Text.Encoding
112+import Data.Universe
113+import Network.HTTP.Types.URI
114+import Obelisk.Route.TH
115+import Text.Read (readMaybe)
116+
117+-- Design goals:
118+-- No start-up time on the frontend (not yet met)
119+-- Able to ensure that there aren't overlapping routes prior to deployment
120+-- Easy to write common types of parsers
121+-- Completeness checking
122+
123+-- Laws:
124+-- We statically know that all valid Routes can be turned into valid URIs
125+-- - This means we need to know whether there are any overlaps, e.g. with the static file namespace
126+
127+--TODO:
128+-- Backend:
129+-- Redirect the user to a canonical route
130+-- Pre-render
131+-- Frontend:
132+-- Intercept links that can be kept within the app
133+-- Fragments
134+
135+--------------------------------------------------------------------------------
136+-- Subroutes/paths
137+--------------------------------------------------------------------------------
138+
139+type R f = DSum f Identity --TODO: Better name
140+
141+--TODO: COMPLETE pragma
142+infixr 5 :/
143+pattern (:/) :: f a -> a -> R f
144+pattern a :/ b = a :=> Identity b
145+
146+mapSome :: (forall a. f a -> g a) -> Some f -> Some g
147+mapSome f (Some.This a) = Some.This $ f a
148+
149+--------------------------------------------------------------------------------
150+-- Encoder fundamentals
151+--------------------------------------------------------------------------------
152+
153+-- | This is the type of route encoder/decoders. It is parameterised over two monads: Firstly, the monad
154+-- used to check the validity of the encoder (i.e. that it is total), secondly the monad used for parsing
155+-- during the decode phase. The following two parameters are respectively the type of decoded data, and the
156+-- encoded type.
157+newtype Encoder check parse decoded encoded =
158+ Encoder { unEncoder :: check (EncoderImpl parse decoded encoded) }
159+
160+unsafeEncoder :: check (EncoderImpl parse decoded encoded) -> Encoder check parse decoded encoded
161+unsafeEncoder = Encoder
162+
163+-- | The internal type used to construct primitive 'Encoder' values.
164+-- Law:
165+-- forall p. _encoderImpl_decode ve . _encoderImpl_encode ve p == pure
166+-- Note that the reverse may not be the case: when parsing, a route may be canonicalized, and erroneous routes may be collapsed to a single 404 route. However, as a consequence of the law, encode . decode must be idempotent.
167+data EncoderImpl parse decoded encoded = EncoderImpl
168+ { _encoderImpl_decode :: !(encoded -> parse decoded) -- Can fail; can lose information; must always succeed on outputs of `_encoderImpl_encode` and result in the original value
169+ , _encoderImpl_encode :: !(decoded -> encoded) -- Must be injective
170+ }
171+
172+-- | Once an 'Encoder' has been checked, so that its check monad has become 'Identity', and its parser is total
173+-- so that its parse monad is also 'Identity', it may be used to actually decode by applying this function.
174+decode :: Encoder Identity Identity decoded encoded -> encoded -> decoded
175+decode e x = runIdentity (tryDecode e x)
176+
177+-- | Once an 'Encoder' has been checked, so that its check monad has become 'Identity', even if the same is not true of the
178+-- parse monad, we may still attempt to decode with it in its parse monad.
179+tryDecode :: Encoder Identity parse decoded encoded -> encoded -> parse decoded
180+tryDecode (Encoder (Identity impl)) x = _encoderImpl_decode impl x
181+
182+-- | Similar to 'decode' above, once an encoder has been checked so that its check monad is Identity, it
183+-- can be used to actually encode by using this. Note that while there's no constraint on the parse monad here,
184+-- one should usually be applying decode and encode to the same 'Encoder'
185+encode :: Encoder Identity parse decoded encoded -> decoded -> encoded
186+encode (Encoder (Identity impl)) x = _encoderImpl_encode impl x
187+
188+-- | This is a primitive used to build encoders which can't fail to check. It should not be used unless one is
189+-- reasonably certain that the law given for 'EncoderImpl' above holds.
190+unsafeMkEncoder :: (Applicative check) => EncoderImpl parse decoded encoded -> Encoder check parse decoded encoded
191+unsafeMkEncoder impl = Encoder (pure impl)
192+
193+-- | Transform the check monad of an 'Encoder' by applying a natural transformation.
194+hoistCheck :: (forall t. check t -> check' t) -> Encoder check parse a b -> Encoder check' parse a b
195+hoistCheck f (Encoder x) = Encoder (f x)
196+
197+-- | Transform the parse monad of an 'Encoder' by applying a natural transformation.
198+hoistParse :: (Functor check)
199+ => (forall t. parse t -> parse' t) -> Encoder check parse a b -> Encoder check parse' a b
200+hoistParse f (Encoder x) = Encoder (fmap (\(EncoderImpl dec enc) -> EncoderImpl (f . dec) enc) x)
201+
202+-- | Check an 'Encoder', transforming it into one whose check monad is anything we want (usually Identity).
203+checkEncoder :: (Applicative check', Functor check)
204+ => Encoder check parse decoded encoded
205+ -> check (Encoder check' parse decoded encoded)
206+checkEncoder = fmap unsafeMkEncoder . unEncoder
207+
208+instance (Applicative check, Monad parse) => Category (Encoder check parse) where
209+ id = Encoder $ pure id
210+ Encoder f . Encoder g = Encoder $ liftA2 (.) f g
211+
212+instance Monad parse => Category (EncoderImpl parse) where
213+ id = EncoderImpl
214+ { _encoderImpl_decode = pure
215+ , _encoderImpl_encode = id
216+ }
217+ f . g = EncoderImpl
218+ { _encoderImpl_decode = _encoderImpl_decode g <=< _encoderImpl_decode f
219+ , _encoderImpl_encode = _encoderImpl_encode f . _encoderImpl_encode g
220+ }
221+
222+instance Monad parse => PFunctor (,) (EncoderImpl parse) (EncoderImpl parse) where
223+ first f = bimap f id
224+instance Monad parse => QFunctor (,) (EncoderImpl parse) (EncoderImpl parse) where
225+ second g = bimap id g
226+instance Monad parse => Bifunctor (,) (EncoderImpl parse) (EncoderImpl parse) (EncoderImpl parse) where
227+ bimap f g = EncoderImpl
228+ { _encoderImpl_encode = bimap (_encoderImpl_encode f) (_encoderImpl_encode g)
229+ , _encoderImpl_decode = \(a, b) -> liftA2 (,) (_encoderImpl_decode f a) (_encoderImpl_decode g b)
230+ }
231+
232+instance (Applicative check, Monad parse) => PFunctor (,) (Encoder check parse) (Encoder check parse) where
233+ first f = bimap f id
234+instance (Applicative check, Monad parse) => QFunctor (,) (Encoder check parse) (Encoder check parse) where
235+ second g = bimap id g
236+instance (Applicative check, Monad parse) => Bifunctor (,) (Encoder check parse) (Encoder check parse) (Encoder check parse) where
237+ bimap f g = Encoder $ liftA2 bimap (unEncoder f) (unEncoder g)
238+
239+instance (Traversable f, Monad parse) => Cat.Functor f (EncoderImpl parse) (EncoderImpl parse) where
240+ fmap ve = EncoderImpl
241+ { _encoderImpl_encode = fmap $ _encoderImpl_encode ve
242+ , _encoderImpl_decode = traverse $ _encoderImpl_decode ve
243+ }
244+
245+instance (Traversable f, Monad check, Monad parse) => Cat.Functor f (Encoder check parse) (Encoder check parse) where
246+ fmap e = Encoder $ do
247+ ve <- unEncoder e
248+ pure $ Cat.fmap ve
249+
250+instance Monad parse => Associative (EncoderImpl parse) (,) where
251+ associate = EncoderImpl
252+ { _encoderImpl_encode = associate
253+ , _encoderImpl_decode = pure . disassociate
254+ }
255+ disassociate = EncoderImpl
256+ { _encoderImpl_encode = disassociate
257+ , _encoderImpl_decode = pure . associate
258+ }
259+
260+instance Monad parse => Monoidal (EncoderImpl parse) (,) where
261+ type Id (EncoderImpl parse) (,) = ()
262+ idl = EncoderImpl
263+ { _encoderImpl_encode = idl
264+ , _encoderImpl_decode = pure . coidl
265+ }
266+ idr = EncoderImpl
267+ { _encoderImpl_encode = idr
268+ , _encoderImpl_decode = pure . coidr
269+ }
270+ coidl = EncoderImpl
271+ { _encoderImpl_encode = coidl
272+ , _encoderImpl_decode = pure . idl
273+ }
274+ coidr = EncoderImpl
275+ { _encoderImpl_encode = coidr
276+ , _encoderImpl_decode = pure . idr
277+ }
278+
279+instance (Applicative check, Monad parse) => Associative (Encoder check parse) (,) where
280+ associate = Encoder $ pure associate
281+ disassociate = Encoder $ pure disassociate
282+
283+instance (Applicative check, Monad parse) => Monoidal (Encoder check parse) (,) where
284+ type Id (Encoder check parse) (,) = ()
285+ idl = Encoder $ pure idl
286+ idr = Encoder $ pure idr
287+ coidl = Encoder $ pure coidl
288+ coidr = Encoder $ pure coidr
289+
290+--------------------------------------------------------------------------------
291+-- Specific instances of encoders
292+--------------------------------------------------------------------------------
293+
294+-- | Given a valid 'Iso' from lens, construct an 'Encoder'
295+isoEncoder :: (Applicative check, Applicative parse) => Iso' a b -> Encoder check parse a b
296+isoEncoder f = unsafeMkEncoder $ EncoderImpl
297+ { _encoderImpl_encode = view f
298+ , _encoderImpl_decode = pure . view (from f)
299+ }
300+
301+wrappedEncoder :: (Wrapped a, Applicative check, Applicative parse) => Encoder check parse (Unwrapped a) a
302+wrappedEncoder = isoEncoder $ from _Wrapped'
303+
304+unwrappedEncoder :: (Wrapped a, Applicative check, Applicative parse) => Encoder check parse a (Unwrapped a)
305+unwrappedEncoder = isoEncoder _Wrapped'
306+
307+-- | Encodea a value by simply applying 'Just'
308+justEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a (Maybe a)
309+justEncoder = prismEncoder _Just
310+
311+-- |
312+nothingEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse () (Maybe a)
313+nothingEncoder = prismEncoder _Nothing
314+
315+someConstEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Const a)) a
316+someConstEncoder = unsafeMkEncoder $ EncoderImpl
317+ { _encoderImpl_encode = \(Some.This (Const a)) -> a
318+ , _encoderImpl_decode = pure . Some.This . Const
319+ }
320+
321+-- | WARNING: This is only safe if the Show and Read instances for 'a' are
322+-- inverses of each other
323+unsafeTshowEncoder :: (Show a, Read a, Applicative check, MonadError Text parse) => Encoder check parse a Text
324+unsafeTshowEncoder = unsafeMkEncoder $ EncoderImpl
325+ { _encoderImpl_encode = tshow
326+ , _encoderImpl_decode = \raw -> case readMaybe $ T.unpack raw of
327+ Nothing -> throwError $ "unsafeTshowEncoderImpl: couldn't decode " <> tshow raw
328+ Just parsed -> pure parsed
329+ }
330+
331+newtype EncoderFunc check parse p r = EncoderFunc { runEncoderImplFunc :: forall a. p a -> Encoder check parse a r }
332+
333+newtype Flip f a b = Flip { unFlip :: f b a }
334+
335+checkEnum1EncoderFunc
336+ :: forall check check' parse p r.
337+ ( Universe (Some p)
338+ , GCompare p
339+ , Monad check
340+ , Applicative check'
341+ )
342+ => (forall a. p a -> Encoder check parse a r)
343+ -> check (EncoderFunc check' parse p r)
344+checkEnum1EncoderFunc f = do
345+ (encoderImpls :: DMap p (Flip (EncoderImpl parse) r)) <- DMap.fromList <$>
346+ traverse (\(Some.This p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
347+ pure $ EncoderFunc $ \p -> unsafeMkEncoder . unFlip $
348+ DMap.findWithDefault (error "checkEnum1EncoderFunc: EncoderImpl not found (should be impossible)") p encoderImpls
349+
350+-- | This type is used by pathComponentEncoder to allow the user to indicate how to treat various cases when encoding a dependent sum of type `(R p)`.
351+data SegmentResult check parse a =
352+ PathEnd (Encoder check parse a (Map Text (Maybe Text))) -- ^ Indicate that the path is finished, with an Encoder that translates the corresponding value into query parameters
353+ | PathSegment Text (Encoder check parse a PageName) -- ^ Indicate that the key should be represented by an additional path segment with the given 'Text', and give an Encoder for translating the corresponding value into the remainder of the route.
354+
355+-- | Encode a dependent sum of type `(R p)` into a PageName (i.e. the path and query part of a URL) by using the
356+-- supplied function to decide how to encode the constructors of p using the SegmentResult type. It is important
357+-- that the number of values of type `(Some p)` be relatively small in order for checking to complete quickly.
358+pathComponentEncoder
359+ :: forall check parse p.
360+ ( Universe (Some p)
361+ , GShow p
362+ , GCompare p
363+ , MonadError Text check
364+ , MonadError Text parse )
365+ => (forall a. p a -> SegmentResult check parse a)
366+ -> Encoder check parse (R p) PageName
367+pathComponentEncoder f = Encoder $ do
368+ let extractEncoder = \case
369+ PathEnd e -> first (unitEncoder []) . coidl . e
370+ PathSegment _ e -> e
371+ extractPathSegment = \case
372+ PathEnd _ -> Nothing
373+ PathSegment t _ -> Just t
374+ EncoderFunc f' <- checkEnum1EncoderFunc (extractEncoder . f)
375+ unEncoder (pathComponentEncoderImpl (enum1Encoder (extractPathSegment . f)) f')
376+
377+pathComponentEncoderImpl :: forall check parse p. (Monad check, Monad parse)
378+ => (Encoder check parse (Some p) (Maybe Text))
379+ -> (forall a. p a -> Encoder Identity parse a PageName)
380+ -> Encoder check parse (R p) PageName
381+pathComponentEncoderImpl this rest =
382+ chainEncoder (lensEncoder (\(_, b) a -> (a, b)) Prelude.fst consEncoder) this rest
383+
384+--NOTE: Naming convention in this module is to always talk about things in the *encoding* direction, never in the *decoding* direction
385+
386+chainEncoder
387+ :: forall check parse p r b.
388+ ( Monad check
389+ , Monad parse
390+ )
391+ => Encoder check parse (b, r) r
392+ -> Encoder check parse (Some p) b
393+ -> (forall a. p a -> Encoder Identity parse a r)
394+ -> Encoder check parse (R p) r
395+chainEncoder cons this rest = Encoder $ do
396+ consValid <- unEncoder cons
397+ thisValid <- unEncoder this
398+ pure $ EncoderImpl
399+ { _encoderImpl_decode = \v -> do
400+ (here, following) <- _encoderImpl_decode consValid v
401+ Some.This r <- _encoderImpl_decode thisValid here
402+ (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
403+ , _encoderImpl_encode = \(r :/ s) ->
404+ _encoderImpl_encode consValid
405+ ( _encoderImpl_encode thisValid $ Some.This r
406+ , _encoderImpl_encode (runIdentity . unEncoder $ rest r) s)
407+ }
408+
409+--TODO: Do this in terms of a lens instead
410+lensEncoder :: (Applicative check, Monad parse)
411+ => (b -> [a] -> b) -> (b -> [a]) -> Encoder check parse (c, [a]) [a] -> Encoder check parse (c, b) b
412+lensEncoder set get g = Encoder $ do
413+ gImpl <- unEncoder g
414+ pure $ EncoderImpl
415+ { _encoderImpl_encode = \(ma, b) -> set b $ _encoderImpl_encode gImpl (ma, get b)
416+ , _encoderImpl_decode = \b -> do
417+ (ma, la) <- _encoderImpl_decode gImpl $ get b
418+ pure (ma, set b la)
419+ }
420+
421+consEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Maybe a, [a]) [a] --TODO: Really shouldn't *always* have the [a], even in the Nothing case
422+consEncoder = unsafeMkEncoder $ EncoderImpl
423+ { _encoderImpl_encode = \(h, t) -> maybeToList h <> t
424+ , _encoderImpl_decode = pure . \case
425+ [] -> (Nothing, [])
426+ h:t -> (Just h, t)
427+ }
428+
429+tshow :: Show a => a -> Text
430+tshow = T.pack . show
431+
432+shadowEncoder
433+ :: ( Universe a
434+ , MonadError Text check
435+ , Show a
436+ , Show b
437+ , Show c
438+ , check ~ parse --TODO: Get rid of this
439+ )
440+ => Encoder check parse a c -- ^ Overlaps; should have a small number of possible routes
441+ -> Encoder check parse b c -- ^ Gets overlapped
442+ -> Encoder check parse (Either a b) c
443+shadowEncoder f g = Encoder $ do
444+ vf <- unEncoder f
445+ vg <- unEncoder g
446+ let gCanParse c = catchError (Just <$> _encoderImpl_decode vg c) (\_ -> pure Nothing)
447+ overlaps <- fmap catMaybes $ forM universe $ \a -> do
448+ let c = _encoderImpl_encode vf a
449+ mb <- gCanParse c
450+ pure $ fmap (\b -> (a, b, c)) mb
451+ case overlaps of
452+ [] -> pure ()
453+ _ -> throwError $ "shadowEncoder: overlap detected: " <> T.unlines
454+ (flip fmap overlaps $ \(a, b, c) -> "first encoder encodes " <> tshow a <> " as " <> tshow c <> ", which second encoder decodes as " <> tshow b)
455+ pure $ EncoderImpl
456+ { _encoderImpl_encode = \case
457+ Left a -> _encoderImpl_encode vf a
458+ Right b -> _encoderImpl_encode vg b
459+ , _encoderImpl_decode = \c -> (Left <$> _encoderImpl_decode vf c) `catchError` \_ -> Right <$> _encoderImpl_decode vg c
460+ }
461+
462+enum1Encoder
463+ :: ( Universe (Some p)
464+ , GShow p
465+ , GCompare p
466+ , MonadError Text check
467+ , MonadError Text parse
468+ , Ord r
469+ , Show r
470+ )
471+ => (forall a. p a -> r) -> Encoder check parse (Some p) r
472+enum1Encoder f = enumEncoder $ \(Some.This p) -> f p
473+
474+-- | Encode an enumerable, bounded type. WARNING: Don't use this on types that
475+-- have a large number of values - it will use a lot of memory.
476+enumEncoder :: forall parse check p r. (Universe p, Show p, Ord p, Ord r, MonadError Text parse, MonadError Text check, Show r) => (p -> r) -> Encoder check parse p r
477+enumEncoder f = Encoder $ do
478+ let reversed = Map.fromListWith (<>) [ (f p, Set.singleton p) | p <- universe ]
479+ checkSingleton k vs = case Set.toList vs of
480+ [] -> error "enumEncoder: empty reverse mapping; should be impossible"
481+ [e] -> Success e
482+ _ -> Failure $ Map.singleton k vs
483+ showRedundant :: r -> Set p -> [Text]
484+ showRedundant k vs = (" " <> tshow k <> " can decode to any of:")
485+ : fmap ((" "<>) . tshow) (Set.toList vs)
486+ case itraverse checkSingleton reversed :: Validation (Map r (Set p)) (Map r p) of
487+ Failure ambiguousEntries -> throwError $ T.unlines $
488+ "enumEncoder: ambiguous encodings detected:" : concat (Map.elems $ imap showRedundant ambiguousEntries)
489+ Success m -> pure $ EncoderImpl
490+ { _encoderImpl_decode = \r -> case Map.lookup r m of
491+ Just a -> pure a
492+ Nothing -> throwError $ "enumEncoder: not recognized: " <> tshow r --TODO: Report this as a better type
493+ , _encoderImpl_encode = f
494+ }
495+
496+unitEncoder :: (Applicative check, MonadError Text parse, Show r, Eq r) => r -> Encoder check parse () r
497+unitEncoder expected = unsafeMkEncoder $ EncoderImpl
498+ { _encoderImpl_decode = \obtained ->
499+ if obtained == expected
500+ then pure ()
501+ else throwError $ "endEncoderImpl: expected " <> tshow expected <> ", got " <> tshow obtained
502+ , _encoderImpl_encode = \_ -> expected
503+ }
504+
505+singlePathSegmentEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse Text PageName
506+singlePathSegmentEncoder = pathOnlyEncoder . singletonListEncoder
507+
508+pathOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse [Text] PageName
509+pathOnlyEncoder = unsafeMkEncoder $ EncoderImpl
510+ { _encoderImpl_decode = \(path, query) ->
511+ if query == mempty
512+ then pure path
513+ else throwError "pathOnlyEncoderImpl: query was provided"
514+ , _encoderImpl_encode = \path -> (path, mempty)
515+ }
516+
517+singletonListEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a [a]
518+singletonListEncoder = unsafeMkEncoder $ EncoderImpl
519+ { _encoderImpl_decode = \case
520+ [a] -> pure a
521+ l -> throwError $ "singletonListEncoderImpl: expected one item, got " <> tshow (length l)
522+ , _encoderImpl_encode = (:[])
523+ }
524+
525+splitTextNonEmpty :: Text -> Text -> NonEmpty Text
526+splitTextNonEmpty separator v = case T.splitOn separator v of
527+ [] -> error "splitTextNonEmpty: Data.Text.splitOn should never return an empty list"
528+ h : t -> h :| t
529+
530+--TODO: To know this is reversible, we must know that the separator isn't included anywhere in the input text
531+pathSegmentsTextEncoder :: (Applicative check, Applicative parse) => Encoder check parse (NonEmpty Text) Text
532+pathSegmentsTextEncoder = unsafeMkEncoder $ EncoderImpl
533+ { _encoderImpl_encode = T.intercalate "/" . fmap (urlEncodeText False) . toList
534+ , _encoderImpl_decode = pure . fmap (urlDecodeText False) . splitTextNonEmpty "/"
535+ }
536+
537+queryParametersTextEncoder :: (Applicative check, Applicative parse) => Encoder check parse [(Text, Maybe Text)] Text
538+queryParametersTextEncoder = Encoder $ pure $ EncoderImpl
539+ { _encoderImpl_encode = \case
540+ [] -> ""
541+ params -> T.intercalate "&" (fmap encodeParameter params)
542+ , _encoderImpl_decode = pure . \case
543+ "" -> []
544+ encoded ->
545+ let h :| t = splitTextNonEmpty "&" encoded
546+ in fmap decodeParameter $ h : t
547+ }
548+ where
549+ encodeParameter (k, mv) = urlEncodeText True k <> case mv of
550+ Nothing -> ""
551+ Just v -> "=" <> urlEncodeText True v
552+ decodeParameter t =
553+ let (k, eqV) = T.breakOn "=" t
554+ mv = T.stripPrefix "=" eqV
555+ in (urlDecodeText True k, urlDecodeText True <$> mv)
556+
557+urlEncodeText :: Bool -> Text -> Text
558+urlEncodeText q = decodeUtf8 . urlEncode q . encodeUtf8
559+
560+urlDecodeText :: Bool -> Text -> Text
561+urlDecodeText q = decodeUtf8 . urlDecode q . encodeUtf8
562+
563+listToNonEmptyEncoder :: (Applicative check, Applicative parse, Monoid a, Eq a) => Encoder check parse [a] (NonEmpty a)
564+listToNonEmptyEncoder = Encoder $ pure $ EncoderImpl
565+ { _encoderImpl_encode = \case
566+ [] -> mempty :| []
567+ h : t -> h :| t
568+ , _encoderImpl_decode = \(h :| t) -> pure $
569+ if h == mempty
570+ then []
571+ else h : t
572+ }
573+
574+prefixTextEncoder :: (Applicative check, MonadError Text parse) => Text -> Encoder check parse Text Text
575+prefixTextEncoder p = Encoder $ pure $ EncoderImpl
576+ { _encoderImpl_encode = mappend p
577+ , _encoderImpl_decode = \v -> case T.stripPrefix p v of
578+ Nothing -> throwError $ "prefixTextEncoder: wrong prefix; expected " <> tshow p <> ", got " <> tshow (T.take (T.length p) v)
579+ Just stripped -> pure stripped
580+ }
581+
582+prefixNonemptyTextEncoder :: (Applicative check, MonadError Text parse) => Text -> Encoder check parse Text Text
583+prefixNonemptyTextEncoder p = Encoder $ pure $ EncoderImpl
584+ { _encoderImpl_encode = \case
585+ "" -> ""
586+ v -> p <> v
587+ , _encoderImpl_decode = \case
588+ "" -> pure ""
589+ v -> case T.stripPrefix p v of
590+ Nothing -> throwError $ "prefixTextEncoder: wrong prefix; expected " <> tshow p
591+ Just stripped -> pure stripped
592+ }
593+
594+unpackTextEncoder :: (Applicative check, Applicative parse) => Encoder check parse Text String
595+unpackTextEncoder = Encoder $ pure $ EncoderImpl
596+ { _encoderImpl_encode = T.unpack
597+ , _encoderImpl_decode = pure . T.pack
598+ }
599+
600+toListMapEncoder :: (Applicative check, Applicative parse, Ord k) => Encoder check parse (Map k v) [(k, v)]
601+toListMapEncoder = Encoder $ pure $ EncoderImpl
602+ { _encoderImpl_encode = Map.toList
603+ , _encoderImpl_decode = pure . Map.fromList --TODO: Should we be stricter about repeated keys?
604+ }
605+
606+joinPairTextEncoder :: (MonadError Text check, MonadError Text parse) => Text -> Encoder check parse (Text, Text) Text
607+joinPairTextEncoder = Encoder . \case
608+ "" -> throwError "joinPairTextEncoder: empty separator"
609+ separator -> pure $ EncoderImpl
610+ { _encoderImpl_encode = \(k, v) -> k <> separator <> v
611+ , _encoderImpl_decode = \r ->
612+ let (kt, vt) = T.breakOn separator r
613+ in case vt of
614+ -- The separator was not found
615+ "" -> throwError $ "joinPairTextEncoder: separator not found; expected " <> tshow separator
616+ _ -> return (kt, T.drop (T.length separator) vt)
617+ }
618+
619+--TODO: Rewrite this by composing the given prism with a lens on the first element of the DSum
620+-- Or, more likely, the user can compose it themselves
621+rPrism :: forall f g. (forall a. Prism' (f a) (g a)) -> Prism' (R f) (R g)
622+rPrism p = prism (\(g :/ x) -> g ^. re p :/ x) (\(f :/ x) -> bimap (:/ x) (:/ x) $ matching p f)
623+
624+-- | An encoder that only works on the items available via the prism. An error will be thrown in the parse monad
625+-- if the prism doesn't match.
626+prismEncoder :: (Applicative check, MonadError Text parse) => Prism' b a -> Encoder check parse a b
627+prismEncoder p = unsafeMkEncoder $ EncoderImpl
628+ { _encoderImpl_encode = (^. re p)
629+ , _encoderImpl_decode = \r -> case r ^? p of
630+ Just a -> pure a
631+ Nothing -> throwError "prismEncoder: value is not present in the prism"
632+ }
633+
634+-- | A URL path and query string, in which trailing slashes don't matter in the path
635+-- and duplicate query parameters are not allowed. A final goal of encoders using this library
636+-- will frequently be to produce this.
637+type PageName = ([Text], Map Text (Maybe Text))
638+
639+-- | A path (separated by slashes), and a query string.
640+type PathQuery = (String, String)
641+
642+-- | Encode a PageName into a path and query string.
643+pageNameEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse PageName PathQuery
644+pageNameEncoder = bimap
645+ (unpackTextEncoder . prefixTextEncoder "/" . pathSegmentsTextEncoder . listToNonEmptyEncoder)
646+ (unpackTextEncoder . prefixNonemptyTextEncoder "?" . queryParametersTextEncoder . toListMapEncoder)
647+
648+-- | Handle an error in parsing, for example, in order to redirect to a 404 page.
649+handleEncoder
650+ :: (Functor check)
651+ => (e -> a)
652+ -> Encoder check (Either e) a b
653+ -> Encoder check Identity a b
654+handleEncoder recover e = Encoder $ do
655+ i <- unEncoder e
656+ return $ i
657+ { _encoderImpl_decode = \a -> pure $ case _encoderImpl_decode i a of
658+ Right r -> r
659+ Left err -> recover err
660+ }
661+
662+--------------------------------------------------------------------------------
663+-- Actual obelisk route info
664+--------------------------------------------------------------------------------
665+
666+-- | A type which can represent Obelisk-specific resource routes, in addition to application specific routes which serve your
667+-- frontend.
668+data ObeliskRoute :: (* -> *) -> * -> * where
669+ -- We need to have the `f a` as an argument here, because otherwise we have no way to specifically check for overlap between us and the given encoder
670+ ObeliskRoute_App :: f a -> ObeliskRoute f a
671+ ObeliskRoute_Resource :: ResourceRoute a -> ObeliskRoute f a
672+
673+instance Universe (Some f) => Universe (Some (ObeliskRoute f)) where
674+ universe = fmap (\(Some.This x) -> Some.This (ObeliskRoute_App x)) universe
675+ ++ fmap (\(Some.This x) -> Some.This (ObeliskRoute_Resource x)) universe
676+
677+instance GEq f => GEq (ObeliskRoute f) where
678+ geq (ObeliskRoute_App x) (ObeliskRoute_App y) = geq x y
679+ geq (ObeliskRoute_Resource x) (ObeliskRoute_Resource y) = geq x y
680+ geq _ _ = Nothing
681+
682+instance GCompare f => GCompare (ObeliskRoute f) where
683+ gcompare (ObeliskRoute_App x) (ObeliskRoute_App y) = gcompare x y
684+ gcompare (ObeliskRoute_Resource x) (ObeliskRoute_Resource y) = gcompare x y
685+ gcompare (ObeliskRoute_App _) (ObeliskRoute_Resource _) = GLT
686+ gcompare (ObeliskRoute_Resource _) (ObeliskRoute_App _) = GGT
687+
688+-- | A type representing the various resource routes served by Obelisk. These can in principle map to any physical routes you want,
689+-- but sane defaults are provided by 'resourceRouteSegment'
690+data ResourceRoute :: * -> * where
691+ ResourceRoute_Static :: ResourceRoute [Text] -- This [Text] represents the *path in our static files directory*, not necessarily the URL path that the asset gets served at (although that will often be "/static/this/text/thing")
692+ ResourceRoute_Ghcjs :: ResourceRoute [Text]
693+ ResourceRoute_JSaddleWarp :: ResourceRoute (R JSaddleWarpRoute)
694+
695+-- | If there are no additional backend routes in your app (i.e. ObeliskRoute gives you all the routes you need),
696+-- this constructs a suitable 'Encoder' to use for encoding routes to 'PageName's. If you do have additional backend routes,
697+-- you'll want to use 'pathComponentEncoder' yourself, applied to a function that will likely use obeliskRouteSegment in order to
698+-- handle the ObeliskRoute case (i.e. Obelisk resource routes and app frontend routes).
699+obeliskRouteEncoder :: forall check parse appRoute.
700+ ( Universe (Some (ObeliskRoute appRoute))
701+ , GCompare (ObeliskRoute appRoute)
702+ , GShow appRoute
703+ , MonadError Text check
704+ , check ~ parse --TODO: Get rid of this
705+ )
706+ => (forall a. appRoute a -> SegmentResult check parse a)
707+ -> Encoder check parse (R (ObeliskRoute appRoute)) PageName
708+obeliskRouteEncoder appRouteSegment = pathComponentEncoder $ \r ->
709+ obeliskRouteSegment r appRouteSegment
710+
711+-- | From a function which explains how app-specific frontend routes translate into segments, produce a function which does the
712+-- same for ObeliskRoute. This uses the given function for the 'ObeliskRoute_App' case, and 'resourceRouteSegment' for the
713+-- 'ObeliskRoute_Resource' case.
714+obeliskRouteSegment :: forall check parse appRoute a.
715+ (MonadError Text check, MonadError Text parse)
716+ => ObeliskRoute appRoute a
717+ -> (forall a. appRoute a -> SegmentResult check parse a)
718+ -> SegmentResult check parse a
719+obeliskRouteSegment r appRouteSegment = case r of
720+ ObeliskRoute_App appRoute -> appRouteSegment appRoute
721+ ObeliskRoute_Resource resourceRoute -> resourceRouteSegment resourceRoute
722+
723+-- | A function which gives a sane default for how to encode Obelisk resource routes. It's given in this form, because it will
724+-- be combined with other such segment encoders before 'pathComponentEncoder' turns it into a proper 'Encoder'.
725+resourceRouteSegment :: (MonadError Text check, MonadError Text parse) => ResourceRoute a -> SegmentResult check parse a
726+resourceRouteSegment = \case
727+ ResourceRoute_Static -> PathSegment "static" $ pathOnlyEncoder
728+ ResourceRoute_Ghcjs -> PathSegment "ghcjs" $ pathOnlyEncoder
729+ ResourceRoute_JSaddleWarp -> PathSegment "jsaddle" $ jsaddleWarpRouteEncoder
730+
731+data JSaddleWarpRoute :: * -> * where
732+ JSaddleWarpRoute_JavaScript :: JSaddleWarpRoute ()
733+ JSaddleWarpRoute_WebSocket :: JSaddleWarpRoute ()
734+ JSaddleWarpRoute_Sync :: JSaddleWarpRoute [Text]
735+
736+jsaddleWarpRouteEncoder :: (MonadError Text check, MonadError Text parse) => Encoder check parse (R JSaddleWarpRoute) PageName
737+jsaddleWarpRouteEncoder = pathComponentEncoder $ \case
738+ JSaddleWarpRoute_JavaScript -> PathSegment "jsaddle.js" $ unitEncoder mempty
739+ JSaddleWarpRoute_WebSocket -> PathEnd $ unitEncoder mempty
740+ JSaddleWarpRoute_Sync -> PathSegment "sync" $ pathOnlyEncoder
741+
742+instance GShow appRoute => GShow (ObeliskRoute appRoute) where
743+ gshowsPrec prec = \case
744+ ObeliskRoute_App appRoute -> showParen (prec > 10) $
745+ showString "ObeliskRoute_App " . gshowsPrec 11 appRoute
746+ ObeliskRoute_Resource appRoute -> showParen (prec > 10) $
747+ showString "ObeliskRoute_Resource " . gshowsPrec 11 appRoute
748+
749+data IndexOnlyRoute :: * -> * where
750+ IndexOnlyRoute :: IndexOnlyRoute ()
751+
752+indexOnlyRouteSegment :: (Applicative check, MonadError Text parse) => IndexOnlyRoute a -> SegmentResult check parse a
753+indexOnlyRouteSegment = \case
754+ IndexOnlyRoute -> PathEnd $ unitEncoder mempty
755+
756+indexOnlyRouteEncoder :: (MonadError Text check, MonadError Text parse) => Encoder check parse (R IndexOnlyRoute) PageName
757+indexOnlyRouteEncoder = pathComponentEncoder indexOnlyRouteSegment
758+
759+someSumEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Sum a b)) (Either (Some a) (Some b))
760+someSumEncoder = Encoder $ pure $ EncoderImpl
761+ { _encoderImpl_encode = \(Some.This t) -> case t of
762+ InL l -> Left $ Some.This l
763+ InR r -> Right $ Some.This r
764+ , _encoderImpl_decode = pure . \case
765+ Left (Some.This l) -> Some.This (InL l)
766+ Right (Some.This r) -> Some.This (InR r)
767+ }
768+
769+data Void1 :: * -> * where {}
770+
771+instance Universe (Some Void1) where
772+ universe = []
773+
774+void1Encoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Some Void1) a
775+void1Encoder = Encoder $ pure $ EncoderImpl
776+ { _encoderImpl_encode = \case
777+ Some.This f -> case f of {}
778+ , _encoderImpl_decode = \_ -> throwError "void1Encoder: can't decode anything"
779+ }
780+
781+instance GShow Void1 where
782+ gshowsPrec _ = \case {}
783+
784+concat <$> mapM deriveRouteComponent
785+ [ ''ResourceRoute
786+ , ''JSaddleWarpRoute
787+ , ''IndexOnlyRoute
788+ ]
789+
790+makePrisms ''ObeliskRoute
791+
792+deriveGEq ''Void1
793+deriveGCompare ''Void1
794+
795+--TODO: decodeURIComponent as appropriate
lib/route/src/Obelisk/Route/Frontend.hsnew file+322
@@ -0,0 +1,322 @@
1+{-# LANGUAGE ApplicativeDo #-}
2+{-# LANGUAGE CPP #-}
3+{-# LANGUAGE RecursiveDo #-}
4+{-# LANGUAGE StandaloneDeriving #-}
5+{-# LANGUAGE ConstraintKinds #-}
6+{-# LANGUAGE FlexibleContexts #-}
7+{-# LANGUAGE FlexibleInstances #-}
8+{-# LANGUAGE GADTs #-}
9+{-# LANGUAGE KindSignatures #-}
10+{-# LANGUAGE LambdaCase #-}
11+{-# LANGUAGE MultiParamTypeClasses #-}
12+{-# LANGUAGE OverloadedStrings #-}
13+{-# LANGUAGE PatternSynonyms #-}
14+{-# LANGUAGE RankNTypes #-}
15+{-# LANGUAGE ScopedTypeVariables #-}
16+{-# LANGUAGE TemplateHaskell #-}
17+{-# LANGUAGE TypeFamilies #-}
18+{-# LANGUAGE TypeOperators #-}
19+{-# LANGUAGE ViewPatterns #-}
20+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
21+{-# LANGUAGE UndecidableInstances #-}
22+{-# LANGUAGE DataKinds #-}
23+{-# LANGUAGE FunctionalDependencies #-}
24+
25+module Obelisk.Route.Frontend
26+ ( module Obelisk.Route
27+ , pattern (:~)
28+ , Routed
29+ , RoutedT
30+ , runRoutedT
31+ , askRoute
32+ , withRoutedT
33+ , mapRoutedT
34+ , subRoute
35+ , subRoute_
36+ , runRouteViewT
37+ , SetRouteT(..)
38+ , SetRoute(..)
39+ , runSetRouteT
40+ , mapSetRouteT
41+ ) where
42+
43+import Prelude hiding ((.), id)
44+
45+import Obelisk.Route
46+
47+import Control.Category (Category (..), (.))
48+import Control.Category.Cartesian
49+import Control.Lens hiding (Bifunctor, bimap, universe)
50+import Control.Monad.Fix
51+import Control.Monad.Primitive
52+import Control.Monad.Ref
53+import Control.Monad.Trans
54+import Control.Monad.Trans.Control
55+import Control.Monad.Trans.Reader
56+import Data.Coerce
57+import Data.Constraint (Dict (..))
58+import Data.Dependent.Sum (DSum (..))
59+import Data.GADT.Compare
60+import Data.Monoid
61+import qualified Data.Text as T
62+import Data.Functor.Compose
63+import Reflex.Class
64+import Reflex.Host.Class
65+import Reflex.PostBuild.Class
66+import Reflex.TriggerEvent.Class
67+import Reflex.PerformEvent.Class
68+import Reflex.EventWriter.Class
69+import Reflex.EventWriter.Base
70+import Reflex.Dynamic
71+import Reflex.Dom.Builder.Class
72+import Data.Type.Coercion
73+import Language.Javascript.JSaddle --TODO: Get rid of this - other platforms can also be routed
74+import Reflex.Dom.Core
75+import qualified GHCJS.DOM.Types as DOM
76+import Network.URI
77+#if defined(ios_HOST_OS)
78+import Data.Maybe (fromMaybe)
79+import qualified Data.List as L
80+#endif
81+
82+import Unsafe.Coerce
83+
84+infixr 5 :~
85+pattern (:~) :: Reflex t => f a -> Dynamic t a -> DSum f (Compose (Dynamic t) Identity)
86+pattern a :~ b <- a :=> (coerceDynamic . getCompose -> b)
87+
88+class Routed t r m where
89+ askRoute :: m (Dynamic t r)
90+
91+instance Monad m => Routed t r (RoutedT t r m) where
92+ askRoute = RoutedT ask
93+
94+newtype RoutedT t r m a = RoutedT { unRoutedT :: ReaderT (Dynamic t r) m a }
95+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, HasJSContext, MonadIO, MonadReflexCreateTrigger t, HasDocument)
96+
97+instance Prerender js m => Prerender js (RoutedT t r m) where
98+ prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
99+
100+instance Requester t m => Requester t (RoutedT t r m) where
101+ type Request (RoutedT t r m) = Request m
102+ type Response (RoutedT t r m) = Response m
103+ requesting = RoutedT . requesting
104+ requesting_ = RoutedT . requesting_
105+
106+#ifndef ghcjs_HOST_OS
107+deriving instance MonadJSM m => MonadJSM (RoutedT t r m)
108+#endif
109+
110+instance PerformEvent t m => PerformEvent t (RoutedT t r m) where
111+ type Performable (RoutedT t r m) = Performable m
112+ performEvent = lift . performEvent
113+ performEvent_ = lift . performEvent_
114+
115+instance MonadRef m => MonadRef (RoutedT t r m) where
116+ type Ref (RoutedT t r m) = Ref m
117+ newRef = lift . newRef
118+ readRef = lift . readRef
119+ writeRef r = lift . writeRef r
120+
121+instance HasJS x m => HasJS x (RoutedT t r m) where
122+ type JSX (RoutedT t r m) = JSX m
123+ liftJS = lift . liftJS
124+
125+deriving instance EventWriter t w m => EventWriter t w (RoutedT t r m)
126+
127+instance MonadTransControl (RoutedT t r) where
128+ type StT (RoutedT t r) a = StT (ReaderT (Dynamic t r)) a
129+ liftWith = defaultLiftWith RoutedT unRoutedT
130+ restoreT = defaultRestoreT RoutedT
131+
132+instance PrimMonad m => PrimMonad (RoutedT t r m ) where
133+ type PrimState (RoutedT t r m) = PrimState m
134+ primitive = lift . primitive
135+
136+instance DomBuilder t m => DomBuilder t (RoutedT t r m) where
137+ type DomBuilderSpace (RoutedT t r m) = DomBuilderSpace m
138+
139+instance Adjustable t m => Adjustable t (RoutedT t r m) where
140+ runWithReplace a0 a' = RoutedT $ runWithReplace (coerce a0) $ coerceEvent a'
141+ traverseIntMapWithKeyWithAdjust f a0 a' = RoutedT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
142+ traverseDMapWithKeyWithAdjust f a0 a' = RoutedT $ traverseDMapWithKeyWithAdjust (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
143+ traverseDMapWithKeyWithAdjustWithMove f a0 a' = RoutedT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
144+
145+instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RoutedT t r m) where
146+ tellQueryIncremental = lift . tellQueryIncremental
147+ askQueryResult = lift askQueryResult
148+ queryIncremental = lift . queryIncremental
149+
150+runRoutedT :: RoutedT t r m a -> Dynamic t r -> m a
151+runRoutedT = runReaderT . unRoutedT
152+
153+mapRoutedT :: (m a -> n a) -> RoutedT t r m a -> RoutedT t r n a
154+mapRoutedT f = RoutedT . mapReaderT f . unRoutedT
155+
156+withRoutedT :: (Dynamic t r -> Dynamic t r') -> RoutedT t r' m a -> RoutedT t r m a
157+withRoutedT f = RoutedT . withReaderT f . unRoutedT
158+
159+subRoute_ :: (MonadFix m, MonadHold t m, GEq r, Adjustable t m) => (forall a. r a -> RoutedT t a m ()) -> RoutedT t (R r) m ()
160+subRoute_ f = factorRouted $ strictDynWidget_ $ \(c :=> r') -> do
161+ runRoutedT (f c) r'
162+
163+subRoute :: (MonadFix m, MonadHold t m, GEq r, Adjustable t m) => (forall a. r a -> RoutedT t a m b) -> RoutedT t (R r) m (Dynamic t b)
164+subRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
165+ runRoutedT (f c) r'
166+
167+dsumValueCoercion :: Coercion f g -> Coercion (DSum k f) (DSum k g)
168+dsumValueCoercion Coercion = Coercion
169+
170+dynamicIdentityCoercion :: Coercion (Compose (Dynamic t) Identity) (Dynamic t)
171+dynamicIdentityCoercion = unsafeCoerce (Coercion :: Coercion (Identity ()) ()) --TODO: Is it possible to prove this?
172+
173+factorRouted :: (Reflex t, MonadFix m, MonadHold t m, GEq f) => RoutedT t (DSum f (Dynamic t)) m a -> RoutedT t (DSum f Identity) m a
174+factorRouted r = RoutedT $ ReaderT $ \d -> do
175+ d' <- factorDyn d
176+ runRoutedT r $ (coerceWith (dynamicCoercion $ dsumValueCoercion dynamicIdentityCoercion) d')
177+
178+-- | WARNING: The input 'Dynamic' must be fully constructed when this is run
179+strictDynWidget :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m b) -> RoutedT t a m (Dynamic t b)
180+strictDynWidget f = RoutedT $ ReaderT $ \r -> do
181+ r0 <- sample $ current r
182+ (result0, result') <- runWithReplace (f r0) $ f <$> updated r
183+ holdDyn result0 result'
184+
185+strictDynWidget_ :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m ()) -> RoutedT t a m ()
186+strictDynWidget_ f = RoutedT $ ReaderT $ \r -> do
187+ r0 <- sample $ current r
188+ (_, _) <- runWithReplace (f r0) $ f <$> updated r
189+ pure ()
190+
191+newtype SetRouteT t r m a = SetRouteT { unSetRouteT :: EventWriterT t (Endo r) m a }
192+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, HasJSContext, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomBuilder t)
193+
194+mapSetRouteT :: (forall x. m x -> n x) -> SetRouteT t r m a -> SetRouteT t r n a
195+mapSetRouteT f (SetRouteT x) = SetRouteT (mapEventWriterT f x)
196+
197+runSetRouteT :: (Reflex t, Monad m) => SetRouteT t r m a -> m (a, Event t (Endo r))
198+runSetRouteT = runEventWriterT . unSetRouteT
199+
200+class Reflex t => SetRoute t r m | m -> t r where
201+ setRoute :: Event t r -> m ()
202+ modifyRoute :: Event t (r -> r) -> m ()
203+ setRoute = modifyRoute . fmap const
204+
205+instance (Reflex t, Monad m) => SetRoute t r (SetRouteT t r m) where
206+ modifyRoute = SetRouteT . tellEvent . fmap Endo
207+
208+instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m) where
209+ modifyRoute = lift . modifyRoute
210+
211+instance Prerender js m => Prerender js (SetRouteT t r m) where
212+ prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
213+
214+instance Requester t m => Requester t (SetRouteT t r m) where
215+ type Request (SetRouteT t r m) = Request m
216+ type Response (SetRouteT t r m) = Response m
217+ requesting = SetRouteT . requesting
218+ requesting_ = SetRouteT . requesting_
219+
220+#ifndef ghcjs_HOST_OS
221+deriving instance MonadJSM m => MonadJSM (SetRouteT t r m)
222+#endif
223+
224+instance PerformEvent t m => PerformEvent t (SetRouteT t r m) where
225+ type Performable (SetRouteT t r m) = Performable m
226+ performEvent = lift . performEvent
227+ performEvent_ = lift . performEvent_
228+
229+instance MonadRef m => MonadRef (SetRouteT t r m) where
230+ type Ref (SetRouteT t r m) = Ref m
231+ newRef = lift . newRef
232+ readRef = lift . readRef
233+ writeRef r = lift . writeRef r
234+
235+instance HasJS x m => HasJS x (SetRouteT t r m) where
236+ type JSX (SetRouteT t r m) = JSX m
237+ liftJS = lift . liftJS
238+
239+instance PrimMonad m => PrimMonad (SetRouteT t r m ) where
240+ type PrimState (SetRouteT t r m) = PrimState m
241+ primitive = lift . primitive
242+
243+instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
244+ runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
245+ traverseIntMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
246+ traverseDMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseDMapWithKeyWithAdjust (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
247+ traverseDMapWithKeyWithAdjustWithMove f a0 a' = SetRouteT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
248+
249+instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (SetRouteT t r m) where
250+ tellQueryIncremental = lift . tellQueryIncremental
251+ askQueryResult = lift askQueryResult
252+ queryIncremental = lift . queryIncremental
253+
254+runRouteViewT
255+ :: forall t m r a.
256+ ( TriggerEvent t m
257+ , PerformEvent t m
258+ , MonadHold t m
259+ , MonadJSM m
260+ , MonadJSM (Performable m)
261+ , MonadFix m
262+ )
263+ => (Encoder Identity Identity r PageName)
264+ -> RoutedT t r (SetRouteT t r m) a
265+ -> m a
266+runRouteViewT routeEncoder a = do
267+ rec historyState <- manageHistory $ HistoryCommand_PushState <$> setState
268+ let theEncoder = pageNameEncoder . hoistParse (pure . runIdentity) routeEncoder
269+ -- NB: The can only fail if the uriPath doesn't begin with a '/' or if the uriQuery
270+ -- is nonempty, but begins with a character that isn't '?'. Since we don't expect
271+ -- this ever to happen, we'll just handle it by failing completely with 'error'.
272+ route :: Dynamic t r
273+ route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath &&& uriQuery) . _historyItem_uri) historyState
274+ where
275+ errorLeft (Left e) = error (T.unpack e)
276+ errorLeft (Right x) = x
277+ (result, changeState) <- runSetRouteT $ runRoutedT a route
278+ let f (currentHistoryState, oldRoute) change =
279+ let newRoute = appEndo change oldRoute
280+ (newPath, newQuery) = encode theEncoder newRoute
281+ in HistoryStateUpdate
282+ { _historyStateUpdate_state = DOM.SerializedScriptValue jsNull
283+ -- We always provide "" as the title. On Firefox, Chrome, and
284+ -- Edge, this parameter does nothing. On Safari, "" has the
285+ -- same behavior as other browsers (as far as I can tell), but
286+ -- anything else sets the title for the back button list item
287+ -- the *next* time pushState is called, unless the page title
288+ -- is changed in the interim. Since the Safari functionality
289+ -- is near-pointless and also confusing, I'm not going to even
290+ -- bother exposing it; if there ends up being a real use case,
291+ -- we can change this function later to accommodate.
292+ -- See: https://github.com/whatwg/html/issues/2174
293+ , _historyStateUpdate_title = ""
294+ , _historyStateUpdate_uri = Just $ setAdaptedUriPath newPath $ (_historyItem_uri currentHistoryState)
295+ { uriQuery = newQuery
296+ }
297+ }
298+ setState = attachWith f ((,) <$> current historyState <*> current route) changeState
299+ return result
300+
301+-- On ios due to sandboxing when loading the page from a file adapt the
302+-- path to be based on the hash.
303+
304+adaptedUriPath :: URI -> String
305+#if defined(ios_HOST_OS)
306+adaptedUriPath = hashToPath . uriFragment
307+
308+hashToPath :: String -> String
309+hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
310+#else
311+adaptedUriPath = uriPath
312+#endif
313+
314+setAdaptedUriPath :: String -> URI -> URI
315+#if defined(ios_HOST_OS)
316+setAdaptedUriPath s u = u { uriFragment = pathToHash s }
317+
318+pathToHash :: String -> String
319+pathToHash = ('#' :) . fromMaybe "" . L.stripPrefix "/"
320+#else
321+setAdaptedUriPath s u = u { uriPath = s }
322+#endif
lib/route/src/Obelisk/Route/TH.hsnew file+18
@@ -0,0 +1,18 @@
1+module Obelisk.Route.TH (deriveRouteComponent) where
2+
3+import Data.GADT.Show.TH
4+import Data.GADT.Compare.TH
5+import Data.Universe.TH
6+import Language.Haskell.TH
7+
8+-- | Derive all the typeclasses needed for a RouteComponent type. The argument should be the name of a type of kind @k -> *@
9+deriveRouteComponent :: Name -> Q [Dec]
10+deriveRouteComponent x = concat <$> traverse ($ x)
11+ [ deriveGShow
12+ , deriveGEq
13+ , deriveGCompare
14+ , deriveShowTagIdentity
15+ , deriveEqTagIdentity
16+ , deriveOrdTagIdentity
17+ , deriveSomeUniverse
18+ ]
lib/run/obelisk-run.cabal+14−1
@@ -10,6 +10,9 @@ library
1010 attoparsec
1111 , base
1212 , bytestring
13+ , dependent-sum
14+ , dependent-sum-template
15+ , ghcjs-dom
1316 , http-client
1417 , http-reverse-proxy
1518 , http-types
@@ -17,15 +20,25 @@ library
1720 , jsaddle-warp
1821 , lens
1922 , modern-uri
23+ , mtl
2024 , network
25+ , obelisk-backend
2126 , obelisk-executable-config
27+ , obelisk-executable-config-inject
28+ , obelisk-frontend
29+ , obelisk-route
2230 , process
31+ , ref-tf
32+ , reflex
2333 , reflex-dom
34+ , snap-core
2435 , streaming-commons
2536 , text
37+ , universe
2638 , wai
39+ , wai-websockets
2740 , warp
2841 , websockets
2942 exposed-modules:
3043 Obelisk.Run
31− ghc-options: -Wall
44+ ghc-options: -Wall -fwarn-redundant-constraints
lib/run/src/Obelisk/Run.hs+79−28
@@ -1,19 +1,40 @@
1+{-# LANGUAGE ConstraintKinds #-}
2+{-# LANGUAGE EmptyCase #-}
3+{-# LANGUAGE EmptyDataDecls #-}
4+{-# LANGUAGE FlexibleContexts #-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE GADTs #-}
7+{-# LANGUAGE KindSignatures #-}
18{-# LANGUAGE LambdaCase #-}
9+{-# LANGUAGE MultiParamTypeClasses #-}
210{-# LANGUAGE OverloadedStrings #-}
11+{-# LANGUAGE Rank2Types #-}
12+{-# LANGUAGE RecursiveDo #-}
313{-# LANGUAGE ScopedTypeVariables #-}
14+{-# LANGUAGE TemplateHaskell #-}
15+{-# LANGUAGE TypeFamilies #-}
16+{-# LANGUAGE UndecidableInstances #-}
17+{-# OPTIONS_GHC -fno-warn-orphans #-} -- Due to instance HasJS x (EventWriterT t w m)
418module Obelisk.Run where
519
20+import Prelude hiding ((.), id)
21+
22+import Control.Category
623import Control.Concurrent
724import Control.Exception
8−import Control.Lens ((^?), _Just, _Right)
25+import Control.Lens ((%~), (^?), _Just, _Right)
926import qualified Data.Attoparsec.ByteString.Char8 as A
1027import Data.ByteString (ByteString)
1128import qualified Data.ByteString.Char8 as BSC
1229import qualified Data.ByteString.Lazy.Char8 as BSLC
30+import Data.Dependent.Sum (DSum (..))
31+import Data.Functor.Identity
32+import Data.Functor.Sum
1333import Data.List (uncons)
1434import Data.Maybe
1535import Data.Semigroup ((<>))
1636import Data.Streaming.Network (bindPortTCP)
37+import Data.Text (Text)
1738import qualified Data.Text as T
1839import Language.Javascript.JSaddle.Run (syncPoint)
1940import Language.Javascript.JSaddle.WebSockets
@@ -28,6 +49,9 @@ import Network.Wai.Handler.Warp.Internal (settingsHost, settingsPort)
2849import Network.WebSockets (ConnectionOptions)
2950import Network.WebSockets.Connection (defaultConnectionOptions)
3051import Obelisk.ExecutableConfig (get)
52+import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
53+import Obelisk.Frontend
54+import Obelisk.Route.Frontend
3155import Reflex.Dom.Core
3256import System.Environment
3357import System.IO
@@ -35,20 +59,32 @@ import System.Process
3559import Text.URI (URI)
3660import qualified Text.URI as URI
3761import Text.URI.Lens
62+import Obelisk.Backend
3863
39−run :: Int -- ^ Port to run the backend
40− -> IO () -- ^ Backend
41− -> (StaticWidget () (), Widget () ()) -- ^ Frontend widget (head, body)
42− -> IO ()
64+run
65+ :: Int -- ^ Port to run the backend
66+ -> Backend fullRoute frontendRoute -- ^ Backend
67+ -> Frontend (R frontendRoute) -- ^ Frontend
68+ -> IO ()
4369run port backend frontend = do
70+ prettifyOutput
4471 let handleBackendErr (e :: IOException) = hPutStrLn stderr $ "backend stopped; make a change to your code to reload - error " <> show e
45− backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] backend
46− putStrLn $ "Backend running on port " <> show port
47− let conf = defRunConfig { _runConfig_redirectPort = port }
48− runWidget conf frontend `finally` killThread backendTid
72+ --TODO: Use Obelisk.Backend.runBackend; this will require separating the checking and running phases
73+ case checkEncoder $ _backend_routeEncoder backend of
74+ Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
75+ Right validFullEncoder -> do
76+ backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
77+ _backend_run backend $ \serveRoute -> do
78+ runSnapWithCommandLineArgs $ do
79+ getRouteWith validFullEncoder >>= \case
80+ Identity r -> case r of
81+ InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
82+ InR obeliskRoute :=> Identity a -> serveDefaultObeliskApp frontend $ obeliskRoute :/ a
83+ let conf = defRunConfig { _runConfig_redirectPort = port }
84+ runWidget conf frontend validFullEncoder `finally` killThread backendTid
4985
5086getConfigRoute :: IO (Maybe URI)
51−getConfigRoute = get "common/route" >>= \case
87+getConfigRoute = get "config/common/route" >>= \case
5288 Just r -> case URI.mkURI $ T.strip r of
5389 Just route -> pure $ Just route
5490 Nothing -> do
@@ -59,8 +95,8 @@ getConfigRoute = get "common/route" >>= \case
5995defAppUri :: URI
6096defAppUri = fromMaybe (error "defAppUri") $ URI.mkURI "http://127.0.0.1:8000"
6197
62−runWidget :: RunConfig -> (StaticWidget () (), Widget () ()) -> IO ()
63−runWidget conf (h, b) = do
98+runWidget :: RunConfig -> Frontend (R route) -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> IO ()
99+runWidget conf frontend validFullEncoder = do
64100 uri <- fromMaybe defAppUri <$> getConfigRoute
65101 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
66102 redirectHost = _runConfig_redirectHost conf
@@ -73,25 +109,40 @@ runWidget conf (h, b) = do
73109 close
74110 (\skt -> do
75111 man <- newManager defaultManagerSettings
76− app <- obeliskApp defaultConnectionOptions h b (fallbackProxy redirectHost redirectPort man)
112+ app <- obeliskApp defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
77113 runSettingsSocket settings skt app)
78114
79−obeliskApp :: ConnectionOptions -> StaticWidget () () -> Widget () () -> Application -> IO Application
80−obeliskApp opts h b backend = do
81− html <- BSLC.fromStrict <$> indexHtml h
82− let entryPoint = mainWidget' b >> syncPoint
83− jsaddleOr opts entryPoint $ \req sendResponse -> case (W.requestMethod req, W.pathInfo req) of
84− ("GET", []) -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "text/html")] html
85− ("GET", ["jsaddle.js"]) -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] $ jsaddleJs False
86− _ -> backend req sendResponse
115+obeliskApp
116+ :: forall route backendRoute. ConnectionOptions
117+ -> Frontend (R route)
118+ -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
119+ -> URI
120+ -> Application
121+ -> IO Application
122+obeliskApp opts frontend validFullEncoder uri backend = do
123+ let entryPoint = do
124+ runFrontend validFullEncoder frontend
125+ syncPoint
126+ jsaddlePath <- URI.mkPathPiece "jsaddle"
127+ let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
128+ Right (jsaddleWarpRouteValidEncoder :: Encoder Identity (Either Text) (R JSaddleWarpRoute) PageName) <- return $ checkEncoder jsaddleWarpRouteEncoder
129+ jsaddle <- jsaddleWithAppOr opts entryPoint $ \_ sendResponse -> sendResponse $ W.responseLBS H.status500 [("Content-Type", "text/plain")] "obeliskApp: jsaddle got a bad URL"
130+ return $ \req sendResponse -> case tryDecode validFullEncoder (W.pathInfo req, mempty) of --TODO: Query strings
131+ Identity r -> case r of
132+ InR (ObeliskRoute_Resource ResourceRoute_JSaddleWarp) :=> Identity jsaddleRoute -> case jsaddleRoute of
133+ JSaddleWarpRoute_JavaScript :/ () -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] $ jsaddleJs' (Just jsaddleUri) False
134+ _ -> flip jsaddle sendResponse $ req
135+ { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
136+ }
137+ InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
138+ html <- renderJsaddleFrontend (appRouteComponent :/ appRouteRest) frontend
139+ sendResponse $ W.responseLBS H.status200 [("Content-Type", "text/html")] $ BSLC.fromStrict html
140+ _ -> backend req sendResponse
87141
88−indexHtml :: StaticWidget () () -> IO ByteString
89−indexHtml h = do
90− ((), bs) <- renderStatic $ el "html" $ do
91− el "head" $ h
92− el "body" $ return ()
93− elAttr "script" ("src" =: "/jsaddle.js") $ return ()
94− return $ "<!DOCTYPE html>" <> bs
142+renderJsaddleFrontend :: route -> Frontend route -> IO ByteString
143+renderJsaddleFrontend r f =
144+ let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
145+ in renderFrontendHtml r (_frontend_head f >> injectExecutableConfigs) (_frontend_body f >> jsaddleScript)
95146
96147-- | like 'bindPortTCP' but reconnects on exception
97148bindPortTCPRetry :: Settings
lib/selftest/obelisk-selftest.cabal+2−1
@@ -6,7 +6,6 @@ build-type: Simple
66cabal-version: >=1.8
77
88library
9− default-language: Haskell2010
109 hs-source-dirs: src
1110 build-depends:
1211 base
@@ -16,6 +15,7 @@ library
1615 , http-client
1716 , http-types
1817 , HUnit
18+ , obelisk-cliapp
1919 , obelisk-run
2020 , process
2121 , network
@@ -24,6 +24,7 @@ library
2424 , text
2525 exposed-modules:
2626 Obelisk.SelfTest
27+ ghc-options: -Wall
2728
2829executable obelisk-selftest
2930 main-is: src-bin/obelisk-selftest.hs
lib/selftest/src/Obelisk/SelfTest.hs+68−40
@@ -1,14 +1,15 @@
11{-# LANGUAGE LambdaCase #-}
22{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE PatternGuards #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
35{-# LANGUAGE TypeApplications #-}
46module Obelisk.SelfTest where
57
6−import Control.Exception (Exception, throw, bracket)
8+import Control.Exception (bracket, throw)
79import Control.Monad
810import Control.Monad.IO.Class
911import Data.Bool (bool)
1012import Data.Function (fix)
11−import Data.Maybe (isJust)
1213import Data.Semigroup (Semigroup, (<>))
1314import qualified Data.Set as Set
1415import Data.String
@@ -21,15 +22,17 @@ import qualified Network.Socket as Socket
2122import Shelly
2223import System.Directory (withCurrentDirectory)
2324import System.Environment
24−import System.Exit (ExitCode (..), exitWith)
25+import System.Exit (ExitCode (..))
2526import System.Info
2627import System.IO (Handle, hClose)
2728import System.IO.Temp
29+import System.Process (readProcessWithExitCode)
2830import System.Timeout
2931import Test.Hspec
3032import Test.HUnit.Base
31−import System.Process (readProcessWithExitCode)
3233
34+import Obelisk.CliApp hiding (runCli)
35+import qualified Obelisk.CliApp as CliApp
3336import Obelisk.Run (getConfigRoute)
3437
3538data ObRunState
@@ -50,18 +53,44 @@ commit msg = void $ run "git"
5053 , doubleQuotes msg
5154 ]
5255
56+runCli :: MonadIO m => CliT IO a -> m a
57+runCli f = liftIO $ do
58+ c <- newCliConfig Notice False False
59+ CliApp.runCli c f
60+
61+tshow :: Show a => a -> Text
62+tshow = T.pack . show
63+
64+-- | Like `shelly` but used when running `ob` commands
65+--
66+-- This tests the NIX_REMOTE workaround, whether that gets printed
67+-- on failure, and whether retrying with the suggested workaround
68+-- leads to the test succeeding.
69+shellyOb :: MonadIO m => (Sh a -> Sh a) -> Sh a -> m a
70+shellyOb f obTest = shelly $ f $ do
71+ obTest `catch_sh` \(e :: RunFailed) -> do
72+ obStderr <- lastStderr
73+ if T.isInfixOf "export NIX_REMOTE" obStderr
74+ then do
75+ runCli $ do
76+ putLog Notice "Detected NIX_REMOTE suggestion in stderr of failed ob"
77+ putLog Warning "Retrying this test with NIX_REMOTE=daemon"
78+ setenv "NIX_REMOTE" "daemon"
79+ f obTest
80+ else throw e
81+
5382main :: IO ()
5483main = do
55− isVerbose <- getArgs >>= \case
56− ["-v"] -> pure True
57− [] -> putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output" *> pure False
58− _ -> putStrLn "Usage: selftest [-v]" *> exitWith (ExitFailure 2)
84+ -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
85+ isVerbose <- (elem "-v") <$> getArgs
86+ unless isVerbose $
87+ putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
5988 let verbosity = bool silently verbosely isVerbose
6089 obeliskImpl <- fromString <$> getEnv "OBELISK_IMPL"
6190 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
6291 withSystemTempDirectory "blank-project" $ \blankProject ->
6392 hspec $ do
64− let shelly_ = void . shelly . verbosity
93+ let shelly_ = void . shellyOb verbosity
6594
6695 inProj :: Sh a -> IO ()
6796 inProj = shelly_ . chdir (fromString blankProject)
@@ -75,7 +104,7 @@ main = do
75104 revParseHead = T.strip <$> run "git" ["rev-parse", "HEAD"]
76105
77106 commitAll = do
78− run "git" ["add", "."]
107+ run_ "git" ["add", "."]
79108 commit "checkpoint"
80109 revParseHead
81110
@@ -92,31 +121,32 @@ main = do
92121 it "works with symlink" $ inTmp $ \_ -> run "ob" ["init", "--symlink", obeliskImpl]
93122
94123 it "doesn't create anything when given an invalid impl" $ inTmp $ \tmp -> do
95− errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
124+ void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
96125 ls tmp >>= liftIO . assertEqual "" []
97126
98127 it "produces a valid route config" $ inTmp $ \tmp -> do
99− run "ob" ["init"]
128+ run_ "ob" ["init"]
100129 liftIO $ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ getConfigRoute `shouldNotReturn` Nothing
101130
102131 describe "ob run" $ do
103132 it "works in root directory" $ inTmp $ \_ -> do
104− run "ob" ["init"]
133+ run_ "ob" ["init"]
105134 testObRunInDir Nothing httpManager
106135 it "works in sub directory" $ inTmp $ \_ -> do
107− run "ob" ["init"]
136+ run_ "ob" ["init"]
108137 testObRunInDir (Just "frontend") httpManager
109138
110139 describe "obelisk project" $ parallel $ do
111140 it "can build obelisk command" $ shelly_ $ run "nix-build" ["-A", "command" , obeliskImpl]
112141 it "can build obelisk skeleton" $ shelly_ $ run "nix-build" ["-A", "skeleton", obeliskImpl]
113142 it "can build obelisk shell" $ shelly_ $ run "nix-build" ["-A", "shell", obeliskImpl]
114− it "can build everything" $ shelly_ $ run "nix-build" [obeliskImpl]
143+ -- See https://github.com/obsidiansystems/obelisk/issues/101
144+ -- it "can build everything" $ shelly_ $ run "nix-build" [obeliskImpl]
115145
116146 describe "blank initialized project" $ do
117147 it "can be created" $ inProj $ do
118− run "ob" ["init"]
119− run "git" ["init"]
148+ run_ "ob" ["init"]
149+ run_ "git" ["init"]
120150 commitAll
121151
122152 it "can build ghc.backend" $ inProj $ do
@@ -131,7 +161,7 @@ main = do
131161 forM_ ["ghc", "ghcjs"] $ \compiler -> do
132162 let
133163 shell = "shells." <> compiler
134− inShell cmd = run "nix-shell" ["-A", fromString shell, "--run", cmd]
164+ inShell cmd' = run "nix-shell" ["-A", fromString shell, "--run", cmd']
135165 it ("can enter " <> shell) $ inProj $ inShell "exit"
136166 it ("can build in " <> shell) $ inProj $ inShell $ "cabal new-build --" <> fromString compiler <> " all"
137167
@@ -156,15 +186,15 @@ main = do
156186 assertRevNE e eu
157187
158188 it "can pack and unpack plain git repos" $ do
159− withSystemTempDirectory "git-repo" $ \dir -> shelly_ $ do
189+ shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
160190 let repo = toTextIgnore $ dir </> ("repo" :: String)
161− run_ "git" ["clone", "https://git.haskell.org/packages/primitive.git", repo]
191+ run_ "git" ["clone", "https://github.com/haskell/process.git", repo]
162192 origHash <- chdir (fromText repo) revParseHead
163193
164194 run_ "ob" ["thunk", "pack", repo]
165195 packedFiles <- Set.fromList <$> ls (fromText repo)
166− liftIO $ assertEqual "" packedFiles $
167− Set.fromList $ (repo </>) <$> ["default.nix", "git.json", ".attr-cache" :: String]
196+ liftIO $ assertEqual "" packedFiles $ Set.fromList $ (repo </>) <$>
197+ ["default.nix", "github.json", ".attr-cache" :: String]
168198
169199 run_ "ob" ["thunk", "unpack", repo]
170200 chdir (fromText repo) $ do
@@ -174,7 +204,7 @@ main = do
174204 testThunkPack $ fromText repo
175205
176206 it "aborts thunk pack when there are uncommitted files" $ inProj $ do
177− unpack
207+ void $ unpack
178208 testThunkPack (blankProject </> thunk)
179209
180210-- | Run `ob run` in the given directory (maximum of one level deep)
@@ -191,33 +221,31 @@ testObRunInDir mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e ->
191221 maybe id (\_ -> chdir "..") mdir $ alterRouteTo newUri stdout
192222 (_, runningUri) <- handleObRunStdout httpManager stdout
193223 if runningUri /= newUri
194− then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
195− else exit 0
224+ then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
225+ else exit 0
196226
197227testThunkPack :: Shelly.FilePath -> Sh ()
198−testThunkPack path = withTempFile (T.unpack $ toTextIgnore path) "test-file" $ \file handle -> do
199− let try_sh :: Exception e => Sh a -> Sh (Either e a)
200− try_sh a = catch_sh (a >>= pure . Right) (pure . Left)
201− pack' = readProcessWithExitCode "ob" ["thunk", "pack", T.unpack $ toTextIgnore path] ""
202− ensureThunkPackFails err = liftIO $ pack' >>= \case
203− (code, out, _err)
228+testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
229+ let pack' = readProcessWithExitCode "ob" ["thunk", "pack", T.unpack $ toTextIgnore path'] ""
230+ ensureThunkPackFails q = liftIO $ pack' >>= \case
231+ (code, out, err)
204232 | code == ExitSuccess -> fail "ob thunk pack succeeded when it should have failed"
205− | err `T.isInfixOf` T.pack out -> pure ()
206− | otherwise -> fail $ "ob thunk pack failed for an unexpected reason: " <> show out
207− git = chdir path . run "git"
233+ | q `T.isInfixOf` T.pack (out <> err) -> pure ()
234+ | otherwise -> fail $ "ob thunk pack failed for an unexpected reason: " <> show out <> "\nstderr: " <> err
235+ git = chdir path' . run "git"
208236 -- Untracked files
209− ensureThunkPackFails "unsaved modifications"
210− git ["add", T.pack file]
237+ ensureThunkPackFails "Untracked files"
238+ void $ git ["add", T.pack file]
211239 -- Uncommitted files (staged)
212− ensureThunkPackFails "unsaved modifications"
213− chdir path $ commit "test commit"
240+ ensureThunkPackFails "unsaved"
241+ chdir path' $ commit "test commit"
214242 -- Non-pushed commits in any branch
215243 ensureThunkPackFails "not been pushed"
216244 -- Uncommitted files (unstaged)
217245 liftIO $ T.hPutStrLn handle "test file" >> hClose handle
218− ensureThunkPackFails "unsaved modifications"
246+ ensureThunkPackFails "modified"
219247 -- Existing stashes
220− git ["stash"]
248+ void $ git ["stash"]
221249 ensureThunkPackFails "has stashes"
222250
223251-- | Blocks until a non-empty line is available
migration/001efbbe866b66eebc6ee1ea0a906f2d-b0c1d039514a686529561f186332c91e/obelisk-handoffnew file
No changes to the content.
migration/001efbbe866b66eebc6ee1ea0a906f2d-b0c1d039514a686529561f186332c91e/obelisk-upgradenew file
No changes to the content.
migration/00a480749e96ef7e21fbe8f4943152b1-abdb013baf130e1bd7e7fd04e535c44c/obelisk-handoffnew file
No changes to the content.
migration/00a480749e96ef7e21fbe8f4943152b1-abdb013baf130e1bd7e7fd04e535c44c/obelisk-upgradenew file
No changes to the content.
migration/00bee4377690b5040bd5b3702dce2a88-98629a336dcd5fa3524a09f8ea6ad636/obelisk-handoffnew file
No changes to the content.
migration/00bee4377690b5040bd5b3702dce2a88-98629a336dcd5fa3524a09f8ea6ad636/obelisk-upgradenew file
No changes to the content.
migration/00c627540326314c7154cc209e8a71e9-67bfa5b784a35e66875fb74ad84a38c0/obelisk-handoffnew file
No changes to the content.
migration/00c627540326314c7154cc209e8a71e9-67bfa5b784a35e66875fb74ad84a38c0/obelisk-upgradenew file
No changes to the content.
migration/01c0ee6c93a3f9f69e097c41169927ac-2913c440c7ad7731aae96e60a86bf7a8/obelisk-handoffnew file
No changes to the content.
migration/01c0ee6c93a3f9f69e097c41169927ac-2913c440c7ad7731aae96e60a86bf7a8/obelisk-upgradenew file
No changes to the content.
migration/01ddc6354698efa1ae5822da3be562dc-a536d01b9c489f5e00e30abf27427da0/obelisk-handoffnew file
No changes to the content.
migration/01ddc6354698efa1ae5822da3be562dc-a536d01b9c489f5e00e30abf27427da0/obelisk-upgradenew file
No changes to the content.
migration/01fb7610e4184ad1b491267fcb0c7f43-c0c1452986b8d4c181bb0c6c83c56a2f/obelisk-handoffnew file
No changes to the content.
migration/01fb7610e4184ad1b491267fcb0c7f43-c0c1452986b8d4c181bb0c6c83c56a2f/obelisk-upgradenew file
No changes to the content.
migration/0298e61f6a03e553df08dcef03e27bd1-06d184d1fb27966294a8fee1c7df7cb6/obelisk-handoffnew file
No changes to the content.
migration/0298e61f6a03e553df08dcef03e27bd1-06d184d1fb27966294a8fee1c7df7cb6/obelisk-upgradenew file
No changes to the content.
migration/02d0f8b5c7472e6f8a99ec2cfcb59807-5b6770965bbef440db49b0703cd243a3/obelisk-handoffnew file
No changes to the content.
migration/02d0f8b5c7472e6f8a99ec2cfcb59807-5b6770965bbef440db49b0703cd243a3/obelisk-upgradenew file
No changes to the content.
migration/02f532fd8bb64a167a5349c2ba8e3331-1088e2464720e12067dd74683546b132/obelisk-handoffnew file
No changes to the content.
migration/02f532fd8bb64a167a5349c2ba8e3331-1088e2464720e12067dd74683546b132/obelisk-upgradenew file
No changes to the content.
migration/02fb88dab35884e09deef1721c1cfc13-ae824b83b569b3b23b8e198190371ca0/obelisk-handoffnew file
No changes to the content.
migration/02fb88dab35884e09deef1721c1cfc13-ae824b83b569b3b23b8e198190371ca0/obelisk-upgradenew file
No changes to the content.
migration/030ed18dacf3571cbb9e3e7d9d99627b-d37d70fe5d7d11e68b59ce12022281c9/obelisk-handoffnew file
No changes to the content.
migration/030ed18dacf3571cbb9e3e7d9d99627b-d37d70fe5d7d11e68b59ce12022281c9/obelisk-upgradenew file
No changes to the content.
migration/03b845f308e73a1567eb843dfca182d2-e9ec84bfe7099b713c0fda32308f5815/obelisk-handoffnew file
No changes to the content.
migration/03b845f308e73a1567eb843dfca182d2-e9ec84bfe7099b713c0fda32308f5815/obelisk-upgradenew file
No changes to the content.
migration/03b9747848d7f3e995b118f776ef96c7-3da702ef7f531ad6db874ac3f85e9c06/obelisk-handoffnew file+1
migration/03b9747848d7f3e995b118f776ef96c7-3da702ef7f531ad6db874ac3f85e9c06/obelisk-upgradenew file
No changes to the content.
migration/04a5ae41e8fcef5f719cb84d01430382-f391c6a74ee14217ce4565c278fceed6/obelisk-handoffnew file
No changes to the content.
migration/04a5ae41e8fcef5f719cb84d01430382-f391c6a74ee14217ce4565c278fceed6/obelisk-upgradenew file
No changes to the content.
migration/04dd95ecb889ac1b0a616ddfeec781c4-f790ac9400501511b4ee045e22bc5fe1/obelisk-handoffnew file
No changes to the content.
migration/04dd95ecb889ac1b0a616ddfeec781c4-f790ac9400501511b4ee045e22bc5fe1/obelisk-upgradenew file
No changes to the content.
migration/0535e06dd0d61f3e6970b491a17cf1f8-28a6ada033c86e5ae914cc31cecb6239/obelisk-handoffnew file
No changes to the content.
migration/0535e06dd0d61f3e6970b491a17cf1f8-28a6ada033c86e5ae914cc31cecb6239/obelisk-upgradenew file
No changes to the content.
migration/053c0b2f84a7a2eadeb5c3f9575ffa0d-2600ab2ec96d3177d2a6b3462e09862d/obelisk-handoffnew file
No changes to the content.
migration/053c0b2f84a7a2eadeb5c3f9575ffa0d-2600ab2ec96d3177d2a6b3462e09862d/obelisk-upgradenew file
No changes to the content.
migration/063665b78b6705e7de952f73fa9797bf-172e629829334d2b33cda8d3b5e0b148/obelisk-handoffnew file
No changes to the content.
migration/063665b78b6705e7de952f73fa9797bf-172e629829334d2b33cda8d3b5e0b148/obelisk-upgradenew file
No changes to the content.
migration/06d184d1fb27966294a8fee1c7df7cb6-b1692927da5ed59af72fac5be8094fa2/obelisk-handoffnew file
No changes to the content.
migration/06d184d1fb27966294a8fee1c7df7cb6-b1692927da5ed59af72fac5be8094fa2/obelisk-upgradenew file
No changes to the content.
migration/071f5b3326b8886de3eddc927b80eb10-28e25b7a8812869a709579cd34e7bcb4/obelisk-handoffnew file
No changes to the content.
migration/071f5b3326b8886de3eddc927b80eb10-28e25b7a8812869a709579cd34e7bcb4/obelisk-upgradenew file
No changes to the content.
migration/07900e6c41ab9a78917f66fcf7e0d9b8-554c459ff2cced3e1b11241ed403890d/obelisk-handoffnew file
No changes to the content.
migration/07900e6c41ab9a78917f66fcf7e0d9b8-554c459ff2cced3e1b11241ed403890d/obelisk-upgradenew file
No changes to the content.
migration/07900e6c41ab9a78917f66fcf7e0d9b8-8c84f81d9b09a5e0f028f3c6341ffb36/obelisk-handoffnew file
No changes to the content.
migration/07900e6c41ab9a78917f66fcf7e0d9b8-8c84f81d9b09a5e0f028f3c6341ffb36/obelisk-upgradenew file+15
@@ -0,0 +1,15 @@
1+# ob deploy: HTTPs support
2+
3+Obelisk deployments now support HTTPs. You must upgrade the deployment repo
4+associated with the obelisk project as follows:
5+
6+```
7+cd /your/project's/deployment/repo
8+echo True > enable_https
9+echo admin@yoursite.com > admin_email
10+echo https://yoursite.com/ > config/common/route
11+git add ... && git commit ...
12+ob deploy push
13+```
14+
15+If you do not wish to enable HTTPs, write `False` to `enable_https` instead.
migration/0804ef73b54a4dc7fbde18147f33b4f9-00bee4377690b5040bd5b3702dce2a88/obelisk-handoffnew file
No changes to the content.
migration/0804ef73b54a4dc7fbde18147f33b4f9-00bee4377690b5040bd5b3702dce2a88/obelisk-upgradenew file
No changes to the content.
migration/08dbdf23867a8efa680283ad251d3429-f2a9ba4a93c3a04acbee2211bca9e27a/obelisk-handoffnew file
No changes to the content.
migration/08dbdf23867a8efa680283ad251d3429-f2a9ba4a93c3a04acbee2211bca9e27a/obelisk-upgradenew file
No changes to the content.
migration/0abd9d31c77be8c9a9e83bf1f73276f6-fc42c5fc24031fc6a8836a10cfb45143/obelisk-handoffnew file
No changes to the content.
migration/0abd9d31c77be8c9a9e83bf1f73276f6-fc42c5fc24031fc6a8836a10cfb45143/obelisk-upgradenew file
No changes to the content.
migration/0b2cab546a1526414096980fca85d0d3-99aebbfcac51725464bc59571b7d71b8/obelisk-handoffnew file
No changes to the content.
migration/0b2cab546a1526414096980fca85d0d3-99aebbfcac51725464bc59571b7d71b8/obelisk-upgradenew file
No changes to the content.
migration/0c6127453069ad707e34ddfedb67e2f9-7c98ef4a123c1a580d368d14e54961b7/obelisk-handoffnew file
No changes to the content.
migration/0c6127453069ad707e34ddfedb67e2f9-7c98ef4a123c1a580d368d14e54961b7/obelisk-upgradenew file
No changes to the content.
migration/0c9826e42d5d4fa9988c7db53722826e-c56de2f95c90def16704f62639246bbc/obelisk-handoffnew file
No changes to the content.
migration/0c9826e42d5d4fa9988c7db53722826e-c56de2f95c90def16704f62639246bbc/obelisk-upgradenew file
No changes to the content.
migration/0c991bc3c06801f72df48486ee78e350-58479e482d3828ce44f2c8371ed9135a/obelisk-handoffnew file
No changes to the content.
migration/0c991bc3c06801f72df48486ee78e350-58479e482d3828ce44f2c8371ed9135a/obelisk-upgradenew file
No changes to the content.
migration/0d3b4cfe9b4c72b830780a4c44942176-214979923f2d7e6278856493c1978fdc/obelisk-handoffnew file
No changes to the content.
migration/0d3b4cfe9b4c72b830780a4c44942176-214979923f2d7e6278856493c1978fdc/obelisk-upgradenew file
No changes to the content.
migration/0da1150f0e9fe57d1fafbb6cabe24fa1-a632452cf9c5956becd1a40512472b43/obelisk-handoffnew file
No changes to the content.
migration/0da1150f0e9fe57d1fafbb6cabe24fa1-a632452cf9c5956becd1a40512472b43/obelisk-upgradenew file
No changes to the content.
migration/0e4d0f0f0af0925474b0d772be2b145f-b82e9c3fe22feff4bb3a6be49943fd6c/obelisk-handoffnew file
No changes to the content.
migration/0e4d0f0f0af0925474b0d772be2b145f-b82e9c3fe22feff4bb3a6be49943fd6c/obelisk-upgradenew file
No changes to the content.
migration/0ec6f67d94d3913cf3243d813238ec39-0804ef73b54a4dc7fbde18147f33b4f9/obelisk-handoffnew file
No changes to the content.
migration/0ec6f67d94d3913cf3243d813238ec39-0804ef73b54a4dc7fbde18147f33b4f9/obelisk-upgradenew file
No changes to the content.
migration/0f78d9d6875933336bbf0c1c8888b73f-d1b3f5f5375558bf91d55c88547784b9/obelisk-handoffnew file
No changes to the content.
migration/0f78d9d6875933336bbf0c1c8888b73f-d1b3f5f5375558bf91d55c88547784b9/obelisk-upgradenew file
No changes to the content.
migration/0fb3a912a8aaf5ef6f734a25f0bce7e2-cccf4140dc336263489bcf67c8c58f86/obelisk-handoffnew file
No changes to the content.
migration/0fb3a912a8aaf5ef6f734a25f0bce7e2-cccf4140dc336263489bcf67c8c58f86/obelisk-upgradenew file
No changes to the content.
migration/0fba399e046852d4ecf8e1dad831a2f9-6620e6dc5622593e5f671127af9ade05/obelisk-handoffnew file
No changes to the content.
migration/0fba399e046852d4ecf8e1dad831a2f9-6620e6dc5622593e5f671127af9ade05/obelisk-upgradenew file
No changes to the content.
migration/0fe60df438daa8a2cca5f2b569e361a5-2e3e85cc59aa31f24f18c027542c0510/obelisk-handoffnew file
No changes to the content.
migration/0fe60df438daa8a2cca5f2b569e361a5-2e3e85cc59aa31f24f18c027542c0510/obelisk-upgradenew file
No changes to the content.
migration/1088e2464720e12067dd74683546b132-b2282679f6f63cec89d921c3dd512d1e/obelisk-handoffnew file
No changes to the content.
migration/1088e2464720e12067dd74683546b132-b2282679f6f63cec89d921c3dd512d1e/obelisk-upgradenew file
No changes to the content.
migration/11a5917ea91e021f1fa541a2fe158c80-d8adb57b2f2266e128ebb6f6d7e65920/obelisk-handoffnew file
No changes to the content.
migration/11a5917ea91e021f1fa541a2fe158c80-d8adb57b2f2266e128ebb6f6d7e65920/obelisk-upgradenew file
No changes to the content.
migration/11f38b38c2d38004bb8a9dbfe2ef3e4a-29d9b8b8ae9698888f3ea90b10ff5e92/obelisk-handoffnew file
No changes to the content.
migration/11f38b38c2d38004bb8a9dbfe2ef3e4a-29d9b8b8ae9698888f3ea90b10ff5e92/obelisk-upgradenew file
No changes to the content.
migration/1269155fffe21926f3704d5077e04332-1a97559d049bf85cebb762b7e7b70db1/obelisk-handoffnew file
No changes to the content.
migration/1269155fffe21926f3704d5077e04332-1a97559d049bf85cebb762b7e7b70db1/obelisk-upgradenew file
No changes to the content.
migration/12c8a24c34c2d8441f3181d179c2baab-bd6728737dffcc3505d30ada344e2f4b/obelisk-handoffnew file
No changes to the content.
migration/12c8a24c34c2d8441f3181d179c2baab-bd6728737dffcc3505d30ada344e2f4b/obelisk-upgradenew file
No changes to the content.
migration/131e1d5cc8163ce9a174a8782be23fe7-e323ce1352e512109f1609276fb509b4/obelisk-handoffnew file
No changes to the content.
migration/131e1d5cc8163ce9a174a8782be23fe7-e323ce1352e512109f1609276fb509b4/obelisk-upgradenew file
No changes to the content.
migration/13f6767460e94079cdfa6d5e1df79ecb-c106f9398d5c203a1a296b85c7411bf9/obelisk-handoffnew file
No changes to the content.
migration/13f6767460e94079cdfa6d5e1df79ecb-c106f9398d5c203a1a296b85c7411bf9/obelisk-upgradenew file
No changes to the content.
migration/145650b66e83adf5bdb8261593076379-e2be519915794ef130249f2be7640382/obelisk-handoffnew file
No changes to the content.
migration/145650b66e83adf5bdb8261593076379-e2be519915794ef130249f2be7640382/obelisk-upgradenew file
No changes to the content.
migration/14840816db4b5cfd5580ae099022604a-aaf979dd0121603a8b48127e1ea47c7d/obelisk-handoffnew file
No changes to the content.
migration/14840816db4b5cfd5580ae099022604a-aaf979dd0121603a8b48127e1ea47c7d/obelisk-upgradenew file
No changes to the content.
migration/15f3f3f517b38664131b85da0594522f-9715967793be68dfd8c9a7d631f8d589/obelisk-handoffnew file
No changes to the content.
migration/15f3f3f517b38664131b85da0594522f-9715967793be68dfd8c9a7d631f8d589/obelisk-upgradenew file
No changes to the content.
migration/1629d0d70e360df6b2280fac36d21e8a-a825fbb74e8d050a5ce10ec4c62b83f5/obelisk-handoffnew file
No changes to the content.
migration/1629d0d70e360df6b2280fac36d21e8a-a825fbb74e8d050a5ce10ec4c62b83f5/obelisk-upgradenew file
No changes to the content.
migration/166c5f8e4a0f1dfd227b075a96237386-a5904e3a899e346fe2fe26314f69c075/obelisk-handoffnew file
No changes to the content.
migration/166c5f8e4a0f1dfd227b075a96237386-a5904e3a899e346fe2fe26314f69c075/obelisk-upgradenew file
No changes to the content.
migration/16b24e1a12b93ea83d7656a09d5e3338-8a64ec5f2ee43d69607499f085560857/obelisk-handoffnew file
No changes to the content.
migration/16b24e1a12b93ea83d7656a09d5e3338-8a64ec5f2ee43d69607499f085560857/obelisk-upgradenew file
No changes to the content.
migration/17089ea0edc0cf4b6f616936f8a06174-a34d758508a0a5d08bc10e76482c4d11/obelisk-handoffnew file
No changes to the content.
migration/17089ea0edc0cf4b6f616936f8a06174-a34d758508a0a5d08bc10e76482c4d11/obelisk-upgradenew file
No changes to the content.
migration/172e629829334d2b33cda8d3b5e0b148-a808a04519bbf2874e41028ded9c180c/obelisk-handoffnew file
No changes to the content.
migration/172e629829334d2b33cda8d3b5e0b148-a808a04519bbf2874e41028ded9c180c/obelisk-upgradenew file
No changes to the content.
migration/174fa1072ee767be39b7b01aecea5714-6566f0555737425bae9106d1a4a0a2dd/obelisk-handoffnew file
No changes to the content.
migration/174fa1072ee767be39b7b01aecea5714-6566f0555737425bae9106d1a4a0a2dd/obelisk-upgradenew file
No changes to the content.
migration/1859cc62bb2f89f528bb2aa170c36e84-be5a7b436cef0ae35cb074700d5f0d00/obelisk-handoffnew file
No changes to the content.
migration/1859cc62bb2f89f528bb2aa170c36e84-be5a7b436cef0ae35cb074700d5f0d00/obelisk-upgradenew file
No changes to the content.
migration/190460dabd2b9773c3fe81902d0c8a62-e7385c9eae2b807a483b73be4295b207/obelisk-handoffnew file
No changes to the content.
migration/190460dabd2b9773c3fe81902d0c8a62-e7385c9eae2b807a483b73be4295b207/obelisk-upgradenew file
No changes to the content.
migration/1a08b154ae5f898992128e229090c4d8-4e95e2f8ef50420371b71022c644e25d/obelisk-handoffnew file
No changes to the content.
migration/1a08b154ae5f898992128e229090c4d8-4e95e2f8ef50420371b71022c644e25d/obelisk-upgradenew file
No changes to the content.
migration/1a97559d049bf85cebb762b7e7b70db1-5cb98d0250bf5224b94e544a5140a54c/obelisk-handoffnew file
No changes to the content.
migration/1a97559d049bf85cebb762b7e7b70db1-5cb98d0250bf5224b94e544a5140a54c/obelisk-upgradenew file
No changes to the content.
migration/1ada2d50d7847a12dbe29df4097b5bdc-11a5917ea91e021f1fa541a2fe158c80/obelisk-handoffnew file
No changes to the content.
migration/1ada2d50d7847a12dbe29df4097b5bdc-11a5917ea91e021f1fa541a2fe158c80/obelisk-upgradenew file
No changes to the content.
migration/1b1686b03b41bd840ff46776cec7b33a-31a676fd9e1f69bd07380480ec879da0/obelisk-handoffnew file
No changes to the content.
migration/1b1686b03b41bd840ff46776cec7b33a-31a676fd9e1f69bd07380480ec879da0/obelisk-upgradenew file
No changes to the content.
migration/1c1c3f2b4426d5d26377eef08dbccb3c-29abdd56a2dbbf3a2a0715210d441d06/obelisk-handoffnew file
No changes to the content.
migration/1c1c3f2b4426d5d26377eef08dbccb3c-29abdd56a2dbbf3a2a0715210d441d06/obelisk-upgradenew file
No changes to the content.
migration/1c2c93819c07f33ad899e39ec6769c3c-909b6b2066cbdc991e6c16455f13637c/obelisk-handoffnew file
No changes to the content.
migration/1c2c93819c07f33ad899e39ec6769c3c-909b6b2066cbdc991e6c16455f13637c/obelisk-upgradenew file
No changes to the content.
migration/1c4fc23cbf5653613c6060668e065f37-5b252c24612c2ba868580add9d9c19e3/obelisk-handoffnew file
No changes to the content.
migration/1c4fc23cbf5653613c6060668e065f37-5b252c24612c2ba868580add9d9c19e3/obelisk-upgradenew file
No changes to the content.
migration/1c9bdb5c544f2e5432f549653a61dabe-20926beb4c30e2efa819d23e5694645e/obelisk-handoffnew file
No changes to the content.
migration/1c9bdb5c544f2e5432f549653a61dabe-20926beb4c30e2efa819d23e5694645e/obelisk-upgradenew file
No changes to the content.
migration/1cc8d98f6f61441570fdde2d697e6e3c-69fafe6ab155ff215b6d9d330f700ba1/obelisk-handoffnew file
No changes to the content.
migration/1cc8d98f6f61441570fdde2d697e6e3c-69fafe6ab155ff215b6d9d330f700ba1/obelisk-upgradenew file
No changes to the content.
migration/1d084e187591225f0e8fa6fa824f8863-02f532fd8bb64a167a5349c2ba8e3331/obelisk-handoffnew file
No changes to the content.
migration/1d084e187591225f0e8fa6fa824f8863-02f532fd8bb64a167a5349c2ba8e3331/obelisk-upgradenew file
No changes to the content.
migration/1df871cede0758cabe43971d9091279d-0b2cab546a1526414096980fca85d0d3/obelisk-handoffnew file
No changes to the content.
migration/1df871cede0758cabe43971d9091279d-0b2cab546a1526414096980fca85d0d3/obelisk-upgradenew file
No changes to the content.
migration/1e36dc8817e2784df36d643b7499092d-ba0ccd5ef6110f6c5133d9d6ea6277f0/obelisk-handoffnew file
No changes to the content.
migration/1e36dc8817e2784df36d643b7499092d-ba0ccd5ef6110f6c5133d9d6ea6277f0/obelisk-upgradenew file
No changes to the content.
migration/1e376437f069662df26178e4a7873190-ca5ee8f372ecb07b09283081f0298733/obelisk-handoffnew file
No changes to the content.
migration/1e376437f069662df26178e4a7873190-ca5ee8f372ecb07b09283081f0298733/obelisk-upgradenew file
No changes to the content.
migration/1f09fcf196bfe90ed52f113c38ffbfdb-d16784e9a2d93db7f953941e3003fa8c/obelisk-handoffnew file
No changes to the content.
migration/1f09fcf196bfe90ed52f113c38ffbfdb-d16784e9a2d93db7f953941e3003fa8c/obelisk-upgradenew file
No changes to the content.
migration/1f5b6e2a19c87d3820b953f942ab4a27-8b098f49e1fd1aa10ca22b9dc312644e/obelisk-handoffnew file
No changes to the content.
migration/1f5b6e2a19c87d3820b953f942ab4a27-8b098f49e1fd1aa10ca22b9dc312644e/obelisk-upgradenew file
No changes to the content.
migration/1f7fb90cb1e558f5285b2d19a25a1078-5a98cdb6c195e88beac878a3f311c5b5/obelisk-handoffnew file
No changes to the content.
migration/1f7fb90cb1e558f5285b2d19a25a1078-5a98cdb6c195e88beac878a3f311c5b5/obelisk-upgradenew file
No changes to the content.
migration/1fe00204f9d2cd653ff2894eae3d9356-4b3f45ce8aeb17ef18d1d9502b74bb7e/obelisk-handoffnew file
No changes to the content.
migration/1fe00204f9d2cd653ff2894eae3d9356-4b3f45ce8aeb17ef18d1d9502b74bb7e/obelisk-upgradenew file
No changes to the content.
migration/20926beb4c30e2efa819d23e5694645e-212ead9571b83b8164b3f18d2537d52b/obelisk-handoffnew file
No changes to the content.
migration/20926beb4c30e2efa819d23e5694645e-212ead9571b83b8164b3f18d2537d52b/obelisk-upgradenew file
No changes to the content.
migration/212ead9571b83b8164b3f18d2537d52b-3bf8e28473fb05791c5790f8906e2609/obelisk-handoffnew file
No changes to the content.
migration/212ead9571b83b8164b3f18d2537d52b-3bf8e28473fb05791c5790f8906e2609/obelisk-upgradenew file
No changes to the content.
migration/214979923f2d7e6278856493c1978fdc-13f6767460e94079cdfa6d5e1df79ecb/obelisk-handoffnew file
No changes to the content.
migration/214979923f2d7e6278856493c1978fdc-13f6767460e94079cdfa6d5e1df79ecb/obelisk-upgradenew file
No changes to the content.
migration/215b6810b09f219a5cb3842aacc88432-2ef4f9cce6d15ae62d5112d3c224eb7a/obelisk-handoffnew file
No changes to the content.
migration/215b6810b09f219a5cb3842aacc88432-2ef4f9cce6d15ae62d5112d3c224eb7a/obelisk-upgradenew file
No changes to the content.
migration/2164344be67da5d0e57052c15f30646d-0c9826e42d5d4fa9988c7db53722826e/obelisk-handoffnew file
No changes to the content.
migration/2164344be67da5d0e57052c15f30646d-0c9826e42d5d4fa9988c7db53722826e/obelisk-upgradenew file
No changes to the content.
migration/237aeb7eee0ffed230234ee04e49569f-55cf43fb0d3868040c9efbf2f92d538a/obelisk-handoffnew file
No changes to the content.
migration/237aeb7eee0ffed230234ee04e49569f-55cf43fb0d3868040c9efbf2f92d538a/obelisk-upgradenew file
No changes to the content.
migration/23af4d9671962484de53f537a0626d83-7ce32302cfbba8b7567a05144057f8a3/obelisk-handoffnew file
No changes to the content.
migration/23af4d9671962484de53f537a0626d83-7ce32302cfbba8b7567a05144057f8a3/obelisk-upgradenew file
No changes to the content.
migration/24e0d027f4160a6478c1356e51f419cf-4e3e6e8b9d0f4194842e530438ff686f/obelisk-handoffnew file
No changes to the content.
migration/24e0d027f4160a6478c1356e51f419cf-4e3e6e8b9d0f4194842e530438ff686f/obelisk-upgradenew file
No changes to the content.
migration/24f151a674662ac47845237d7a9f6a4f-4fe321cf196b9cd25210c35e09e09d8f/obelisk-handoffnew file
No changes to the content.
migration/24f151a674662ac47845237d7a9f6a4f-4fe321cf196b9cd25210c35e09e09d8f/obelisk-upgradenew file
No changes to the content.
migration/2543866165016a449562ea7a1ee36f6f-1a08b154ae5f898992128e229090c4d8/obelisk-handoffnew file
No changes to the content.
migration/2543866165016a449562ea7a1ee36f6f-1a08b154ae5f898992128e229090c4d8/obelisk-upgradenew file
No changes to the content.
migration/25615a274650370633e8083ed19fbdd0-80cb6b08443d55bb512e4a35b4768b72/obelisk-handoffnew file
No changes to the content.
migration/25615a274650370633e8083ed19fbdd0-80cb6b08443d55bb512e4a35b4768b72/obelisk-upgradenew file
No changes to the content.
migration/2600ab2ec96d3177d2a6b3462e09862d-e2b2d878b0ba6eb653b194523bf0a0e0/obelisk-handoffnew file
No changes to the content.
migration/2600ab2ec96d3177d2a6b3462e09862d-e2b2d878b0ba6eb653b194523bf0a0e0/obelisk-upgradenew file
No changes to the content.
migration/2619464637b85cb35376393fe612ddae-a8ebe201ab7a7b0211de41b14a95192e/obelisk-handoffnew file
No changes to the content.
migration/2619464637b85cb35376393fe612ddae-a8ebe201ab7a7b0211de41b14a95192e/obelisk-upgradenew file
No changes to the content.
migration/26a2491fc8a9658751c1511c777a67bd-470df6504156dc940b8e7f89c148e619/obelisk-handoffnew file
No changes to the content.
migration/26a2491fc8a9658751c1511c777a67bd-470df6504156dc940b8e7f89c148e619/obelisk-upgradenew file
No changes to the content.
migration/270b3b3f7bc510c692e3253a895f122a-c2d4eeb20f2b319a2d9dc5b6b0762f83/obelisk-handoffnew file
No changes to the content.
migration/270b3b3f7bc510c692e3253a895f122a-c2d4eeb20f2b319a2d9dc5b6b0762f83/obelisk-upgradenew file
No changes to the content.
migration/2773d4a82ca59b48faf036755e163a5f-71c6959cf98e05d356ff798ada6db2b8/obelisk-handoffnew file
No changes to the content.
migration/2773d4a82ca59b48faf036755e163a5f-71c6959cf98e05d356ff798ada6db2b8/obelisk-upgradenew file
No changes to the content.
migration/28a6ada033c86e5ae914cc31cecb6239-90b4e20464e08ade5f215f2a4ea28cc6/obelisk-handoffnew file
No changes to the content.
migration/28a6ada033c86e5ae914cc31cecb6239-90b4e20464e08ade5f215f2a4ea28cc6/obelisk-upgradenew file
No changes to the content.
migration/28c5fec3b7b0b14b3625d5c8f60bcf68-4509e2a908da9e497fa67acacdd107b6/obelisk-handoffnew file
No changes to the content.
migration/28c5fec3b7b0b14b3625d5c8f60bcf68-4509e2a908da9e497fa67acacdd107b6/obelisk-upgradenew file
No changes to the content.
migration/28e25b7a8812869a709579cd34e7bcb4-2543866165016a449562ea7a1ee36f6f/obelisk-handoffnew file
No changes to the content.
migration/28e25b7a8812869a709579cd34e7bcb4-2543866165016a449562ea7a1ee36f6f/obelisk-upgradenew file
No changes to the content.
migration/2913c440c7ad7731aae96e60a86bf7a8-a08aebb6f40fb45d174b229532314ac4/obelisk-handoffnew file
No changes to the content.
migration/2913c440c7ad7731aae96e60a86bf7a8-a08aebb6f40fb45d174b229532314ac4/obelisk-upgradenew file
No changes to the content.
migration/29abdd56a2dbbf3a2a0715210d441d06-51c7d001730ddd3d76e0c8bb57038773/obelisk-handoffnew file
No changes to the content.
migration/29abdd56a2dbbf3a2a0715210d441d06-51c7d001730ddd3d76e0c8bb57038773/obelisk-upgradenew file
No changes to the content.
migration/29d9b8b8ae9698888f3ea90b10ff5e92-1ada2d50d7847a12dbe29df4097b5bdc/obelisk-handoffnew file
No changes to the content.
migration/29d9b8b8ae9698888f3ea90b10ff5e92-1ada2d50d7847a12dbe29df4097b5bdc/obelisk-upgradenew file
No changes to the content.
migration/29fd07af89cc31526fca00226ff52a82-50b85e79010ad50ee275eaeba0df6f8d/obelisk-handoffnew file
No changes to the content.
migration/29fd07af89cc31526fca00226ff52a82-50b85e79010ad50ee275eaeba0df6f8d/obelisk-upgradenew file
No changes to the content.
migration/2ba356b187db8db13b8aa40f43ca025f-3c1fc501da00f467c9a48a1ddee2c2b0/obelisk-handoffnew file
No changes to the content.
migration/2ba356b187db8db13b8aa40f43ca025f-3c1fc501da00f467c9a48a1ddee2c2b0/obelisk-upgradenew file
No changes to the content.
migration/2bdd4221307cafa3e98c4d3017cdc912-5f6fe243beb9fcc0c4117536022fc14c/obelisk-handoffnew file
No changes to the content.
migration/2bdd4221307cafa3e98c4d3017cdc912-5f6fe243beb9fcc0c4117536022fc14c/obelisk-upgradenew file
No changes to the content.
migration/2c1521e5a5be64e6d28a9fdae950cd76-92ab733e8991f12cc12148f8d4e1b35d/obelisk-handoffnew file
No changes to the content.
migration/2c1521e5a5be64e6d28a9fdae950cd76-92ab733e8991f12cc12148f8d4e1b35d/obelisk-upgradenew file
No changes to the content.
migration/2ccf977797628dc3b1c002927fedc7a8-46f9c6c66e2bd8980402b6117ca40965/obelisk-handoffnew file
No changes to the content.
migration/2ccf977797628dc3b1c002927fedc7a8-46f9c6c66e2bd8980402b6117ca40965/obelisk-upgradenew file
No changes to the content.
migration/2df092178e167b474dd08dfa46ec3a49-9c94cd459a25b5c2de56067c8fdac620/obelisk-handoffnew file
No changes to the content.
migration/2df092178e167b474dd08dfa46ec3a49-9c94cd459a25b5c2de56067c8fdac620/obelisk-upgradenew file
No changes to the content.
migration/2e3e85cc59aa31f24f18c027542c0510-47357c6c23f049241902bf2e2e686ac8/obelisk-handoffnew file
No changes to the content.
migration/2e3e85cc59aa31f24f18c027542c0510-47357c6c23f049241902bf2e2e686ac8/obelisk-upgradenew file
No changes to the content.
migration/2e7cdc2bea08bb2358219ddefef372e0-a00870cfca85ff8dae5db3a78f5a227d/obelisk-handoffnew file
No changes to the content.
migration/2e7cdc2bea08bb2358219ddefef372e0-a00870cfca85ff8dae5db3a78f5a227d/obelisk-upgradenew file
No changes to the content.
migration/2e9a04a228106f62128ea5bd19d6b25e-947784563e3a27cf6486bed9a3c52f15/obelisk-handoffnew file
No changes to the content.
migration/2e9a04a228106f62128ea5bd19d6b25e-947784563e3a27cf6486bed9a3c52f15/obelisk-upgradenew file
No changes to the content.
migration/2eb907820012f77e60350430d89631fe-494be5819cd601fd2f0c20e1b23b84a3/obelisk-handoffnew file
No changes to the content.
migration/2eb907820012f77e60350430d89631fe-494be5819cd601fd2f0c20e1b23b84a3/obelisk-upgradenew file
No changes to the content.
migration/2ef4f9cce6d15ae62d5112d3c224eb7a-a080b8549e32f21fec11084f4ab1e052/obelisk-handoffnew file
No changes to the content.
migration/2ef4f9cce6d15ae62d5112d3c224eb7a-a080b8549e32f21fec11084f4ab1e052/obelisk-upgradenew file
No changes to the content.
migration/2f3ecb5cdb0ab839a49271d91dd7852a-f9ad863c0bebc103f8b81afdf82e6293/obelisk-handoffnew file
No changes to the content.
migration/2f3ecb5cdb0ab839a49271d91dd7852a-f9ad863c0bebc103f8b81afdf82e6293/obelisk-upgradenew file
No changes to the content.
migration/2f928f9c4a137186f808a231c9e3c141-9e61f37dd79310b71c80ee166aeb4e80/obelisk-handoffnew file
No changes to the content.
migration/2f928f9c4a137186f808a231c9e3c141-9e61f37dd79310b71c80ee166aeb4e80/obelisk-upgradenew file
No changes to the content.
migration/315cd79f97dae2f3669551dc03e0fd27-c801cbcf925c0a7b084ef152e3c08f8e/obelisk-handoffnew file
No changes to the content.
migration/315cd79f97dae2f3669551dc03e0fd27-c801cbcf925c0a7b084ef152e3c08f8e/obelisk-upgradenew file
No changes to the content.
migration/31a676fd9e1f69bd07380480ec879da0-9223532a537f698ef36f4dce6e922a6b/obelisk-handoffnew file
No changes to the content.
migration/31a676fd9e1f69bd07380480ec879da0-9223532a537f698ef36f4dce6e922a6b/obelisk-upgradenew file
No changes to the content.
migration/338b92f88c49c7c3a1d8ec231d9f7189-a4477746ae1a7024bc3cd0807180d418/obelisk-handoffnew file
No changes to the content.
migration/338b92f88c49c7c3a1d8ec231d9f7189-a4477746ae1a7024bc3cd0807180d418/obelisk-upgradenew file
No changes to the content.
migration/33da5c0e6705843afa6e39e65bfa0564-3ec562b7333839d48e9523aec595f3ab/obelisk-handoffnew file
No changes to the content.
migration/33da5c0e6705843afa6e39e65bfa0564-3ec562b7333839d48e9523aec595f3ab/obelisk-upgradenew file
No changes to the content.
migration/34d14d4a444d3bf1487ada6129815df4-afbd010add526d309dccee6895e42d0d/obelisk-handoffnew file
No changes to the content.
migration/34d14d4a444d3bf1487ada6129815df4-afbd010add526d309dccee6895e42d0d/obelisk-upgradenew file
No changes to the content.
migration/35acc1e0c4d9c02d60abd116f3df84ee-ffa62158ed6fdece012980e4ad25e6ac/obelisk-handoffnew file
No changes to the content.
migration/35acc1e0c4d9c02d60abd116f3df84ee-ffa62158ed6fdece012980e4ad25e6ac/obelisk-upgradenew file
No changes to the content.
migration/367616bd36eda757f251a5e8e9096acc-ddaa8e0e42a1ef00154d580a390c9f17/obelisk-handoffnew file
No changes to the content.
migration/367616bd36eda757f251a5e8e9096acc-ddaa8e0e42a1ef00154d580a390c9f17/obelisk-upgradenew file
No changes to the content.
migration/36cdb1c744c3036c2932f01d5fadd9f5-e7b566495114e160f1f4d722b7ed8174/obelisk-handoffnew file
No changes to the content.
migration/36cdb1c744c3036c2932f01d5fadd9f5-e7b566495114e160f1f4d722b7ed8174/obelisk-upgradenew file
No changes to the content.
migration/375acf7f2a3b3c487da2a6b30735d74d-3b614d291e262d0f281b54ff42a6bb66/obelisk-handoffnew file
No changes to the content.
migration/375acf7f2a3b3c487da2a6b30735d74d-3b614d291e262d0f281b54ff42a6bb66/obelisk-upgradenew file
No changes to the content.
migration/37c2ed0ba2cd54c0ec29eae9043bc4c8-190460dabd2b9773c3fe81902d0c8a62/obelisk-handoffnew file
No changes to the content.
migration/37c2ed0ba2cd54c0ec29eae9043bc4c8-190460dabd2b9773c3fe81902d0c8a62/obelisk-upgradenew file
No changes to the content.
migration/388d7d29a65c1713e07a654e91eb9caf-b6ffce85249f30616da76334c7bbec6e/obelisk-handoffnew file
No changes to the content.
migration/388d7d29a65c1713e07a654e91eb9caf-b6ffce85249f30616da76334c7bbec6e/obelisk-upgradenew file
No changes to the content.
migration/39df6544d6b035d4c77cc829729f9c19-9f6c8fd80ab3ff2dcc48bfd211c8b761/obelisk-handoffnew file
No changes to the content.
migration/39df6544d6b035d4c77cc829729f9c19-9f6c8fd80ab3ff2dcc48bfd211c8b761/obelisk-upgradenew file
No changes to the content.
migration/3a207630aa67e5df48d2b87d34db616b-8adb921f4dbb9d363bf6647d304e02fe/obelisk-handoffnew file
No changes to the content.
migration/3a207630aa67e5df48d2b87d34db616b-8adb921f4dbb9d363bf6647d304e02fe/obelisk-upgradenew file
No changes to the content.
migration/3b614d291e262d0f281b54ff42a6bb66-2e9a04a228106f62128ea5bd19d6b25e/obelisk-handoffnew file
No changes to the content.
migration/3b614d291e262d0f281b54ff42a6bb66-2e9a04a228106f62128ea5bd19d6b25e/obelisk-upgradenew file
No changes to the content.
migration/3bc53f3a28cb8f403cc41c11a37eec5d-cd86427cc2d5b2e931d7ab03deb19d20/obelisk-handoffnew file
No changes to the content.
migration/3bc53f3a28cb8f403cc41c11a37eec5d-cd86427cc2d5b2e931d7ab03deb19d20/obelisk-upgradenew file
No changes to the content.
migration/3bf8e28473fb05791c5790f8906e2609-45185a6f053557f93a13cb8cd03963a7/obelisk-handoffnew file
No changes to the content.
migration/3bf8e28473fb05791c5790f8906e2609-45185a6f053557f93a13cb8cd03963a7/obelisk-upgradenew file
No changes to the content.
migration/3c1fc501da00f467c9a48a1ddee2c2b0-cd5b54d21dd0fb868f48624d41021f47/obelisk-handoffnew file
No changes to the content.
migration/3c1fc501da00f467c9a48a1ddee2c2b0-cd5b54d21dd0fb868f48624d41021f47/obelisk-upgradenew file
No changes to the content.
migration/3c4b3152f0608d23d07240e2df3ff8da-c6baede3962454713b73acefcc92fe75/obelisk-handoffnew file
No changes to the content.
migration/3c4b3152f0608d23d07240e2df3ff8da-c6baede3962454713b73acefcc92fe75/obelisk-upgradenew file
No changes to the content.
migration/3c7755e495f20cfac5e4dabe78644c5a-51e25cf740f064faa0a29b8e2afcc109/obelisk-handoffnew file
No changes to the content.
migration/3c7755e495f20cfac5e4dabe78644c5a-51e25cf740f064faa0a29b8e2afcc109/obelisk-upgradenew file
No changes to the content.
migration/3c80eccfe80d0abd36b07bb11e69fc7a-b4f6596f2c16811442d8bb2961fbb721/obelisk-handoffnew file
No changes to the content.
migration/3c80eccfe80d0abd36b07bb11e69fc7a-b4f6596f2c16811442d8bb2961fbb721/obelisk-upgradenew file
No changes to the content.
migration/3cb52c2838303a362e9efb7c7eea37bd-aeafaa5d20bab4439a5d31f29023a839/obelisk-handoffnew file
No changes to the content.
migration/3cb52c2838303a362e9efb7c7eea37bd-aeafaa5d20bab4439a5d31f29023a839/obelisk-upgradenew file
No changes to the content.
migration/3da702ef7f531ad6db874ac3f85e9c06-73bcf3ed6593d074bd097cd43f5145d1/obelisk-handoffnew file
No changes to the content.
migration/3da702ef7f531ad6db874ac3f85e9c06-73bcf3ed6593d074bd097cd43f5145d1/obelisk-upgradenew file
No changes to the content.
migration/3da7d4a3b507d18097a90becf4dde9aa-e1ef432db54d9a6daa9bdeeb543262ae/obelisk-handoffnew file
No changes to the content.
migration/3da7d4a3b507d18097a90becf4dde9aa-e1ef432db54d9a6daa9bdeeb543262ae/obelisk-upgradenew file
No changes to the content.
migration/3daecb03985f83d8af466a9e0541782b-a2665cfd303a4a34a7bfa3b7d0d264bb/obelisk-handoffnew file
No changes to the content.
migration/3daecb03985f83d8af466a9e0541782b-a2665cfd303a4a34a7bfa3b7d0d264bb/obelisk-upgradenew file
No changes to the content.
migration/3e253c6beb567ba8f4c8086a76b70645-d6cacd7f4fab95c4b6aa2d6c1a4ad2a0/obelisk-handoffnew file
No changes to the content.
migration/3e253c6beb567ba8f4c8086a76b70645-d6cacd7f4fab95c4b6aa2d6c1a4ad2a0/obelisk-upgradenew file
No changes to the content.
migration/3e763ebe16cc3a261f5776ab1f1f6d9a-583ec22b9af66e8f629ee404307eb2dc/obelisk-handoffnew file
No changes to the content.
migration/3e763ebe16cc3a261f5776ab1f1f6d9a-583ec22b9af66e8f629ee404307eb2dc/obelisk-upgradenew file
No changes to the content.
migration/3e8a6968585d3d60f3b13652a386aec0-c118e1ee09ec8d9aa3edf04e211ae035/obelisk-handoffnew file
No changes to the content.
migration/3e8a6968585d3d60f3b13652a386aec0-c118e1ee09ec8d9aa3edf04e211ae035/obelisk-upgradenew file
No changes to the content.
migration/3ec562b7333839d48e9523aec595f3ab-7b9c28d09bd962e89b9715500f9e184c/obelisk-handoffnew file
No changes to the content.
migration/3ec562b7333839d48e9523aec595f3ab-7b9c28d09bd962e89b9715500f9e184c/obelisk-upgradenew file
No changes to the content.
migration/3f0687592a77c0c66193652e9cd981fd-fb81153791e49cd00850e91ff7418018/obelisk-handoffnew file
No changes to the content.
migration/3f0687592a77c0c66193652e9cd981fd-fb81153791e49cd00850e91ff7418018/obelisk-upgradenew file
No changes to the content.
migration/3f23549fba6f8e67ba36b4405e8680b3-a6633fc9687566aa84c3d5d5e2bff4fd/obelisk-handoffnew file
No changes to the content.
migration/3f23549fba6f8e67ba36b4405e8680b3-a6633fc9687566aa84c3d5d5e2bff4fd/obelisk-upgradenew file
No changes to the content.
migration/41e3ba178b727517aa1cedc8815abe86-1d084e187591225f0e8fa6fa824f8863/obelisk-handoffnew file
No changes to the content.
migration/41e3ba178b727517aa1cedc8815abe86-1d084e187591225f0e8fa6fa824f8863/obelisk-upgradenew file
No changes to the content.
migration/4205857df2e234a476ad56f84a33e6ed-db327f87e60d4e59bdbfd482e5abe985/obelisk-handoffnew file
No changes to the content.
migration/4205857df2e234a476ad56f84a33e6ed-db327f87e60d4e59bdbfd482e5abe985/obelisk-upgradenew file
No changes to the content.
migration/423088b98df50b986c9197af83c42a25-3a207630aa67e5df48d2b87d34db616b/obelisk-handoffnew file
No changes to the content.
migration/423088b98df50b986c9197af83c42a25-3a207630aa67e5df48d2b87d34db616b/obelisk-upgradenew file
No changes to the content.
migration/42357310c09e345f9360259c6c4102f2-ae8862f8b7292690c852111ca5659ce9/obelisk-handoffnew file
No changes to the content.
migration/42357310c09e345f9360259c6c4102f2-ae8862f8b7292690c852111ca5659ce9/obelisk-upgradenew file
No changes to the content.
migration/4344c1bd0ef04ebc055d5252f58b0bef-2f3ecb5cdb0ab839a49271d91dd7852a/obelisk-handoffnew file
No changes to the content.
migration/4344c1bd0ef04ebc055d5252f58b0bef-2f3ecb5cdb0ab839a49271d91dd7852a/obelisk-upgradenew file
No changes to the content.
migration/4509e2a908da9e497fa67acacdd107b6-1fe00204f9d2cd653ff2894eae3d9356/obelisk-handoffnew file
No changes to the content.
migration/4509e2a908da9e497fa67acacdd107b6-1fe00204f9d2cd653ff2894eae3d9356/obelisk-upgradenew file
No changes to the content.
migration/45185a6f053557f93a13cb8cd03963a7-cffcc6cab6080a35b7bf7c5c7facd6bc/obelisk-handoffnew file
No changes to the content.
migration/45185a6f053557f93a13cb8cd03963a7-cffcc6cab6080a35b7bf7c5c7facd6bc/obelisk-upgradenew file
No changes to the content.
migration/455279b6974b96a673613cd0ccc8ed54-b3ff195b0a90ef5900f9cef2ba75f2fe/obelisk-handoffnew file
No changes to the content.
migration/455279b6974b96a673613cd0ccc8ed54-b3ff195b0a90ef5900f9cef2ba75f2fe/obelisk-upgradenew file
No changes to the content.
migration/463646ba21b356f612539ea34178adf9-b3b107d406a6a7675cd5b9f52e5f7d8e/obelisk-handoffnew file
No changes to the content.
migration/463646ba21b356f612539ea34178adf9-b3b107d406a6a7675cd5b9f52e5f7d8e/obelisk-upgradenew file
No changes to the content.
migration/466a19f9cfa2a103467fe6627225d27f-c67f206b6d2626eb6d36b00122b52784/obelisk-handoffnew file
No changes to the content.
migration/466a19f9cfa2a103467fe6627225d27f-c67f206b6d2626eb6d36b00122b52784/obelisk-upgradenew file
No changes to the content.
migration/469a31d20d69da72e6a3a3ea2bb02f9d-75c9cd9763d0ed89c17d455f49d66a4f/obelisk-handoffnew file
No changes to the content.
migration/469a31d20d69da72e6a3a3ea2bb02f9d-75c9cd9763d0ed89c17d455f49d66a4f/obelisk-upgradenew file
No changes to the content.
migration/46aed07c1a61476dd245ff8e74c0559f-73e2573d9074884d7ae83106d55ad229/obelisk-handoffnew file
No changes to the content.
migration/46aed07c1a61476dd245ff8e74c0559f-73e2573d9074884d7ae83106d55ad229/obelisk-upgradenew file
No changes to the content.
migration/46f9c6c66e2bd8980402b6117ca40965-463646ba21b356f612539ea34178adf9/obelisk-handoffnew file
No changes to the content.
migration/46f9c6c66e2bd8980402b6117ca40965-463646ba21b356f612539ea34178adf9/obelisk-upgradenew file
No changes to the content.
migration/470df6504156dc940b8e7f89c148e619-a5297d8457aa4c327f35d4697f6cb66f/obelisk-handoffnew file
No changes to the content.
migration/470df6504156dc940b8e7f89c148e619-a5297d8457aa4c327f35d4697f6cb66f/obelisk-upgradenew file
No changes to the content.
migration/47357c6c23f049241902bf2e2e686ac8-d62d6d443d9c2261b7e09664398aa005/obelisk-handoffnew file
No changes to the content.
migration/47357c6c23f049241902bf2e2e686ac8-d62d6d443d9c2261b7e09664398aa005/obelisk-upgradenew file
No changes to the content.
migration/475f9a0d63e84e9cf13b8be3986cb603-b95c8d049256fb7360ca14b12acd8544/obelisk-handoffnew file
No changes to the content.
migration/475f9a0d63e84e9cf13b8be3986cb603-b95c8d049256fb7360ca14b12acd8544/obelisk-upgradenew file
No changes to the content.
migration/477a0bb12b48357d1e2b415c91d904e3-486d5218951605d14ec4a1046d3fe83e/obelisk-handoffnew file
No changes to the content.
migration/477a0bb12b48357d1e2b415c91d904e3-486d5218951605d14ec4a1046d3fe83e/obelisk-upgradenew file
No changes to the content.
migration/486d5218951605d14ec4a1046d3fe83e-90489c1a9f7edd46a070d6474dceac86/obelisk-handoffnew file
No changes to the content.
migration/486d5218951605d14ec4a1046d3fe83e-90489c1a9f7edd46a070d6474dceac86/obelisk-upgradenew file
No changes to the content.
migration/48c027da19d895ba03e7b9120f771291-29fd07af89cc31526fca00226ff52a82/obelisk-handoffnew file
No changes to the content.
migration/48c027da19d895ba03e7b9120f771291-29fd07af89cc31526fca00226ff52a82/obelisk-upgradenew file
No changes to the content.
migration/494be5819cd601fd2f0c20e1b23b84a3-c66bc5ba27dec52d144ceb0aa429e449/obelisk-handoffnew file
No changes to the content.
migration/494be5819cd601fd2f0c20e1b23b84a3-c66bc5ba27dec52d144ceb0aa429e449/obelisk-upgradenew file
No changes to the content.
migration/4a037879b8a542f1590cbb87b0242331-4344c1bd0ef04ebc055d5252f58b0bef/obelisk-handoffnew file
No changes to the content.
migration/4a037879b8a542f1590cbb87b0242331-4344c1bd0ef04ebc055d5252f58b0bef/obelisk-upgradenew file
No changes to the content.
migration/4ab915b764f38a1b0b4ae6f619f5c3fa-59e4a192c611a2a9f6736afde9fe1a0e/obelisk-handoffnew file
No changes to the content.
migration/4ab915b764f38a1b0b4ae6f619f5c3fa-59e4a192c611a2a9f6736afde9fe1a0e/obelisk-upgradenew file
No changes to the content.
migration/4ace4414d71235a291f7148f80a0b0f5-9705509619667aacf799e91927c16dc8/obelisk-handoffnew file
No changes to the content.
migration/4ace4414d71235a291f7148f80a0b0f5-9705509619667aacf799e91927c16dc8/obelisk-upgradenew file
No changes to the content.
migration/4aefa80392582ec03a19f946e752d331-cb98f93880e6dea74014976977d6eb93/obelisk-handoffnew file
No changes to the content.
migration/4aefa80392582ec03a19f946e752d331-cb98f93880e6dea74014976977d6eb93/obelisk-upgradenew file
No changes to the content.
migration/4b3f45ce8aeb17ef18d1d9502b74bb7e-4efbe4a73c260cca09fed7169e82a7ff/obelisk-handoffnew file
No changes to the content.
migration/4b3f45ce8aeb17ef18d1d9502b74bb7e-4efbe4a73c260cca09fed7169e82a7ff/obelisk-upgradenew file
No changes to the content.
migration/4b7a36dce2c1cf94554e12d8a7903269-dd6ca52a58ea6cda7b3eaa2e87bb13d9/obelisk-handoffnew file
No changes to the content.
migration/4b7a36dce2c1cf94554e12d8a7903269-dd6ca52a58ea6cda7b3eaa2e87bb13d9/obelisk-upgradenew file
No changes to the content.
migration/4c9372bf9aa6ebed1ea20abf3dbb0075-b641dd9c5359201200e483a784456cc2/obelisk-handoffnew file
No changes to the content.
migration/4c9372bf9aa6ebed1ea20abf3dbb0075-b641dd9c5359201200e483a784456cc2/obelisk-upgradenew file
No changes to the content.
migration/4d6197d8166de78f882376102e36300b-4205857df2e234a476ad56f84a33e6ed/obelisk-handoffnew file
No changes to the content.
migration/4d6197d8166de78f882376102e36300b-4205857df2e234a476ad56f84a33e6ed/obelisk-upgradenew file
No changes to the content.
migration/4d85ee8cdb0db20a5662b09567a2bbe7-b6425c7661af2116b912a558e9548013/obelisk-handoffnew file
No changes to the content.
migration/4d85ee8cdb0db20a5662b09567a2bbe7-b6425c7661af2116b912a558e9548013/obelisk-upgradenew file
No changes to the content.
migration/4da182e21be35feeee484dc85be7a075-14840816db4b5cfd5580ae099022604a/obelisk-handoffnew file
No changes to the content.
migration/4da182e21be35feeee484dc85be7a075-14840816db4b5cfd5580ae099022604a/obelisk-upgradenew file
No changes to the content.
migration/4db97ca86399c557e5ee0d04d0838acc-c203adc5fed9bf79f67df43d72a4631a/obelisk-handoffnew file
No changes to the content.
migration/4db97ca86399c557e5ee0d04d0838acc-c203adc5fed9bf79f67df43d72a4631a/obelisk-upgradenew file
No changes to the content.
migration/4e3e6e8b9d0f4194842e530438ff686f-b7e30314f9d0e69efdc84c7eb4e3bcc3/obelisk-handoffnew file
No changes to the content.
migration/4e3e6e8b9d0f4194842e530438ff686f-b7e30314f9d0e69efdc84c7eb4e3bcc3/obelisk-upgradenew file
No changes to the content.
migration/4e95e2f8ef50420371b71022c644e25d-8df9e256d8e43fa4e22d953024269b5b/obelisk-handoffnew file
No changes to the content.
migration/4e95e2f8ef50420371b71022c644e25d-8df9e256d8e43fa4e22d953024269b5b/obelisk-upgradenew file
No changes to the content.
migration/4efbe4a73c260cca09fed7169e82a7ff-315cd79f97dae2f3669551dc03e0fd27/obelisk-handoffnew file
No changes to the content.
migration/4efbe4a73c260cca09fed7169e82a7ff-315cd79f97dae2f3669551dc03e0fd27/obelisk-upgradenew file
No changes to the content.
migration/4fe321cf196b9cd25210c35e09e09d8f-56d7fd24c8a996e49bb26dd797819897/obelisk-handoffnew file
No changes to the content.
migration/4fe321cf196b9cd25210c35e09e09d8f-56d7fd24c8a996e49bb26dd797819897/obelisk-upgradenew file
No changes to the content.
migration/4ff5bf819ea43a638abbcfa071bbf3ef-975cd8245d06ad09e4fb176cfc9af5e0/obelisk-handoffnew file
No changes to the content.
migration/4ff5bf819ea43a638abbcfa071bbf3ef-975cd8245d06ad09e4fb176cfc9af5e0/obelisk-upgradenew file
No changes to the content.
migration/50b85e79010ad50ee275eaeba0df6f8d-e327773e1525eaa1eb2112dff8f41b26/obelisk-handoffnew file
No changes to the content.
migration/50b85e79010ad50ee275eaeba0df6f8d-e327773e1525eaa1eb2112dff8f41b26/obelisk-upgradenew file
No changes to the content.
migration/51c7d001730ddd3d76e0c8bb57038773-423088b98df50b986c9197af83c42a25/obelisk-handoffnew file
No changes to the content.
migration/51c7d001730ddd3d76e0c8bb57038773-423088b98df50b986c9197af83c42a25/obelisk-upgradenew file
No changes to the content.
migration/51e25cf740f064faa0a29b8e2afcc109-7bc4c944b24621967a6334f522437cc1/obelisk-handoffnew file
No changes to the content.
migration/51e25cf740f064faa0a29b8e2afcc109-7bc4c944b24621967a6334f522437cc1/obelisk-upgradenew file
No changes to the content.
migration/51f3d5068259f596b811450f0bf25419-f9716ad47c84e0b7ac7d426e8ad059f3/obelisk-handoffnew file
No changes to the content.
migration/51f3d5068259f596b811450f0bf25419-f9716ad47c84e0b7ac7d426e8ad059f3/obelisk-upgradenew file
No changes to the content.
migration/52330622ae996023b3f80a40991521a4-1df871cede0758cabe43971d9091279d/obelisk-handoffnew file
No changes to the content.
migration/52330622ae996023b3f80a40991521a4-1df871cede0758cabe43971d9091279d/obelisk-upgradenew file
No changes to the content.
migration/52df183d0f32d5a1e9ebe276b6d83d37-08dbdf23867a8efa680283ad251d3429/obelisk-handoffnew file
No changes to the content.
migration/52df183d0f32d5a1e9ebe276b6d83d37-08dbdf23867a8efa680283ad251d3429/obelisk-upgradenew file
No changes to the content.
migration/53d29061537ddaf7530aef88624ab266-9fe2922674fcee5e840c9f52cf534f88/obelisk-handoffnew file
No changes to the content.
migration/53d29061537ddaf7530aef88624ab266-9fe2922674fcee5e840c9f52cf534f88/obelisk-upgradenew file
No changes to the content.
migration/53fdf819333b86dfb485bf16a8f28db2-93fc625d41bae9fcf06b9febeacb686d/obelisk-handoffnew file
No changes to the content.
migration/53fdf819333b86dfb485bf16a8f28db2-93fc625d41bae9fcf06b9febeacb686d/obelisk-upgradenew file
No changes to the content.
migration/554c459ff2cced3e1b11241ed403890d-3543ea607791bc2e19419d22918823bd/obelisk-handoffnew file
No changes to the content.
migration/554c459ff2cced3e1b11241ed403890d-3543ea607791bc2e19419d22918823bd/obelisk-upgradenew file
No changes to the content.
migration/554c459ff2cced3e1b11241ed403890d-6a18939264224d6ce24c7426249edfea/obelisk-handoffnew file
No changes to the content.
migration/554c459ff2cced3e1b11241ed403890d-6a18939264224d6ce24c7426249edfea/obelisk-upgradenew file
No changes to the content.
migration/55cf43fb0d3868040c9efbf2f92d538a-e2706f337218a12a3bdddd18fa3f4cfd/obelisk-handoffnew file
No changes to the content.
migration/55cf43fb0d3868040c9efbf2f92d538a-e2706f337218a12a3bdddd18fa3f4cfd/obelisk-upgradenew file
No changes to the content.
migration/55d9c5eb3b86c9cb27966f849b2d32cf-0e4d0f0f0af0925474b0d772be2b145f/obelisk-handoffnew file
No changes to the content.
migration/55d9c5eb3b86c9cb27966f849b2d32cf-0e4d0f0f0af0925474b0d772be2b145f/obelisk-upgradenew file
No changes to the content.
migration/5631293b75143d170b8fca5579e15fee-0abd9d31c77be8c9a9e83bf1f73276f6/obelisk-handoffnew file
No changes to the content.
migration/5631293b75143d170b8fca5579e15fee-0abd9d31c77be8c9a9e83bf1f73276f6/obelisk-upgradenew file
No changes to the content.
migration/569227e5e7754c1890022850fd552292-ff1105290f89350bc9deb6eb907e0a53/obelisk-handoffnew file
No changes to the content.
migration/569227e5e7754c1890022850fd552292-ff1105290f89350bc9deb6eb907e0a53/obelisk-upgradenew file
No changes to the content.
migration/56d7fd24c8a996e49bb26dd797819897-61f734d4c59e46f03e92d2b86299c7c9/obelisk-handoffnew file
No changes to the content.
migration/56d7fd24c8a996e49bb26dd797819897-61f734d4c59e46f03e92d2b86299c7c9/obelisk-upgradenew file
No changes to the content.
migration/5734398c41e4eb5bb58d933ed2e12041-fb374b0721716a1e849e64daf6da73c4/obelisk-handoffnew file
No changes to the content.
migration/5734398c41e4eb5bb58d933ed2e12041-fb374b0721716a1e849e64daf6da73c4/obelisk-upgradenew file
No changes to the content.
migration/58391ba73c13d922e8a12116b28caad2-215b6810b09f219a5cb3842aacc88432/obelisk-handoffnew file
No changes to the content.
migration/58391ba73c13d922e8a12116b28caad2-215b6810b09f219a5cb3842aacc88432/obelisk-upgradenew file
No changes to the content.
migration/583ec22b9af66e8f629ee404307eb2dc-6c7f4af3693c31e1a2c6a2c3f189ddf5/obelisk-handoffnew file
No changes to the content.
migration/583ec22b9af66e8f629ee404307eb2dc-6c7f4af3693c31e1a2c6a2c3f189ddf5/obelisk-upgradenew file
No changes to the content.
migration/58479e482d3828ce44f2c8371ed9135a-174fa1072ee767be39b7b01aecea5714/obelisk-handoffnew file
No changes to the content.
migration/58479e482d3828ce44f2c8371ed9135a-174fa1072ee767be39b7b01aecea5714/obelisk-upgradenew file
No changes to the content.
migration/589eb89cdb09655c5edde4cd2a4deb80-c5a306045ad998f906f80748fe18487f/obelisk-handoffnew file
No changes to the content.
migration/589eb89cdb09655c5edde4cd2a4deb80-c5a306045ad998f906f80748fe18487f/obelisk-upgradenew file
No changes to the content.
migration/58f9d23d2cede2081dc6f6cfa2283f60-41e3ba178b727517aa1cedc8815abe86/obelisk-handoffnew file
No changes to the content.
migration/58f9d23d2cede2081dc6f6cfa2283f60-41e3ba178b727517aa1cedc8815abe86/obelisk-upgradenew file
No changes to the content.
migration/59742705e53838ae13066a2239a72040-95955d1dd0981ebb538ee941cde1f963/obelisk-handoffnew file
No changes to the content.
migration/59742705e53838ae13066a2239a72040-95955d1dd0981ebb538ee941cde1f963/obelisk-upgradenew file
No changes to the content.
migration/59e4a192c611a2a9f6736afde9fe1a0e-8143888349912db9780231c370ea2060/obelisk-handoffnew file
No changes to the content.
migration/59e4a192c611a2a9f6736afde9fe1a0e-8143888349912db9780231c370ea2060/obelisk-upgradenew file
No changes to the content.
migration/5a98cdb6c195e88beac878a3f311c5b5-66d79b4c393877a72aec18f227049207/obelisk-handoffnew file
No changes to the content.
migration/5a98cdb6c195e88beac878a3f311c5b5-66d79b4c393877a72aec18f227049207/obelisk-upgradenew file
No changes to the content.
migration/5b252c24612c2ba868580add9d9c19e3-6f104c322bbaff565981739406290402/obelisk-handoffnew file
No changes to the content.
migration/5b252c24612c2ba868580add9d9c19e3-6f104c322bbaff565981739406290402/obelisk-upgradenew file
No changes to the content.
migration/5b6770965bbef440db49b0703cd243a3-11f38b38c2d38004bb8a9dbfe2ef3e4a/obelisk-handoffnew file
No changes to the content.
migration/5b6770965bbef440db49b0703cd243a3-11f38b38c2d38004bb8a9dbfe2ef3e4a/obelisk-upgradenew file
No changes to the content.
migration/5cb98d0250bf5224b94e544a5140a54c-fa18f7522903a21b615dbee24c26bfbe/obelisk-handoffnew file
No changes to the content.
migration/5cb98d0250bf5224b94e544a5140a54c-fa18f7522903a21b615dbee24c26bfbe/obelisk-upgradenew file
No changes to the content.
migration/5d90abd974b4ec53091a8cb3fc2258ce-d40e0e3051a76c4c198cc11c2e8e4b7c/obelisk-handoffnew file
No changes to the content.
migration/5d90abd974b4ec53091a8cb3fc2258ce-d40e0e3051a76c4c198cc11c2e8e4b7c/obelisk-upgradenew file
No changes to the content.
migration/5f0d3f7480d67df7deebdb880726b356-4ab915b764f38a1b0b4ae6f619f5c3fa/obelisk-handoffnew file
No changes to the content.
migration/5f0d3f7480d67df7deebdb880726b356-4ab915b764f38a1b0b4ae6f619f5c3fa/obelisk-upgradenew file
No changes to the content.
migration/5f307639ccf6a64309166bfe58037397-702c65f35bc47641d60580d636ede330/obelisk-handoffnew file
No changes to the content.
migration/5f307639ccf6a64309166bfe58037397-702c65f35bc47641d60580d636ede330/obelisk-upgradenew file
No changes to the content.
migration/5f6fe243beb9fcc0c4117536022fc14c-f41b690c74938e347cf042cee2d4fc72/obelisk-handoffnew file
No changes to the content.
migration/5f6fe243beb9fcc0c4117536022fc14c-f41b690c74938e347cf042cee2d4fc72/obelisk-upgradenew file
No changes to the content.
migration/6095bdaccd933d319c84cde7983e2d10-477a0bb12b48357d1e2b415c91d904e3/obelisk-handoffnew file
No changes to the content.
migration/6095bdaccd933d319c84cde7983e2d10-477a0bb12b48357d1e2b415c91d904e3/obelisk-upgradenew file
No changes to the content.
migration/610dcf99f7eb8c8b591ff3f5f1722938-131e1d5cc8163ce9a174a8782be23fe7/obelisk-handoffnew file
No changes to the content.
migration/610dcf99f7eb8c8b591ff3f5f1722938-131e1d5cc8163ce9a174a8782be23fe7/obelisk-upgradenew file
No changes to the content.
migration/61f734d4c59e46f03e92d2b86299c7c9-02d0f8b5c7472e6f8a99ec2cfcb59807/obelisk-handoffnew file
No changes to the content.
migration/61f734d4c59e46f03e92d2b86299c7c9-02d0f8b5c7472e6f8a99ec2cfcb59807/obelisk-upgradenew file
No changes to the content.
migration/63422a9bade5c703461b600e03e227f0-01ddc6354698efa1ae5822da3be562dc/obelisk-handoffnew file
No changes to the content.
migration/63422a9bade5c703461b600e03e227f0-01ddc6354698efa1ae5822da3be562dc/obelisk-upgradenew file
No changes to the content.
migration/63ff9426130baa080f37400a86c55fdd-569227e5e7754c1890022850fd552292/obelisk-handoffnew file
No changes to the content.
migration/63ff9426130baa080f37400a86c55fdd-569227e5e7754c1890022850fd552292/obelisk-upgradenew file
No changes to the content.
migration/64ee9e40f4943e0122398e48019f5616-cd38e239c47fb92fabd867b7f86fc99a/obelisk-handoffnew file
No changes to the content.
migration/64ee9e40f4943e0122398e48019f5616-cd38e239c47fb92fabd867b7f86fc99a/obelisk-upgradenew file
No changes to the content.
migration/65071e255719af6389857646bc087d37-02fb88dab35884e09deef1721c1cfc13/obelisk-handoffnew file
No changes to the content.
migration/65071e255719af6389857646bc087d37-02fb88dab35884e09deef1721c1cfc13/obelisk-upgradenew file
No changes to the content.
migration/6558f8bda2aa17d76c509dde0a3dea56-2ccf977797628dc3b1c002927fedc7a8/obelisk-handoffnew file
No changes to the content.
migration/6558f8bda2aa17d76c509dde0a3dea56-2ccf977797628dc3b1c002927fedc7a8/obelisk-upgradenew file
No changes to the content.
migration/6566f0555737425bae9106d1a4a0a2dd-1f7fb90cb1e558f5285b2d19a25a1078/obelisk-handoffnew file
No changes to the content.
migration/6566f0555737425bae9106d1a4a0a2dd-1f7fb90cb1e558f5285b2d19a25a1078/obelisk-upgradenew file
No changes to the content.
migration/6620e6dc5622593e5f671127af9ade05-4ff5bf819ea43a638abbcfa071bbf3ef/obelisk-handoffnew file
No changes to the content.
migration/6620e6dc5622593e5f671127af9ade05-4ff5bf819ea43a638abbcfa071bbf3ef/obelisk-upgradenew file
No changes to the content.
migration/662eb30d6e4612f130edc3f6d37d92a2-94dd4eb45a4e7eb945027d5fd8eb4fa6/obelisk-handoffnew file
No changes to the content.
migration/662eb30d6e4612f130edc3f6d37d92a2-94dd4eb45a4e7eb945027d5fd8eb4fa6/obelisk-upgradenew file
No changes to the content.
migration/66a9b6b9e9bc4a5b0cfa78127479b231-88765caf54c2bf9f23ea4d05816a9f65/obelisk-handoffnew file
No changes to the content.
migration/66a9b6b9e9bc4a5b0cfa78127479b231-88765caf54c2bf9f23ea4d05816a9f65/obelisk-upgradenew file
No changes to the content.
migration/66d79b4c393877a72aec18f227049207-04dd95ecb889ac1b0a616ddfeec781c4/obelisk-handoffnew file
No changes to the content.
migration/66d79b4c393877a72aec18f227049207-04dd95ecb889ac1b0a616ddfeec781c4/obelisk-upgradenew file
No changes to the content.
migration/66f7a274206ad9f6d3c5701e4bcffc9d-2e7cdc2bea08bb2358219ddefef372e0/obelisk-handoffnew file
No changes to the content.
migration/66f7a274206ad9f6d3c5701e4bcffc9d-2e7cdc2bea08bb2358219ddefef372e0/obelisk-upgradenew file
No changes to the content.
migration/6708d14c71c475ac494808e75660a385-6095bdaccd933d319c84cde7983e2d10/obelisk-handoffnew file
No changes to the content.
migration/6708d14c71c475ac494808e75660a385-6095bdaccd933d319c84cde7983e2d10/obelisk-upgradenew file
No changes to the content.
migration/67173f110f2c54d135e6943b65118303-17089ea0edc0cf4b6f616936f8a06174/obelisk-handoffnew file
No changes to the content.
migration/67173f110f2c54d135e6943b65118303-17089ea0edc0cf4b6f616936f8a06174/obelisk-upgradenew file
No changes to the content.
migration/67bfa5b784a35e66875fb74ad84a38c0-f3ec62d09836804e39fad9d814137845/obelisk-handoffnew file
No changes to the content.
migration/67bfa5b784a35e66875fb74ad84a38c0-f3ec62d09836804e39fad9d814137845/obelisk-upgradenew file
No changes to the content.
migration/68cca18f813a16eb0cc2c3ed7fa08044-ee3dccc89ea6fdaaf9c1146b92861e90/obelisk-handoffnew file
No changes to the content.
migration/68cca18f813a16eb0cc2c3ed7fa08044-ee3dccc89ea6fdaaf9c1146b92861e90/obelisk-upgradenew file
No changes to the content.
migration/69537089a0e97845ffe05c73c3cc1536-e92786eaf56c32fd6b452c4a841d6317/obelisk-handoffnew file
No changes to the content.
migration/69537089a0e97845ffe05c73c3cc1536-e92786eaf56c32fd6b452c4a841d6317/obelisk-upgradenew file
No changes to the content.
migration/69cd775958e25e3b2cff7e4897f2a9bb-fca48edfdaa31b036176b97b04384682/obelisk-handoffnew file
No changes to the content.
migration/69cd775958e25e3b2cff7e4897f2a9bb-fca48edfdaa31b036176b97b04384682/obelisk-upgradenew file
No changes to the content.
migration/69fafe6ab155ff215b6d9d330f700ba1-6558f8bda2aa17d76c509dde0a3dea56/obelisk-handoffnew file
No changes to the content.
migration/69fafe6ab155ff215b6d9d330f700ba1-6558f8bda2aa17d76c509dde0a3dea56/obelisk-upgradenew file
No changes to the content.
migration/6a18939264224d6ce24c7426249edfea-e66afc8befac4d76b5780a960f768c31/obelisk-handoffnew file
No changes to the content.
migration/6a18939264224d6ce24c7426249edfea-e66afc8befac4d76b5780a960f768c31/obelisk-upgradenew file+15
@@ -0,0 +1,15 @@
1+# ob deploy: HTTPs support
2+
3+Obelisk deployments now support HTTPs. You must upgrade the deployment repo
4+associated with the obelisk project as follows:
5+
6+```
7+cd /your/project's/deployment/repo
8+echo True > enable_https
9+echo admin@yoursite.com > admin_email
10+echo https://yoursite.com/ > config/common/route
11+git add ... && git commit ...
12+ob deploy push
13+```
14+
15+If you do not wish to enable HTTPs, write `False` to `enable_https` instead.
migration/6c7f4af3693c31e1a2c6a2c3f189ddf5-388d7d29a65c1713e07a654e91eb9caf/obelisk-handoffnew file
No changes to the content.
migration/6c7f4af3693c31e1a2c6a2c3f189ddf5-388d7d29a65c1713e07a654e91eb9caf/obelisk-upgradenew file
No changes to the content.
migration/6da2e5b02ab780d12527640722d2b511-ca88f98cf9fb63d2feaef7fc19b30eca/obelisk-handoffnew file
No changes to the content.
migration/6da2e5b02ab780d12527640722d2b511-ca88f98cf9fb63d2feaef7fc19b30eca/obelisk-upgradenew file
No changes to the content.
migration/6dc299cb41f8977785a5d676cbcd32d7-237aeb7eee0ffed230234ee04e49569f/obelisk-handoffnew file
No changes to the content.
migration/6dc299cb41f8977785a5d676cbcd32d7-237aeb7eee0ffed230234ee04e49569f/obelisk-upgradenew file
No changes to the content.
migration/6f104c322bbaff565981739406290402-f98b82f5d142ce83733a3f18bdacb138/obelisk-handoffnew file
No changes to the content.
migration/6f104c322bbaff565981739406290402-f98b82f5d142ce83733a3f18bdacb138/obelisk-upgradenew file
No changes to the content.
migration/6f3a0f756847ee5aa208a1dc79404779-00c627540326314c7154cc209e8a71e9/obelisk-handoffnew file
No changes to the content.
migration/6f3a0f756847ee5aa208a1dc79404779-00c627540326314c7154cc209e8a71e9/obelisk-upgradenew file
No changes to the content.
migration/6fc44f180bb825d5b372444339681135-6dc299cb41f8977785a5d676cbcd32d7/obelisk-handoffnew file
No changes to the content.
migration/6fc44f180bb825d5b372444339681135-6dc299cb41f8977785a5d676cbcd32d7/obelisk-upgradenew file
No changes to the content.
migration/702c65f35bc47641d60580d636ede330-9217718d2b3b217dbba72e7091459548/obelisk-handoffnew file
No changes to the content.
migration/702c65f35bc47641d60580d636ede330-9217718d2b3b217dbba72e7091459548/obelisk-upgradenew file
No changes to the content.
migration/71a47d4d6856c1956c0d1c9de9af231f-bb9a87fc325204313d89dbfb689dba2f/obelisk-handoffnew file
No changes to the content.
migration/71a47d4d6856c1956c0d1c9de9af231f-bb9a87fc325204313d89dbfb689dba2f/obelisk-upgradenew file
No changes to the content.
migration/71c6959cf98e05d356ff798ada6db2b8-55d9c5eb3b86c9cb27966f849b2d32cf/obelisk-handoffnew file
No changes to the content.
migration/71c6959cf98e05d356ff798ada6db2b8-55d9c5eb3b86c9cb27966f849b2d32cf/obelisk-upgradenew file
No changes to the content.
migration/71da39c91abb23488021a1e9142507d5-16b24e1a12b93ea83d7656a09d5e3338/obelisk-handoffnew file
No changes to the content.
migration/71da39c91abb23488021a1e9142507d5-16b24e1a12b93ea83d7656a09d5e3338/obelisk-upgradenew file
No changes to the content.
migration/721a02f3a7549e7d6efeaed087bfacb5-52df183d0f32d5a1e9ebe276b6d83d37/obelisk-handoffnew file
No changes to the content.
migration/721a02f3a7549e7d6efeaed087bfacb5-52df183d0f32d5a1e9ebe276b6d83d37/obelisk-upgradenew file
No changes to the content.
migration/729aaf942e685bb37fdbd5e97293bbcb-7b2b9cefa7dd57d51b587c3501a1b9ef/obelisk-handoffnew file
No changes to the content.
migration/729aaf942e685bb37fdbd5e97293bbcb-7b2b9cefa7dd57d51b587c3501a1b9ef/obelisk-upgradenew file
No changes to the content.
migration/73bcf3ed6593d074bd097cd43f5145d1-375acf7f2a3b3c487da2a6b30735d74d/obelisk-handoffnew file
No changes to the content.
migration/73bcf3ed6593d074bd097cd43f5145d1-375acf7f2a3b3c487da2a6b30735d74d/obelisk-upgradenew file
No changes to the content.
migration/73e2573d9074884d7ae83106d55ad229-729aaf942e685bb37fdbd5e97293bbcb/obelisk-handoffnew file
No changes to the content.
migration/73e2573d9074884d7ae83106d55ad229-729aaf942e685bb37fdbd5e97293bbcb/obelisk-upgradenew file
No changes to the content.
migration/7447d29f4712c72458c049cdb9380753-e24a771ee59f24e45afc0a27307f2070/obelisk-handoffnew file
No changes to the content.
migration/7447d29f4712c72458c049cdb9380753-e24a771ee59f24e45afc0a27307f2070/obelisk-upgradenew file
No changes to the content.
migration/749df832c9305e1c6309a794b0c1b201-2619464637b85cb35376393fe612ddae/obelisk-handoffnew file
No changes to the content.
migration/749df832c9305e1c6309a794b0c1b201-2619464637b85cb35376393fe612ddae/obelisk-upgradenew file
No changes to the content.
migration/75c9cd9763d0ed89c17d455f49d66a4f-94372bb14f8afd19bce24f8402c7564c/obelisk-handoffnew file
No changes to the content.
migration/75c9cd9763d0ed89c17d455f49d66a4f-94372bb14f8afd19bce24f8402c7564c/obelisk-upgradenew file
No changes to the content.
migration/77a721a8fa97d1f84dd2af622089871b-cb2d040cc16752b1dc89eecf200ea798/obelisk-handoffnew file
No changes to the content.
migration/77a721a8fa97d1f84dd2af622089871b-cb2d040cc16752b1dc89eecf200ea798/obelisk-upgradenew file
No changes to the content.
migration/79357758056556793db0472562d62ff9-f15c9c2f894c8cc4fac07e2cdef47180/obelisk-handoffnew file
No changes to the content.
migration/79357758056556793db0472562d62ff9-f15c9c2f894c8cc4fac07e2cdef47180/obelisk-upgradenew file
No changes to the content.
migration/7adece557941a7d7ac7c150f77f758be-67173f110f2c54d135e6943b65118303/obelisk-handoffnew file
No changes to the content.
migration/7adece557941a7d7ac7c150f77f758be-67173f110f2c54d135e6943b65118303/obelisk-upgradenew file
No changes to the content.
migration/7b2b9cefa7dd57d51b587c3501a1b9ef-1269155fffe21926f3704d5077e04332/obelisk-handoffnew file
No changes to the content.
migration/7b2b9cefa7dd57d51b587c3501a1b9ef-1269155fffe21926f3704d5077e04332/obelisk-upgradenew file
No changes to the content.
migration/7b75ffdcd86b5a330d9c325ad5767393-97873167212295a0fdc1219102b6b600/obelisk-handoffnew file
No changes to the content.
migration/7b75ffdcd86b5a330d9c325ad5767393-97873167212295a0fdc1219102b6b600/obelisk-upgradenew file
No changes to the content.
migration/7b9c28d09bd962e89b9715500f9e184c-df71cd82e801ca701bf4bc44b80b6d50/obelisk-handoffnew file
No changes to the content.
migration/7b9c28d09bd962e89b9715500f9e184c-df71cd82e801ca701bf4bc44b80b6d50/obelisk-upgradenew file
No changes to the content.
migration/7bc4c944b24621967a6334f522437cc1-3e8a6968585d3d60f3b13652a386aec0/obelisk-handoffnew file
No changes to the content.
migration/7bc4c944b24621967a6334f522437cc1-3e8a6968585d3d60f3b13652a386aec0/obelisk-upgradenew file
No changes to the content.
migration/7c0ab1210424b615251ef57036d316c3-b8aa42d4f2e5ef7db9ba34ecf6c42079/obelisk-handoffnew file
No changes to the content.
migration/7c0ab1210424b615251ef57036d316c3-b8aa42d4f2e5ef7db9ba34ecf6c42079/obelisk-upgradenew file
No changes to the content.
migration/7c98ef4a123c1a580d368d14e54961b7-adad34a1075a2fd51e1a1c06ec9d002d/obelisk-handoffnew file
No changes to the content.
migration/7c98ef4a123c1a580d368d14e54961b7-adad34a1075a2fd51e1a1c06ec9d002d/obelisk-upgradenew file
No changes to the content.
migration/7ce32302cfbba8b7567a05144057f8a3-28c5fec3b7b0b14b3625d5c8f60bcf68/obelisk-handoffnew file
No changes to the content.
migration/7ce32302cfbba8b7567a05144057f8a3-28c5fec3b7b0b14b3625d5c8f60bcf68/obelisk-upgradenew file
No changes to the content.
migration/7cf814f7404638f1c1be662a24d7cfea-e609d945b642fd9d4ce99931e3699db7/obelisk-handoffnew file
No changes to the content.
migration/7cf814f7404638f1c1be662a24d7cfea-e609d945b642fd9d4ce99931e3699db7/obelisk-upgradenew file
No changes to the content.
migration/7dd60550d7d79bd3923d91d67ecb433a-46aed07c1a61476dd245ff8e74c0559f/obelisk-handoffnew file
No changes to the content.
migration/7dd60550d7d79bd3923d91d67ecb433a-46aed07c1a61476dd245ff8e74c0559f/obelisk-upgradenew file
No changes to the content.
migration/7f46d84c4d303f30602b920f96acf871-97c7e1ea2c1ddb0ef23d2e4a9279f65c/obelisk-handoffnew file
No changes to the content.
migration/7f46d84c4d303f30602b920f96acf871-97c7e1ea2c1ddb0ef23d2e4a9279f65c/obelisk-upgradenew file
No changes to the content.
migration/7fac40a2398039b364746989bb78798b-68cca18f813a16eb0cc2c3ed7fa08044/obelisk-handoffnew file
No changes to the content.
migration/7fac40a2398039b364746989bb78798b-68cca18f813a16eb0cc2c3ed7fa08044/obelisk-upgradenew file
No changes to the content.
migration/7fc381538805adac19c54b73ea118601-982c57eada4652019d0b0cf6ad355d25/obelisk-handoffnew file
No changes to the content.
migration/7fc381538805adac19c54b73ea118601-982c57eada4652019d0b0cf6ad355d25/obelisk-upgradenew file
No changes to the content.
migration/80cb6b08443d55bb512e4a35b4768b72-59742705e53838ae13066a2239a72040/obelisk-handoffnew file
No changes to the content.
migration/80cb6b08443d55bb512e4a35b4768b72-59742705e53838ae13066a2239a72040/obelisk-upgradenew file
No changes to the content.
migration/81223455459db28cdbc990c3d4e6eedd-71da39c91abb23488021a1e9142507d5/obelisk-handoffnew file
No changes to the content.
migration/81223455459db28cdbc990c3d4e6eedd-71da39c91abb23488021a1e9142507d5/obelisk-upgradenew file
No changes to the content.
migration/8143888349912db9780231c370ea2060-beaa30317dadbef830531237279cbca4/obelisk-handoffnew file
No changes to the content.
migration/8143888349912db9780231c370ea2060-beaa30317dadbef830531237279cbca4/obelisk-upgradenew file
No changes to the content.
migration/81f3dbeef0d78bfb523cd2ac113b8ad8-4ace4414d71235a291f7148f80a0b0f5/obelisk-handoffnew file
No changes to the content.
migration/81f3dbeef0d78bfb523cd2ac113b8ad8-4ace4414d71235a291f7148f80a0b0f5/obelisk-upgradenew file
No changes to the content.
migration/84f4d5c5491abb855d8dbe3acf3bc8e6-01fb7610e4184ad1b491267fcb0c7f43/obelisk-handoffnew file
No changes to the content.
migration/84f4d5c5491abb855d8dbe3acf3bc8e6-01fb7610e4184ad1b491267fcb0c7f43/obelisk-upgradenew file
No changes to the content.
migration/85c2c4fb8472d50eaa97aed0bdc9b142-3e253c6beb567ba8f4c8086a76b70645/obelisk-handoffnew file
No changes to the content.
migration/85c2c4fb8472d50eaa97aed0bdc9b142-3e253c6beb567ba8f4c8086a76b70645/obelisk-upgradenew file
No changes to the content.
migration/8746b8f08b75b2bb7c22d8f17f22d72d-e3b3014d7e0dc983fe118d32546654e5/obelisk-handoffnew file
No changes to the content.
migration/8746b8f08b75b2bb7c22d8f17f22d72d-e3b3014d7e0dc983fe118d32546654e5/obelisk-upgradenew file
No changes to the content.
migration/88046de6fbbdad3f6a8a5d311948ff9b-4a037879b8a542f1590cbb87b0242331/obelisk-handoffnew file
No changes to the content.
migration/88046de6fbbdad3f6a8a5d311948ff9b-4a037879b8a542f1590cbb87b0242331/obelisk-upgradenew file
No changes to the content.
migration/88439725720c7ba784750cda91157dfd-69cd775958e25e3b2cff7e4897f2a9bb/obelisk-handoffnew file
No changes to the content.
migration/88439725720c7ba784750cda91157dfd-69cd775958e25e3b2cff7e4897f2a9bb/obelisk-upgradenew file
No changes to the content.
migration/88765caf54c2bf9f23ea4d05816a9f65-52330622ae996023b3f80a40991521a4/obelisk-handoffnew file
No changes to the content.
migration/88765caf54c2bf9f23ea4d05816a9f65-52330622ae996023b3f80a40991521a4/obelisk-upgradenew file
No changes to the content.
migration/8911be694899014078c8e79fc858be45-33da5c0e6705843afa6e39e65bfa0564/obelisk-handoffnew file
No changes to the content.
migration/8911be694899014078c8e79fc858be45-33da5c0e6705843afa6e39e65bfa0564/obelisk-upgradenew file
No changes to the content.
migration/8a64ec5f2ee43d69607499f085560857-cba28ee4b736cf8ed43cb342ccaef12e/obelisk-handoffnew file
No changes to the content.
migration/8a64ec5f2ee43d69607499f085560857-cba28ee4b736cf8ed43cb342ccaef12e/obelisk-upgradenew file
No changes to the content.
migration/8adb921f4dbb9d363bf6647d304e02fe-bb2f504aa4f6f1ab8ed75697dc773efb/obelisk-handoffnew file
No changes to the content.
migration/8adb921f4dbb9d363bf6647d304e02fe-bb2f504aa4f6f1ab8ed75697dc773efb/obelisk-upgradenew file
No changes to the content.
migration/8b098f49e1fd1aa10ca22b9dc312644e-0ec6f67d94d3913cf3243d813238ec39/obelisk-handoffnew file
No changes to the content.
migration/8b098f49e1fd1aa10ca22b9dc312644e-0ec6f67d94d3913cf3243d813238ec39/obelisk-upgradenew file
No changes to the content.
migration/8c7718f94e2676c957fe844b7dc2637c-96c3d49b08a9d2cfb059197c60f49c05/obelisk-handoffnew file
No changes to the content.
migration/8c7718f94e2676c957fe844b7dc2637c-96c3d49b08a9d2cfb059197c60f49c05/obelisk-upgradenew file
No changes to the content.
migration/8c9599be2461288eee7e78d7a34643d8-053c0b2f84a7a2eadeb5c3f9575ffa0d/obelisk-handoffnew file
No changes to the content.
migration/8c9599be2461288eee7e78d7a34643d8-053c0b2f84a7a2eadeb5c3f9575ffa0d/obelisk-upgradenew file
No changes to the content.
migration/8d5dba70e93ae7e784ea238281842408-c93529e579e64263fd91663216fd3fe7/obelisk-handoffnew file
No changes to the content.
migration/8d5dba70e93ae7e784ea238281842408-c93529e579e64263fd91663216fd3fe7/obelisk-upgradenew file
No changes to the content.
migration/8dcf46725e839f4a5dcb7a9f0235e01b-0da1150f0e9fe57d1fafbb6cabe24fa1/obelisk-handoffnew file
No changes to the content.
migration/8dcf46725e839f4a5dcb7a9f0235e01b-0da1150f0e9fe57d1fafbb6cabe24fa1/obelisk-upgradenew file
No changes to the content.
migration/8df9e256d8e43fa4e22d953024269b5b-9d86cedf551a6ee24c5ae6779021b925/obelisk-handoffnew file
No changes to the content.
migration/8df9e256d8e43fa4e22d953024269b5b-9d86cedf551a6ee24c5ae6779021b925/obelisk-upgradenew file
No changes to the content.
migration/8e01c7fee83aa74229a77772d8c928f3-8911be694899014078c8e79fc858be45/obelisk-handoffnew file
No changes to the content.
migration/8e01c7fee83aa74229a77772d8c928f3-8911be694899014078c8e79fc858be45/obelisk-upgradenew file
No changes to the content.
migration/8e1e426afe242ea3079f160bccd94cf0-610dcf99f7eb8c8b591ff3f5f1722938/obelisk-handoffnew file
No changes to the content.
migration/8e1e426afe242ea3079f160bccd94cf0-610dcf99f7eb8c8b591ff3f5f1722938/obelisk-upgradenew file
No changes to the content.
migration/8fca2c4d6478ed8628925852b2af45bf-03b845f308e73a1567eb843dfca182d2/obelisk-handoffnew file
No changes to the content.
migration/8fca2c4d6478ed8628925852b2af45bf-03b845f308e73a1567eb843dfca182d2/obelisk-upgradenew file
No changes to the content.
migration/90489c1a9f7edd46a070d6474dceac86-7fac40a2398039b364746989bb78798b/obelisk-handoffnew file
No changes to the content.
migration/90489c1a9f7edd46a070d6474dceac86-7fac40a2398039b364746989bb78798b/obelisk-upgradenew file
No changes to the content.
migration/909b6b2066cbdc991e6c16455f13637c-1e36dc8817e2784df36d643b7499092d/obelisk-handoffnew file
No changes to the content.
migration/909b6b2066cbdc991e6c16455f13637c-1e36dc8817e2784df36d643b7499092d/obelisk-upgradenew file
No changes to the content.
migration/90b4e20464e08ade5f215f2a4ea28cc6-b7630e5f30252987bf899c74de3953ad/obelisk-handoffnew file
No changes to the content.
migration/90b4e20464e08ade5f215f2a4ea28cc6-b7630e5f30252987bf899c74de3953ad/obelisk-upgradenew file
No changes to the content.
migration/9217718d2b3b217dbba72e7091459548-88046de6fbbdad3f6a8a5d311948ff9b/obelisk-handoffnew file
No changes to the content.
migration/9217718d2b3b217dbba72e7091459548-88046de6fbbdad3f6a8a5d311948ff9b/obelisk-upgradenew file
No changes to the content.
migration/9223532a537f698ef36f4dce6e922a6b-d327ceb69d24e40dc11792840454cff9/obelisk-handoffnew file
No changes to the content.
migration/9223532a537f698ef36f4dce6e922a6b-d327ceb69d24e40dc11792840454cff9/obelisk-upgradenew file
No changes to the content.
migration/92ab733e8991f12cc12148f8d4e1b35d-e4b89dd259f3831bc60b56147b3586a6/obelisk-handoffnew file
No changes to the content.
migration/92ab733e8991f12cc12148f8d4e1b35d-e4b89dd259f3831bc60b56147b3586a6/obelisk-upgradenew file
No changes to the content.
migration/930e9213e072e87c207f93461bd3c96c-7f46d84c4d303f30602b920f96acf871/obelisk-handoffnew file
No changes to the content.
migration/930e9213e072e87c207f93461bd3c96c-7f46d84c4d303f30602b920f96acf871/obelisk-upgradenew file
No changes to the content.
migration/93f164bbd887f688fd49bb80afffccd6-f8eee9431c8b7a767d96da0a8cfae95d/obelisk-handoffnew file
No changes to the content.
migration/93f164bbd887f688fd49bb80afffccd6-f8eee9431c8b7a767d96da0a8cfae95d/obelisk-upgradenew file
No changes to the content.
migration/93fc625d41bae9fcf06b9febeacb686d-662eb30d6e4612f130edc3f6d37d92a2/obelisk-handoffnew file
No changes to the content.
migration/93fc625d41bae9fcf06b9febeacb686d-662eb30d6e4612f130edc3f6d37d92a2/obelisk-upgradenew file
No changes to the content.
migration/94252585266fa745a91733888e2f50b7-d207ab99328b8ece5fb27fa1f4dbf9f6/obelisk-handoffnew file
No changes to the content.
migration/94252585266fa745a91733888e2f50b7-d207ab99328b8ece5fb27fa1f4dbf9f6/obelisk-upgradenew file
No changes to the content.
migration/94372bb14f8afd19bce24f8402c7564c-7dd60550d7d79bd3923d91d67ecb433a/obelisk-handoffnew file
No changes to the content.
migration/94372bb14f8afd19bce24f8402c7564c-7dd60550d7d79bd3923d91d67ecb433a/obelisk-upgradenew file
No changes to the content.
migration/947784563e3a27cf6486bed9a3c52f15-25615a274650370633e8083ed19fbdd0/obelisk-handoffnew file
No changes to the content.
migration/947784563e3a27cf6486bed9a3c52f15-25615a274650370633e8083ed19fbdd0/obelisk-upgradenew file
No changes to the content.
migration/94dd4eb45a4e7eb945027d5fd8eb4fa6-e22c51145e08d4a089003460ae80a7f9/obelisk-handoffnew file
No changes to the content.
migration/94dd4eb45a4e7eb945027d5fd8eb4fa6-e22c51145e08d4a089003460ae80a7f9/obelisk-upgradenew file
No changes to the content.
migration/95955d1dd0981ebb538ee941cde1f963-07900e6c41ab9a78917f66fcf7e0d9b8/obelisk-handoffnew file
No changes to the content.
migration/95955d1dd0981ebb538ee941cde1f963-07900e6c41ab9a78917f66fcf7e0d9b8/obelisk-upgradenew file
No changes to the content.
migration/959ae2925bcd2b5beff631a8e7aeaf00-071f5b3326b8886de3eddc927b80eb10/obelisk-handoffnew file
No changes to the content.
migration/959ae2925bcd2b5beff631a8e7aeaf00-071f5b3326b8886de3eddc927b80eb10/obelisk-upgradenew file
No changes to the content.
migration/962e2bd1ae8d0c233f8da89ecebffe50-d6c090699acd25fc60213ff4bd9eebcf/obelisk-handoffnew file
No changes to the content.
migration/962e2bd1ae8d0c233f8da89ecebffe50-d6c090699acd25fc60213ff4bd9eebcf/obelisk-upgradenew file
No changes to the content.
migration/96c3d49b08a9d2cfb059197c60f49c05-6da2e5b02ab780d12527640722d2b511/obelisk-handoffnew file
No changes to the content.
migration/96c3d49b08a9d2cfb059197c60f49c05-6da2e5b02ab780d12527640722d2b511/obelisk-upgradenew file
No changes to the content.
migration/9705509619667aacf799e91927c16dc8-2773d4a82ca59b48faf036755e163a5f/obelisk-handoffnew file
No changes to the content.
migration/9705509619667aacf799e91927c16dc8-2773d4a82ca59b48faf036755e163a5f/obelisk-upgradenew file
No changes to the content.
migration/9715967793be68dfd8c9a7d631f8d589-d532356dfb65a4c8a39452d2ebca2516/obelisk-handoffnew file
No changes to the content.
migration/9715967793be68dfd8c9a7d631f8d589-d532356dfb65a4c8a39452d2ebca2516/obelisk-upgradenew file
No changes to the content.
migration/975cd8245d06ad09e4fb176cfc9af5e0-3f23549fba6f8e67ba36b4405e8680b3/obelisk-handoffnew file
No changes to the content.
migration/975cd8245d06ad09e4fb176cfc9af5e0-3f23549fba6f8e67ba36b4405e8680b3/obelisk-upgradenew file
No changes to the content.
migration/97873167212295a0fdc1219102b6b600-88439725720c7ba784750cda91157dfd/obelisk-handoffnew file
No changes to the content.
migration/97873167212295a0fdc1219102b6b600-88439725720c7ba784750cda91157dfd/obelisk-upgradenew file
No changes to the content.
migration/97c7e1ea2c1ddb0ef23d2e4a9279f65c-d747c3d0620b2034aca44265dd1c8c86/obelisk-handoffnew file
No changes to the content.
migration/97c7e1ea2c1ddb0ef23d2e4a9279f65c-d747c3d0620b2034aca44265dd1c8c86/obelisk-upgradenew file
No changes to the content.
migration/982c57eada4652019d0b0cf6ad355d25-6708d14c71c475ac494808e75660a385/obelisk-handoffnew file
No changes to the content.
migration/982c57eada4652019d0b0cf6ad355d25-6708d14c71c475ac494808e75660a385/obelisk-upgradenew file
No changes to the content.
migration/98629a336dcd5fa3524a09f8ea6ad636-8c9599be2461288eee7e78d7a34643d8/obelisk-handoffnew file
No changes to the content.
migration/98629a336dcd5fa3524a09f8ea6ad636-8c9599be2461288eee7e78d7a34643d8/obelisk-upgradenew file
No changes to the content.
migration/98da197f133bca81e533ab5ffbf4c82d-a0b418c582d5c6ef9e0cd9a19abe046e/obelisk-handoffnew file
No changes to the content.
migration/98da197f133bca81e533ab5ffbf4c82d-a0b418c582d5c6ef9e0cd9a19abe046e/obelisk-upgradenew file
No changes to the content.
migration/98f9e0bbd21e36b0f4daa440d11e7177-bab3871bcfbfcb3d2bad4495d44ae4f8/obelisk-handoffnew file
No changes to the content.
migration/98f9e0bbd21e36b0f4daa440d11e7177-bab3871bcfbfcb3d2bad4495d44ae4f8/obelisk-upgradenew file
No changes to the content.
migration/99aebbfcac51725464bc59571b7d71b8-34d14d4a444d3bf1487ada6129815df4/obelisk-handoffnew file
No changes to the content.
migration/99aebbfcac51725464bc59571b7d71b8-34d14d4a444d3bf1487ada6129815df4/obelisk-upgradenew file
No changes to the content.
migration/9b33a49f0c978e27b752fa457aca97cc-51f3d5068259f596b811450f0bf25419/obelisk-handoffnew file
No changes to the content.
migration/9b33a49f0c978e27b752fa457aca97cc-51f3d5068259f596b811450f0bf25419/obelisk-upgradenew file
No changes to the content.
migration/9b9d73736773550dde7d36de2498b029-69537089a0e97845ffe05c73c3cc1536/obelisk-handoffnew file
No changes to the content.
migration/9b9d73736773550dde7d36de2498b029-69537089a0e97845ffe05c73c3cc1536/obelisk-upgradenew file
No changes to the content.
migration/9c94cd459a25b5c2de56067c8fdac620-721a02f3a7549e7d6efeaed087bfacb5/obelisk-handoffnew file
No changes to the content.
migration/9c94cd459a25b5c2de56067c8fdac620-721a02f3a7549e7d6efeaed087bfacb5/obelisk-upgradenew file
No changes to the content.
migration/9cef2dd1885f16455ff9b6801ec99454-63ff9426130baa080f37400a86c55fdd/obelisk-handoffnew file
No changes to the content.
migration/9cef2dd1885f16455ff9b6801ec99454-63ff9426130baa080f37400a86c55fdd/obelisk-upgradenew file
No changes to the content.
migration/9d86cedf551a6ee24c5ae6779021b925-1c9bdb5c544f2e5432f549653a61dabe/obelisk-handoffnew file
No changes to the content.
migration/9d86cedf551a6ee24c5ae6779021b925-1c9bdb5c544f2e5432f549653a61dabe/obelisk-upgradenew file
No changes to the content.
migration/9e05cc6b81c6380ba0ac59bad1021cac-ce7f0d15d71d36d4bcf4fbadcdfa1b58/obelisk-handoffnew file
No changes to the content.
migration/9e05cc6b81c6380ba0ac59bad1021cac-ce7f0d15d71d36d4bcf4fbadcdfa1b58/obelisk-upgradenew file
No changes to the content.
migration/9e61f37dd79310b71c80ee166aeb4e80-466a19f9cfa2a103467fe6627225d27f/obelisk-handoffnew file
No changes to the content.
migration/9e61f37dd79310b71c80ee166aeb4e80-466a19f9cfa2a103467fe6627225d27f/obelisk-upgradenew file
No changes to the content.
migration/9f6c8fd80ab3ff2dcc48bfd211c8b761-24f151a674662ac47845237d7a9f6a4f/obelisk-handoffnew file
No changes to the content.
migration/9f6c8fd80ab3ff2dcc48bfd211c8b761-24f151a674662ac47845237d7a9f6a4f/obelisk-upgradenew file
No changes to the content.
migration/9f71fc9d2ed805fecbc8c4a3aae5685b-cb2197d0267b52385ee39bf4adcfe475/obelisk-handoffnew file
No changes to the content.
migration/9f71fc9d2ed805fecbc8c4a3aae5685b-cb2197d0267b52385ee39bf4adcfe475/obelisk-upgradenew file
No changes to the content.
migration/9fb504b182cbd8dfb666175b651e0b49-338b92f88c49c7c3a1d8ec231d9f7189/obelisk-handoffnew file
No changes to the content.
migration/9fb504b182cbd8dfb666175b651e0b49-338b92f88c49c7c3a1d8ec231d9f7189/obelisk-upgradenew file
No changes to the content.
migration/9fe2922674fcee5e840c9f52cf534f88-ac92878eb85c5283bcc5f3f34cd35aa6/obelisk-handoffnew file
No changes to the content.
migration/9fe2922674fcee5e840c9f52cf534f88-ac92878eb85c5283bcc5f3f34cd35aa6/obelisk-upgradenew file
No changes to the content.
migration/a00870cfca85ff8dae5db3a78f5a227d-c2502429502b46133a749f021d188cf0/obelisk-handoffnew file
No changes to the content.
migration/a00870cfca85ff8dae5db3a78f5a227d-c2502429502b46133a749f021d188cf0/obelisk-upgradenew file
No changes to the content.
migration/a080b8549e32f21fec11084f4ab1e052-bd7c24fc03bf82e5a990515531a69147/obelisk-handoffnew file
No changes to the content.
migration/a080b8549e32f21fec11084f4ab1e052-bd7c24fc03bf82e5a990515531a69147/obelisk-upgradenew file
No changes to the content.
migration/a08aebb6f40fb45d174b229532314ac4-1c1c3f2b4426d5d26377eef08dbccb3c/obelisk-handoffnew file
No changes to the content.
migration/a08aebb6f40fb45d174b229532314ac4-1c1c3f2b4426d5d26377eef08dbccb3c/obelisk-upgradenew file
No changes to the content.
migration/a0b418c582d5c6ef9e0cd9a19abe046e-63422a9bade5c703461b600e03e227f0/obelisk-handoffnew file
No changes to the content.
migration/a0b418c582d5c6ef9e0cd9a19abe046e-63422a9bade5c703461b600e03e227f0/obelisk-upgradenew file
No changes to the content.
migration/a0f5ac2ff5c6a957e565f898a9fc8bfb-bf3fbc8db3d6fd42a37982dfb5a30b5c/obelisk-handoffnew file
No changes to the content.
migration/a0f5ac2ff5c6a957e565f898a9fc8bfb-bf3fbc8db3d6fd42a37982dfb5a30b5c/obelisk-upgradenew file
No changes to the content.
migration/a262678b7d849de5a9732d06199666ae-ebb95c5864d1d1d2f78fbf7cc8235dac/obelisk-handoffnew file
No changes to the content.
migration/a262678b7d849de5a9732d06199666ae-ebb95c5864d1d1d2f78fbf7cc8235dac/obelisk-upgradenew file
No changes to the content.
migration/a2665cfd303a4a34a7bfa3b7d0d264bb-aff3f143ddaa4ed3ffad873d6638e277/obelisk-handoffnew file
No changes to the content.
migration/a2665cfd303a4a34a7bfa3b7d0d264bb-aff3f143ddaa4ed3ffad873d6638e277/obelisk-upgradenew file
No changes to the content.
migration/a2b33f8dc1f90b8dddce942b174be5b8-00a480749e96ef7e21fbe8f4943152b1/obelisk-handoffnew file
No changes to the content.
migration/a2b33f8dc1f90b8dddce942b174be5b8-00a480749e96ef7e21fbe8f4943152b1/obelisk-upgradenew file
No changes to the content.
migration/a2bdbf1985b75442e78e84523c4ced5f-48c027da19d895ba03e7b9120f771291/obelisk-handoffnew file
No changes to the content.
migration/a2bdbf1985b75442e78e84523c4ced5f-48c027da19d895ba03e7b9120f771291/obelisk-upgradenew file
No changes to the content.
migration/a34d758508a0a5d08bc10e76482c4d11-8fca2c4d6478ed8628925852b2af45bf/obelisk-handoffnew file
No changes to the content.
migration/a34d758508a0a5d08bc10e76482c4d11-8fca2c4d6478ed8628925852b2af45bf/obelisk-upgradenew file
No changes to the content.
migration/a4477746ae1a7024bc3cd0807180d418-fb5110e527cf77c568bcae7d435ee035/obelisk-handoffnew file
No changes to the content.
migration/a4477746ae1a7024bc3cd0807180d418-fb5110e527cf77c568bcae7d435ee035/obelisk-upgradenew file
No changes to the content.
migration/a5297d8457aa4c327f35d4697f6cb66f-2f928f9c4a137186f808a231c9e3c141/obelisk-handoffnew file
No changes to the content.
migration/a5297d8457aa4c327f35d4697f6cb66f-2f928f9c4a137186f808a231c9e3c141/obelisk-upgradenew file
No changes to the content.
migration/a536d01b9c489f5e00e30abf27427da0-b8a8ffc909e5fc350d9f8a6a90e7b3d9/obelisk-handoffnew file
No changes to the content.
migration/a536d01b9c489f5e00e30abf27427da0-b8a8ffc909e5fc350d9f8a6a90e7b3d9/obelisk-upgradenew file
No changes to the content.
migration/a5904e3a899e346fe2fe26314f69c075-e4513b485b6ba8b7f4bd34b12458ff0c/obelisk-handoffnew file
No changes to the content.
migration/a5904e3a899e346fe2fe26314f69c075-e4513b485b6ba8b7f4bd34b12458ff0c/obelisk-upgradenew file
No changes to the content.
migration/a5bbc0b9ceab9c3f7ca32a7da8c1f685-3c4b3152f0608d23d07240e2df3ff8da/obelisk-handoffnew file
No changes to the content.
migration/a5bbc0b9ceab9c3f7ca32a7da8c1f685-3c4b3152f0608d23d07240e2df3ff8da/obelisk-upgradenew file
No changes to the content.
migration/a5e40d67714a1b2daa06e3fb3293216d-04a5ae41e8fcef5f719cb84d01430382/obelisk-handoffnew file
No changes to the content.
migration/a5e40d67714a1b2daa06e3fb3293216d-04a5ae41e8fcef5f719cb84d01430382/obelisk-upgradenew file
No changes to the content.
migration/a632452cf9c5956becd1a40512472b43-d6a3ea5f7eb3c95f5e5ea09fb1330270/obelisk-handoffnew file
No changes to the content.
migration/a632452cf9c5956becd1a40512472b43-d6a3ea5f7eb3c95f5e5ea09fb1330270/obelisk-upgradenew file
No changes to the content.
migration/a6633fc9687566aa84c3d5d5e2bff4fd-b5cab336f6244293913433101cd0feff/obelisk-handoffnew file
No changes to the content.
migration/a6633fc9687566aa84c3d5d5e2bff4fd-b5cab336f6244293913433101cd0feff/obelisk-upgradenew file
No changes to the content.
migration/a6d50c7e756dfd1e3ae891111b81a1ae-270b3b3f7bc510c692e3253a895f122a/obelisk-handoffnew file
No changes to the content.
migration/a6d50c7e756dfd1e3ae891111b81a1ae-270b3b3f7bc510c692e3253a895f122a/obelisk-upgradenew file
No changes to the content.
migration/a7c7b80a875d1863f027c97eb433b884-cbe155a3e856fb7eaad0bd18feb408be/obelisk-handoffnew file
No changes to the content.
migration/a7c7b80a875d1863f027c97eb433b884-cbe155a3e856fb7eaad0bd18feb408be/obelisk-upgradenew file
No changes to the content.
migration/a808a04519bbf2874e41028ded9c180c-8e1e426afe242ea3079f160bccd94cf0/obelisk-handoffnew file
No changes to the content.
migration/a808a04519bbf2874e41028ded9c180c-8e1e426afe242ea3079f160bccd94cf0/obelisk-upgradenew file
No changes to the content.
migration/a825fbb74e8d050a5ce10ec4c62b83f5-c3298d81428117be58196b80853e3c7b/obelisk-handoffnew file
No changes to the content.
migration/a825fbb74e8d050a5ce10ec4c62b83f5-c3298d81428117be58196b80853e3c7b/obelisk-upgradenew file
No changes to the content.
migration/a826f01311fe00ef5cf87568832605d3-9fb504b182cbd8dfb666175b651e0b49/obelisk-handoffnew file
No changes to the content.
migration/a826f01311fe00ef5cf87568832605d3-9fb504b182cbd8dfb666175b651e0b49/obelisk-upgradenew file
No changes to the content.
migration/a878ad346b9305ad2ae3fd85410a7ed7-0f78d9d6875933336bbf0c1c8888b73f/obelisk-handoffnew file
No changes to the content.
migration/a878ad346b9305ad2ae3fd85410a7ed7-0f78d9d6875933336bbf0c1c8888b73f/obelisk-upgradenew file
No changes to the content.
migration/a8ebe201ab7a7b0211de41b14a95192e-65071e255719af6389857646bc087d37/obelisk-handoffnew file
No changes to the content.
migration/a8ebe201ab7a7b0211de41b14a95192e-65071e255719af6389857646bc087d37/obelisk-upgradenew file
No changes to the content.
migration/a96d491705acc139315264fd340d228f-be615dc4d60c9d8710fbd120857c2fa1/obelisk-handoffnew file
No changes to the content.
migration/a96d491705acc139315264fd340d228f-be615dc4d60c9d8710fbd120857c2fa1/obelisk-upgradenew file
No changes to the content.
migration/aa8e6712fd46824acdea303068634078-dc95f39fb27634ccbca7318ff2065608/obelisk-handoffnew file
No changes to the content.
migration/aa8e6712fd46824acdea303068634078-dc95f39fb27634ccbca7318ff2065608/obelisk-upgradenew file
No changes to the content.
migration/aaf979dd0121603a8b48127e1ea47c7d-4b7a36dce2c1cf94554e12d8a7903269/obelisk-handoffnew file
No changes to the content.
migration/aaf979dd0121603a8b48127e1ea47c7d-4b7a36dce2c1cf94554e12d8a7903269/obelisk-upgradenew file
No changes to the content.
migration/abdb013baf130e1bd7e7fd04e535c44c-39df6544d6b035d4c77cc829729f9c19/obelisk-handoffnew file
No changes to the content.
migration/abdb013baf130e1bd7e7fd04e535c44c-39df6544d6b035d4c77cc829729f9c19/obelisk-upgradenew file
No changes to the content.
migration/ac5e28caddd6832d0bf1f7445a4a6a28-e01cc01684deaf4c75ad5d65270bf3d5/obelisk-handoffnew file
No changes to the content.
migration/ac5e28caddd6832d0bf1f7445a4a6a28-e01cc01684deaf4c75ad5d65270bf3d5/obelisk-upgradenew file
No changes to the content.
migration/ac92878eb85c5283bcc5f3f34cd35aa6-145650b66e83adf5bdb8261593076379/obelisk-handoffnew file
No changes to the content.
migration/ac92878eb85c5283bcc5f3f34cd35aa6-145650b66e83adf5bdb8261593076379/obelisk-upgradenew file
No changes to the content.
migration/adad34a1075a2fd51e1a1c06ec9d002d-930e9213e072e87c207f93461bd3c96c/obelisk-handoffnew file
No changes to the content.
migration/adad34a1075a2fd51e1a1c06ec9d002d-930e9213e072e87c207f93461bd3c96c/obelisk-upgradenew file
No changes to the content.
migration/ae4328daeb7d2c52c534a687fbd184b6-3e763ebe16cc3a261f5776ab1f1f6d9a/obelisk-handoffnew file
No changes to the content.
migration/ae4328daeb7d2c52c534a687fbd184b6-3e763ebe16cc3a261f5776ab1f1f6d9a/obelisk-upgradenew file
No changes to the content.
migration/ae824b83b569b3b23b8e198190371ca0-81f3dbeef0d78bfb523cd2ac113b8ad8/obelisk-handoffnew file
No changes to the content.
migration/ae824b83b569b3b23b8e198190371ca0-81f3dbeef0d78bfb523cd2ac113b8ad8/obelisk-upgradenew file
No changes to the content.
migration/ae8862f8b7292690c852111ca5659ce9-ae4328daeb7d2c52c534a687fbd184b6/obelisk-handoffnew file
No changes to the content.
migration/ae8862f8b7292690c852111ca5659ce9-ae4328daeb7d2c52c534a687fbd184b6/obelisk-upgradenew file
No changes to the content.
migration/aeafaa5d20bab4439a5d31f29023a839-79357758056556793db0472562d62ff9/obelisk-handoffnew file
No changes to the content.
migration/aeafaa5d20bab4439a5d31f29023a839-79357758056556793db0472562d62ff9/obelisk-upgradenew file
No changes to the content.
migration/aef6ac052515a869c7d3061d4ccc69dd-15f3f3f517b38664131b85da0594522f/obelisk-handoffnew file
No changes to the content.
migration/aef6ac052515a869c7d3061d4ccc69dd-15f3f3f517b38664131b85da0594522f/obelisk-upgradenew file
No changes to the content.
migration/afbd010add526d309dccee6895e42d0d-6f3a0f756847ee5aa208a1dc79404779/obelisk-handoffnew file
No changes to the content.
migration/afbd010add526d309dccee6895e42d0d-6f3a0f756847ee5aa208a1dc79404779/obelisk-upgradenew file
No changes to the content.
migration/aff3f143ddaa4ed3ffad873d6638e277-6fc44f180bb825d5b372444339681135/obelisk-handoffnew file
No changes to the content.
migration/aff3f143ddaa4ed3ffad873d6638e277-6fc44f180bb825d5b372444339681135/obelisk-upgradenew file
No changes to the content.
migration/b0c1d039514a686529561f186332c91e-a878ad346b9305ad2ae3fd85410a7ed7/obelisk-handoffnew file
No changes to the content.
migration/b0c1d039514a686529561f186332c91e-a878ad346b9305ad2ae3fd85410a7ed7/obelisk-upgradenew file
No changes to the content.
migration/b1692927da5ed59af72fac5be8094fa2-d5ba6f13296205e5bd88ca2756107df0/obelisk-handoffnew file
No changes to the content.
migration/b1692927da5ed59af72fac5be8094fa2-d5ba6f13296205e5bd88ca2756107df0/obelisk-upgradenew file
No changes to the content.
migration/b2282679f6f63cec89d921c3dd512d1e-1f09fcf196bfe90ed52f113c38ffbfdb/obelisk-handoffnew file
No changes to the content.
migration/b2282679f6f63cec89d921c3dd512d1e-1f09fcf196bfe90ed52f113c38ffbfdb/obelisk-upgradenew file
No changes to the content.
migration/b3b107d406a6a7675cd5b9f52e5f7d8e-0d3b4cfe9b4c72b830780a4c44942176/obelisk-handoffnew file
No changes to the content.
migration/b3b107d406a6a7675cd5b9f52e5f7d8e-0d3b4cfe9b4c72b830780a4c44942176/obelisk-upgradenew file
No changes to the content.
migration/b3ff195b0a90ef5900f9cef2ba75f2fe-24e0d027f4160a6478c1356e51f419cf/obelisk-handoffnew file
No changes to the content.
migration/b3ff195b0a90ef5900f9cef2ba75f2fe-24e0d027f4160a6478c1356e51f419cf/obelisk-upgradenew file
No changes to the content.
migration/b4f6596f2c16811442d8bb2961fbb721-3c7755e495f20cfac5e4dabe78644c5a/obelisk-handoffnew file
No changes to the content.
migration/b4f6596f2c16811442d8bb2961fbb721-3c7755e495f20cfac5e4dabe78644c5a/obelisk-upgradenew file
No changes to the content.
migration/b5cab336f6244293913433101cd0feff-8746b8f08b75b2bb7c22d8f17f22d72d/obelisk-handoffnew file
No changes to the content.
migration/b5cab336f6244293913433101cd0feff-8746b8f08b75b2bb7c22d8f17f22d72d/obelisk-upgradenew file
No changes to the content.
migration/b641dd9c5359201200e483a784456cc2-cc4307fc88e3486b1c3f9ce07db95808/obelisk-handoffnew file
No changes to the content.
migration/b641dd9c5359201200e483a784456cc2-cc4307fc88e3486b1c3f9ce07db95808/obelisk-upgradenew file
No changes to the content.
migration/b6425c7661af2116b912a558e9548013-4aefa80392582ec03a19f946e752d331/obelisk-handoffnew file
No changes to the content.
migration/b6425c7661af2116b912a558e9548013-4aefa80392582ec03a19f946e752d331/obelisk-upgradenew file
No changes to the content.
migration/b68a785fbf27a63334ef1d1fe82b2dac-d6b34eba6ed4f89875a6615fe2c41ec2/obelisk-handoffnew file
No changes to the content.
migration/b68a785fbf27a63334ef1d1fe82b2dac-d6b34eba6ed4f89875a6615fe2c41ec2/obelisk-upgradenew file
No changes to the content.
migration/b6ffce85249f30616da76334c7bbec6e-66a9b6b9e9bc4a5b0cfa78127479b231/obelisk-handoffnew file
No changes to the content.
migration/b6ffce85249f30616da76334c7bbec6e-66a9b6b9e9bc4a5b0cfa78127479b231/obelisk-upgradenew file
No changes to the content.
migration/b7630e5f30252987bf899c74de3953ad-3f0687592a77c0c66193652e9cd981fd/obelisk-handoffnew file
No changes to the content.
migration/b7630e5f30252987bf899c74de3953ad-3f0687592a77c0c66193652e9cd981fd/obelisk-upgradenew file
No changes to the content.
migration/b7e30314f9d0e69efdc84c7eb4e3bcc3-e4051219d89b3c9a8350fe824ad96a82/obelisk-handoffnew file
No changes to the content.
migration/b7e30314f9d0e69efdc84c7eb4e3bcc3-e4051219d89b3c9a8350fe824ad96a82/obelisk-upgradenew file
No changes to the content.
migration/b82e9c3fe22feff4bb3a6be49943fd6c-fbb3eb3209281031dc5183d289eb4e75/obelisk-handoffnew file
No changes to the content.
migration/b82e9c3fe22feff4bb3a6be49943fd6c-fbb3eb3209281031dc5183d289eb4e75/obelisk-upgradenew file
No changes to the content.
migration/b8a8ffc909e5fc350d9f8a6a90e7b3d9-aef6ac052515a869c7d3061d4ccc69dd/obelisk-handoffnew file
No changes to the content.
migration/b8a8ffc909e5fc350d9f8a6a90e7b3d9-aef6ac052515a869c7d3061d4ccc69dd/obelisk-upgradenew file
No changes to the content.
migration/b8aa42d4f2e5ef7db9ba34ecf6c42079-37c2ed0ba2cd54c0ec29eae9043bc4c8/obelisk-handoffnew file
No changes to the content.
migration/b8aa42d4f2e5ef7db9ba34ecf6c42079-37c2ed0ba2cd54c0ec29eae9043bc4c8/obelisk-upgradenew file
No changes to the content.
migration/b95c8d049256fb7360ca14b12acd8544-e7b17aa124e17c357cead8081a58f089/obelisk-handoffnew file
No changes to the content.
migration/b95c8d049256fb7360ca14b12acd8544-e7b17aa124e17c357cead8081a58f089/obelisk-upgradenew file
No changes to the content.
migration/ba0ccd5ef6110f6c5133d9d6ea6277f0-e78ec85bec14f4784faeef4d8d3e4d0a/obelisk-handoffnew file
No changes to the content.
migration/ba0ccd5ef6110f6c5133d9d6ea6277f0-e78ec85bec14f4784faeef4d8d3e4d0a/obelisk-upgradenew file
No changes to the content.
migration/ba4829dc61dcc12bcafd8de5fc8a76dc-2164344be67da5d0e57052c15f30646d/obelisk-handoffnew file
No changes to the content.
migration/ba4829dc61dcc12bcafd8de5fc8a76dc-2164344be67da5d0e57052c15f30646d/obelisk-upgradenew file
No changes to the content.
migration/bab3871bcfbfcb3d2bad4495d44ae4f8-9cef2dd1885f16455ff9b6801ec99454/obelisk-handoffnew file
No changes to the content.
migration/bab3871bcfbfcb3d2bad4495d44ae4f8-9cef2dd1885f16455ff9b6801ec99454/obelisk-upgradenew file
No changes to the content.
migration/bb2f504aa4f6f1ab8ed75697dc773efb-bcd13da3a7fba8514084c1a9dbeabb31/obelisk-handoffnew file
No changes to the content.
migration/bb2f504aa4f6f1ab8ed75697dc773efb-bcd13da3a7fba8514084c1a9dbeabb31/obelisk-upgradenew file
No changes to the content.
migration/bb9a87fc325204313d89dbfb689dba2f-d2d86ce0ca16b143b49374302a9e9e2f/obelisk-handoffnew file
No changes to the content.
migration/bb9a87fc325204313d89dbfb689dba2f-d2d86ce0ca16b143b49374302a9e9e2f/obelisk-upgradenew file
No changes to the content.
migration/bc7688ff47218c0e13d02d917b03caa0-2bdd4221307cafa3e98c4d3017cdc912/obelisk-handoffnew file
No changes to the content.
migration/bc7688ff47218c0e13d02d917b03caa0-2bdd4221307cafa3e98c4d3017cdc912/obelisk-upgradenew file
No changes to the content.
migration/bcd13da3a7fba8514084c1a9dbeabb31-8dcf46725e839f4a5dcb7a9f0235e01b/obelisk-handoffnew file
No changes to the content.
migration/bcd13da3a7fba8514084c1a9dbeabb31-8dcf46725e839f4a5dcb7a9f0235e01b/obelisk-upgradenew file
No changes to the content.
migration/bd6728737dffcc3505d30ada344e2f4b-1cc8d98f6f61441570fdde2d697e6e3c/obelisk-handoffnew file
No changes to the content.
migration/bd6728737dffcc3505d30ada344e2f4b-1cc8d98f6f61441570fdde2d697e6e3c/obelisk-upgradenew file
No changes to the content.
migration/bd7c24fc03bf82e5a990515531a69147-a5e40d67714a1b2daa06e3fb3293216d/obelisk-handoffnew file
No changes to the content.
migration/bd7c24fc03bf82e5a990515531a69147-a5e40d67714a1b2daa06e3fb3293216d/obelisk-upgradenew file
No changes to the content.
migration/bd8f5fa095c3eb19b87d85247b36f1df-5d90abd974b4ec53091a8cb3fc2258ce/obelisk-handoffnew file
No changes to the content.
migration/bd8f5fa095c3eb19b87d85247b36f1df-5d90abd974b4ec53091a8cb3fc2258ce/obelisk-upgradenew file
No changes to the content.
migration/be5a7b436cef0ae35cb074700d5f0d00-7c0ab1210424b615251ef57036d316c3/obelisk-handoffnew file
No changes to the content.
migration/be5a7b436cef0ae35cb074700d5f0d00-7c0ab1210424b615251ef57036d316c3/obelisk-upgradenew file
No changes to the content.
migration/be615dc4d60c9d8710fbd120857c2fa1-1e376437f069662df26178e4a7873190/obelisk-handoffnew file
No changes to the content.
migration/be615dc4d60c9d8710fbd120857c2fa1-1e376437f069662df26178e4a7873190/obelisk-upgradenew file
No changes to the content.
migration/beaa30317dadbef830531237279cbca4-a0f5ac2ff5c6a957e565f898a9fc8bfb/obelisk-handoffnew file
No changes to the content.
migration/beaa30317dadbef830531237279cbca4-a0f5ac2ff5c6a957e565f898a9fc8bfb/obelisk-upgradenew file
No changes to the content.
migration/bf22c91488f01ebf503c1786ee805d0c-64ee9e40f4943e0122398e48019f5616/obelisk-handoffnew file
No changes to the content.
migration/bf22c91488f01ebf503c1786ee805d0c-64ee9e40f4943e0122398e48019f5616/obelisk-upgradenew file
No changes to the content.
migration/bf3fbc8db3d6fd42a37982dfb5a30b5c-0c6127453069ad707e34ddfedb67e2f9/obelisk-handoffnew file
No changes to the content.
migration/bf3fbc8db3d6fd42a37982dfb5a30b5c-0c6127453069ad707e34ddfedb67e2f9/obelisk-upgradenew file
No changes to the content.
migration/bf81caf2a7d528f86acacc1f5d2ffcfa-a5bbc0b9ceab9c3f7ca32a7da8c1f685/obelisk-handoffnew file
No changes to the content.
migration/bf81caf2a7d528f86acacc1f5d2ffcfa-a5bbc0b9ceab9c3f7ca32a7da8c1f685/obelisk-upgradenew file
No changes to the content.
migration/c0811b664036c7996fcfd19fee241a10-a2bdbf1985b75442e78e84523c4ced5f/obelisk-handoffnew file
No changes to the content.
migration/c0811b664036c7996fcfd19fee241a10-a2bdbf1985b75442e78e84523c4ced5f/obelisk-upgradenew file
No changes to the content.
migration/c0c1452986b8d4c181bb0c6c83c56a2f-bf22c91488f01ebf503c1786ee805d0c/obelisk-handoffnew file
No changes to the content.
migration/c0c1452986b8d4c181bb0c6c83c56a2f-bf22c91488f01ebf503c1786ee805d0c/obelisk-upgradenew file
No changes to the content.
migration/c106f9398d5c203a1a296b85c7411bf9-1c2c93819c07f33ad899e39ec6769c3c/obelisk-handoffnew file
No changes to the content.
migration/c106f9398d5c203a1a296b85c7411bf9-1c2c93819c07f33ad899e39ec6769c3c/obelisk-upgradenew file
No changes to the content.
migration/c118e1ee09ec8d9aa3edf04e211ae035-749df832c9305e1c6309a794b0c1b201/obelisk-handoffnew file
No changes to the content.
migration/c118e1ee09ec8d9aa3edf04e211ae035-749df832c9305e1c6309a794b0c1b201/obelisk-upgradenew file
No changes to the content.
migration/c203adc5fed9bf79f67df43d72a4631a-e6980e47ba847245dfab61fb4d5b8ce5/obelisk-handoffnew file
No changes to the content.
migration/c203adc5fed9bf79f67df43d72a4631a-e6980e47ba847245dfab61fb4d5b8ce5/obelisk-upgradenew file
No changes to the content.
migration/c2502429502b46133a749f021d188cf0-93f164bbd887f688fd49bb80afffccd6/obelisk-handoffnew file
No changes to the content.
migration/c2502429502b46133a749f021d188cf0-93f164bbd887f688fd49bb80afffccd6/obelisk-upgradenew file
No changes to the content.
migration/c2d4eeb20f2b319a2d9dc5b6b0762f83-01c0ee6c93a3f9f69e097c41169927ac/obelisk-handoffnew file
No changes to the content.
migration/c2d4eeb20f2b319a2d9dc5b6b0762f83-01c0ee6c93a3f9f69e097c41169927ac/obelisk-upgradenew file
No changes to the content.
migration/c2d581d48dd821a94d7906ca8f33b504-81223455459db28cdbc990c3d4e6eedd/obelisk-handoffnew file
No changes to the content.
migration/c2d581d48dd821a94d7906ca8f33b504-81223455459db28cdbc990c3d4e6eedd/obelisk-upgradenew file
No changes to the content.
migration/c3298d81428117be58196b80853e3c7b-f68462dc88ed7fc451d1c757c040cfee/obelisk-handoffnew file
No changes to the content.
migration/c3298d81428117be58196b80853e3c7b-f68462dc88ed7fc451d1c757c040cfee/obelisk-upgradenew file
No changes to the content.
migration/c3a8b70b1290031feb21880914cb081e-c2d581d48dd821a94d7906ca8f33b504/obelisk-handoffnew file
No changes to the content.
migration/c3a8b70b1290031feb21880914cb081e-c2d581d48dd821a94d7906ca8f33b504/obelisk-upgradenew file
No changes to the content.
migration/c56de2f95c90def16704f62639246bbc-77a721a8fa97d1f84dd2af622089871b/obelisk-handoffnew file
No changes to the content.
migration/c56de2f95c90def16704f62639246bbc-77a721a8fa97d1f84dd2af622089871b/obelisk-upgradenew file
No changes to the content.
migration/c5a306045ad998f906f80748fe18487f-e474bad466cd6e413fba753746eb58ef/obelisk-handoffnew file
No changes to the content.
migration/c5a306045ad998f906f80748fe18487f-e474bad466cd6e413fba753746eb58ef/obelisk-upgradenew file
No changes to the content.
migration/c66bc5ba27dec52d144ceb0aa429e449-1b1686b03b41bd840ff46776cec7b33a/obelisk-handoffnew file
No changes to the content.
migration/c66bc5ba27dec52d144ceb0aa429e449-1b1686b03b41bd840ff46776cec7b33a/obelisk-upgradenew file
No changes to the content.
migration/c67f206b6d2626eb6d36b00122b52784-3bc53f3a28cb8f403cc41c11a37eec5d/obelisk-handoffnew file
No changes to the content.
migration/c67f206b6d2626eb6d36b00122b52784-3bc53f3a28cb8f403cc41c11a37eec5d/obelisk-upgradenew file
No changes to the content.
migration/c6baede3962454713b73acefcc92fe75-a96d491705acc139315264fd340d228f/obelisk-handoffnew file
No changes to the content.
migration/c6baede3962454713b73acefcc92fe75-a96d491705acc139315264fd340d228f/obelisk-upgradenew file
No changes to the content.
migration/c6f38304ee53762590c67020f30d2ab4-58391ba73c13d922e8a12116b28caad2/obelisk-handoffnew file
No changes to the content.
migration/c6f38304ee53762590c67020f30d2ab4-58391ba73c13d922e8a12116b28caad2/obelisk-upgradenew file
No changes to the content.
migration/c7dbf17de8aebd3043617ea6db790dcc-d35f6eb2d1b066d366c9a89ab26144e1/obelisk-handoffnew file
No changes to the content.
migration/c7dbf17de8aebd3043617ea6db790dcc-d35f6eb2d1b066d366c9a89ab26144e1/obelisk-upgradenew file
No changes to the content.
migration/c801cbcf925c0a7b084ef152e3c08f8e-8d5dba70e93ae7e784ea238281842408/obelisk-handoffnew file
No changes to the content.
migration/c801cbcf925c0a7b084ef152e3c08f8e-8d5dba70e93ae7e784ea238281842408/obelisk-upgradenew file
No changes to the content.
migration/c93529e579e64263fd91663216fd3fe7-12c8a24c34c2d8441f3181d179c2baab/obelisk-handoffnew file
No changes to the content.
migration/c93529e579e64263fd91663216fd3fe7-12c8a24c34c2d8441f3181d179c2baab/obelisk-upgradenew file
No changes to the content.
migration/ca5ee8f372ecb07b09283081f0298733-36cdb1c744c3036c2932f01d5fadd9f5/obelisk-handoffnew file
No changes to the content.
migration/ca5ee8f372ecb07b09283081f0298733-36cdb1c744c3036c2932f01d5fadd9f5/obelisk-upgradenew file
No changes to the content.
migration/ca88f98cf9fb63d2feaef7fc19b30eca-962e2bd1ae8d0c233f8da89ecebffe50/obelisk-handoffnew file
No changes to the content.
migration/ca88f98cf9fb63d2feaef7fc19b30eca-962e2bd1ae8d0c233f8da89ecebffe50/obelisk-upgradenew file
No changes to the content.
migration/cb2197d0267b52385ee39bf4adcfe475-fbe0c6b69a63f3a44109a2c7526bc577/obelisk-handoffnew file
No changes to the content.
migration/cb2197d0267b52385ee39bf4adcfe475-fbe0c6b69a63f3a44109a2c7526bc577/obelisk-upgradenew file
No changes to the content.
migration/cb2d040cc16752b1dc89eecf200ea798-5734398c41e4eb5bb58d933ed2e12041/obelisk-handoffnew file
No changes to the content.
migration/cb2d040cc16752b1dc89eecf200ea798-5734398c41e4eb5bb58d933ed2e12041/obelisk-upgradenew file
No changes to the content.
migration/cb98f93880e6dea74014976977d6eb93-a7c7b80a875d1863f027c97eb433b884/obelisk-handoffnew file
No changes to the content.
migration/cb98f93880e6dea74014976977d6eb93-a7c7b80a875d1863f027c97eb433b884/obelisk-upgradenew file
No changes to the content.
migration/cba28ee4b736cf8ed43cb342ccaef12e-8e01c7fee83aa74229a77772d8c928f3/obelisk-handoffnew file
No changes to the content.
migration/cba28ee4b736cf8ed43cb342ccaef12e-8e01c7fee83aa74229a77772d8c928f3/obelisk-upgradenew file
No changes to the content.
migration/cbe155a3e856fb7eaad0bd18feb408be-4c9372bf9aa6ebed1ea20abf3dbb0075/obelisk-handoffnew file
No changes to the content.
migration/cbe155a3e856fb7eaad0bd18feb408be-4c9372bf9aa6ebed1ea20abf3dbb0075/obelisk-upgradenew file
No changes to the content.
migration/cc4307fc88e3486b1c3f9ce07db95808-0fba399e046852d4ecf8e1dad831a2f9/obelisk-handoffnew file
No changes to the content.
migration/cc4307fc88e3486b1c3f9ce07db95808-0fba399e046852d4ecf8e1dad831a2f9/obelisk-upgradenew file
No changes to the content.
migration/ccadc106cd03c9c32e77457d3adc77a1-aa8e6712fd46824acdea303068634078/obelisk-handoffnew file
No changes to the content.
migration/ccadc106cd03c9c32e77457d3adc77a1-aa8e6712fd46824acdea303068634078/obelisk-upgradenew file
No changes to the content.
migration/cccf4140dc336263489bcf67c8c58f86-d50a30fd9643486c5848399d5ba7f1f1/obelisk-handoffnew file
No changes to the content.
migration/cccf4140dc336263489bcf67c8c58f86-d50a30fd9643486c5848399d5ba7f1f1/obelisk-upgradenew file
No changes to the content.
migration/cd38e239c47fb92fabd867b7f86fc99a-7447d29f4712c72458c049cdb9380753/obelisk-handoffnew file
No changes to the content.
migration/cd38e239c47fb92fabd867b7f86fc99a-7447d29f4712c72458c049cdb9380753/obelisk-upgradenew file
No changes to the content.
migration/cd5b54d21dd0fb868f48624d41021f47-0fe60df438daa8a2cca5f2b569e361a5/obelisk-handoffnew file
No changes to the content.
migration/cd5b54d21dd0fb868f48624d41021f47-0fe60df438daa8a2cca5f2b569e361a5/obelisk-upgradenew file
No changes to the content.
migration/cd86427cc2d5b2e931d7ab03deb19d20-bd8f5fa095c3eb19b87d85247b36f1df/obelisk-handoffnew file
No changes to the content.
migration/cd86427cc2d5b2e931d7ab03deb19d20-bd8f5fa095c3eb19b87d85247b36f1df/obelisk-upgradenew file
No changes to the content.
migration/ce7f0d15d71d36d4bcf4fbadcdfa1b58-4db97ca86399c557e5ee0d04d0838acc/obelisk-handoffnew file
No changes to the content.
migration/ce7f0d15d71d36d4bcf4fbadcdfa1b58-4db97ca86399c557e5ee0d04d0838acc/obelisk-upgradenew file
No changes to the content.
migration/cffcc6cab6080a35b7bf7c5c7facd6bc-5631293b75143d170b8fca5579e15fee/obelisk-handoffnew file
No changes to the content.
migration/cffcc6cab6080a35b7bf7c5c7facd6bc-5631293b75143d170b8fca5579e15fee/obelisk-upgradenew file
No changes to the content.
migration/d125109aa22f485d282f6ceb7cd5936e-9b9d73736773550dde7d36de2498b029/obelisk-handoffnew file
No changes to the content.
migration/d125109aa22f485d282f6ceb7cd5936e-9b9d73736773550dde7d36de2498b029/obelisk-upgradenew file
No changes to the content.
migration/d16784e9a2d93db7f953941e3003fa8c-a262678b7d849de5a9732d06199666ae/obelisk-handoffnew file
No changes to the content.
migration/d16784e9a2d93db7f953941e3003fa8c-a262678b7d849de5a9732d06199666ae/obelisk-upgradenew file
No changes to the content.
migration/d17b41a4669ac17960559614c830d69a-66f7a274206ad9f6d3c5701e4bcffc9d/obelisk-handoffnew file
No changes to the content.
migration/d17b41a4669ac17960559614c830d69a-66f7a274206ad9f6d3c5701e4bcffc9d/obelisk-upgradenew file
No changes to the content.
migration/d1b3f5f5375558bf91d55c88547784b9-c0811b664036c7996fcfd19fee241a10/obelisk-handoffnew file
No changes to the content.
migration/d1b3f5f5375558bf91d55c88547784b9-c0811b664036c7996fcfd19fee241a10/obelisk-upgradenew file
No changes to the content.
migration/d207ab99328b8ece5fb27fa1f4dbf9f6-e6a85005bd7e5d343bc23e17bfc345f6/obelisk-handoffnew file
No changes to the content.
migration/d207ab99328b8ece5fb27fa1f4dbf9f6-e6a85005bd7e5d343bc23e17bfc345f6/obelisk-upgradenew file
No changes to the content.
migration/d2d86ce0ca16b143b49374302a9e9e2f-84f4d5c5491abb855d8dbe3acf3bc8e6/obelisk-handoffnew file
No changes to the content.
migration/d2d86ce0ca16b143b49374302a9e9e2f-84f4d5c5491abb855d8dbe3acf3bc8e6/obelisk-upgradenew file
No changes to the content.
migration/d327ceb69d24e40dc11792840454cff9-de469e6488ddf192f2104ec0a030daf7/obelisk-handoffnew file
No changes to the content.
migration/d327ceb69d24e40dc11792840454cff9-de469e6488ddf192f2104ec0a030daf7/obelisk-upgradenew file
No changes to the content.
migration/d35f6eb2d1b066d366c9a89ab26144e1-03b9747848d7f3e995b118f776ef96c7/obelisk-handoffnew file
No changes to the content.
migration/d35f6eb2d1b066d366c9a89ab26144e1-03b9747848d7f3e995b118f776ef96c7/obelisk-upgradenew file
No changes to the content.
migration/d37d70fe5d7d11e68b59ce12022281c9-5f0d3f7480d67df7deebdb880726b356/obelisk-handoffnew file
No changes to the content.
migration/d37d70fe5d7d11e68b59ce12022281c9-5f0d3f7480d67df7deebdb880726b356/obelisk-upgradenew file
No changes to the content.
migration/d40e0e3051a76c4c198cc11c2e8e4b7c-1859cc62bb2f89f528bb2aa170c36e84/obelisk-handoffnew file
No changes to the content.
migration/d40e0e3051a76c4c198cc11c2e8e4b7c-1859cc62bb2f89f528bb2aa170c36e84/obelisk-upgradenew file
No changes to the content.
migration/d50a30fd9643486c5848399d5ba7f1f1-1f5b6e2a19c87d3820b953f942ab4a27/obelisk-handoffnew file
No changes to the content.
migration/d50a30fd9643486c5848399d5ba7f1f1-1f5b6e2a19c87d3820b953f942ab4a27/obelisk-upgradenew file
No changes to the content.
migration/d532356dfb65a4c8a39452d2ebca2516-bf81caf2a7d528f86acacc1f5d2ffcfa/obelisk-handoffnew file
No changes to the content.
migration/d532356dfb65a4c8a39452d2ebca2516-bf81caf2a7d528f86acacc1f5d2ffcfa/obelisk-upgradenew file
No changes to the content.
migration/d5ba6f13296205e5bd88ca2756107df0-26a2491fc8a9658751c1511c777a67bd/obelisk-handoffnew file
No changes to the content.
migration/d5ba6f13296205e5bd88ca2756107df0-26a2491fc8a9658751c1511c777a67bd/obelisk-upgradenew file
No changes to the content.
migration/d62d6d443d9c2261b7e09664398aa005-fcc617e973168e1210379a6afe7171be/obelisk-handoffnew file
No changes to the content.
migration/d62d6d443d9c2261b7e09664398aa005-fcc617e973168e1210379a6afe7171be/obelisk-upgradenew file
No changes to the content.
migration/d6a3ea5f7eb3c95f5e5ea09fb1330270-bc7688ff47218c0e13d02d917b03caa0/obelisk-handoffnew file
No changes to the content.
migration/d6a3ea5f7eb3c95f5e5ea09fb1330270-bc7688ff47218c0e13d02d917b03caa0/obelisk-upgradenew file
No changes to the content.
migration/d6b34eba6ed4f89875a6615fe2c41ec2-3daecb03985f83d8af466a9e0541782b/obelisk-handoffnew file
No changes to the content.
migration/d6b34eba6ed4f89875a6615fe2c41ec2-3daecb03985f83d8af466a9e0541782b/obelisk-upgradenew file
No changes to the content.
migration/d6c090699acd25fc60213ff4bd9eebcf-0298e61f6a03e553df08dcef03e27bd1/obelisk-handoffnew file
No changes to the content.
migration/d6c090699acd25fc60213ff4bd9eebcf-0298e61f6a03e553df08dcef03e27bd1/obelisk-upgradenew file
No changes to the content.
migration/d6cacd7f4fab95c4b6aa2d6c1a4ad2a0-4d6197d8166de78f882376102e36300b/obelisk-handoffnew file
No changes to the content.
migration/d6cacd7f4fab95c4b6aa2d6c1a4ad2a0-4d6197d8166de78f882376102e36300b/obelisk-upgradenew file
No changes to the content.
migration/d747c3d0620b2034aca44265dd1c8c86-eeb970434a53f4883c9f0255db555581/obelisk-handoffnew file
No changes to the content.
migration/d747c3d0620b2034aca44265dd1c8c86-eeb970434a53f4883c9f0255db555581/obelisk-upgradenew file
No changes to the content.
migration/d8adb57b2f2266e128ebb6f6d7e65920-ccadc106cd03c9c32e77457d3adc77a1/obelisk-handoffnew file
No changes to the content.
migration/d8adb57b2f2266e128ebb6f6d7e65920-ccadc106cd03c9c32e77457d3adc77a1/obelisk-upgradenew file
No changes to the content.
migration/da23605ee4058c8474687e74a3999354-dfd7dbceea141c60bbfca962d50c1c06/obelisk-handoffnew file
No changes to the content.
migration/da23605ee4058c8474687e74a3999354-dfd7dbceea141c60bbfca962d50c1c06/obelisk-upgradenew file
No changes to the content.
migration/db327f87e60d4e59bdbfd482e5abe985-eb99c1a66237ba99cd1f6d244cafb9cc/obelisk-handoffnew file
No changes to the content.
migration/db327f87e60d4e59bdbfd482e5abe985-eb99c1a66237ba99cd1f6d244cafb9cc/obelisk-upgradenew file
No changes to the content.
migration/dc95f39fb27634ccbca7318ff2065608-0fb3a912a8aaf5ef6f734a25f0bce7e2/obelisk-handoffnew file
No changes to the content.
migration/dc95f39fb27634ccbca7318ff2065608-0fb3a912a8aaf5ef6f734a25f0bce7e2/obelisk-upgradenew file
No changes to the content.
migration/dd6ca52a58ea6cda7b3eaa2e87bb13d9-a6d50c7e756dfd1e3ae891111b81a1ae/obelisk-handoffnew file
No changes to the content.
migration/dd6ca52a58ea6cda7b3eaa2e87bb13d9-a6d50c7e756dfd1e3ae891111b81a1ae/obelisk-upgradenew file
No changes to the content.
migration/ddaa8e0e42a1ef00154d580a390c9f17-53d29061537ddaf7530aef88624ab266/obelisk-handoffnew file
No changes to the content.
migration/ddaa8e0e42a1ef00154d580a390c9f17-53d29061537ddaf7530aef88624ab266/obelisk-upgradenew file
No changes to the content.
migration/de469e6488ddf192f2104ec0a030daf7-3da7d4a3b507d18097a90becf4dde9aa/obelisk-handoffnew file
No changes to the content.
migration/de469e6488ddf192f2104ec0a030daf7-3da7d4a3b507d18097a90becf4dde9aa/obelisk-upgradenew file
No changes to the content.
migration/df71cd82e801ca701bf4bc44b80b6d50-f423ecd4d096de9dca37a67b8b4143eb/obelisk-handoffnew file
No changes to the content.
migration/df71cd82e801ca701bf4bc44b80b6d50-f423ecd4d096de9dca37a67b8b4143eb/obelisk-upgradenew file
No changes to the content.
migration/dfd7dbceea141c60bbfca962d50c1c06-030ed18dacf3571cbb9e3e7d9d99627b/obelisk-handoffnew file
No changes to the content.
migration/dfd7dbceea141c60bbfca962d50c1c06-030ed18dacf3571cbb9e3e7d9d99627b/obelisk-upgradenew file
No changes to the content.
migration/e01cc01684deaf4c75ad5d65270bf3d5-c7dbf17de8aebd3043617ea6db790dcc/obelisk-handoffnew file
No changes to the content.
migration/e01cc01684deaf4c75ad5d65270bf3d5-c7dbf17de8aebd3043617ea6db790dcc/obelisk-upgradenew file
No changes to the content.
migration/e1ef432db54d9a6daa9bdeeb543262ae-2ba356b187db8db13b8aa40f43ca025f/obelisk-handoffnew file
No changes to the content.
migration/e1ef432db54d9a6daa9bdeeb543262ae-2ba356b187db8db13b8aa40f43ca025f/obelisk-upgradenew file
No changes to the content.
migration/e22c51145e08d4a089003460ae80a7f9-42357310c09e345f9360259c6c4102f2/obelisk-handoffnew file
No changes to the content.
migration/e22c51145e08d4a089003460ae80a7f9-42357310c09e345f9360259c6c4102f2/obelisk-upgradenew file
No changes to the content.
migration/e24a771ee59f24e45afc0a27307f2070-2eb907820012f77e60350430d89631fe/obelisk-handoffnew file
No changes to the content.
migration/e24a771ee59f24e45afc0a27307f2070-2eb907820012f77e60350430d89631fe/obelisk-upgradenew file
No changes to the content.
migration/e2706f337218a12a3bdddd18fa3f4cfd-475f9a0d63e84e9cf13b8be3986cb603/obelisk-handoffnew file
No changes to the content.
migration/e2706f337218a12a3bdddd18fa3f4cfd-475f9a0d63e84e9cf13b8be3986cb603/obelisk-upgradenew file
No changes to the content.
migration/e2b2d878b0ba6eb653b194523bf0a0e0-5f307639ccf6a64309166bfe58037397/obelisk-handoffnew file
No changes to the content.
migration/e2b2d878b0ba6eb653b194523bf0a0e0-5f307639ccf6a64309166bfe58037397/obelisk-upgradenew file
No changes to the content.
migration/e2be519915794ef130249f2be7640382-455279b6974b96a673613cd0ccc8ed54/obelisk-handoffnew file
No changes to the content.
migration/e2be519915794ef130249f2be7640382-455279b6974b96a673613cd0ccc8ed54/obelisk-upgradenew file
No changes to the content.
migration/e323ce1352e512109f1609276fb509b4-2c1521e5a5be64e6d28a9fdae950cd76/obelisk-handoffnew file
No changes to the content.
migration/e323ce1352e512109f1609276fb509b4-2c1521e5a5be64e6d28a9fdae950cd76/obelisk-upgradenew file
No changes to the content.
migration/e327773e1525eaa1eb2112dff8f41b26-f595b01939b4c87b7c146ea9ff331b39/obelisk-handoffnew file
No changes to the content.
migration/e327773e1525eaa1eb2112dff8f41b26-f595b01939b4c87b7c146ea9ff331b39/obelisk-upgradenew file
No changes to the content.
migration/e3b3014d7e0dc983fe118d32546654e5-063665b78b6705e7de952f73fa9797bf/obelisk-handoffnew file
No changes to the content.
migration/e3b3014d7e0dc983fe118d32546654e5-063665b78b6705e7de952f73fa9797bf/obelisk-upgradenew file
No changes to the content.
migration/e4051219d89b3c9a8350fe824ad96a82-166c5f8e4a0f1dfd227b075a96237386/obelisk-handoffnew file
No changes to the content.
migration/e4051219d89b3c9a8350fe824ad96a82-166c5f8e4a0f1dfd227b075a96237386/obelisk-upgradenew file
No changes to the content.
migration/e4513b485b6ba8b7f4bd34b12458ff0c-3c80eccfe80d0abd36b07bb11e69fc7a/obelisk-handoffnew file
No changes to the content.
migration/e4513b485b6ba8b7f4bd34b12458ff0c-3c80eccfe80d0abd36b07bb11e69fc7a/obelisk-upgradenew file
No changes to the content.
migration/e474bad466cd6e413fba753746eb58ef-f19b3699616f3b1de3a277f9cba617c7/obelisk-handoffnew file
No changes to the content.
migration/e474bad466cd6e413fba753746eb58ef-f19b3699616f3b1de3a277f9cba617c7/obelisk-upgradenew file
No changes to the content.
migration/e4b89dd259f3831bc60b56147b3586a6-98da197f133bca81e533ab5ffbf4c82d/obelisk-handoffnew file
No changes to the content.
migration/e4b89dd259f3831bc60b56147b3586a6-98da197f133bca81e533ab5ffbf4c82d/obelisk-upgradenew file
No changes to the content.
migration/e609d945b642fd9d4ce99931e3699db7-85c2c4fb8472d50eaa97aed0bdc9b142/obelisk-handoffnew file
No changes to the content.
migration/e609d945b642fd9d4ce99931e3699db7-85c2c4fb8472d50eaa97aed0bdc9b142/obelisk-upgradenew file
No changes to the content.
migration/e6980e47ba847245dfab61fb4d5b8ce5-58f9d23d2cede2081dc6f6cfa2283f60/obelisk-handoffnew file
No changes to the content.
migration/e6980e47ba847245dfab61fb4d5b8ce5-58f9d23d2cede2081dc6f6cfa2283f60/obelisk-upgradenew file
No changes to the content.
migration/e6a85005bd7e5d343bc23e17bfc345f6-98f9e0bbd21e36b0f4daa440d11e7177/obelisk-handoffnew file
No changes to the content.
migration/e6a85005bd7e5d343bc23e17bfc345f6-98f9e0bbd21e36b0f4daa440d11e7177/obelisk-upgradenew file
No changes to the content.
migration/e7288bb723a4e5b7139ac67842963287-7b75ffdcd86b5a330d9c325ad5767393/obelisk-handoffnew file
No changes to the content.
migration/e7288bb723a4e5b7139ac67842963287-7b75ffdcd86b5a330d9c325ad5767393/obelisk-upgradenew file
No changes to the content.
migration/e7385c9eae2b807a483b73be4295b207-fb02559b6f723d599e12931a81be3a1b/obelisk-handoffnew file
No changes to the content.
migration/e7385c9eae2b807a483b73be4295b207-fb02559b6f723d599e12931a81be3a1b/obelisk-upgradenew file
No changes to the content.
migration/e7667778f2ab3055aa3ccc68443d14df-7adece557941a7d7ac7c150f77f758be/obelisk-handoffnew file
No changes to the content.
migration/e7667778f2ab3055aa3ccc68443d14df-7adece557941a7d7ac7c150f77f758be/obelisk-upgradenew file
No changes to the content.
migration/e78ec85bec14f4784faeef4d8d3e4d0a-e7667778f2ab3055aa3ccc68443d14df/obelisk-handoffnew file
No changes to the content.
migration/e78ec85bec14f4784faeef4d8d3e4d0a-e7667778f2ab3055aa3ccc68443d14df/obelisk-upgradenew file
No changes to the content.
migration/e7b17aa124e17c357cead8081a58f089-ac5e28caddd6832d0bf1f7445a4a6a28/obelisk-handoffnew file
No changes to the content.
migration/e7b17aa124e17c357cead8081a58f089-ac5e28caddd6832d0bf1f7445a4a6a28/obelisk-upgradenew file
No changes to the content.
migration/e7b566495114e160f1f4d722b7ed8174-7cf814f7404638f1c1be662a24d7cfea/obelisk-handoffnew file
No changes to the content.
migration/e7b566495114e160f1f4d722b7ed8174-7cf814f7404638f1c1be662a24d7cfea/obelisk-upgradenew file
No changes to the content.
migration/e92786eaf56c32fd6b452c4a841d6317-f24a7fb6e3261c4919ea2a2fee83ed6b/obelisk-handoffnew file
No changes to the content.
migration/e92786eaf56c32fd6b452c4a841d6317-f24a7fb6e3261c4919ea2a2fee83ed6b/obelisk-upgradenew file
No changes to the content.
migration/e9ec84bfe7099b713c0fda32308f5815-f0a064ca5f709233204168c136b99a7e/obelisk-handoffnew file
No changes to the content.
migration/e9ec84bfe7099b713c0fda32308f5815-f0a064ca5f709233204168c136b99a7e/obelisk-upgradenew file
No changes to the content.
migration/eb99c1a66237ba99cd1f6d244cafb9cc-da23605ee4058c8474687e74a3999354/obelisk-handoffnew file
No changes to the content.
migration/eb99c1a66237ba99cd1f6d244cafb9cc-da23605ee4058c8474687e74a3999354/obelisk-upgradenew file
No changes to the content.
migration/ebb95c5864d1d1d2f78fbf7cc8235dac-ba4829dc61dcc12bcafd8de5fc8a76dc/obelisk-handoffnew file
No changes to the content.
migration/ebb95c5864d1d1d2f78fbf7cc8235dac-ba4829dc61dcc12bcafd8de5fc8a76dc/obelisk-upgradenew file
No changes to the content.
migration/ec11d10a6131abca9222a8f8026fb5e4-f3d494ffee7091cd588bbdd2fd9bcdf9/obelisk-handoffnew file
No changes to the content.
migration/ec11d10a6131abca9222a8f8026fb5e4-f3d494ffee7091cd588bbdd2fd9bcdf9/obelisk-upgradenew file
No changes to the content.
migration/ec94619fa3f25db1ca84d179e47db1ca-23af4d9671962484de53f537a0626d83/obelisk-handoffnew file
No changes to the content.
migration/ec94619fa3f25db1ca84d179e47db1ca-23af4d9671962484de53f537a0626d83/obelisk-upgradenew file
No changes to the content.
migration/ed95a99cdde4adc90c6a8e2de82410eb-1c4fc23cbf5653613c6060668e065f37/obelisk-handoffnew file
No changes to the content.
migration/ed95a99cdde4adc90c6a8e2de82410eb-1c4fc23cbf5653613c6060668e065f37/obelisk-upgradenew file
No changes to the content.
migration/ee3dccc89ea6fdaaf9c1146b92861e90-f3eac05465851232f8174a7a347d1c4b/obelisk-handoffnew file
No changes to the content.
migration/ee3dccc89ea6fdaaf9c1146b92861e90-f3eac05465851232f8174a7a347d1c4b/obelisk-upgradenew file
No changes to the content.
migration/eeb970434a53f4883c9f0255db555581-a2b33f8dc1f90b8dddce942b174be5b8/obelisk-handoffnew file
No changes to the content.
migration/eeb970434a53f4883c9f0255db555581-a2b33f8dc1f90b8dddce942b174be5b8/obelisk-upgradenew file
No changes to the content.
migration/ef33c06500054e09c97eee9e9c55485a-367616bd36eda757f251a5e8e9096acc/obelisk-handoffnew file
No changes to the content.
migration/ef33c06500054e09c97eee9e9c55485a-367616bd36eda757f251a5e8e9096acc/obelisk-upgradenew file
No changes to the content.
migration/ef6aebc8cba2af6c4f930c981b06e8ac-d17b41a4669ac17960559614c830d69a/obelisk-handoffnew file
No changes to the content.
migration/ef6aebc8cba2af6c4f930c981b06e8ac-d17b41a4669ac17960559614c830d69a/obelisk-upgradenew file
No changes to the content.
migration/f0a064ca5f709233204168c136b99a7e-ef6aebc8cba2af6c4f930c981b06e8ac/obelisk-handoffnew file
No changes to the content.
migration/f0a064ca5f709233204168c136b99a7e-ef6aebc8cba2af6c4f930c981b06e8ac/obelisk-upgradenew file
No changes to the content.
migration/f15c9c2f894c8cc4fac07e2cdef47180-94252585266fa745a91733888e2f50b7/obelisk-handoffnew file
No changes to the content.
migration/f15c9c2f894c8cc4fac07e2cdef47180-94252585266fa745a91733888e2f50b7/obelisk-upgradenew file
No changes to the content.
migration/f19b3699616f3b1de3a277f9cba617c7-4d85ee8cdb0db20a5662b09567a2bbe7/obelisk-handoffnew file
No changes to the content.
migration/f19b3699616f3b1de3a277f9cba617c7-4d85ee8cdb0db20a5662b09567a2bbe7/obelisk-upgradenew file
No changes to the content.
migration/f24a7fb6e3261c4919ea2a2fee83ed6b-f913f01e90549c56a8b05f8100cfa21d/obelisk-handoffnew file
No changes to the content.
migration/f24a7fb6e3261c4919ea2a2fee83ed6b-f913f01e90549c56a8b05f8100cfa21d/obelisk-upgradenew file
No changes to the content.
migration/f2a9ba4a93c3a04acbee2211bca9e27a-ec94619fa3f25db1ca84d179e47db1ca/obelisk-handoffnew file
No changes to the content.
migration/f2a9ba4a93c3a04acbee2211bca9e27a-ec94619fa3f25db1ca84d179e47db1ca/obelisk-upgradenew file
No changes to the content.
migration/f391c6a74ee14217ce4565c278fceed6-e7288bb723a4e5b7139ac67842963287/obelisk-handoffnew file
No changes to the content.
migration/f391c6a74ee14217ce4565c278fceed6-e7288bb723a4e5b7139ac67842963287/obelisk-upgradenew file
No changes to the content.
migration/f3d494ffee7091cd588bbdd2fd9bcdf9-b68a785fbf27a63334ef1d1fe82b2dac/obelisk-handoffnew file
No changes to the content.
migration/f3d494ffee7091cd588bbdd2fd9bcdf9-b68a785fbf27a63334ef1d1fe82b2dac/obelisk-upgradenew file
No changes to the content.
migration/f3eac05465851232f8174a7a347d1c4b-1629d0d70e360df6b2280fac36d21e8a/obelisk-handoffnew file
No changes to the content.
migration/f3eac05465851232f8174a7a347d1c4b-1629d0d70e360df6b2280fac36d21e8a/obelisk-upgradenew file
No changes to the content.
migration/f3ec62d09836804e39fad9d814137845-ef33c06500054e09c97eee9e9c55485a/obelisk-handoffnew file
No changes to the content.
migration/f3ec62d09836804e39fad9d814137845-ef33c06500054e09c97eee9e9c55485a/obelisk-upgradenew file
No changes to the content.
migration/f41b690c74938e347cf042cee2d4fc72-7fc381538805adac19c54b73ea118601/obelisk-handoffnew file
No changes to the content.
migration/f41b690c74938e347cf042cee2d4fc72-7fc381538805adac19c54b73ea118601/obelisk-upgradenew file
No changes to the content.
migration/f423ecd4d096de9dca37a67b8b4143eb-469a31d20d69da72e6a3a3ea2bb02f9d/obelisk-handoffnew file
No changes to the content.
migration/f423ecd4d096de9dca37a67b8b4143eb-469a31d20d69da72e6a3a3ea2bb02f9d/obelisk-upgradenew file
No changes to the content.
migration/f595b01939b4c87b7c146ea9ff331b39-4da182e21be35feeee484dc85be7a075/obelisk-handoffnew file
No changes to the content.
migration/f595b01939b4c87b7c146ea9ff331b39-4da182e21be35feeee484dc85be7a075/obelisk-upgradenew file
No changes to the content.
migration/f68462dc88ed7fc451d1c757c040cfee-589eb89cdb09655c5edde4cd2a4deb80/obelisk-handoffnew file
No changes to the content.
migration/f68462dc88ed7fc451d1c757c040cfee-589eb89cdb09655c5edde4cd2a4deb80/obelisk-upgradenew file
No changes to the content.
migration/f790ac9400501511b4ee045e22bc5fe1-a826f01311fe00ef5cf87568832605d3/obelisk-handoffnew file
No changes to the content.
migration/f790ac9400501511b4ee045e22bc5fe1-a826f01311fe00ef5cf87568832605d3/obelisk-upgradenew file
No changes to the content.
migration/f8e51a9e8ac6598f7c073b09e4ba09b5-ff57aebc7325bd7e02b436fec79e799c/obelisk-handoffnew file
No changes to the content.
migration/f8e51a9e8ac6598f7c073b09e4ba09b5-ff57aebc7325bd7e02b436fec79e799c/obelisk-upgradenew file
No changes to the content.
migration/f8eee9431c8b7a767d96da0a8cfae95d-9e05cc6b81c6380ba0ac59bad1021cac/obelisk-handoffnew file
No changes to the content.
migration/f8eee9431c8b7a767d96da0a8cfae95d-9e05cc6b81c6380ba0ac59bad1021cac/obelisk-upgradenew file
No changes to the content.
migration/f913f01e90549c56a8b05f8100cfa21d-001efbbe866b66eebc6ee1ea0a906f2d/obelisk-handoffnew file
No changes to the content.
migration/f913f01e90549c56a8b05f8100cfa21d-001efbbe866b66eebc6ee1ea0a906f2d/obelisk-upgradenew file
No changes to the content.
migration/f9716ad47c84e0b7ac7d426e8ad059f3-ec11d10a6131abca9222a8f8026fb5e4/obelisk-handoffnew file
No changes to the content.
migration/f9716ad47c84e0b7ac7d426e8ad059f3-ec11d10a6131abca9222a8f8026fb5e4/obelisk-upgradenew file
No changes to the content.
migration/f98b82f5d142ce83733a3f18bdacb138-35acc1e0c4d9c02d60abd116f3df84ee/obelisk-handoffnew file
No changes to the content.
migration/f98b82f5d142ce83733a3f18bdacb138-35acc1e0c4d9c02d60abd116f3df84ee/obelisk-upgradenew file
No changes to the content.
migration/f9ad863c0bebc103f8b81afdf82e6293-c6f38304ee53762590c67020f30d2ab4/obelisk-handoffnew file
No changes to the content.
migration/f9ad863c0bebc103f8b81afdf82e6293-c6f38304ee53762590c67020f30d2ab4/obelisk-upgradenew file
No changes to the content.
migration/fa18f7522903a21b615dbee24c26bfbe-0535e06dd0d61f3e6970b491a17cf1f8/obelisk-handoffnew file
No changes to the content.
migration/fa18f7522903a21b615dbee24c26bfbe-0535e06dd0d61f3e6970b491a17cf1f8/obelisk-upgradenew file
No changes to the content.
migration/fb02559b6f723d599e12931a81be3a1b-71a47d4d6856c1956c0d1c9de9af231f/obelisk-handoffnew file
No changes to the content.
migration/fb02559b6f723d599e12931a81be3a1b-71a47d4d6856c1956c0d1c9de9af231f/obelisk-upgradenew file
No changes to the content.
migration/fb374b0721716a1e849e64daf6da73c4-f8e51a9e8ac6598f7c073b09e4ba09b5/obelisk-handoffnew file
No changes to the content.
migration/fb374b0721716a1e849e64daf6da73c4-f8e51a9e8ac6598f7c073b09e4ba09b5/obelisk-upgradenew file
No changes to the content.
migration/fb5110e527cf77c568bcae7d435ee035-53fdf819333b86dfb485bf16a8f28db2/obelisk-handoffnew file
No changes to the content.
migration/fb5110e527cf77c568bcae7d435ee035-53fdf819333b86dfb485bf16a8f28db2/obelisk-upgradenew file
No changes to the content.
migration/fb81153791e49cd00850e91ff7418018-9b33a49f0c978e27b752fa457aca97cc/obelisk-handoffnew file
No changes to the content.
migration/fb81153791e49cd00850e91ff7418018-9b33a49f0c978e27b752fa457aca97cc/obelisk-upgradenew file
No changes to the content.
migration/fbb3eb3209281031dc5183d289eb4e75-8c7718f94e2676c957fe844b7dc2637c/obelisk-handoffnew file
No changes to the content.
migration/fbb3eb3209281031dc5183d289eb4e75-8c7718f94e2676c957fe844b7dc2637c/obelisk-upgradenew file
No changes to the content.
migration/fbe0c6b69a63f3a44109a2c7526bc577-c3a8b70b1290031feb21880914cb081e/obelisk-handoffnew file
No changes to the content.
migration/fbe0c6b69a63f3a44109a2c7526bc577-c3a8b70b1290031feb21880914cb081e/obelisk-upgradenew file
No changes to the content.
migration/fc42c5fc24031fc6a8836a10cfb45143-d125109aa22f485d282f6ceb7cd5936e/obelisk-handoffnew file
No changes to the content.
migration/fc42c5fc24031fc6a8836a10cfb45143-d125109aa22f485d282f6ceb7cd5936e/obelisk-upgradenew file
No changes to the content.
migration/fca48edfdaa31b036176b97b04384682-2df092178e167b474dd08dfa46ec3a49/obelisk-handoffnew file
No changes to the content.
migration/fca48edfdaa31b036176b97b04384682-2df092178e167b474dd08dfa46ec3a49/obelisk-upgradenew file
No changes to the content.
migration/fcc617e973168e1210379a6afe7171be-ed95a99cdde4adc90c6a8e2de82410eb/obelisk-handoffnew file
No changes to the content.
migration/fcc617e973168e1210379a6afe7171be-ed95a99cdde4adc90c6a8e2de82410eb/obelisk-upgradenew file
No changes to the content.
migration/ff1105290f89350bc9deb6eb907e0a53-0c991bc3c06801f72df48486ee78e350/obelisk-handoffnew file
No changes to the content.
migration/ff1105290f89350bc9deb6eb907e0a53-0c991bc3c06801f72df48486ee78e350/obelisk-upgradenew file
No changes to the content.
migration/ff57aebc7325bd7e02b436fec79e799c-9f71fc9d2ed805fecbc8c4a3aae5685b/obelisk-handoffnew file
No changes to the content.
migration/ff57aebc7325bd7e02b436fec79e799c-9f71fc9d2ed805fecbc8c4a3aae5685b/obelisk-upgradenew file
No changes to the content.
migration/ffa62158ed6fdece012980e4ad25e6ac-959ae2925bcd2b5beff631a8e7aeaf00/obelisk-handoffnew file
No changes to the content.
migration/ffa62158ed6fdece012980e4ad25e6ac-959ae2925bcd2b5beff631a8e7aeaf00/obelisk-upgradenew file
No changes to the content.
release.nix+33−11
@@ -1,23 +1,45 @@
1−{ self ? import ./. {}
2−, pkgs ? self.nixpkgs
1+{ local-self ? import ./. {}
2+, local-pkgs ? local-self.nixpkgs
33}:
44
5−with pkgs;
5+with local-pkgs.lib;
66
7−{
8− inherit (self) command;
9− built-skeleton = (import ./skeleton {}).all;
7+genAttrs [ "x86_64-linux" "x86_64-darwin" ] (system: {
8+ inherit (import ./. { inherit system; }) command;
9+ built-skeleton = (import ./skeleton { inherit system; }).all;
1010 tests = {
1111 #TODO: Doesn't work; see discussion in https://www.pivotaltracker.com/story/show/157265140
12− #ob-init = runCommand "ob-init" {
12+ # ob-init = runCommand "ob-init" {
1313 # nativeBuildInputs = [
1414 # self.command
1515 # nix
1616 # ];
17+ # } ''
18+ # # mkdir $out
19+ # # cd $out
20+ # # ob init --symlink=${self.path}
21+ #'';
22+
23+ #TODO: Re-enable this
24+ #verify-migration = runCommand "verify-migration" {
25+ # nativeBuildInputs = [
26+ # self.command
27+ # nix # obelisk uses `nix-hash`
28+ # ];
1729 #} ''
18− # mkdir $out
19− # cd $out
20− # ob init --symlink=${self.path}
30+ # set -e
31+ # # Fix permissions so `removePathForcibly` works on copied-to-tmp paths
32+ # cp -a ${self.pathGit} $TMPDIR/obelisk
33+ # cd $TMPDIR/obelisk
34+ # chmod -R u+w .
35+ #
36+ # (set -x;
37+ # ob internal hash;
38+ # ob internal verify-migration;
39+ # )
40+ #
41+ # mkdir -p $out
42+ # touch $out/done
2143 #'';
2244 };
23−}
45+})
scripts/ghcid-this-libnew file+4
@@ -0,0 +1,4 @@
1+#!/usr/bin/env bash
2+set -euo pipefail
3+
4+nix-shell -E "with (import ../.. {}); reflex-platform.workOn haskellPackageSets.ghc haskellPackageSets.ghc.obelisk-$(basename $PWD)" --run "cabal configure ; ghcid -c'cabal repl'"
skeleton/.gitignore+2−1
@@ -2,5 +2,6 @@ dist-newstyle
22result
33result-android
44result-ios
5+result-exe
56.attr-cache
6−ghcid-output.txt
7+ghcid-output.txt
skeleton/.obelisk/.gitignorenew file+3
@@ -0,0 +1,3 @@
1+/impl
2+!/impl/default.nix
3+!/impl/github.json
skeleton/backend/backend.cabal+2−6
@@ -11,22 +11,18 @@ library
1111 , common
1212 , frontend
1313 , obelisk-backend
14+ , obelisk-route
1415 exposed-modules:
1516 Backend
1617 ghc-options: -Wall
1718
1819executable backend
1920 main-is: main.hs
20− hs-source-dirs: src
21+ hs-source-dirs: src-bin
2122 if impl(ghcjs)
2223 buildable: False
2324 build-depends: base
2425 , backend
2526 , common
2627 , frontend
27− , lens
28− , modern-uri
2928 , obelisk-backend
30− , obelisk-executable-config
31− , obelisk-executable-config-inject
32− , text
skeleton/backend/src-bin/main.hsnew file+6
@@ -0,0 +1,6 @@
1+import Backend
2+import Frontend
3+import Obelisk.Backend
4+
5+main :: IO ()
6+main = runBackend backend frontend
skeleton/backend/src/Backend.hs+11−6
@@ -1,10 +1,15 @@
1+{-# LANGUAGE EmptyCase #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE LambdaCase #-}
4+{-# LANGUAGE RankNTypes #-}
5+{-# LANGUAGE TypeFamilies #-}
16module Backend where
27
3−import Common.Api
4−import Frontend
5−import qualified Obelisk.Backend as Ob
8+import Common.Route
9+import Obelisk.Backend
610
7−backend :: IO ()
8−backend = Ob.backend Ob.def
9− { Ob._backendConfig_head = fst frontend
11+backend :: Backend BackendRoute FrontendRoute
12+backend = Backend
13+ { _backend_run = \serve -> serve $ const $ return ()
14+ , _backend_routeEncoder = backendRouteEncoder
1015 }
skeleton/backend/src/main.hsdeleted−20
@@ -1,20 +0,0 @@
1−{-# LANGUAGE OverloadedStrings #-}
2−import Control.Exception
3−import Control.Lens
4−import Data.Maybe
5−import Data.Text.Encoding
6−import Obelisk.ExecutableConfig (get)
7−import System.Environment
8−import qualified Text.URI as URI
9−import Text.URI.Lens
10−
11−import Backend
12−
13−main :: IO ()
14−main = do
15− Just route <- get "common/route"
16− port <- case URI.mkURI route of
17− Left err -> fail $ show err
18− Right uri -> return $ fromMaybe 8000 $ uri ^? uriAuthority . _Right . authPort . _Just
19− withArgs ["--port", show port] backend
20−
skeleton/common/common.cabal+4
@@ -6,6 +6,10 @@ build-type: Simple
66library
77 hs-source-dirs: src
88 build-depends: base
9+ , obelisk-route
10+ , mtl
11+ , text
912
1013 exposed-modules:
1114 Common.Api
15+ Common.Route
skeleton/common/src/Common/Api.hs+1−1
@@ -1,4 +1,4 @@
11module Common.Api where
22
33commonStuff :: String
4−commonStuff = "here is a common string"
4+commonStuff = "Here is a string defined in code common to the frontend and backend."
skeleton/common/src/Common/Route.hsnew file+52
@@ -0,0 +1,52 @@
1+{-# LANGUAGE EmptyCase #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE GADTs #-}
4+{-# LANGUAGE LambdaCase #-}
5+{-# LANGUAGE RankNTypes #-}
6+{-# LANGUAGE TemplateHaskell #-}
7+{-# LANGUAGE KindSignatures #-}
8+{-# LANGUAGE EmptyCase #-}
9+{-# LANGUAGE MultiParamTypeClasses #-}
10+{-# LANGUAGE FlexibleInstances #-}
11+{-# LANGUAGE OverloadedStrings #-}
12+
13+module Common.Route where
14+
15+{- -- You will probably want these imports for composing Encoders.
16+import Prelude hiding (id, (.))
17+import Control.Category
18+-}
19+
20+import Data.Text (Text)
21+import Data.Functor.Identity
22+import Data.Functor.Sum
23+
24+import Obelisk.Route
25+import Obelisk.Route.TH
26+
27+data BackendRoute :: * -> * where
28+ -- | Used to handle unparseable routes.
29+ BackendRoute_Missing :: BackendRoute ()
30+ -- You can define any routes that will be handled specially by the backend here.
31+ -- i.e. These do not serve the frontend, but do something different, such as serving static files.
32+
33+data FrontendRoute :: * -> * where
34+ FrontendRoute_Main :: FrontendRoute ()
35+ -- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend.
36+
37+backendRouteEncoder
38+ :: Encoder (Either Text) Identity (R (Sum BackendRoute (ObeliskRoute FrontendRoute))) PageName
39+backendRouteEncoder = handleEncoder (const (InL BackendRoute_Missing :/ ())) $
40+ pathComponentEncoder $ \case
41+ InL backendRoute -> case backendRoute of
42+ BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty
43+ InR obeliskRoute -> obeliskRouteSegment obeliskRoute $ \case
44+ -- The encoder given to PathEnd determines how to parse query parameters,
45+ -- in this example, we have none, so we insist on it.
46+ FrontendRoute_Main -> PathEnd $ unitEncoder mempty
47+
48+
49+concat <$> mapM deriveRouteComponent
50+ [ ''BackendRoute
51+ , ''FrontendRoute
52+ ]
skeleton/devel/Devel.hsdeleted−10
@@ -1,10 +0,0 @@
1−module Devel where
2−
3−import Obelisk.Run
4−
5−import Backend
6−import Frontend
7−
8−main :: Int -> IO ()
9−main port = run port backend frontend
10−
skeleton/frontend/frontend.cabal+5−2
@@ -7,6 +7,8 @@ library
77 hs-source-dirs: src
88 build-depends: base
99 , common
10+ , obelisk-frontend
11+ , obelisk-route
1012 , reflex-dom
1113 , static
1214 , text
@@ -16,12 +18,13 @@ library
1618
1719executable frontend
1820 main-is: main.hs
19− hs-source-dirs: src
21+ hs-source-dirs: src-bin
2022 build-depends: base
2123 , common
24+ , obelisk-frontend
25+ , obelisk-route
2226 , reflex-dom
2327 , static
24− , text
2528 , frontend
2629 --TODO: Make these ghc-options optional
2730 ghc-options: -threaded
skeleton/frontend/src-bin/main.hsnew file+10
@@ -0,0 +1,10 @@
1+import Frontend
2+import Common.Route
3+import Obelisk.Frontend
4+import Obelisk.Route.Frontend
5+import Reflex.Dom
6+
7+main :: IO ()
8+main = do
9+ let Right validFullEncoder = checkEncoder backendRouteEncoder
10+ run $ runFrontend validFullEncoder frontend
skeleton/frontend/src/Frontend.hs+8−5
@@ -4,16 +4,19 @@
44module Frontend where
55
66import qualified Data.Text as T
7+import Obelisk.Frontend
8+import Obelisk.Route
79import Reflex.Dom.Core
810
911import Common.Api
12+import Common.Route
1013import Static
1114
12−frontend :: (StaticWidget x (), Widget x ())
13−frontend = (head', body)
14− where
15− head' = el "title" $ text "Obelisk Minimal Example"
16− body = do
15+frontend :: Frontend (R FrontendRoute)
16+frontend = Frontend
17+ { _frontend_head = el "title" $ text "Obelisk Minimal Example"
18+ , _frontend_body = do
1719 text "Welcome to Obelisk!"
1820 el "p" $ text $ T.pack commonStuff
1921 elAttr "img" ("src" =: static @"obelisk.jpg") blank
22+ }
skeleton/frontend/src/main.hsdeleted−5
@@ -1,5 +0,0 @@
1−import Reflex.Dom
2−import Frontend
3−
4−main :: IO ()
5−main = mainWidget $ snd frontend