Repository: paularmstrong/build-tracker
Pull request #42 (2019-03-18 03:43:22)

Update d3-scale in group packages to the latest version 🚀

## The dependency [d3-scale](https://github.com/d3/d3-scale) was updated from `2.2.2` to `3.0.0`.
This version is **not covered** by your **current version range**.

If you don’t accept this pull request, your project will work just like it did before. However, you might be missing out on a bunch of new features, fixes and/or performance improvements from the dependency update.

---

[Find out more about this release](https://github.com/d3/d3-scale).

<details>
  <summary>FAQ and help</summary>

  There is a collection of [frequently asked questions](https://greenkeeper.io/faq.html). If those don’t help, you can always [ask the humans behind Greenkeeper](https://github.com/greenkeeperio/greenkeeper/issues/new).
</details>

---


Your [Greenkeeper](https://greenkeeper.io) bot :palm_tree:




Patch:
diff --git a/.env b/.env
new file mode 100644
index 0000000..abc7a39
--- /dev/null
+++ b/.env
@@ -0,0 +1,4 @@
+PGHOST=ec2-184-73-216-48.compute-1.amazonaws.com
+PGDATABASE=dbsg5fjn3bijf6
+PGUSER=dztfgqjzuzrrmr
+PGPASSWORD=7fd509bc90de0e08696e7ab6ec9a38717a60eac33099ed1fe6e8f01cb414c596
diff --git a/.github/main.workflow b/.github/main.workflow
index 7ff80e9..5c98211 100644
--- a/.github/main.workflow
+++ b/.github/main.workflow
@@ -24,7 +24,7 @@ action "Add labels" {
 
 workflow "On push" {
   on = "push"
-  resolves = ["Lint", "Verify tests", "Verify types", "Upload build"]
+  resolves = ["Lint", "Verify tests", "Verify types"]
 }
 
 action "Filters for GitHub Actions" {
@@ -56,18 +56,6 @@ action "Verify types" {
   args = "tsc"
 }
 
-action "Build" {
-  uses = "nuxt/actions-yarn@master"
-  needs = ["Install dependencies"]
-  args = "build"
-}
-
-action "Upload build" {
-  uses = "nuxt/actions-yarn@master"
-  needs = ["Build"]
-  args = "ts-node src/cli/src/bin.ts upload-build -c demo/build-tracker-cli.config.js"
-}
-
 action "Install PR dependencies" {
   uses = "nuxt/actions-yarn@master"
   args = "install --frozen-lockfile"
diff --git a/Procfile b/Procfile
deleted file mode 100644
index 26b7da7..0000000
--- a/Procfile
+++ /dev/null
@@ -1,2 +0,0 @@
-release: demo/build.sh
-web: demo/run.sh
diff --git a/config/postgres.js b/config/postgres.js
index a9515fa..67be902 100644
--- a/config/postgres.js
+++ b/config/postgres.js
@@ -3,9 +3,6 @@ const withPostgres = require('@build-tracker/plugin-with-postgres').default;
 module.exports = withPostgres({
   dev: true,
   pg: {
-    connectionString:
-      'postgres://licfoidvowmlqn:b447598b2fdc51cda4c456753916ded22edc2e6ba8036fa40ffef1836fa63246@ec2-54-221-243-211.compute-1.amazonaws.com:5432/d379g346in5ekb',
     ssl: true
-  },
-  url: 'http://localhost:3000'
+  }
 });
diff --git a/demo/build-tracker-cli.config.js b/demo/build-tracker-cli.config.js
deleted file mode 100644
index 27836e0..0000000
--- a/demo/build-tracker-cli.config.js
+++ /dev/null
@@ -1,17 +0,0 @@
-const path = require('path');
-
-const repoRoot = path.join(__dirname, '..');
-
-module.exports = {
-  applicationUrl: 'https://build-tracker-demo.herokuapp.com',
-  artifacts: ['src/*/dist/**/*.js', 'plugins/*/dist/**/*.js'],
-  baseDir: repoRoot,
-  cwd: repoRoot,
-  filenameHash: fileName => {
-    const parts = path.basename(fileName, '.js').split('.');
-    return parts.length > 1 ? parts[parts.length - 1] : null;
-  },
-  nameMapper: fileName => {
-    return fileName.replace(/\.js$/, '').replace(/(plugins|src|dist)\//g, '');
-  }
-};
diff --git a/demo/build-tracker.config.js b/demo/build-tracker.config.js
deleted file mode 100644
index feae680..0000000
--- a/demo/build-tracker.config.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const withPostgres = require('@build-tracker/plugin-with-postgres').default;
-
-module.exports = withPostgres({
-  pg: {
-    connectionString: process.env.DATABASE_URL,
-    ssl: true
-  },
-  port: process.env.PORT,
-  url: 'https://build-tracker-demo.herokuapp.com'
-});
diff --git a/demo/build.sh b/demo/build.sh
deleted file mode 100755
index 0669471..0000000
--- a/demo/build.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-cd `dirname $0`/../
-
-node src/server/dist/index.js setup -c demo/build-tracker.config.js
diff --git a/demo/run.sh b/demo/run.sh
deleted file mode 100755
index 69d54a1..0000000
--- a/demo/run.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-cd `dirname $0`/../
-
-node src/server/dist/index.js run -c demo/build-tracker.config.js
diff --git a/plugins/with-postgres/README.md b/plugins/with-postgres/README.md
index e348dfc..ee9b335 100644
--- a/plugins/with-postgres/README.md
+++ b/plugins/with-postgres/README.md
@@ -9,7 +9,6 @@ const withPostgres = require('@build-tracker/plugin-with-postgres');
 
 module.exports = withPostgres({
   pg: {
-    connectionString: '', // default: process.env.DATABASE_URL
     user: '', // default: process.env.PGUSER
     host: '', // default: process.env.PGHOST
     database: '', // default: process.env.PGPASSWORD
@@ -24,10 +23,6 @@ module.exports = withPostgres({
 
 All configuration options that are able to fall back on `process.env` environment variables can be written to your systems `ENV` or to a local `.env` file via [dotenv](https://github.com/motdotla/dotenv#readme).
 
-### `connectionString: string = process.env.DATABASE_URL`
-
-Optional. Use a single connection string to bypass the individual configs for `host`, `database`, `user`, `password`, and `port`.
-
 ### `host: string = process.env.PGHOST`
 
 Database host.
diff --git a/plugins/with-postgres/src/queries.ts b/plugins/with-postgres/src/queries.ts
index 60b8307..bd82ffe 100644
--- a/plugins/with-postgres/src/queries.ts
+++ b/plugins/with-postgres/src/queries.ts
@@ -25,13 +25,13 @@ export default class Queries {
   public insert = async ({ meta, artifacts }: BuildStruct): Promise<string> => {
     const build = new Build(meta, artifacts);
     const res = await this._pool.query(
-      'INSERT INTO builds (revision, timestamp, parentRevision, meta, artifacts) VALUES ($1, $2, $3, $4, $5)',
+      'INSERT INTO builds (revision, timestamp, parentRevision, meta, artifacts) VALUES ($1, $2, $3, @4)',
       [
         build.getMetaValue('revision'),
         build.meta.timestamp,
         build.getMetaValue('parentRevision'),
-        JSON.stringify(build.meta),
-        JSON.stringify(build.artifacts)
+        build.meta,
+        build.artifacts
       ]
     );
 
@@ -68,7 +68,7 @@ export default class Queries {
   };
 
   public getRecent = async (limit: number = 20): Promise<Array<BuildStruct>> => {
-    const res = await this._pool.query('SELECT meta, artifacts FROM builds ORDER BY timestamp LIMIT $1', [limit]);
+    const res = await this._pool.query('SELECT meta, artifacts FROM builds LIMIT $1 ORDER BY timestamp', [limit]);
     if (res.rowCount === 0) {
       throw new NotFoundError();
     }
diff --git a/src/app/package.json b/src/app/package.json
index d16d20f..524487c 100644
--- a/src/app/package.json
+++ b/src/app/package.json
@@ -18,7 +18,7 @@
     "cross-fetch": "^3.0.1",
     "d3-axis": "^1.0.12",
     "d3-color": "^1.2.3",
-    "d3-scale": "^2.2.2",
+    "d3-scale": "^3.0.0",
     "d3-scale-chromatic": "^1.3.3",
     "d3-selection": "^1.4.0",
     "d3-shape": "^1.3.4",
diff --git a/src/app/src/components/ComparisonTable/ArtifactCell.tsx b/src/app/src/components/ComparisonTable/ArtifactCell.tsx
index 23d6e1f..64ecfc0 100644
--- a/src/app/src/components/ComparisonTable/ArtifactCell.tsx
+++ b/src/app/src/components/ComparisonTable/ArtifactCell.tsx
@@ -2,12 +2,11 @@
  * Copyright (c) 2019 Paul Armstrong
  */
 import * as Theme from '../../theme';
-import Hoverable from '../Hoverable';
 import { hsl } from 'd3-color';
 import React from 'react';
-import { Th } from '../Table';
+import { Th } from './../Table';
 import { ArtifactCell as ACell, GroupCell as GCell } from '@build-tracker/comparator';
-import { StyleProp, StyleSheet, Switch, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
+import { StyleProp, StyleSheet, Switch, Text, View, ViewStyle } from 'react-native';
 
 interface Props {
   cell: ACell | GCell;
@@ -16,7 +15,6 @@ interface Props {
   isActive: boolean;
   onDisable: (artifactName: string) => void;
   onEnable: (artifactName: string) => void;
-  onFocus: (artifactName: string) => void;
   style?: StyleProp<ViewStyle>;
 }
 
@@ -28,7 +26,6 @@ export const ArtifactCell = (props: Props): React.ReactElement => {
     isActive,
     onDisable,
     onEnable,
-    onFocus,
     style
   } = props;
   const brighterColor = hsl(color);
@@ -39,24 +36,15 @@ export const ArtifactCell = (props: Props): React.ReactElement => {
     toggled ? onEnable(text) : onDisable(text);
   };
 
-  const handlePress = React.useCallback(() => {
-    onFocus(text);
-  }, [onFocus, text]);
-
   return (
     <Th style={style}>
       <View style={styles.artifact}>
-        <Hoverable>
-          {isHovered => (
-            <TouchableOpacity accessibilityRole="button" onPress={handlePress} style={styles.name}>
-              <Text style={[isHovered && styles.hoveredText]}>{text}</Text>
-            </TouchableOpacity>
-          )}
-        </Hoverable>
+        <View style={styles.name}>
+          <Text>{text}</Text>
+        </View>
         <View style={styles.switch}>
           {
             // @ts-ignore
-
             <Switch
               activeThumbColor={color}
               activeTrackColor={brighterColor.toString()}
@@ -79,16 +67,11 @@ const styles = StyleSheet.create({
   },
   name: {
     flexShrink: 1,
-    flexGrow: 1,
-    alignItems: 'flex-start',
     justifyContent: 'center',
     paddingEnd: Theme.Spacing.Xsmall
   },
   switch: {
     paddingStart: Theme.Spacing.Xsmall
-  },
-  hoveredText: {
-    color: Theme.Color.Primary30
   }
 });
 
diff --git a/src/app/src/components/ComparisonTable/BodyRow.tsx b/src/app/src/components/ComparisonTable/BodyRow.tsx
index 6918cbe..7601998 100644
--- a/src/app/src/components/ComparisonTable/BodyRow.tsx
+++ b/src/app/src/components/ComparisonTable/BodyRow.tsx
@@ -25,7 +25,6 @@ interface Props {
   isHovered: boolean;
   onDisableArtifact: (artifactName: string) => void;
   onEnableArtifact: (artifactName: string) => void;
-  onFocusArtifact: (artifactName: string) => void;
   onHoverArtifact: (revision: string) => void;
   row: ArtifactRow | GroupRow;
   rowIndex: number;
@@ -39,7 +38,6 @@ export const BodyRow = (props: Props): React.ReactElement => {
     isHovered,
     onDisableArtifact,
     onEnableArtifact,
-    onFocusArtifact,
     onHoverArtifact,
     row,
     rowIndex,
@@ -57,7 +55,6 @@ export const BodyRow = (props: Props): React.ReactElement => {
             isActive={isActive}
             onDisable={onDisableArtifact}
             onEnable={onEnableArtifact}
-            onFocus={onFocusArtifact}
           />
         );
       }
diff --git a/src/app/src/components/ComparisonTable/ComparisonTable.tsx b/src/app/src/components/ComparisonTable/ComparisonTable.tsx
index 759b5e1..0dba4fc 100644
--- a/src/app/src/components/ComparisonTable/ComparisonTable.tsx
+++ b/src/app/src/components/ComparisonTable/ComparisonTable.tsx
@@ -18,7 +18,6 @@ interface Props {
   hoveredArtifacts: Array<string>;
   onDisableArtifacts: (artifactNames: Array<string>) => void;
   onEnableArtifacts: (artifactNames: Array<string>) => void;
-  onFocusArtifacts: (artifactName: Array<string>) => void;
   onFocusRevision: (revision: string) => void;
   onHoverArtifacts: (artifactNames: Array<string>) => void;
   onRemoveRevision: (revision: string) => void;
@@ -33,7 +32,6 @@ const ComparisonTable = (props: Props): React.ReactElement => {
     hoveredArtifacts,
     onDisableArtifacts,
     onEnableArtifacts,
-    onFocusArtifacts,
     onFocusRevision,
     onHoverArtifacts,
     onRemoveRevision,
@@ -67,13 +65,6 @@ const ComparisonTable = (props: Props): React.ReactElement => {
     [onEnableArtifacts]
   );
 
-  const handleFocusArtifact = React.useCallback(
-    (artifactName: string): void => {
-      onFocusArtifacts([artifactName]);
-    },
-    [onFocusArtifacts]
-  );
-
   return (
     <Table onMouseLeave={handleMouseOut} style={styles.table}>
       <Thead>
@@ -89,7 +80,6 @@ const ComparisonTable = (props: Props): React.ReactElement => {
               key={groupName}
               onDisable={onDisableArtifacts}
               onEnable={onEnableArtifacts}
-              onFocus={onFocusArtifacts}
               onHover={onHoverArtifacts}
               row={row}
               sizeKey={sizeKey}
@@ -112,7 +102,6 @@ const ComparisonTable = (props: Props): React.ReactElement => {
               key={artifactName}
               onDisableArtifact={handleDisableArtifact}
               onEnableArtifact={handleEnableArtifact}
-              onFocusArtifact={handleFocusArtifact}
               onHoverArtifact={handleHoverArtifact}
               row={row}
               rowIndex={i}
diff --git a/src/app/src/components/ComparisonTable/GroupCell.tsx b/src/app/src/components/ComparisonTable/GroupCell.tsx
index d5fdb38..10f3820 100644
--- a/src/app/src/components/ComparisonTable/GroupCell.tsx
+++ b/src/app/src/components/ComparisonTable/GroupCell.tsx
@@ -4,11 +4,10 @@
 import * as Theme from '../../theme';
 import FolderIcon from '../../icons/Folder';
 import { GroupCell as GCell } from '@build-tracker/comparator';
-import Hoverable from '../Hoverable';
 import React from 'react';
 import { Th } from '../Table';
 import Tooltip from '../Tooltip';
-import { StyleProp, StyleSheet, Switch, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
+import { StyleProp, StyleSheet, Switch, Text, View, ViewStyle } from 'react-native';
 
 interface Props {
   cell: GCell;
@@ -16,7 +15,6 @@ interface Props {
   isActive: boolean;
   onDisable: (artifactNames: Array<string>) => void;
   onEnable: (artifactNames: Array<string>) => void;
-  onFocus: (artifactNames: Array<string>) => void;
   style?: StyleProp<ViewStyle>;
 }
 
@@ -27,7 +25,6 @@ export const GroupCell = (props: Props): React.ReactElement => {
     isActive,
     onDisable,
     onEnable,
-    onFocus,
     style
   } = props;
 
@@ -45,33 +42,23 @@ export const GroupCell = (props: Props): React.ReactElement => {
     setShowTooltip(showTooltip => !showTooltip);
   }, []);
 
-  const handleFocus = React.useCallback(() => {
-    onFocus(artifactNames);
-  }, [artifactNames, onFocus]);
-
   return (
     <Th style={style}>
       <View style={styles.artifact}>
-        <Hoverable>
-          {isHovered => (
-            <TouchableOpacity accessibilityRole="button" onPress={handleFocus} style={styles.name}>
-              <View ref={nameRef}>
-                <Text style={isHovered && styles.hoveredText}>
-                  {
-                    // @ts-ignore
-                    <FolderIcon
-                      onMouseEnter={handleToggleTooltip}
-                      onMouseLeave={handleToggleTooltip}
-                      style={[styles.folder, isHovered && styles.hoveredText]}
-                      testID="groupicon"
-                    />
-                  }{' '}
-                  {text}
-                </Text>
-              </View>
-            </TouchableOpacity>
-          )}
-        </Hoverable>
+        <View ref={nameRef} style={styles.name}>
+          <Text>
+            {
+              // @ts-ignore
+              <FolderIcon
+                onMouseEnter={handleToggleTooltip}
+                onMouseLeave={handleToggleTooltip}
+                style={styles.folder}
+                testID="groupicon"
+              />
+            }{' '}
+            {text}
+          </Text>
+        </View>
         {showTooltip ? <Tooltip relativeTo={nameRef} text={`${artifactNames.join(', ')}`} /> : null}
         <View style={styles.switch}>
           {
@@ -98,8 +85,6 @@ const styles = StyleSheet.create({
   },
   name: {
     flexShrink: 1,
-    flexGrow: 1,
-    alignItems: 'flex-start',
     justifyContent: 'center',
     paddingEnd: Theme.Spacing.Xsmall
   },
@@ -108,9 +93,6 @@ const styles = StyleSheet.create({
   },
   switch: {
     paddingStart: Theme.Spacing.Xsmall
-  },
-  hoveredText: {
-    color: Theme.Color.Primary30
   }
 });
 
diff --git a/src/app/src/components/ComparisonTable/GroupRow.tsx b/src/app/src/components/ComparisonTable/GroupRow.tsx
index 85e983d..8b5e54c 100644
--- a/src/app/src/components/ComparisonTable/GroupRow.tsx
+++ b/src/app/src/components/ComparisonTable/GroupRow.tsx
@@ -20,14 +20,13 @@ interface Props {
   isActive: boolean;
   onDisable: (artifactNames: Array<string>) => void;
   onEnable: (artifactNames: Array<string>) => void;
-  onFocus: (artifactName: Array<string>) => void;
   onHover: (artifactNames: Array<string>) => void;
   row: GRow;
   sizeKey: string;
 }
 
 export const GroupRow = (props: Props): React.ReactElement => {
-  const { isActive, onDisable, onEnable, onFocus, onHover, row, sizeKey } = props;
+  const { isActive, onDisable, onEnable, onHover, row, sizeKey } = props;
 
   const mapGroupCell = (cell: GCell | TCell | TDCell, i: number): React.ReactElement | void => {
     switch (cell.type) {
@@ -39,7 +38,6 @@ export const GroupRow = (props: Props): React.ReactElement => {
             isActive={isActive}
             onDisable={onDisable}
             onEnable={onEnable}
-            onFocus={onFocus}
             style={styles.cell}
           />
         );
diff --git a/src/app/src/components/ComparisonTable/RevisionCell.tsx b/src/app/src/components/ComparisonTable/RevisionCell.tsx
index 450e0b3..576fc50 100644
--- a/src/app/src/components/ComparisonTable/RevisionCell.tsx
+++ b/src/app/src/components/ComparisonTable/RevisionCell.tsx
@@ -48,7 +48,7 @@ export const RevisionCell = (props: Props): React.ReactElement => {
               ref={contentRef}
               style={styles.content}
             >
-              <Text style={styles.revision}>{formatSha(cell.revision)}</Text>
+              <Text>{formatSha(cell.revision)}</Text>
             </TouchableOpacity>
           </Th>
         )}
@@ -71,9 +71,6 @@ const styles = StyleSheet.create({
     justifyContent: 'center',
     width: '100%',
     height: '100%'
-  },
-  revision: {
-    fontWeight: 'bold'
   }
 });
 
diff --git a/src/app/src/components/ComparisonTable/RevisionDeltaCell.tsx b/src/app/src/components/ComparisonTable/RevisionDeltaCell.tsx
index 7a0928b..a831c6d 100644
--- a/src/app/src/components/ComparisonTable/RevisionDeltaCell.tsx
+++ b/src/app/src/components/ComparisonTable/RevisionDeltaCell.tsx
@@ -6,7 +6,7 @@ import { formatSha } from '@build-tracker/formatting';
 import React from 'react';
 import { Th } from '../Table';
 import Tooltip from '../Tooltip';
-import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';
+import { StyleProp, Text, View, ViewStyle } from 'react-native';
 
 interface Props {
   cell: Cell;
@@ -31,7 +31,7 @@ export const RevisionDeltaCell = (props: Props): React.ReactElement => {
       {
         // @ts-ignore
         <View onMouseEnter={handleEnter} onMouseLeave={handleExit} ref={viewRef} testID="delta">
-          <Text style={styles.delta}>{`𝚫${deltaIndex}`}</Text>
+          <Text>{`𝚫${deltaIndex}`}</Text>
         </View>
       }
       {showTooltip ? (
@@ -41,10 +41,4 @@ export const RevisionDeltaCell = (props: Props): React.ReactElement => {
   );
 };
 
-const styles = StyleSheet.create({
-  delta: {
-    fontWeight: 'bold'
-  }
-});
-
 export default React.memo(RevisionDeltaCell);
diff --git a/src/app/src/components/Snackbar.tsx b/src/app/src/components/Snackbar.tsx
deleted file mode 100644
index fe454ca..0000000
--- a/src/app/src/components/Snackbar.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Copyright (c) 2019 Paul Armstrong
- */
-import * as Theme from '../theme';
-import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
-import React from 'react';
-import ReactDOM from 'react-dom';
-import { StyleSheet, Text, View } from 'react-native';
-
-interface Props {
-  text: string;
-}
-
-const Snackbar = (props: Props): React.ReactElement => {
-  const { text } = props;
-  const portalRoot = canUseDOM && document.getElementById('snackbarPortal');
-
-  const bar = (
-    // @ts-ignore
-    <View accessibilityRole="alert" style={styles.root}>
-      <Text style={styles.text}>{text}</Text>
-    </View>
-  );
-
-  return portalRoot ? ReactDOM.createPortal(bar, portalRoot) : bar;
-};
-
-const styles = StyleSheet.create({
-  root: {
-    position: 'absolute',
-    bottom: Theme.Spacing.Normal,
-    left: Theme.Spacing.Normal,
-    width: 'auto',
-    paddingVertical: Theme.Spacing.Small,
-    paddingHorizontal: Theme.Spacing.Normal,
-    backgroundColor: Theme.Color.Black,
-    borderRadius: Theme.BorderRadius.Normal,
-    shadowOffset: { width: 0, height: 2 },
-    shadowColor: Theme.Color.Black,
-    shadowOpacity: 0.3,
-    shadowRadius: 5,
-    elevation: 10,
-    animationKeyframes: [
-      {
-        from: { transform: [{ scale: 0.85 }], opacity: 0 },
-        to: { transform: [{ scale: 1 }], opacity: 1 }
-      }
-    ],
-    animationDuration: '0.1s',
-    animationTimingFunction: Theme.MotionTiming.Accelerate,
-    animationIterationCount: 1
-  },
-  text: {
-    color: Theme.TextColor.Black
-  }
-});
-
-export default Snackbar;
diff --git a/src/app/src/screens/Main.tsx b/src/app/src/screens/Main.tsx
index 9a49a72..9856bc5 100644
--- a/src/app/src/screens/Main.tsx
+++ b/src/app/src/screens/Main.tsx
@@ -13,7 +13,6 @@ import MenuIcon from '../icons/Menu';
 import MenuItem from '../components/MenuItem';
 import React from 'react';
 import { ScaleSequential } from 'd3-scale';
-import Snackbar from '../components/Snackbar';
 import { BudgetLevel, BudgetType } from '@build-tracker/types';
 import { Clipboard, StyleSheet, View } from 'react-native';
 import Comparator, { ArtifactRow } from '@build-tracker/comparator';
@@ -79,8 +78,6 @@ const Main = (props: Props): React.ReactElement => {
   const [hoveredArtifacts, setHoveredArtifacts] = React.useState<Array<string>>([]);
   const [disabledArtifactsVisible, setDisabledArtifactsVisible] = React.useState<boolean>(true);
 
-  const [messages, setMessages] = React.useState<Array<string>>([]);
-
   const [, forceUpdate] = React.useState(0);
 
   const showDrawer = React.useCallback((): void => {
@@ -111,16 +108,6 @@ const Main = (props: Props): React.ReactElement => {
     forceUpdate(Date.now());
   }, []);
 
-  const handleFocusArtifacts = React.useCallback((artifactNames: Array<string>): void => {
-    setActiveArtifacts(activeArtifacts => {
-      return Object.keys(activeArtifacts).reduce((memo, artifactName) => {
-        memo[artifactName] = artifactNames.includes(artifactName) ? true : false;
-        return memo;
-      }, {});
-    });
-    forceUpdate(Date.now());
-  }, []);
-
   const handleSelectSizeKey = React.useCallback((name: string): void => {
     setSizeKey(name);
     forceUpdate(Date.now());
@@ -178,7 +165,6 @@ const Main = (props: Props): React.ReactElement => {
         sizeKey
       })
     );
-    setMessages(messages => [...messages, 'Copied table as markdown'].filter(Boolean));
   }, [activeComparator, artifactFilter, sizeKey]);
 
   const handleCopyAsCsv = React.useCallback((): void => {
@@ -188,17 +174,8 @@ const Main = (props: Props): React.ReactElement => {
         sizeKey
       })
     );
-    setMessages(messages => [...messages, 'Copied table as CSV'].filter(Boolean));
   }, [activeComparator, artifactFilter, sizeKey]);
 
-  React.useEffect(() => {
-    if (messages.length) {
-      setTimeout(() => {
-        setMessages(messages => messages.slice(1));
-      }, 4000);
-    }
-  }, [messages]);
-
   return (
     <View style={styles.layout}>
       <DrawerView
@@ -240,7 +217,6 @@ const Main = (props: Props): React.ReactElement => {
             sizeKey={sizeKey}
           />
         </View>
-        {messages.length ? <Snackbar key={messages[0]} text={messages[0]} /> : null}
         {compareRevisions.length ? (
           <React.Suspense fallback={null}>
             <Comparison
@@ -252,7 +228,6 @@ const Main = (props: Props): React.ReactElement => {
               hoveredArtifacts={hoveredArtifacts}
               onDisableArtifacts={handleDisableArtifacts}
               onEnableArtifacts={handleEnableArtifacts}
-              onFocusArtifacts={handleFocusArtifacts}
               onFocusRevision={setFocusedRevision}
               onUnfocusRevision={handleUnfocusRevision}
               onHoverArtifacts={handleHoverArtifacts}
diff --git a/src/app/src/server/index.tsx b/src/app/src/server/index.tsx
index 2af2e03..be2e8e6 100644
--- a/src/app/src/server/index.tsx
+++ b/src/app/src/server/index.tsx
@@ -24,7 +24,6 @@ ${css}
 <div id="root">${html}</div>
 <div id="menuPortal"></div>
 <div id="tooltipPortal"></div>
-<div id="snackbarPortal"></div>
 <script nonce="${nonce}">window.__PROPS__=${JSON.stringify(props)}</script>
 ${scripts.map(script => `<script nonce="${nonce}" src="/client/${script}"></script>`).join('')}
   `;
diff --git a/src/app/src/views/Comparison.tsx b/src/app/src/views/Comparison.tsx
index e0095a4..00da89e 100644
--- a/src/app/src/views/Comparison.tsx
+++ b/src/app/src/views/Comparison.tsx
@@ -23,7 +23,6 @@ const Comparison = (props: Props): React.ReactElement => {
     hoveredArtifacts,
     onDisableArtifacts,
     onEnableArtifacts,
-    onFocusArtifacts,
     onFocusRevision,
     onHoverArtifacts,
     onRemoveRevision,
@@ -43,7 +42,6 @@ const Comparison = (props: Props): React.ReactElement => {
             hoveredArtifacts={hoveredArtifacts}
             onDisableArtifacts={onDisableArtifacts}
             onEnableArtifacts={onEnableArtifacts}
-            onFocusArtifacts={onFocusArtifacts}
             onFocusRevision={onFocusRevision}
             onHoverArtifacts={onHoverArtifacts}
             onRemoveRevision={onRemoveRevision}
diff --git a/src/cli/src/commands/create-build.ts b/src/cli/src/commands/create-build.ts
index 15e412e..024a439 100644
--- a/src/cli/src/commands/create-build.ts
+++ b/src/cli/src/commands/create-build.ts
@@ -12,42 +12,23 @@ export const description = 'Construct a build for the current commit';
 
 interface Args {
   config?: string;
-  out: boolean;
-  'skip-dirty-check': boolean;
 }
 
 const group = 'Create a build';
 
 export const builder = (yargs): Argv<Args> =>
-  yargs
-    .usage(`Usage: $0 ${command}`)
-    .option('config', {
-      alias: 'c',
-      description: 'Override path to the build-tracker CLI config file',
-      group,
-      normalize: true
-    })
-    .option('out', {
-      alias: 'o',
-      default: true,
-      description: 'Write the build to stdout',
-      group,
-      type: 'boolean'
-    })
-    .option('skip-dirty-check', {
-      default: false,
-      description: 'Skip the git work tree state check',
-      group,
-      type: 'boolean'
-    });
+  yargs.usage(`Usage: $0 ${command}`).option('config', {
+    alias: 'c',
+    description: 'Override path to the build-tracker CLI config file',
+    group,
+    normalize: true
+  });
 
 export const handler = async (args: Args): Promise<{}> => {
   const config = await getConfig(args.config);
-  if (!args['skip-dirty-check']) {
-    const isDirty = await Git.isDirty(config.cwd);
-    if (isDirty) {
-      throw new Error('Current work tree is dirty. Please commit all changes before proceeding');
-    }
+  const isDirty = await Git.isDirty(config.cwd);
+  if (isDirty) {
+    throw new Error('Current work tree is dirty. Please commit all changes before proceeding');
   }
 
   const { artifacts: artifactStats } = await statArtifacts({ config: args.config, out: false });
@@ -69,7 +50,7 @@ export const handler = async (args: Args): Promise<{}> => {
   const revision = await Git.getCurrentRevision(config.cwd);
   const { timestamp, name, subject } = await Git.getRevisionDetails(revision, config.cwd);
 
-  const build = {
+  return Promise.resolve({
     meta: {
       parentRevision,
       revision,
@@ -78,11 +59,5 @@ export const handler = async (args: Args): Promise<{}> => {
       subject
     },
     artifacts
-  };
-
-  if (args.out) {
-    process.stdout.write(JSON.stringify(build, null, 2));
-  }
-
-  return Promise.resolve(build);
+  });
 };
diff --git a/src/cli/src/commands/upload-build.ts b/src/cli/src/commands/upload-build.ts
deleted file mode 100644
index 6569563..0000000
--- a/src/cli/src/commands/upload-build.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * Copyright (c) 2019 Paul Armstrong
- */
-import { Argv } from 'yargs';
-import { handler as createBuild } from './create-build';
-import getConfig from '../modules/config';
-import http from 'http';
-import https from 'https';
-import { URL } from 'url';
-
-export const command = 'upload-build';
-
-export const description = 'Upload a build for the current commit';
-
-interface Args {
-  config?: string;
-  out: boolean;
-  'skip-dirty-check': boolean;
-}
-
-const group = 'Create a build';
-
-export const builder = (yargs): Argv<Args> =>
-  yargs
-    .usage(`Usage: $0 ${command}`)
-    .option('config', {
-      alias: 'c',
-      description: 'Override path to the build-tracker CLI config file',
-      group,
-      normalize: true
-    })
-    .option('skip-dirty-check', {
-      default: false,
-      description: 'Skip the git work tree state check',
-      group,
-      type: 'boolean'
-    });
-
-export const handler = async (args: Args): Promise<void> => {
-  const config = await getConfig(args.config);
-  const build = await createBuild({ ...args, out: false });
-
-  const url = new URL(`${config.applicationUrl}/api/builds`);
-  const httpProtocol = url.protocol === 'https:' ? https : http;
-
-  const requestOptions = {
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/json'
-    }
-  };
-
-  return new Promise((resolve, reject) => {
-    const req = httpProtocol.request(url, requestOptions, (res: http.IncomingMessage) => {
-      res.setEncoding('utf8');
-      res.on('error', error => {
-        process.stderr.write(error.toString());
-      });
-
-      res.on('data', data => {
-        process.stdout.write(data);
-      });
-
-      res.on('end', () => {
-        resolve();
-      });
-    });
-
-    req.on('error', error => {
-      process.stderr.write(error.toString());
-      reject(error);
-      process.exit(1);
-    });
-
-    req.write(JSON.stringify(build));
-    req.end();
-  });
-};
diff --git a/src/cli/src/modules/git.ts b/src/cli/src/modules/git.ts
index 5a2a594..35bfcf8 100644
--- a/src/cli/src/modules/git.ts
+++ b/src/cli/src/modules/git.ts
@@ -12,24 +12,23 @@ export function isDirty(cwd: string = process.cwd()): Promise<boolean> {
 }
 
 export function getDefaultBranch(cwd: string = process.cwd()): Promise<string> {
-  return spawn('git', ['remote', 'show', 'origin'], { cwd }).then(
+  return spawn('git', ['symbolic-ref', 'refs/remotes/origin/HEAD'], { cwd }).then(
     (buffer: Buffer): string => {
-      const matches = buffer.toString().match(/HEAD branch: (\S+)/);
-      return matches[1];
+      return buffer.toString().replace(/^refs\/remotes\/origin/, '');
     }
   );
 }
 
-export function getParentRevision(branch: string, cwd: string = process.cwd()): Promise<string> {
-  return spawn('git', ['merge-base', 'HEAD', `origin/${branch}`], { cwd }).then(
+export function getParentRevision(master: string, cwd: string = process.cwd()): Promise<string> {
+  return spawn('git', ['merge-base', 'HEAD', `origin/${master}`], { cwd }).then(
     (buffer: Buffer): string => {
-      return buffer.toString().trim();
+      return buffer.toString();
     }
   );
 }
 
 export function getCurrentRevision(cwd: string = process.cwd()): Promise<string> {
-  return spawn('git', ['rev-parse', 'HEAD'], { cwd }).then((buffer: Buffer): string => buffer.toString().trim());
+  return spawn('git', ['rev-parse', 'HEAD'], { cwd }).then((buffer: Buffer): string => buffer.toString());
 }
 
 interface RevDetails {
diff --git a/src/cli/src/modules/spawn.ts b/src/cli/src/modules/spawn.ts
index 113ee0a..b4ef452 100644
--- a/src/cli/src/modules/spawn.ts
+++ b/src/cli/src/modules/spawn.ts
@@ -37,7 +37,7 @@ export default function spawn(command: string, args?: ReadonlyArray<string>, opt
 
     proc.on('close', code => {
       if (code !== 0) {
-        return reject({ code, stderr: Buffer.concat(stderr).toString(), errors });
+        return reject({ code, stderr: Buffer.concat(stderr), errors });
       }
       resolve(Buffer.concat(stdout));
     });
diff --git a/src/server/src/api/index.ts b/src/server/src/api/index.ts
index 4b4c467..48c87b0 100644
--- a/src/server/src/api/index.ts
+++ b/src/server/src/api/index.ts
@@ -16,7 +16,7 @@ const middleware = (
   handlers?: Handlers
 ): express.Router => {
   const { onBuildInsert = defaultBuildInsert } = handlers || {};
-  router.post('/api/builds', insertBuild(queries.build, appConfig, onBuildInsert));
+  router.post('/api/builds', insertBuild(queries.build.byRevision, appConfig, onBuildInsert));
   router.get('/api/builds/:limit?', queryByRecent(queries.builds));
   router.get('/api/builds/range/:startRevision..:endRevision', queryByRevisionRange(queries.builds));
   router.get('/api/builds/time/:startTimestamp..:endTimestamp', queryByTimeRange(queries.builds));
diff --git a/src/server/src/api/insert.ts b/src/server/src/api/insert.ts
index f8d0f3c..f672229 100644
--- a/src/server/src/api/insert.ts
+++ b/src/server/src/api/insert.ts
@@ -4,41 +4,30 @@
 import { AppConfig } from '@build-tracker/types';
 import Build from '@build-tracker/build';
 import Comparator from '@build-tracker/comparator';
-import { NotFoundError } from '@build-tracker/api-errors';
 import { Queries } from '../types';
 import { Request, RequestHandler, Response } from 'express';
 
 export const insertBuild = (
-  queries: Queries['build'],
+  getParent: Queries['build']['byRevision'],
   appConfig: AppConfig,
   onInserted: (comparator: Comparator) => Promise<void> = () => Promise.resolve()
 ): RequestHandler => (req: Request, res: Response): void => {
   const { artifacts, meta } = req.body;
   const build = new Build(meta, artifacts);
-  queries.insert(build).then(() =>
-    queries
-      .byRevision(build.getMetaValue('parentRevision'))
-      .then(parentBuild => {
-        return new Comparator({
-          artifactBudgets: appConfig.artifacts.budgets,
-          artifactFilters: appConfig.artifacts.filters,
-          builds: [build, new Build(parentBuild.meta, parentBuild.artifacts)]
-        });
-      })
-      .then(comparator => {
-        return onInserted(comparator).then(() => comparator);
-      })
-      .then(comparator => {
-        res.send({
-          comparator: comparator.toJSON()
-        });
-      })
-      .catch(error => {
-        if (error instanceof NotFoundError) {
-          res.send({ error });
-          return;
-        }
-        res.send({ error });
-      })
-  );
+  getParent(build.getMetaValue('parentRevision'))
+    .then(parentBuild => {
+      return new Comparator({
+        artifactBudgets: appConfig.artifacts.budgets,
+        artifactFilters: appConfig.artifacts.filters,
+        builds: [build, new Build(parentBuild.meta, parentBuild.artifacts)]
+      });
+    })
+    .then(comparator => {
+      return onInserted(comparator).then(() => comparator);
+    })
+    .then(comparator => {
+      res.send({
+        comparator: comparator.toJSON()
+      });
+    });
 };


Test patch:
diff --git a/plugins/with-postgres/src/__tests__/queries.test.ts b/plugins/with-postgres/src/__tests__/queries.test.ts
index 50aa98a..b638c94 100644
--- a/plugins/with-postgres/src/__tests__/queries.test.ts
+++ b/plugins/with-postgres/src/__tests__/queries.test.ts
@@ -50,8 +50,8 @@ describe('withPostgres', () => {
       query.mockReturnValue(Promise.resolve({ rowCount: 1, rows: [build] }));
       return queries.insert(build).then(res => {
         expect(query).toHaveBeenCalledWith(
-          'INSERT INTO builds (revision, timestamp, parentRevision, meta, artifacts) VALUES ($1, $2, $3, $4, $5)',
-          ['12345', now, 'abcdef', JSON.stringify(build.meta), JSON.stringify(build.artifacts)]
+          'INSERT INTO builds (revision, timestamp, parentRevision, meta, artifacts) VALUES ($1, $2, $3, @4)',
+          ['12345', now, 'abcdef', build.meta, build.artifacts]
         );
         expect(res).toEqual('12345');
       });
@@ -138,7 +138,7 @@ describe('withPostgres', () => {
       query.mockReturnValue(Promise.resolve({ rowCount: 2, rows: [row1, row2] }));
       const queries = new Queries(new Pool());
       return queries.getRecent(2).then(res => {
-        expect(query).toHaveBeenCalledWith('SELECT meta, artifacts FROM builds ORDER BY timestamp LIMIT $1', [2]);
+        expect(query).toHaveBeenCalledWith('SELECT meta, artifacts FROM builds LIMIT $1 ORDER BY timestamp', [2]);
         expect(res).toEqual([row1, row2]);
       });
     });
diff --git a/src/app/src/components/ComparisonTable/__tests__/ArtifactCell.test.tsx b/src/app/src/components/ComparisonTable/__tests__/ArtifactCell.test.tsx
index a327fed..3546ed0 100644
--- a/src/app/src/components/ComparisonTable/__tests__/ArtifactCell.test.tsx
+++ b/src/app/src/components/ComparisonTable/__tests__/ArtifactCell.test.tsx
@@ -10,15 +10,15 @@ import { fireEvent, render } from 'react-native-testing-library';
 describe('ArtifactCell', () => {
   describe('switch', () => {
     test('toggles with the name and on', () => {
+      const handleDisable = jest.fn();
       const handleEnable = jest.fn();
       const { getByType } = render(
         <ArtifactCell
           cell={{ type: CellType.ARTIFACT, text: 'tacos' }}
           color="red"
           isActive={false}
-          onDisable={jest.fn()}
+          onDisable={handleDisable}
           onEnable={handleEnable}
-          onFocus={jest.fn()}
         />
       );
       fireEvent(getByType(Switch), 'valueChange', true);
@@ -27,36 +27,18 @@ describe('ArtifactCell', () => {
 
     test('toggles with the name and off', () => {
       const handleDisable = jest.fn();
+      const handleEnable = jest.fn();
       const { getByType } = render(
         <ArtifactCell
           cell={{ type: CellType.ARTIFACT, text: 'tacos' }}
           color="red"
           isActive
           onDisable={handleDisable}
-          onEnable={jest.fn()}
-          onFocus={jest.fn()}
+          onEnable={handleEnable}
         />
       );
       fireEvent(getByType(Switch), 'valueChange', false);
       expect(handleDisable).toHaveBeenCalledWith('tacos');
     });
   });
-
-  describe('focus', () => {
-    test('focuses the artifact', () => {
-      const handleFocus = jest.fn();
-      const { getByProps } = render(
-        <ArtifactCell
-          cell={{ type: CellType.ARTIFACT, text: 'tacos' }}
-          color="red"
-          isActive={false}
-          onDisable={jest.fn()}
-          onEnable={jest.fn()}
-          onFocus={handleFocus}
-        />
-      );
-      fireEvent(getByProps({ accessibilityRole: 'button' }), 'press');
-      expect(handleFocus).toHaveBeenCalledWith('tacos');
-    });
-  });
 });
diff --git a/src/app/src/components/ComparisonTable/__tests__/BodyRow.test.tsx b/src/app/src/components/ComparisonTable/__tests__/BodyRow.test.tsx
index 70b4286..6c2646f 100644
--- a/src/app/src/components/ComparisonTable/__tests__/BodyRow.test.tsx
+++ b/src/app/src/components/ComparisonTable/__tests__/BodyRow.test.tsx
@@ -25,7 +25,6 @@ describe('BodyRow', () => {
           isHovered={false}
           onDisableArtifact={handleDisable}
           onEnableArtifact={handleEnable}
-          onFocusArtifact={jest.fn()}
           onHoverArtifact={jest.fn()}
           row={row}
           rowIndex={1}
@@ -61,7 +60,6 @@ describe('BodyRow', () => {
           isHovered={false}
           onDisableArtifact={jest.fn()}
           onEnableArtifact={jest.fn()}
-          onFocusArtifact={jest.fn()}
           onHoverArtifact={jest.fn()}
           row={row}
           rowIndex={1}
@@ -86,7 +84,6 @@ describe('BodyRow', () => {
           isHovered={false}
           onDisableArtifact={jest.fn()}
           onEnableArtifact={jest.fn()}
-          onFocusArtifact={jest.fn()}
           onHoverArtifact={jest.fn()}
           row={row}
           rowIndex={1}
@@ -113,7 +110,6 @@ describe('BodyRow', () => {
           isHovered={false}
           onDisableArtifact={jest.fn()}
           onEnableArtifact={jest.fn()}
-          onFocusArtifact={jest.fn()}
           onHoverArtifact={jest.fn()}
           row={row}
           rowIndex={1}
@@ -137,7 +133,6 @@ describe('BodyRow', () => {
           isHovered
           onDisableArtifact={jest.fn()}
           onEnableArtifact={jest.fn()}
-          onFocusArtifact={jest.fn()}
           onHoverArtifact={jest.fn()}
           row={row}
           rowIndex={1}
@@ -159,7 +154,6 @@ describe('BodyRow', () => {
           isHovered
           onDisableArtifact={jest.fn()}
           onEnableArtifact={jest.fn()}
-          onFocusArtifact={jest.fn()}
           onHoverArtifact={handleHoverArtifact}
           row={row}
           rowIndex={1}
@@ -180,7 +174,6 @@ describe('BodyRow', () => {
           isHovered={false}
           onDisableArtifact={jest.fn()}
           onEnableArtifact={jest.fn()}
-          onFocusArtifact={jest.fn()}
           onHoverArtifact={handleHoverArtifact}
           row={row}
           rowIndex={1}
diff --git a/src/app/src/components/ComparisonTable/__tests__/ComparisonTable.test.tsx b/src/app/src/components/ComparisonTable/__tests__/ComparisonTable.test.tsx
index 277b65b..2435734 100644
--- a/src/app/src/components/ComparisonTable/__tests__/ComparisonTable.test.tsx
+++ b/src/app/src/components/ComparisonTable/__tests__/ComparisonTable.test.tsx
@@ -34,7 +34,6 @@ describe('ComparisonTable', () => {
           hoveredArtifacts={[]}
           onDisableArtifacts={jest.fn()}
           onEnableArtifacts={jest.fn()}
-          onFocusArtifacts={jest.fn()}
           onFocusRevision={jest.fn()}
           onHoverArtifacts={jest.fn()}
           onRemoveRevision={jest.fn()}
@@ -57,7 +56,6 @@ describe('ComparisonTable', () => {
           hoveredArtifacts={[]}
           onDisableArtifacts={jest.fn()}
           onEnableArtifacts={jest.fn()}
-          onFocusArtifacts={jest.fn()}
           onFocusRevision={jest.fn()}
           onHoverArtifacts={jest.fn()}
           onRemoveRevision={jest.fn()}
@@ -82,7 +80,6 @@ describe('ComparisonTable', () => {
           hoveredArtifacts={[]}
           onDisableArtifacts={handleDisableArtifacts}
           onEnableArtifacts={jest.fn()}
-          onFocusArtifacts={jest.fn()}
           onFocusRevision={jest.fn()}
           onHoverArtifacts={jest.fn()}
           onRemoveRevision={jest.fn()}
@@ -105,7 +102,6 @@ describe('ComparisonTable', () => {
           hoveredArtifacts={[]}
           onDisableArtifacts={jest.fn()}
           onEnableArtifacts={handleEnableArtifacts}
-          onFocusArtifacts={jest.fn()}
           onFocusRevision={jest.fn()}
           onHoverArtifacts={jest.fn()}
           onRemoveRevision={jest.fn()}
@@ -115,29 +111,6 @@ describe('ComparisonTable', () => {
       fireEvent(getByProps({ cell: comparator.matrixArtifacts[1][0] }), 'enable', 'vendor');
       expect(handleEnableArtifacts).toHaveBeenCalledWith(['vendor']);
     });
-
-    test('focus', () => {
-      const handleFocusArtifacts = jest.fn();
-      const comparator = new Comparator({ builds });
-      const { getByProps } = render(
-        <ComparisonTable
-          activeArtifacts={{ vendor: true, main: false }}
-          colorScale={ColorScale.Magma}
-          comparator={comparator}
-          disabledArtifactsVisible
-          hoveredArtifacts={[]}
-          onDisableArtifacts={jest.fn()}
-          onEnableArtifacts={jest.fn()}
-          onFocusArtifacts={handleFocusArtifacts}
-          onFocusRevision={jest.fn()}
-          onHoverArtifacts={jest.fn()}
-          onRemoveRevision={jest.fn()}
-          sizeKey="stat"
-        />
-      );
-      fireEvent(getByProps({ cell: comparator.matrixArtifacts[1][0] }), 'focusArtifact', 'vendor');
-      expect(handleFocusArtifacts).toHaveBeenCalledWith(['vendor']);
-    });
   });
 
   describe('hovering', () => {
@@ -153,7 +126,6 @@ describe('ComparisonTable', () => {
           hoveredArtifacts={[]}
           onDisableArtifacts={jest.fn()}
           onEnableArtifacts={jest.fn()}
-          onFocusArtifacts={jest.fn()}
           onFocusRevision={jest.fn()}
           onHoverArtifacts={handleHoverArtifacts}
           onRemoveRevision={jest.fn()}
@@ -176,7 +148,6 @@ describe('ComparisonTable', () => {
           hoveredArtifacts={[]}
           onDisableArtifacts={jest.fn()}
           onEnableArtifacts={jest.fn()}
-          onFocusArtifacts={jest.fn()}
           onFocusRevision={jest.fn()}
           onHoverArtifacts={handleHoverArtifacts}
           onRemoveRevision={jest.fn()}
diff --git a/src/app/src/components/ComparisonTable/__tests__/GroupCell.test.tsx b/src/app/src/components/ComparisonTable/__tests__/GroupCell.test.tsx
index cc80683..9c647ba 100644
--- a/src/app/src/components/ComparisonTable/__tests__/GroupCell.test.tsx
+++ b/src/app/src/components/ComparisonTable/__tests__/GroupCell.test.tsx
@@ -16,7 +16,6 @@ describe('GroupCell', () => {
           isActive={false}
           onDisable={jest.fn()}
           onEnable={jest.fn()}
-          onFocus={jest.fn()}
         />
       );
       fireEvent(getByTestId('groupicon'), 'mouseEnter');
@@ -30,7 +29,6 @@ describe('GroupCell', () => {
           isActive={false}
           onDisable={jest.fn()}
           onEnable={jest.fn()}
-          onFocus={jest.fn()}
         />
       );
       fireEvent(getByTestId('groupicon'), 'mouseEnter');
@@ -49,7 +47,6 @@ describe('GroupCell', () => {
           isActive={false}
           onDisable={handleDisable}
           onEnable={handleEnable}
-          onFocus={jest.fn()}
         />
       );
       fireEvent(getByType(Switch), 'valueChange', true);
@@ -65,28 +62,10 @@ describe('GroupCell', () => {
           isActive
           onDisable={handleDisable}
           onEnable={handleEnable}
-          onFocus={jest.fn()}
         />
       );
       fireEvent(getByType(Switch), 'valueChange', false);
       expect(handleDisable).toHaveBeenCalledWith(['main']);
     });
   });
-
-  describe('focus', () => {
-    test('focuses the artifact', () => {
-      const handleFocus = jest.fn();
-      const { getByProps } = render(
-        <GroupCell
-          cell={{ type: CellType.GROUP, text: 'tacos', artifactNames: ['main'] }}
-          isActive
-          onDisable={jest.fn()}
-          onEnable={jest.fn()}
-          onFocus={handleFocus}
-        />
-      );
-      fireEvent(getByProps({ accessibilityRole: 'button' }), 'press');
-      expect(handleFocus).toHaveBeenCalledWith(['main']);
-    });
-  });
 });
diff --git a/src/app/src/components/ComparisonTable/__tests__/GroupRow.test.tsx b/src/app/src/components/ComparisonTable/__tests__/GroupRow.test.tsx
index 55ecc31..4b32915 100644
--- a/src/app/src/components/ComparisonTable/__tests__/GroupRow.test.tsx
+++ b/src/app/src/components/ComparisonTable/__tests__/GroupRow.test.tsx
@@ -21,7 +21,6 @@ describe('GroupRow', () => {
           isActive
           onDisable={handleDisable}
           onEnable={handleEnable}
-          onFocus={jest.fn()}
           onHover={jest.fn()}
           row={row}
           sizeKey="stat"
@@ -41,15 +40,7 @@ describe('GroupRow', () => {
         { type: CellType.TOTAL, name: 'tacos', sizes: { stat: 4 } }
       ];
       const { getByType } = render(
-        <GroupRow
-          isActive
-          onDisable={jest.fn()}
-          onEnable={jest.fn()}
-          onFocus={jest.fn()}
-          onHover={jest.fn()}
-          row={row}
-          sizeKey="stat"
-        />
+        <GroupRow isActive onDisable={jest.fn()} onEnable={jest.fn()} onHover={jest.fn()} row={row} sizeKey="stat" />
       );
       expect(getByType(TotalCell).props).toMatchObject({
         cell: row[1],
@@ -71,15 +62,7 @@ describe('GroupRow', () => {
         }
       ];
       const { getByType } = render(
-        <GroupRow
-          isActive
-          onDisable={jest.fn()}
-          onEnable={jest.fn()}
-          onFocus={jest.fn()}
-          onHover={jest.fn()}
-          row={row}
-          sizeKey="stat"
-        />
+        <GroupRow isActive onDisable={jest.fn()} onEnable={jest.fn()} onHover={jest.fn()} row={row} sizeKey="stat" />
       );
       expect(getByType(DeltaCell).props).toMatchObject({
         cell: row[1],
@@ -97,7 +80,6 @@ describe('GroupRow', () => {
           isActive
           onDisable={jest.fn()}
           onEnable={jest.fn()}
-          onFocus={jest.fn()}
           onHover={handleHoverArtifacts}
           row={row}
           sizeKey="stat"
@@ -115,7 +97,6 @@ describe('GroupRow', () => {
           isActive={false}
           onDisable={jest.fn()}
           onEnable={jest.fn()}
-          onFocus={jest.fn()}
           onHover={handleHoverArtifacts}
           row={row}
           sizeKey="stat"
diff --git a/src/app/src/components/__tests__/Snackbar.test.tsx b/src/app/src/components/__tests__/Snackbar.test.tsx
deleted file mode 100644
index 4dd8124..0000000
--- a/src/app/src/components/__tests__/Snackbar.test.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Copyright (c) 2019 Paul Armstrong
- */
-import React from 'react';
-import { render } from 'react-testing-library';
-import Snackbar from '../Snackbar';
-
-describe('Snackbar', () => {
-  test('renders to a portal', () => {
-    const portal = document.createElement('div');
-    portal.setAttribute('id', 'snackbarPortal');
-    document.body.appendChild(portal);
-
-    const { queryAllByRole, queryAllByText } = render(<Snackbar text="foobar" />, {
-      container: portal
-    });
-
-    expect(queryAllByRole('alert')).toHaveLength(1);
-    expect(queryAllByText('foobar')).toHaveLength(1);
-  });
-
-  test('renders directly without a portal available', () => {
-    const { queryAllByRole, queryAllByText } = render(<Snackbar text="foobar" />);
-
-    expect(queryAllByRole('alert')).toHaveLength(1);
-    expect(queryAllByText('foobar')).toHaveLength(1);
-  });
-});
diff --git a/src/app/src/screens/__tests__/Main.test.tsx b/src/app/src/screens/__tests__/Main.test.tsx
index ffe6b1b..cbb121d 100644
--- a/src/app/src/screens/__tests__/Main.test.tsx
+++ b/src/app/src/screens/__tests__/Main.test.tsx
@@ -117,19 +117,6 @@ describe('Main', () => {
       expect(getByType(Graph).props.activeArtifacts).toMatchObject({ main: true, vendor: false, shared: false });
     });
 
-    test('can focus artifacts', async () => {
-      const { getByType } = render(<Main url={url} />);
-      fireEvent(getByType(Graph), 'selectRevision', '22abb6f829a07ca96ff56deeadf4d0e8fc2dbb04');
-      await flushMicrotasksQueue(); // ensure dynamic imports are loaded
-      fireEvent(getByType(Comparison), 'focusArtifacts', ['main', 'vendor']);
-      expect(getByType(Comparison).props.activeArtifacts).toMatchObject({
-        main: true,
-        vendor: true,
-        shared: false
-      });
-      expect(getByType(Graph).props.activeArtifacts).toMatchObject({ main: true, vendor: true, shared: false });
-    });
-
     test('can toggle visibility of disabled artifacts', async () => {
       const { getByType, queryAllByProps } = render(<Main url={url} />);
       fireEvent(getByType(Graph), 'selectRevision', '22abb6f829a07ca96ff56deeadf4d0e8fc2dbb04');
@@ -214,11 +201,6 @@ describe('Main', () => {
   });
 
   describe('overflow items', () => {
-    let clipboardSpy;
-    beforeEach(() => {
-      clipboardSpy = jest.spyOn(Clipboard, 'setString').mockImplementation(() => {});
-    });
-
     test('can clear selected revisions', async () => {
       const { getByType, getByProps, queryAllByType } = render(<Main url={url} />);
       fireEvent(getByType(Graph), 'selectRevision', '243024909db66ac3c3e48d2ffe4015f049609834');
@@ -231,6 +213,7 @@ describe('Main', () => {
     });
 
     test('can copy as markdown', async () => {
+      const clipboardSpy = jest.spyOn(Clipboard, 'setString').mockImplementation(() => {});
       const { getByType, getByProps } = render(<Main url={url} />);
       fireEvent(getByType(Graph), 'selectRevision', '22abb6f829a07ca96ff56deeadf4d0e8fc2dbb04');
       await flushMicrotasksQueue(); // ensure dynamic imports are loaded
@@ -244,19 +227,8 @@ describe('Main', () => {
       expect(clipboardSpy).toHaveBeenCalledWith(comparator.toMarkdown());
     });
 
-    test('shows a message when copied as markdown', async () => {
-      const { getByProps, getByType, queryAllByProps, queryAllByText } = render(<Main url={url} />);
-      fireEvent(getByType(Graph), 'selectRevision', '22abb6f829a07ca96ff56deeadf4d0e8fc2dbb04');
-      await flushMicrotasksQueue(); // ensure dynamic imports are loaded
-
-      fireEvent.press(getByProps({ title: 'More actions' }));
-      fireEvent.press(getByProps({ label: 'Copy as markdown' }));
-
-      expect(queryAllByProps({ accessibilityRole: 'alert' })).toHaveLength(1);
-      expect(queryAllByText('Copied table as markdown')).toHaveLength(1);
-    });
-
     test('can copy as csv', async () => {
+      const clipboardSpy = jest.spyOn(Clipboard, 'setString').mockImplementation(() => {});
       const { getByProps, getByType } = render(<Main url={url} />);
       fireEvent(getByType(Graph), 'selectRevision', '243024909db66ac3c3e48d2ffe4015f049609834');
       await flushMicrotasksQueue(); // ensure dynamic imports are loaded
@@ -268,34 +240,5 @@ describe('Main', () => {
 
       expect(clipboardSpy).toHaveBeenCalledWith(comparator.toCsv());
     });
-
-    test('shows a message when copied as csv', async () => {
-      const { getByProps, getByType, queryAllByProps, queryAllByText } = render(<Main url={url} />);
-      fireEvent(getByType(Graph), 'selectRevision', '22abb6f829a07ca96ff56deeadf4d0e8fc2dbb04');
-      await flushMicrotasksQueue(); // ensure dynamic imports are loaded
-
-      fireEvent.press(getByProps({ title: 'More actions' }));
-      fireEvent.press(getByProps({ label: 'Copy as CSV' }));
-
-      expect(queryAllByProps({ accessibilityRole: 'alert' })).toHaveLength(1);
-      expect(queryAllByText('Copied table as CSV')).toHaveLength(1);
-    });
-  });
-
-  describe('messages', () => {
-    test('removes messages after some time ', async () => {
-      jest.spyOn(Clipboard, 'setString').mockImplementation(() => {});
-      const { getByProps, getByType, queryAllByProps, update } = render(<Main url={url} />);
-      fireEvent(getByType(Graph), 'selectRevision', '22abb6f829a07ca96ff56deeadf4d0e8fc2dbb04');
-      await flushMicrotasksQueue(); // ensure dynamic imports are loaded
-
-      fireEvent.press(getByProps({ title: 'More actions' }));
-      fireEvent.press(getByProps({ label: 'Copy as CSV' }));
-
-      expect(queryAllByProps({ accessibilityRole: 'alert' })).toHaveLength(1);
-      update(<Main url={url} />);
-      jest.runAllTimers();
-      expect(queryAllByProps({ accessibilityRole: 'alert' })).toHaveLength(0);
-    });
   });
 });
diff --git a/src/cli/src/commands/__tests__/create-build.test.ts b/src/cli/src/commands/__tests__/create-build.test.ts
index a828343..d7f59ed 100644
--- a/src/cli/src/commands/__tests__/create-build.test.ts
+++ b/src/cli/src/commands/__tests__/create-build.test.ts
@@ -6,64 +6,25 @@ import * as Git from '../../modules/git';
 import * as path from 'path';
 import yargs from 'yargs';
 
-const config = path.join(
-  path.dirname(require.resolve('@build-tracker/fixtures')),
-  'cli-configs/rc/.build-trackerrc.js'
-);
-
 describe('create-build', () => {
   describe('builder', () => {
     test('defaults config', () => {
       const args = Command.builder(yargs([]));
       expect(args.argv).toEqual({
         $0: expect.any(String),
-        _: [],
-        o: true,
-        out: true,
-        'skip-dirty-check': false,
-        skipDirtyCheck: false
+        _: []
       });
     });
   });
 
   describe('handler', () => {
-    test('writes the artifact stats to stdout', () => {
-      const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementationOnce(() => true);
-      jest.spyOn(Git, 'getDefaultBranch').mockReturnValue(Promise.resolve('master'));
-      jest.spyOn(Git, 'getParentRevision').mockReturnValue(Promise.resolve('1234567'));
-      jest.spyOn(Git, 'getCurrentRevision').mockReturnValue(Promise.resolve('abcdefg'));
-      jest
-        .spyOn(Git, 'getRevisionDetails')
-        .mockReturnValue(Promise.resolve({ timestamp: 1234567890, name: 'Jimmy', subject: 'tacos' }));
-
-      return Command.handler({ config, out: true, 'skip-dirty-check': true }).then(() => {
-        expect(writeSpy).toHaveBeenCalledWith(expect.stringMatching('\\"parentRevision\\": \\"1234567\\"'));
-      });
-    });
-
-    test('allows skipping the git worktree check', () => {
-      jest.spyOn(Git, 'isDirty').mockReturnValue(Promise.resolve(true));
-      jest.spyOn(Git, 'getDefaultBranch').mockReturnValue(Promise.resolve('master'));
-      jest.spyOn(Git, 'getParentRevision').mockReturnValue(Promise.resolve('1234567'));
-      jest.spyOn(Git, 'getCurrentRevision').mockReturnValue(Promise.resolve('abcdefg'));
-      jest
-        .spyOn(Git, 'getRevisionDetails')
-        .mockReturnValue(Promise.resolve({ timestamp: 1234567890, name: 'Jimmy', subject: 'tacos' }));
-
-      return Command.handler({ config, out: false, 'skip-dirty-check': true }).then(res => {
-        expect(res).toEqual(expect.any(Object));
-      });
-    });
-
     test('throws if the working tree is dirty', () => {
       jest.spyOn(Git, 'isDirty').mockReturnValue(Promise.resolve(true));
-      return Command.handler({ config, out: false, 'skip-dirty-check': false }).catch(err => {
-        expect(err).toBeInstanceOf(Error);
-        expect(err.message).toEqual('Current work tree is dirty. Please commit all changes before proceeding');
-      });
+      expect(Command.handler({})).rejects.toThrow();
     });
 
     test('returns a JSON representation of a build', () => {
+      jest.spyOn(Git, 'isDirty').mockReturnValue(Promise.resolve(false));
       jest.spyOn(Git, 'getDefaultBranch').mockReturnValue(Promise.resolve('master'));
       jest.spyOn(Git, 'getParentRevision').mockReturnValue(Promise.resolve('1234567'));
       jest.spyOn(Git, 'getCurrentRevision').mockReturnValue(Promise.resolve('abcdefg'));
@@ -71,8 +32,13 @@ describe('create-build', () => {
         .spyOn(Git, 'getRevisionDetails')
         .mockReturnValue(Promise.resolve({ timestamp: 1234567890, name: 'Jimmy', subject: 'tacos' }));
 
-      return Command.handler({ config, out: false, 'skip-dirty-check': true }).then(res => {
-        expect(res).toMatchObject({
+      return Command.handler({
+        config: path.join(
+          path.dirname(require.resolve('@build-tracker/fixtures')),
+          'cli-configs/rc/.build-trackerrc.js'
+        )
+      }).then(res => {
+        expect(res).toEqual({
           meta: {
             parentRevision: '1234567',
             revision: 'abcdefg',
diff --git a/src/cli/src/commands/__tests__/stat-artifacts.test.ts b/src/cli/src/commands/__tests__/stat-artifacts.test.ts
index a1f8e2a..593fb01 100644
--- a/src/cli/src/commands/__tests__/stat-artifacts.test.ts
+++ b/src/cli/src/commands/__tests__/stat-artifacts.test.ts
@@ -20,7 +20,7 @@ describe('stat-artifacts command', () => {
 
   describe('handler', () => {
     describe('out', () => {
-      test('writes the artifact stats to stdout', () => {
+      test('writes the artifact stats to stdou', () => {
         const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementationOnce(() => true);
         return Command.handler({
           config: path.join(
diff --git a/src/cli/src/commands/__tests__/upload-build.test.ts b/src/cli/src/commands/__tests__/upload-build.test.ts
deleted file mode 100644
index 48659a9..0000000
--- a/src/cli/src/commands/__tests__/upload-build.test.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Copyright (c) 2019 Paul Armstrong
- */
-import * as Command from '../upload-build';
-import yargs from 'yargs';
-
-describe('upload-build', () => {
-  describe('builder', () => {
-    test('defaults config', () => {
-      const args = Command.builder(yargs([]));
-      expect(args.argv).toEqual({
-        $0: expect.any(String),
-        _: [],
-        'skip-dirty-check': false,
-        skipDirtyCheck: false
-      });
-    });
-  });
-
-  describe('handler', () => {
-    test.skip('TODO', () => {});
-  });
-});
diff --git a/src/cli/src/modules/__tests__/git.test.ts b/src/cli/src/modules/__tests__/git.test.ts
index adb1288..7451474 100644
--- a/src/cli/src/modules/__tests__/git.test.ts
+++ b/src/cli/src/modules/__tests__/git.test.ts
@@ -41,7 +41,7 @@ describe('git', () => {
 
   describe('getDefaultBranch', () => {
     test('returns the default branch of the repo', () => {
-      jest.spyOn(Spawn, 'default').mockImplementation(() => Promise.resolve(Buffer.from('asdf\nHEAD branch: chili\n')));
+      jest.spyOn(Spawn, 'default').mockImplementation(() => Promise.resolve(Buffer.from('chili')));
 
       return Git.getDefaultBranch().then(branch => {
         expect(branch).toBe('chili');
@@ -49,9 +49,7 @@ describe('git', () => {
     });
 
     test('uses the process cwd', () => {
-      const spawn = jest
-        .spyOn(Spawn, 'default')
-        .mockImplementation(() => Promise.resolve(Buffer.from('asdf\nHEAD branch: chili\n')));
+      const spawn = jest.spyOn(Spawn, 'default').mockImplementation(() => Promise.resolve(Buffer.from('\n')));
 
       return Git.getDefaultBranch().then(() => {
         expect(spawn).toHaveBeenCalledWith(expect.any(String), expect.any(Array), { cwd: process.cwd() });
@@ -59,9 +57,7 @@ describe('git', () => {
     });
 
     test('uses given cwd', () => {
-      const spawn = jest
-        .spyOn(Spawn, 'default')
-        .mockImplementation(() => Promise.resolve(Buffer.from('asdf\nHEAD branch: chili\n')));
+      const spawn = jest.spyOn(Spawn, 'default').mockImplementation(() => Promise.resolve(Buffer.from('\n')));
 
       return Git.getDefaultBranch('tacos').then(() => {
         expect(spawn).toHaveBeenCalledWith(expect.any(String), expect.any(Array), { cwd: 'tacos' });
diff --git a/src/server/src/api/__tests__/insert.test.ts b/src/server/src/api/__tests__/insert.test.ts
index 0947e22..3f0aa42 100644
--- a/src/server/src/api/__tests__/insert.test.ts
+++ b/src/server/src/api/__tests__/insert.test.ts
@@ -12,35 +12,16 @@ const build = new Build({ revision: 'abc', parentRevision: 'def', timestamp: Dat
 const parentBuild = new Build({ revision: 'def', parentRevision: '123', timestamp: Date.now() }, []);
 
 describe('insert build handler', () => {
-  let queries, app;
+  let getParent, app;
   beforeEach(() => {
-    queries = {
-      byRevision: jest.fn(() => Promise.resolve(parentBuild)),
-      insert: jest.fn(() => Promise.resolve('1234'))
-    };
+    getParent = jest.fn(() => Promise.resolve(parentBuild));
     app = express();
     app.use(bodyParser.json());
   });
 
-  describe('insert', () => {
-    test('inserts the build', () => {
-      const handler = insertBuild(queries, { artifacts: {} });
-      app.post('/test', handler);
-
-      return request(app)
-        .post('/test')
-        .send({ meta: build.meta, artifacts: build.artifacts })
-        .set('Content-Type', 'application/json')
-        .set('Accept', 'application/json')
-        .then(() => {
-          expect(queries.insert).toHaveBeenCalled();
-        });
-    });
-  });
-
   describe('response', () => {
     test('includes the comparator JSON', () => {
-      const handler = insertBuild(queries, { artifacts: {} });
+      const handler = insertBuild(getParent, { artifacts: {} });
       app.post('/test', handler);
 
       return request(app)
@@ -57,7 +38,7 @@ describe('insert build handler', () => {
   describe('onInserted', () => {
     test('called with a comparator of the inserted branch against its parentRevision', () => {
       const handleInsert = jest.fn(() => Promise.resolve());
-      const handler = insertBuild(queries, { artifacts: {} }, handleInsert);
+      const handler = insertBuild(getParent, { artifacts: {} }, handleInsert);
       app.post('/test', handler);
 
       return request(app)