Repository: paularmstrong/build-tracker
Pull request #53 (2019-05-14 21:18:03)

Branch support
<!--
Thank you so much for contributing to open source and the Build Tracker project!
-->

# Problem

One of the main use-cases with Build Tracker is to catch issues with branches before they're merged to your production branch.

# Solution

Add support to insert, read, compare, and visualize changes from the merge-base and any arbitrary build on different branches, but still keep those non-production branch builds separate from the main UI.

# TODO

- [ ] 🤓 Add & update tests (always try to _increase_ test coverage)
- [ ] 🔬 Ensure CI is passing (`yarn lint:ci`, `yarn test`, `yarn tsc:ci`)
- [ ] 📖 Update relevant documentation



Patch:
diff --git a/config/postgres.js b/config/postgres.js
index d7e5c36..fff92e5 100644
--- a/config/postgres.js
+++ b/config/postgres.js
@@ -5,6 +5,7 @@ const { config } = require('dotenv');
 config();
 
 module.exports = withPostgres({
+  defaultBranch: 'next',
   dev: true,
   artifacts: {
     groups: [
diff --git a/demo/build-tracker.config.js b/demo/build-tracker.config.js
index 16e596b..b2ddce3 100644
--- a/demo/build-tracker.config.js
+++ b/demo/build-tracker.config.js
@@ -11,6 +11,7 @@ module.exports = withPostgres({
       }
     ]
   },
+  defaultBranch: 'next',
   pg: {
     connectionString: process.env.DATABASE_URL,
     ssl: true
diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md
index 3ce67e9..c0acf63 100644
--- a/docs/docs/configuration.md
+++ b/docs/docs/configuration.md
@@ -4,4 +4,20 @@ title: Configuration
 sidebar_label: Configuration
 ---
 
-Tacos
+# `build-tracker.config.js`
+
+Create a `build-tracker.config.js` file.
+
+## Example
+
+```js
+module.exports = {
+  defaultBranch: 'master'
+};
+```
+
+### `defaultBranch?: string = 'master'`
+
+Your application will graph this branch by default and ignore builds from other branches.
+
+This is a useful setting if you use and track a branch that is not `master` in your git repository. For example, Build Tracker uses `next` as its default working branch.
diff --git a/plugins/with-postgres/src/queries.ts b/plugins/with-postgres/src/queries.ts
index 60b8307..67b29bb 100644
--- a/plugins/with-postgres/src/queries.ts
+++ b/plugins/with-postgres/src/queries.ts
@@ -25,8 +25,9 @@ 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 (branch, revision, timestamp, parentRevision, meta, artifacts) VALUES ($1, $2, $3, $4, $5, $6)',
       [
+        build.getMetaValue('branch'),
         build.getMetaValue('revision'),
         build.meta.timestamp,
         build.getMetaValue('parentRevision'),
@@ -55,10 +56,14 @@ export default class Queries {
     throw new UnimplementedError(`revision range ${startRevision} - ${endRevision}`);
   };
 
-  public getByTimeRange = async (startTimestamp: number, endTimestamp: number): Promise<Array<BuildStruct>> => {
+  public getByTimeRange = async (
+    startTimestamp: number,
+    endTimestamp: number,
+    branch: string
+  ): Promise<Array<BuildStruct>> => {
     const res = await this._pool.query(
-      'SELECT meta, artifacts FROM builds WHERE timestamp >= $1 AND timestamp <= $2 ORDER BY timestamp',
-      [startTimestamp, endTimestamp]
+      'SELECT meta, artifacts FROM builds WHERE timestamp >= $1 AND timestamp <= $2 AND branch = $3 ORDER BY timestamp',
+      [startTimestamp, endTimestamp, branch]
     );
     if (res.rowCount === 0) {
       throw new NotFoundError();
@@ -67,8 +72,11 @@ export default class Queries {
     return Promise.resolve(res.rows);
   };
 
-  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]);
+  public getRecent = async (limit: number = 20, branch: string): Promise<Array<BuildStruct>> => {
+    const res = await this._pool.query(
+      'SELECT meta, artifacts FROM builds WHERE branch = $1 ORDER BY timestamp LIMIT $2',
+      [branch, limit]
+    );
     if (res.rowCount === 0) {
       throw new NotFoundError();
     }
diff --git a/plugins/with-postgres/src/setup.ts b/plugins/with-postgres/src/setup.ts
index aa7265f..c691419 100644
--- a/plugins/with-postgres/src/setup.ts
+++ b/plugins/with-postgres/src/setup.ts
@@ -9,12 +9,13 @@ export default function setup(pool: Pool): () => Promise<boolean> {
     try {
       await client.query(`CREATE TABLE IF NOT EXISTS builds(
   revision char(64) PRIMARY KEY NOT NULL,
+  branch char(64) NOT NULL,
   parentRevision char(64),
   timestamp int NOT NULL,
   meta jsonb NOT NULL,
   artifacts jsonb NOT NULL
 )`);
-      await client.query('CREATE INDEX IF NOT EXISTS parent ON builds (revision, parentRevision)');
+      await client.query('CREATE INDEX IF NOT EXISTS parent ON builds (revision, parentRevision, branch)');
       await client.query('CREATE INDEX IF NOT EXISTS timestamp ON builds (timestamp)');
     } catch (err) {
       client.release();
diff --git a/src/build/src/index.ts b/src/build/src/index.ts
index 3a9b8ec..18160c0 100644
--- a/src/build/src/index.ts
+++ b/src/build/src/index.ts
@@ -23,6 +23,11 @@ export interface BuildMeta {
    * @type {number}
    */
   timestamp: number;
+  /**
+   * Branch name for this revision. Helps for in-progress work to be filtered from the default UI
+   * @type {string}
+   */
+  branch: BuildMetaItem;
 }
 
 export interface Artifact<AS extends ArtifactSizes> {
diff --git a/src/cli/src/commands/create-build.ts b/src/cli/src/commands/create-build.ts
index 15e412e..43225ff 100644
--- a/src/cli/src/commands/create-build.ts
+++ b/src/cli/src/commands/create-build.ts
@@ -68,14 +68,16 @@ export const handler = async (args: Args): Promise<{}> => {
   const parentRevision = await Git.getParentRevision(defaultBranch, config.cwd);
   const revision = await Git.getCurrentRevision(config.cwd);
   const { timestamp, name, subject } = await Git.getRevisionDetails(revision, config.cwd);
+  const branch = await Git.getBranch(config.cwd);
 
   const build = {
     meta: {
+      author: name,
+      branch,
       parentRevision,
       revision,
-      timestamp,
-      author: name,
-      subject
+      subject,
+      timestamp
     },
     artifacts
   };
diff --git a/src/cli/src/modules/git.ts b/src/cli/src/modules/git.ts
index f5e0360..2bbe12a 100644
--- a/src/cli/src/modules/git.ts
+++ b/src/cli/src/modules/git.ts
@@ -20,6 +20,14 @@ export function getDefaultBranch(cwd: string = process.cwd()): Promise<string> {
   );
 }
 
+export function getBranch(cwd: string = process.cwd()): Promise<string> {
+  return spawn('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd }).then(
+    (buffer: Buffer): string => {
+      return buffer.toString().trim();
+    }
+  );
+}
+
 export function getParentRevision(branch: string, cwd: string = process.cwd()): Promise<string> {
   return spawn('git', ['merge-base', 'HEAD', `origin/${branch}`], { cwd }).then(
     (buffer: Buffer): string => {
diff --git a/src/server/src/api/index.ts b/src/server/src/api/index.ts
index 4b4c467..66d2179 100644
--- a/src/server/src/api/index.ts
+++ b/src/server/src/api/index.ts
@@ -1,27 +1,22 @@
 /**
  * Copyright (c) 2019 Paul Armstrong
  */
-import { AppConfig } from '@build-tracker/types';
 import express from 'express';
+import { Handlers } from '../types';
 import { insertBuild } from './insert';
-import { Handlers, Queries } from '../types';
+import { ServerConfig } from '../server';
 import { queryByRecent, queryByRevision, queryByRevisionRange, queryByRevisions, queryByTimeRange } from './read';
 
 const defaultBuildInsert = (): Promise<void> => Promise.resolve();
 
-const middleware = (
-  router: express.Router,
-  queries: Queries,
-  appConfig: AppConfig,
-  handlers?: Handlers
-): express.Router => {
+const middleware = (router: express.Router, config: ServerConfig, handlers?: Handlers): express.Router => {
   const { onBuildInsert = defaultBuildInsert } = handlers || {};
-  router.post('/api/builds', insertBuild(queries.build, 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));
-  router.get('/api/builds/list/*', queryByRevisions(queries.builds));
-  router.get('/api/build/:revision', queryByRevision(queries.build));
+  router.post('/api/builds', insertBuild(config.queries.build, config, onBuildInsert));
+  router.get('/api/builds/:limit?', queryByRecent(config.queries.builds, config));
+  router.get('/api/builds/range/:startRevision..:endRevision', queryByRevisionRange(config.queries.builds));
+  router.get('/api/builds/time/:startTimestamp..:endTimestamp', queryByTimeRange(config.queries.builds, config));
+  router.get('/api/builds/list/*', queryByRevisions(config.queries.builds));
+  router.get('/api/build/:revision', queryByRevision(config.queries.build));
   return router;
 };
 
diff --git a/src/server/src/api/insert.ts b/src/server/src/api/insert.ts
index f8d0f3c..50c989c 100644
--- a/src/server/src/api/insert.ts
+++ b/src/server/src/api/insert.ts
@@ -1,27 +1,28 @@
 /**
  * Copyright (c) 2019 Paul Armstrong
  */
-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 { ServerConfig } from '../server';
 import { Request, RequestHandler, Response } from 'express';
 
 export const insertBuild = (
   queries: Queries['build'],
-  appConfig: AppConfig,
+  config: ServerConfig,
   onInserted: (comparator: Comparator) => Promise<void> = () => Promise.resolve()
 ): RequestHandler => (req: Request, res: Response): void => {
   const { artifacts, meta } = req.body;
+  const { artifacts: artifactConfig = {} } = config;
   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,
+          artifactBudgets: artifactConfig.budgets,
+          artifactFilters: artifactConfig.filters,
           builds: [build, new Build(parentBuild.meta, parentBuild.artifacts)]
         });
       })
diff --git a/src/server/src/api/read.ts b/src/server/src/api/read.ts
index 300c420..e0869aa 100644
--- a/src/server/src/api/read.ts
+++ b/src/server/src/api/read.ts
@@ -2,8 +2,11 @@
  * Copyright (c) 2019 Paul Armstrong
  */
 import { Queries } from '../types';
+import { ServerConfig } from '../server';
 import { Request, RequestHandler, Response } from 'express';
 
+const DEFAULT_BRANCH = 'master';
+
 export const queryByRevision = (queries: Queries['build']): RequestHandler => (req: Request, res: Response): void => {
   const { revision } = req.params;
   queries
@@ -33,10 +36,18 @@ export const queryByRevisionRange = (queries: Queries['builds']): RequestHandler
     });
 };
 
-export const queryByTimeRange = (queries: Queries['builds']): RequestHandler => (req: Request, res: Response): void => {
+export const queryByTimeRange = (queries: Queries['builds'], config: ServerConfig): RequestHandler => (
+  req: Request,
+  res: Response
+): void => {
   const { startTimestamp, endTimestamp } = req.params;
+  const { branch } = req.query;
   queries
-    .byTimeRange(parseInt(startTimestamp, 10), parseInt(endTimestamp, 10))
+    .byTimeRange(
+      parseInt(startTimestamp, 10),
+      parseInt(endTimestamp, 10),
+      branch || config.defaultBranch || DEFAULT_BRANCH
+    )
     .then(builds => {
       res.send(builds);
     })
@@ -59,10 +70,14 @@ export const queryByRevisions = (queries: Queries['builds']): RequestHandler =>
     });
 };
 
-export const queryByRecent = (queries: Queries['builds']): RequestHandler => (req: Request, res: Response): void => {
+export const queryByRecent = (queries: Queries['builds'], config: ServerConfig): RequestHandler => (
+  req: Request,
+  res: Response
+): void => {
   const { limit } = req.params;
+  const { branch } = req.query;
   queries
-    .recent(limit)
+    .recent(limit ? parseInt(limit, 10) : undefined, branch || config.defaultBranch || DEFAULT_BRANCH)
     .then(builds => {
       res.send(builds);
     })
diff --git a/src/server/src/server.ts b/src/server/src/server.ts
index c31c430..ef14b45 100644
--- a/src/server/src/server.ts
+++ b/src/server/src/server.ts
@@ -14,6 +14,7 @@ import express, { Application, NextFunction, Request, RequestHandler, Response }
 import { Handlers, Queries } from './types';
 
 export interface ServerConfig extends AppConfig {
+  defaultBranch?: string;
   dev?: boolean;
   handlers?: Handlers;
   port?: number;
@@ -44,12 +45,12 @@ export const props = (config: AppConfig, url: string): RequestHandler => (
 };
 
 export default function runBuildTracker(config: ServerConfig): Application {
-  const { handlers, port = 3000, queries, url, ...appConfig } = config;
+  const { handlers, port = 3000, url, ...appConfig } = config;
   const IN_DEV = process.env.NODE_ENV !== 'production' && config.dev;
   app.use(reqLogger);
   app.use(bodyParser.json());
 
-  app.use(api(express.Router(), queries, config, handlers));
+  app.use(api(express.Router(), config, handlers));
 
   app.use(nonce);
   app.use(props(appConfig, url));
diff --git a/src/server/src/types.ts b/src/server/src/types.ts
index 95828fb..75abc21 100644
--- a/src/server/src/types.ts
+++ b/src/server/src/types.ts
@@ -17,8 +17,8 @@ export interface Queries {
   builds: {
     byRevisions: (...revisions: Array<string>) => Promise<Array<Build>>;
     byRevisionRange: (startRevision: string, endRevision: string) => Promise<Array<Build>>;
-    byTimeRange: (startTimestamp: number, endTimestamp: number) => Promise<Array<Build>>;
-    recent: (limit: number) => Promise<Array<Build>>;
+    byTimeRange: (startTimestamp: number, endTimestamp: number, branch: string) => Promise<Array<Build>>;
+    recent: (limit: number, branch: string) => Promise<Array<Build>>;
   };
 }
 


Test patch:
diff --git a/plugins/with-postgres/src/__tests__/queries.test.ts b/plugins/with-postgres/src/__tests__/queries.test.ts
index 50aa98a..f1ba679 100644
--- a/plugins/with-postgres/src/__tests__/queries.test.ts
+++ b/plugins/with-postgres/src/__tests__/queries.test.ts
@@ -41,6 +41,7 @@ describe('withPostgres', () => {
       const now = Date.now();
       const build = {
         meta: {
+          branch: 'master',
           revision: { value: '12345', url: 'https://build-tracker.local' },
           timestamp: now,
           parentRevision: 'abcdef'
@@ -50,8 +51,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 (branch, revision, timestamp, parentRevision, meta, artifacts) VALUES ($1, $2, $3, $4, $5, $6)',
+          ['master', '12345', now, 'abcdef', JSON.stringify(build.meta), JSON.stringify(build.artifacts)]
         );
         expect(res).toEqual('12345');
       });
@@ -61,11 +62,7 @@ describe('withPostgres', () => {
       const queries = new Queries(new Pool());
       const now = Date.now();
       const build = {
-        meta: {
-          revision: 'abcdef',
-          timestamp: now,
-          parentRevision: '12345'
-        },
+        meta: { branch: 'master', revision: 'abcdef', timestamp: now, parentRevision: '12345' },
         artifacts: []
       };
       query.mockReturnValue(Promise.resolve({ rowCount: 0 }));
@@ -77,8 +74,8 @@ describe('withPostgres', () => {
 
   describe('getByRevisions', () => {
     test('selects meta and artifacts', () => {
-      const row1 = { meta: { revision: '12345' }, artifacts: [] };
-      const row2 = { meta: { revision: 'abcde' }, artifacts: [] };
+      const row1 = { meta: { branch: 'master', revision: '12345' }, artifacts: [] };
+      const row2 = { meta: { branch: 'master', revision: 'abcde' }, artifacts: [] };
       query.mockReturnValue(Promise.resolve({ rowCount: 2, rows: [row1, row2] }));
       const queries = new Queries(new Pool());
       return queries.getByRevisions('12345', 'abcde').then(res => {
@@ -109,14 +106,14 @@ describe('withPostgres', () => {
 
   describe('getByTimeRange', () => {
     test('selects meta and artifacts', () => {
-      const row1 = { meta: { revision: '12345' }, artifacts: [] };
-      const row2 = { meta: { revision: 'abcde' }, artifacts: [] };
+      const row1 = { meta: { branch: 'master', revision: '12345' }, artifacts: [] };
+      const row2 = { meta: { branch: 'master', revision: 'abcde' }, artifacts: [] };
       query.mockReturnValue(Promise.resolve({ rowCount: 2, rows: [row1, row2] }));
       const queries = new Queries(new Pool());
-      return queries.getByTimeRange(12345, 67890).then(res => {
+      return queries.getByTimeRange(12345, 67890, 'tacos').then(res => {
         expect(query).toHaveBeenCalledWith(
-          'SELECT meta, artifacts FROM builds WHERE timestamp >= $1 AND timestamp <= $2 ORDER BY timestamp',
-          [12345, 67890]
+          'SELECT meta, artifacts FROM builds WHERE timestamp >= $1 AND timestamp <= $2 AND branch = $3 ORDER BY timestamp',
+          [12345, 67890, 'tacos']
         );
         expect(res).toEqual([row1, row2]);
       });
@@ -125,7 +122,7 @@ describe('withPostgres', () => {
     test('throws with no results', () => {
       query.mockReturnValue(Promise.resolve({ rowCount: 0 }));
       const queries = new Queries(new Pool());
-      return queries.getByTimeRange(12345, 67890).catch(err => {
+      return queries.getByTimeRange(12345, 67890, 'tacos').catch(err => {
         expect(err).toBeInstanceOf(NotFoundError);
       });
     });
@@ -133,12 +130,15 @@ describe('withPostgres', () => {
 
   describe('getRecent', () => {
     test('returns N most recent', () => {
-      const row1 = { meta: { revision: '12345' }, artifacts: [] };
-      const row2 = { meta: { revision: 'abcde' }, artifacts: [] };
+      const row1 = { meta: { branch: 'master', revision: '12345' }, artifacts: [] };
+      const row2 = { meta: { branch: 'master', revision: 'abcde' }, artifacts: [] };
       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]);
+      return queries.getRecent(2, 'master').then(res => {
+        expect(query).toHaveBeenCalledWith(
+          'SELECT meta, artifacts FROM builds WHERE branch = $1 ORDER BY timestamp LIMIT $2',
+          ['master', 2]
+        );
         expect(res).toEqual([row1, row2]);
       });
     });
@@ -146,7 +146,7 @@ describe('withPostgres', () => {
     test('throws with no results', () => {
       query.mockReturnValue(Promise.resolve({ rowCount: 0 }));
       const queries = new Queries(new Pool());
-      return queries.getRecent().catch(err => {
+      return queries.getRecent(undefined, 'tacos').catch(err => {
         expect(err).toBeInstanceOf(NotFoundError);
       });
     });
diff --git a/plugins/with-postgres/src/__tests__/setup.test.ts b/plugins/with-postgres/src/__tests__/setup.test.ts
index 349c2bd..9d44702 100644
--- a/plugins/with-postgres/src/__tests__/setup.test.ts
+++ b/plugins/with-postgres/src/__tests__/setup.test.ts
@@ -28,7 +28,9 @@ describe('withPostgres', () => {
 
   test('creates a multi-index', () => {
     return setupFn().then(() => {
-      expect(query).toHaveBeenCalledWith('CREATE INDEX IF NOT EXISTS parent ON builds (revision, parentRevision)');
+      expect(query).toHaveBeenCalledWith(
+        'CREATE INDEX IF NOT EXISTS parent ON builds (revision, parentRevision, branch)'
+      );
     });
   });
 
diff --git a/src/app/src/components/ComparisonTable/__tests__/ComparisonTable.test.tsx b/src/app/src/components/ComparisonTable/__tests__/ComparisonTable.test.tsx
index 7289cab..e6de3b3 100644
--- a/src/app/src/components/ComparisonTable/__tests__/ComparisonTable.test.tsx
+++ b/src/app/src/components/ComparisonTable/__tests__/ComparisonTable.test.tsx
@@ -11,11 +11,11 @@ import { Table } from '../../Table';
 import { fireEvent, render } from 'react-native-testing-library';
 
 const builds = [
-  new Build({ revision: '123', parentRevision: '000', timestamp: 0 }, [
+  new Build({ branch: 'master', revision: '123', parentRevision: '000', timestamp: 0 }, [
     { name: 'main', hash: '123', sizes: { gzip: 123 } },
     { name: 'vendor', hash: '123', sizes: { gzip: 123 } }
   ]),
-  new Build({ revision: 'abc', parentRevision: '123', timestamp: 0 }, [
+  new Build({ branch: 'master', revision: 'abc', parentRevision: '123', timestamp: 0 }, [
     { name: 'main', hash: '123', sizes: { gzip: 123 } },
     { name: 'vendor', hash: '123', sizes: { gzip: 123 } }
   ])
diff --git a/src/app/src/components/Graph/__tests__/Area.test.tsx b/src/app/src/components/Graph/__tests__/Area.test.tsx
index 683cfdf..0da310a 100644
--- a/src/app/src/components/Graph/__tests__/Area.test.tsx
+++ b/src/app/src/components/Graph/__tests__/Area.test.tsx
@@ -14,11 +14,11 @@ import { scaleLinear, scalePoint } from 'd3-scale';
 describe('Area', () => {
   test('only draws the active artifacts', () => {
     const builds = [
-      new Build({ revision: '123', parentRevision: '000', timestamp: 0 }, [
+      new Build({ branch: 'master', revision: '123', parentRevision: '000', timestamp: 0 }, [
         { name: 'main', hash: '123', sizes: { gzip: 123 } },
         { name: 'vendor', hash: '123', sizes: { gzip: 123 } }
       ]),
-      new Build({ revision: 'abc', parentRevision: '123', timestamp: 0 }, [
+      new Build({ branch: 'master', revision: 'abc', parentRevision: '123', timestamp: 0 }, [
         { name: 'main', hash: '123', sizes: { gzip: 123 } },
         { name: 'vendor', hash: '123', sizes: { gzip: 123 } }
       ])
@@ -60,10 +60,10 @@ describe('Area', () => {
 
   test('can render if an artifact does not exist in a build', () => {
     const builds = [
-      new Build({ revision: '123', parentRevision: '000', timestamp: 0 }, [
+      new Build({ branch: 'master', revision: '123', parentRevision: '000', timestamp: 0 }, [
         { name: 'main', hash: '123', sizes: { stat: 456 } }
       ]),
-      new Build({ revision: 'abc', parentRevision: '123', timestamp: 0 }, [
+      new Build({ branch: 'master', revision: 'abc', parentRevision: '123', timestamp: 0 }, [
         { name: 'main', hash: '123', sizes: { stat: 489 } },
         { name: 'vendor', hash: '123', sizes: { stat: 123 } }
       ])
@@ -108,11 +108,11 @@ describe('Area', () => {
 
   test('reduces luminance of non-hovered artifacts', () => {
     const builds = [
-      new Build({ revision: '123', parentRevision: '000', timestamp: 0 }, [
+      new Build({ branch: 'master', revision: '123', parentRevision: '000', timestamp: 0 }, [
         { name: 'main', hash: '123', sizes: { stat: 456 } },
         { name: 'vendor', hash: '123', sizes: { stat: 123 } }
       ]),
-      new Build({ revision: 'abc', parentRevision: '123', timestamp: 0 }, [
+      new Build({ branch: 'master', revision: 'abc', parentRevision: '123', timestamp: 0 }, [
         { name: 'main', hash: '123', sizes: { stat: 489 } },
         { name: 'vendor', hash: '123', sizes: { stat: 123 } }
       ])
diff --git a/src/app/src/components/Graph/__tests__/HoverOverlay.test.tsx b/src/app/src/components/Graph/__tests__/HoverOverlay.test.tsx
index 9f35b3a..99ca3a2 100644
--- a/src/app/src/components/Graph/__tests__/HoverOverlay.test.tsx
+++ b/src/app/src/components/Graph/__tests__/HoverOverlay.test.tsx
@@ -18,19 +18,19 @@ const yScale = scaleLinear()
   .range([400, 0])
   .domain([0, 168]);
 const builds = [
-  new Build({ revision: '1234567abcdef', parentRevision: '000', timestamp: 0 }, [
+  new Build({ branch: 'master', revision: '1234567abcdef', parentRevision: '000', timestamp: 0 }, [
     { name: 'main', hash: '123', sizes: { gzip: 123 } },
     { name: 'vendor', hash: '123', sizes: { gzip: 45 } }
   ]),
-  new Build({ revision: 'abcdefg1234567', parentRevision: '123', timestamp: 1 }, [
+  new Build({ branch: 'master', revision: 'abcdefg1234567', parentRevision: '123', timestamp: 1 }, [
     { name: 'main', hash: '123', sizes: { gzip: 123 } },
     { name: 'vendor', hash: '123', sizes: { gzip: 45 } }
   ]),
-  new Build({ revision: 'abcd', parentRevision: '123', timestamp: 2 }, [
+  new Build({ branch: 'master', revision: 'abcd', parentRevision: '123', timestamp: 2 }, [
     { name: 'main', hash: '123', sizes: { gzip: 123 } },
     { name: 'vendor', hash: '123', sizes: { gzip: 45 } }
   ]),
-  new Build({ revision: '1234', parentRevision: '123', timestamp: 3 }, [
+  new Build({ branch: 'master', revision: '1234', parentRevision: '123', timestamp: 3 }, [
     { name: 'main', hash: '123', sizes: { gzip: 123 } },
     { name: 'vendor', hash: '123', sizes: { gzip: 45 } }
   ])
diff --git a/src/app/src/components/__tests__/BuildInfo.test.tsx b/src/app/src/components/__tests__/BuildInfo.test.tsx
index 4d0b35c..8e75dee 100644
--- a/src/app/src/components/__tests__/BuildInfo.test.tsx
+++ b/src/app/src/components/__tests__/BuildInfo.test.tsx
@@ -10,7 +10,7 @@ import React from 'react';
 import { StoreContext } from 'redux-react-hook';
 import { fireEvent, render } from 'react-native-testing-library';
 
-const build = new Build({ revision: '1234565', parentRevision: 'abcdef', timestamp: 123 }, []);
+const build = new Build({ branch: 'master', revision: '1234565', parentRevision: 'abcdef', timestamp: 123 }, []);
 
 describe('BuildInfo', () => {
   test('can be closed', () => {
diff --git a/src/app/src/store/__tests__/reducer.test.ts b/src/app/src/store/__tests__/reducer.test.ts
index c6a048a..92882f3 100644
--- a/src/app/src/store/__tests__/reducer.test.ts
+++ b/src/app/src/store/__tests__/reducer.test.ts
@@ -24,10 +24,10 @@ const initialState: State = Object.freeze({
   url: 'https://build-tracker.local'
 });
 
-const buildA = new Build({ revision: '123', parentRevision: '000', timestamp: Date.now() - 400 }, [
+const buildA = new Build({ branch: 'master', revision: '123', parentRevision: '000', timestamp: Date.now() - 400 }, [
   { name: 'tacos', hash: '123', sizes: { stat: 123, gzip: 45 } }
 ]);
-const buildB = new Build({ revision: 'abc', parentRevision: '123', timestamp: Date.now() }, [
+const buildB = new Build({ branch: 'master', revision: 'abc', parentRevision: '123', timestamp: Date.now() }, [
   { name: 'tacos', hash: 'abc', sizes: { stat: 125, gzip: 47 } }
 ]);
 
diff --git a/src/app/src/views/__tests__/Graph.test.tsx b/src/app/src/views/__tests__/Graph.test.tsx
index 1d84962..0e9c35d 100644
--- a/src/app/src/views/__tests__/Graph.test.tsx
+++ b/src/app/src/views/__tests__/Graph.test.tsx
@@ -10,7 +10,7 @@ import React from 'react';
 import { render } from 'react-native-testing-library';
 import { StoreContext } from 'redux-react-hook';
 
-const build = new Build({ revision: '1234565', parentRevision: 'abcdef', timestamp: 123 }, []);
+const build = new Build({ branch: 'master', revision: '1234565', parentRevision: 'abcdef', timestamp: 123 }, []);
 
 describe('Graph', () => {
   test('renders an empty message if no builds in comparator', () => {
diff --git a/src/cli/src/commands/__tests__/create-build.test.ts b/src/cli/src/commands/__tests__/create-build.test.ts
index a828343..f092158 100644
--- a/src/cli/src/commands/__tests__/create-build.test.ts
+++ b/src/cli/src/commands/__tests__/create-build.test.ts
@@ -65,6 +65,7 @@ describe('create-build', () => {
 
     test('returns a JSON representation of a build', () => {
       jest.spyOn(Git, 'getDefaultBranch').mockReturnValue(Promise.resolve('master'));
+      jest.spyOn(Git, 'getBranch').mockReturnValue(Promise.resolve('tacobranch'));
       jest.spyOn(Git, 'getParentRevision').mockReturnValue(Promise.resolve('1234567'));
       jest.spyOn(Git, 'getCurrentRevision').mockReturnValue(Promise.resolve('abcdefg'));
       jest
@@ -74,6 +75,7 @@ describe('create-build', () => {
       return Command.handler({ config, out: false, 'skip-dirty-check': true }).then(res => {
         expect(res).toMatchObject({
           meta: {
+            branch: 'tacobranch',
             parentRevision: '1234567',
             revision: 'abcdefg',
             timestamp: 1234567890,
diff --git a/src/cli/src/modules/__tests__/git.test.ts b/src/cli/src/modules/__tests__/git.test.ts
index 241b666..7f1a484 100644
--- a/src/cli/src/modules/__tests__/git.test.ts
+++ b/src/cli/src/modules/__tests__/git.test.ts
@@ -69,6 +69,35 @@ describe('git', () => {
     });
   });
 
+  describe('getBranch', () => {
+    test('gets the branch name', () => {
+      jest.spyOn(Spawn, 'default').mockImplementation(() => Promise.resolve(Buffer.from(' \n\ttacosaregreat\n')));
+      return Git.getBranch().then(branch => {
+        expect(branch).toBe('tacosaregreat');
+      });
+    });
+
+    test('uses the process cwd', () => {
+      const spawn = jest
+        .spyOn(Spawn, 'default')
+        .mockImplementation(() => Promise.resolve(Buffer.from(' \n\ttacosaregreat\n')));
+
+      return Git.getBranch().then(() => {
+        expect(spawn).toHaveBeenCalledWith(expect.any(String), expect.any(Array), { cwd: process.cwd() });
+      });
+    });
+
+    test('uses given cwd', () => {
+      const spawn = jest
+        .spyOn(Spawn, 'default')
+        .mockImplementation(() => Promise.resolve(Buffer.from(' \n\ttacosaregreat\n')));
+
+      return Git.getBranch('tacos').then(() => {
+        expect(spawn).toHaveBeenCalledWith(expect.any(String), expect.any(Array), { cwd: 'tacos' });
+      });
+    });
+  });
+
   describe('getParentRevision', () => {
     test('returns the merge base for the current revision', () => {
       const spawn = jest.spyOn(Spawn, 'default').mockImplementation(() => Promise.resolve(Buffer.from('123556')));
diff --git a/src/comparator/src/__tests__/BuildDelta.test.ts b/src/comparator/src/__tests__/BuildDelta.test.ts
index 6d19c33..51bc44b 100644
--- a/src/comparator/src/__tests__/BuildDelta.test.ts
+++ b/src/comparator/src/__tests__/BuildDelta.test.ts
@@ -11,14 +11,24 @@ describe('BuildDelta', () => {
     jest.spyOn(Date, 'now').mockReturnValue(1550528708828);
 
     buildA = new Build(
-      { revision: { value: '123', url: 'https://build-tracker.local' }, parentRevision: 'abc', timestamp: Date.now() },
+      {
+        branch: 'master',
+        revision: { value: '123', url: 'https://build-tracker.local' },
+        parentRevision: 'abc',
+        timestamp: Date.now()
+      },
       [
         { name: 'tacos', hash: '123', sizes: { stat: 2, gzip: 1 } },
         { name: 'burritos', hash: '123', sizes: { stat: 3, gzip: 2 } }
       ]
     );
     buildB = new Build(
-      { revision: { value: '456', url: 'https://build-tracker.local' }, parentRevision: 'abc', timestamp: Date.now() },
+      {
+        branch: 'master',
+        revision: { value: '456', url: 'https://build-tracker.local' },
+        parentRevision: 'abc',
+        timestamp: Date.now()
+      },
       [
         { name: 'tacos', hash: '123', sizes: { stat: 1, gzip: 1 } },
         { name: 'burritos', hash: 'abc', sizes: { stat: 6, gzip: 4 } },
@@ -52,6 +62,7 @@ describe('BuildDelta', () => {
         buildA,
         new Build(
           {
+            branch: 'master',
             revision: { value: '123', url: 'https://build-tracker.local' },
             parentRevision: 'abc',
             timestamp: Date.now()
diff --git a/src/comparator/src/__tests__/index.test.ts b/src/comparator/src/__tests__/index.test.ts
index 935844f..da5732c 100644
--- a/src/comparator/src/__tests__/index.test.ts
+++ b/src/comparator/src/__tests__/index.test.ts
@@ -12,15 +12,21 @@ import BuildComparator, {
   TotalDeltaCell
 } from '..';
 
-const build1 = new Build({ revision: '1234567abcdef', parentRevision: 'abcdef', timestamp: 1234567 }, [
-  { name: 'burritos', hash: 'abc', sizes: { stat: 456, gzip: 90 } },
-  { name: 'tacos', hash: 'abc', sizes: { stat: 123, gzip: 45 } }
-]);
-
-const build2 = new Build({ revision: '8901234abcdef', parentRevision: 'abcdef', timestamp: 8901234 }, [
-  { name: 'tacos', hash: 'abc', sizes: { stat: 123, gzip: 43 } },
-  { name: 'churros', hash: 'def', sizes: { stat: 469, gzip: 120 } }
-]);
+const build1 = new Build(
+  { branch: 'master', revision: '1234567abcdef', parentRevision: 'abcdef', timestamp: 1234567 },
+  [
+    { name: 'burritos', hash: 'abc', sizes: { stat: 456, gzip: 90 } },
+    { name: 'tacos', hash: 'abc', sizes: { stat: 123, gzip: 45 } }
+  ]
+);
+
+const build2 = new Build(
+  { branch: 'master', revision: '8901234abcdef', parentRevision: 'abcdef', timestamp: 8901234 },
+  [
+    { name: 'tacos', hash: 'abc', sizes: { stat: 123, gzip: 43 } },
+    { name: 'churros', hash: 'def', sizes: { stat: 469, gzip: 120 } }
+  ]
+);
 
 const artifactFilters = [/burritos/, /churros/];
 
diff --git a/src/server/src/api/__tests__/insert.test.ts b/src/server/src/api/__tests__/insert.test.ts
index 0947e22..a0544c9 100644
--- a/src/server/src/api/__tests__/insert.test.ts
+++ b/src/server/src/api/__tests__/insert.test.ts
@@ -8,23 +8,27 @@ import express from 'express';
 import { insertBuild } from '../insert';
 import request from 'supertest';
 
-const build = new Build({ revision: 'abc', parentRevision: 'def', timestamp: Date.now() }, []);
-const parentBuild = new Build({ revision: 'def', parentRevision: '123', timestamp: Date.now() }, []);
+const build = new Build({ branch: 'master', revision: 'abc', parentRevision: 'def', timestamp: Date.now() }, []);
+const parentBuild = new Build({ branch: 'master', revision: 'def', parentRevision: '123', timestamp: Date.now() }, []);
 
 describe('insert build handler', () => {
-  let queries, app;
+  let app, config, queries;
   beforeEach(() => {
     queries = {
       byRevision: jest.fn(() => Promise.resolve(parentBuild)),
       insert: jest.fn(() => Promise.resolve('1234'))
     };
+    config = {
+      artifacts: {},
+      queries
+    };
     app = express();
     app.use(bodyParser.json());
   });
 
   describe('insert', () => {
     test('inserts the build', () => {
-      const handler = insertBuild(queries, { artifacts: {} });
+      const handler = insertBuild(queries, config);
       app.post('/test', handler);
 
       return request(app)
@@ -40,7 +44,7 @@ describe('insert build handler', () => {
 
   describe('response', () => {
     test('includes the comparator JSON', () => {
-      const handler = insertBuild(queries, { artifacts: {} });
+      const handler = insertBuild(queries, config);
       app.post('/test', handler);
 
       return request(app)
@@ -57,7 +61,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(queries, config, handleInsert);
       app.post('/test', handler);
 
       return request(app)
diff --git a/src/server/src/api/__tests__/read.test.ts b/src/server/src/api/__tests__/read.test.ts
index 523ce60..bae2f81 100644
--- a/src/server/src/api/__tests__/read.test.ts
+++ b/src/server/src/api/__tests__/read.test.ts
@@ -6,7 +6,7 @@ import middleware from '../';
 import request from 'supertest';
 
 describe('read', () => {
-  let build, queries;
+  let build, config, queries;
   beforeEach(() => {
     build = { meta: { revision: '123', parentRevision: '456', timestamp: Date.now() }, artifacts: [] };
     queries = {
@@ -20,12 +20,16 @@ describe('read', () => {
         recent: jest.fn(() => Promise.resolve([build]))
       }
     };
+
+    config = {
+      queries
+    };
   });
 
   describe('queryByRevision', () => {
     test('queries by revision', () => {
       const app = express();
-      app.use(middleware(express.Router(), queries, {}, {}));
+      app.use(middleware(express.Router(), config, {}));
 
       return request(app)
         .get('/api/build/1234567890')
@@ -38,7 +42,7 @@ describe('read', () => {
     test('throws 500 on failure', () => {
       queries.build.byRevision.mockImplementation(() => Promise.reject('tacos'));
       const app = express();
-      app.use(middleware(express.Router(), queries, {}, {}));
+      app.use(middleware(express.Router(), config, {}));
 
       return request(app)
         .get('/api/build/1234567890')
@@ -51,7 +55,7 @@ describe('read', () => {
   describe('queryByRevisionRange', () => {
     test('queries by revision range', () => {
       const app = express();
-      app.use(middleware(express.Router(), queries, {}, {}));
+      app.use(middleware(express.Router(), config, {}));
 
       return request(app)
         .get('/api/builds/range/1234567..abcdef')
@@ -64,7 +68,7 @@ describe('read', () => {
     test('throws 500 on failure', () => {
       queries.builds.byRevisionRange.mockImplementation(() => Promise.reject('tacos'));
       const app = express();
-      app.use(middleware(express.Router(), queries, {}, {}));
+      app.use(middleware(express.Router(), config, {}));
 
       return request(app)
         .get('/api/builds/range/1234567..abcdef')
@@ -77,12 +81,36 @@ describe('read', () => {
   describe('queryByTimeRange', () => {
     test('queries by time range', () => {
       const app = express();
-      app.use(middleware(express.Router(), queries, {}, {}));
+      app.use(middleware(express.Router(), config, {}));
 
       return request(app)
         .get('/api/builds/time/1234567..2345678')
         .then(res => {
-          expect(queries.builds.byTimeRange).toHaveBeenCalledWith(1234567, 2345678);
+          expect(queries.builds.byTimeRange).toHaveBeenCalledWith(1234567, 2345678, 'master');
+          expect(res.body).toEqual([build]);
+        });
+    });
+
+    test('can use query param for branch', () => {
+      const app = express();
+      app.use(middleware(express.Router(), config, {}));
+
+      return request(app)
+        .get('/api/builds/time/1234567..2345678?branch=tacos')
+        .then(res => {
+          expect(queries.builds.byTimeRange).toHaveBeenCalledWith(1234567, 2345678, 'tacos');
+          expect(res.body).toEqual([build]);
+        });
+    });
+
+    test('can use app config for default branch', () => {
+      const app = express();
+      app.use(middleware(express.Router(), { ...config, defaultBranch: 'burritos' }, {}));
+
+      return request(app)
+        .get('/api/builds/time/1234567..2345678')
+        .then(res => {
+          expect(queries.builds.byTimeRange).toHaveBeenCalledWith(1234567, 2345678, 'burritos');
           expect(res.body).toEqual([build]);
         });
     });
@@ -90,7 +118,7 @@ describe('read', () => {
     test('throws 500 on failure', () => {
       queries.builds.byTimeRange.mockImplementation(() => Promise.reject('tacos'));
       const app = express();
-      app.use(middleware(express.Router(), queries, {}, {}));
+      app.use(middleware(express.Router(), config, {}));
 
       return request(app)
         .get('/api/builds/time/1234567..2345678')
@@ -103,7 +131,7 @@ describe('read', () => {
   describe('queryByRevisions', () => {
     test('queries by revision range', () => {
       const app = express();
-      app.use(middleware(express.Router(), queries, {}, {}));
+      app.use(middleware(express.Router(), config, {}));
 
       return request(app)
         .get('/api/builds/list/1234567/abcdef/239587')
@@ -116,7 +144,7 @@ describe('read', () => {
     test('throws 500 on failure', () => {
       queries.builds.byRevisions.mockImplementation(() => Promise.reject('tacos'));
       const app = express();
-      app.use(middleware(express.Router(), queries, {}, {}));
+      app.use(middleware(express.Router(), config, {}));
 
       return request(app)
         .get('/api/builds/list/1234567/abcdef/239587')
@@ -129,24 +157,48 @@ describe('read', () => {
   describe('queryByRecent', () => {
     test('queries recent revisions', () => {
       const app = express();
-      app.use(middleware(express.Router(), queries, {}, {}));
+      app.use(middleware(express.Router(), config, {}));
 
       return request(app)
         .get('/api/builds')
         .then(res => {
-          expect(queries.builds.recent).toHaveBeenCalledWith(undefined);
+          expect(queries.builds.recent).toHaveBeenCalledWith(undefined, 'master');
           expect(res.body).toEqual([build]);
         });
     });
 
     test('queries recent revisions with limit', () => {
       const app = express();
-      app.use(middleware(express.Router(), queries, {}, {}));
+      app.use(middleware(express.Router(), config, {}));
 
       return request(app)
         .get('/api/builds/4')
         .then(res => {
-          expect(queries.builds.recent).toHaveBeenCalledWith('4');
+          expect(queries.builds.recent).toHaveBeenCalledWith(4, 'master');
+          expect(res.body).toEqual([build]);
+        });
+    });
+
+    test('queries recent revisions with query param for branch', () => {
+      const app = express();
+      app.use(middleware(express.Router(), config, {}));
+
+      return request(app)
+        .get('/api/builds/2?branch=tacos')
+        .then(res => {
+          expect(queries.builds.recent).toHaveBeenCalledWith(2, 'tacos');
+          expect(res.body).toEqual([build]);
+        });
+    });
+
+    test('queries recent revisions with app config defaultBranch', () => {
+      const app = express();
+      app.use(middleware(express.Router(), { ...config, defaultBranch: 'burritos' }, {}));
+
+      return request(app)
+        .get('/api/builds/10')
+        .then(res => {
+          expect(queries.builds.recent).toHaveBeenCalledWith(10, 'burritos');
           expect(res.body).toEqual([build]);
         });
     });
@@ -154,7 +206,7 @@ describe('read', () => {
     test('throws 500 on failure', () => {
       queries.builds.recent.mockImplementation(() => Promise.reject('tacos'));
       const app = express();
-      app.use(middleware(express.Router(), queries, {}, {}));
+      app.use(middleware(express.Router(), config, {}));
 
       return request(app)
         .get('/api/builds')