diff --git a/lib/api/gitlab.js b/lib/api/gitlab.js
index f08e37861d6a5675505da668b8c1d8842497c43d..36afaccba3e45805eddd895fd7589a747187f4b2 100644
--- a/lib/api/gitlab.js
+++ b/lib/api/gitlab.js
@@ -10,6 +10,7 @@ module.exports = {
   findFilePaths,
   // Branch
   branchExists,
+  createBranch,
   getBranch,
   getBranchPr,
   getBranchStatus,
@@ -30,6 +31,8 @@ module.exports = {
   getFile,
   getFileContent,
   getFileJson,
+  createFile,
+  updateFile,
 };
 
 // Get all repositories that the user has access to
@@ -340,17 +343,8 @@ async function getFileContent(filePath, branchName) {
 }
 
 async function getFileJson(filePath, branchName) {
-  try {
-    const fileContent = await getFileContent(filePath, branchName);
-    return JSON.parse(fileContent);
-  } catch (error) {
-    if (error.statusCode === 404) {
-      // If file not found, then return null JSON
-      return null;
-    }
-    // Propagate if it's any other error
-    throw error;
-  }
+  const fileContent = await getFileContent(filePath, branchName);
+  return JSON.parse(fileContent);
 }
 
 async function createFile(branchName, filePath, fileContents, message) {
diff --git a/test/_fixtures/logger/index.js b/test/_fixtures/logger/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..94858060fbc11941687072df541322ffeb05b47b
--- /dev/null
+++ b/test/_fixtures/logger/index.js
@@ -0,0 +1,9 @@
+module.exports = {
+  fatal: jest.fn(),
+  error: jest.fn(),
+  warn: jest.fn(),
+  info: jest.fn(),
+  debug: jest.fn(),
+  trace: jest.fn(),
+  child: jest.fn(() => module.exports),
+};
diff --git a/test/api/__snapshots__/gitlab.spec.js.snap b/test/api/__snapshots__/gitlab.spec.js.snap
index d54a7bf4216db663f641f4d60a0035a1c1fa3e6e..9e82ead5d4e3b6a4ad7170f104a390d320d2f54b 100644
--- a/test/api/__snapshots__/gitlab.spec.js.snap
+++ b/test/api/__snapshots__/gitlab.spec.js.snap
@@ -1,5 +1,242 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`api/gitlab addAssignees(issueNo, assignees) should add the given assignees to the issue 1`] = `
+Array [
+  Array [
+    "projects/some%2Frepo/merge_requests/42?assignee_id=someuser",
+  ],
+]
+`;
+
+exports[`api/gitlab addAssignees(issueNo, assignees) should log error if more than one assignee 1`] = `
+Array [
+  Array [
+    "projects/some%2Frepo/merge_requests/42?assignee_id=someuser",
+  ],
+]
+`;
+
+exports[`api/gitlab addLabels(issueNo, labels) should add the given labels to the issue 1`] = `
+Array [
+  Array [
+    "projects/some%2Frepo/merge_requests/42?labels=foo,bar",
+  ],
+]
+`;
+
+exports[`api/gitlab createFile(branchName, filePath, fileContents, message) createBranch(branchName) creates file with v3 1`] = `
+Array [
+  Array [
+    "projects/some-repo/repository/branches",
+    Object {
+      "body": Object {
+        "branch_name": "some-branch",
+        "ref": undefined,
+      },
+    },
+  ],
+]
+`;
+
+exports[`api/gitlab createFile(branchName, filePath, fileContents, message) createBranch(branchName) creates file with v4 1`] = `
+Array [
+  Array [
+    "projects/undefined/repository/branches",
+    Object {
+      "body": Object {
+        "branch": "some-branch",
+        "ref": undefined,
+      },
+    },
+  ],
+]
+`;
+
+exports[`api/gitlab createFile(branchName, filePath, fileContents, message) creates file with v3 1`] = `
+Array [
+  Array [
+    "projects/some-repo/repository/files",
+    Object {
+      "body": Object {
+        "branch_name": "some-branch",
+        "commit_message": "some-message",
+        "content": "c29tZS1jb250ZW50cw==",
+        "encoding": "base64",
+        "file_path": "some-path",
+      },
+    },
+  ],
+]
+`;
+
+exports[`api/gitlab createFile(branchName, filePath, fileContents, message) creates file with v4 1`] = `
+Array [
+  Array [
+    "projects/undefined/repository/files/some-path",
+    Object {
+      "body": Object {
+        "branch": "some-branch",
+        "commit_message": "some-message",
+        "content": "c29tZS1jb250ZW50cw==",
+        "encoding": "base64",
+      },
+    },
+  ],
+]
+`;
+
+exports[`api/gitlab createFile(branchName, filePath, fileContents, message) updateFile(branchName, filePath, fileContents, message) creates file with v3 1`] = `
+Array [
+  Array [
+    "projects/some-repo/repository/files",
+    Object {
+      "body": Object {
+        "branch_name": "some-branch",
+        "commit_message": "some-message",
+        "content": "c29tZS1jb250ZW50cw==",
+        "encoding": "base64",
+        "file_path": "some-path",
+      },
+    },
+  ],
+]
+`;
+
+exports[`api/gitlab createFile(branchName, filePath, fileContents, message) updateFile(branchName, filePath, fileContents, message) creates file with v4 1`] = `
+Array [
+  Array [
+    "projects/undefined/repository/files/some-path",
+    Object {
+      "body": Object {
+        "branch": "some-branch",
+        "commit_message": "some-message",
+        "content": "c29tZS1jb250ZW50cw==",
+        "encoding": "base64",
+      },
+    },
+  ],
+]
+`;
+
+exports[`api/gitlab createPr(branchName, title, body) returns the PR 1`] = `
+Object {
+  "displayNumber": "Merge Request #12345",
+  "id": 1,
+  "iid": 12345,
+  "number": 1,
+}
+`;
+
+exports[`api/gitlab createPr(branchName, title, body) returns the PR 2`] = `
+Array [
+  Array [
+    "projects/undefined/merge_requests",
+    Object {
+      "body": Object {
+        "description": "the-body",
+        "remove_source_branch": true,
+        "source_branch": "some-branch",
+        "target_branch": undefined,
+        "title": "some-title",
+      },
+    },
+  ],
+]
+`;
+
+exports[`api/gitlab getBranch returns a branch 1`] = `"foo"`;
+
+exports[`api/gitlab getBranchPr(branchName) should return null if no PR exists 1`] = `
+Array [
+  Array [
+    "projects/owned",
+  ],
+  Array [
+    "projects/some%2Frepo",
+  ],
+  Array [
+    "user",
+  ],
+  Array [
+    "projects/some%2Frepo/merge_requests?state=opened",
+  ],
+]
+`;
+
+exports[`api/gitlab getBranchPr(branchName) should return the PR object 1`] = `
+Array [
+  Array [
+    "projects/owned",
+  ],
+  Array [
+    "projects/some%2Frepo",
+  ],
+  Array [
+    "user",
+  ],
+  Array [
+    "projects/some%2Frepo/merge_requests?state=opened",
+  ],
+  Array [
+    "projects/some%2Frepo/merge_requests/undefined",
+  ],
+  Array [
+    "projects/some%2Frepo/repository/branches/undefined",
+  ],
+]
+`;
+
+exports[`api/gitlab getBranchPr(branchName) should return the PR object 2`] = `
+Object {
+  "additions": 1,
+  "base": Object {
+    "sha": "1234",
+  },
+  "body": undefined,
+  "commits": 1,
+  "deletions": 1,
+  "displayNumber": "Merge Request #undefined",
+  "number": undefined,
+}
+`;
+
+exports[`api/gitlab getFile(filePath, branchName) gets the file with v3 1`] = `
+Object {
+  "apiVersion": "v3",
+  "defaultBranch": undefined,
+  "email": undefined,
+  "repoName": "some-repo",
+}
+`;
+
+exports[`api/gitlab getFile(filePath, branchName) gets the file with v3 2`] = `"foo"`;
+
+exports[`api/gitlab getFile(filePath, branchName) gets the file with v4 by default 1`] = `"foo"`;
+
+exports[`api/gitlab getFileContent(filePath, branchName) gets the file 1`] = `"~�"`;
+
+exports[`api/gitlab getFileContent(filePath, branchName) throws error for non-404 1`] = `
+Object {
+  "statusCode": 403,
+}
+`;
+
+exports[`api/gitlab getPr(prNo) returns the PR 1`] = `
+Object {
+  "body": "a merge request",
+  "canRebase": true,
+  "description": "a merge request",
+  "displayNumber": "Merge Request #12345",
+  "id": 1,
+  "iid": 12345,
+  "isClosed": true,
+  "isUnmergeable": true,
+  "merge_status": "cannot_be_merged",
+  "number": 12345,
+  "state": "merged",
+}
+`;
+
 exports[`api/gitlab getRepos should return an array of repos 1`] = `
 Array [
   Array [
diff --git a/test/api/github.spec.js b/test/api/github.spec.js
index 8405d9704956bb33c3f91cbd3f3cd2e1f25c8a04..b7d6a366a28c4d38dd1ffc175a344ac2367ec7f5 100644
--- a/test/api/github.spec.js
+++ b/test/api/github.spec.js
@@ -1,10 +1,4 @@
-const bunyan = require('bunyan');
-
-const logger = bunyan.createLogger({
-  name: 'test',
-  stream: process.stdout,
-  level: 'fatal',
-});
+const logger = require('../_fixtures/logger');
 
 describe('api/github', () => {
   let github;
diff --git a/test/api/gitlab.spec.js b/test/api/gitlab.spec.js
index a02d24eccd8d2345f223b69ba3c3ab5266388250..7a2b2538543921d7696d421fea3f2447fcfd3a67 100644
--- a/test/api/gitlab.spec.js
+++ b/test/api/gitlab.spec.js
@@ -1,10 +1,4 @@
-const bunyan = require('bunyan');
-
-const logger = bunyan.createLogger({
-  name: 'test',
-  stream: process.stdout,
-  level: 'fatal',
-});
+const logger = require('../_fixtures/logger');
 
 describe('api/gitlab', () => {
   let gitlab;
@@ -204,4 +198,429 @@ describe('api/gitlab', () => {
       expect(e.statusCode).toBe(500);
     });
   });
+  describe('getBranch', () => {
+    it('returns a branch', async () => {
+      glGot.mockReturnValueOnce({ body: 'foo' });
+      const branch = await gitlab.getBranch('branch-name');
+      expect(branch).toMatchSnapshot();
+    });
+    it('nulls on error', async () => {
+      glGot.mockImplementationOnce(() => {
+        throw new Error('not found');
+      });
+      const branch = await gitlab.getBranch('branch-name');
+      expect(branch).toBe(null);
+    });
+  });
+  describe('getBranchPr(branchName)', () => {
+    it('should return null if no PR exists', async () => {
+      await initRepo('some/repo', 'token');
+      glGot.mockImplementationOnce(() => ({
+        body: [],
+      }));
+      const pr = await gitlab.getBranchPr('somebranch');
+      expect(glGot.mock.calls).toMatchSnapshot();
+      expect(pr).toBe(null);
+    });
+    it('should return the PR object', async () => {
+      await initRepo('some/repo', 'token');
+      glGot.mockImplementationOnce(() => ({
+        body: [{ number: 91, source_branch: 'somebranch' }],
+      }));
+      glGot.mockImplementationOnce(() => ({
+        body: {
+          number: 91,
+          additions: 1,
+          deletions: 1,
+          commits: 1,
+          base: {
+            sha: '1234',
+          },
+        },
+      }));
+      const pr = await gitlab.getBranchPr('somebranch');
+      expect(glGot.mock.calls).toMatchSnapshot();
+      expect(pr).toMatchSnapshot();
+    });
+  });
+  describe('getBranchStatus(branchName)', () => {
+    beforeEach(() => {
+      glGot.mockReturnValueOnce({
+        body: {
+          commit: {
+            id: 1,
+          },
+        },
+      });
+    });
+    it('returns pending if no results', async () => {
+      glGot.mockReturnValueOnce({
+        body: [],
+      });
+      const res = await gitlab.getBranchStatus('some-branch');
+      expect(res).toEqual('pending');
+    });
+    it('returns success if all are success', async () => {
+      glGot.mockReturnValueOnce({
+        body: [{ status: 'success' }, { status: 'success' }],
+      });
+      const res = await gitlab.getBranchStatus('some-branch');
+      expect(res).toEqual('success');
+    });
+    it('returns failure if any are failed', async () => {
+      glGot.mockReturnValueOnce({
+        body: [{ status: 'success' }, { status: 'failed' }],
+      });
+      const res = await gitlab.getBranchStatus('some-branch');
+      expect(res).toEqual('failure');
+    });
+    it('returns custom statuses', async () => {
+      glGot.mockReturnValueOnce({
+        body: [{ status: 'success' }, { status: 'foo' }],
+      });
+      const res = await gitlab.getBranchStatus('some-branch');
+      expect(res).toEqual('foo');
+    });
+  });
+  describe('deleteBranch(branchName)', () => {
+    it('should send delete', async () => {
+      glGot.delete = jest.fn();
+      await gitlab.deleteBranch('some-branch');
+      expect(glGot.delete.mock.calls.length).toBe(1);
+    });
+  });
+  describe('addAssignees(issueNo, assignees)', () => {
+    it('should add the given assignees to the issue', async () => {
+      await initRepo('some/repo', 'token');
+      await gitlab.addAssignees(42, ['someuser']);
+      expect(glGot.put.mock.calls).toMatchSnapshot();
+    });
+    it('should log error if more than one assignee', async () => {
+      await initRepo('some/repo', 'token');
+      await gitlab.addAssignees(42, ['someuser', 'someotheruser']);
+      expect(glGot.put.mock.calls).toMatchSnapshot();
+    });
+  });
+  describe('addReviewers(issueNo, reviewers)', () => {
+    it('should add the given reviewers to the PR', async () => {
+      await initRepo('some/repo', 'token');
+      await gitlab.addReviewers(42, ['someuser', 'someotheruser']);
+    });
+  });
+  describe('addLabels(issueNo, labels)', () => {
+    it('should add the given labels to the issue', async () => {
+      await initRepo('some/repo', 'token');
+      await gitlab.addLabels(42, ['foo', 'bar']);
+      expect(glGot.put.mock.calls).toMatchSnapshot();
+    });
+  });
+  describe('findPr(branchName, prTitle, state)', () => {
+    it('returns null if no results', async () => {
+      glGot.mockReturnValueOnce({
+        body: [],
+      });
+      const pr = await gitlab.findPr('some-branch');
+      expect(pr).toBe(null);
+    });
+    it('returns null if no matching titles', async () => {
+      glGot.mockReturnValueOnce({
+        body: [
+          {
+            source_branch: 'some-branch',
+            id: 1,
+          },
+          {
+            source_branch: 'some-branch',
+            id: 2,
+            title: 'foo',
+          },
+        ],
+      });
+      const pr = await gitlab.findPr('some-branch', 'some-title');
+      expect(pr).toBe(null);
+    });
+    it('returns last result if multiple match', async () => {
+      glGot.mockReturnValueOnce({
+        body: [
+          {
+            source_branch: 'some-branch',
+            id: 1,
+          },
+          {
+            source_branch: 'some-branch',
+            id: 2,
+          },
+        ],
+      });
+      const pr = await gitlab.findPr('some-branch');
+      expect(pr.number).toBe(2);
+    });
+  });
+  describe('checkForClosedPr(branchName, prTitle)', () => {
+    it('returns true if pr exists', async () => {
+      glGot.mockReturnValueOnce({
+        body: [
+          {
+            source_branch: 'some-branch',
+            id: 1,
+          },
+          {
+            source_branch: 'some-branch',
+            id: 2,
+          },
+        ],
+      });
+      const res = await gitlab.checkForClosedPr('some-branch');
+      expect(res).toBe(true);
+    });
+    it('returns false if pr does not exist', async () => {
+      glGot.mockReturnValueOnce({
+        body: [],
+      });
+      const res = await gitlab.checkForClosedPr('some-branch');
+      expect(res).toBe(false);
+    });
+  });
+  describe('createPr(branchName, title, body)', () => {
+    it('returns the PR', async () => {
+      glGot.post.mockReturnValueOnce({
+        body: {
+          id: 1,
+          iid: 12345,
+        },
+      });
+      const pr = await gitlab.createPr('some-branch', 'some-title', 'the-body');
+      expect(pr).toMatchSnapshot();
+      expect(glGot.post.mock.calls).toMatchSnapshot();
+    });
+  });
+  describe('getPr(prNo)', () => {
+    it('returns the PR', async () => {
+      glGot.mockReturnValueOnce({
+        body: {
+          id: 1,
+          iid: 12345,
+          description: 'a merge request',
+          state: 'merged',
+          merge_status: 'cannot_be_merged',
+        },
+      });
+      glGot.mockReturnValueOnce({
+        body: {
+          commit: {},
+        },
+      });
+      const pr = await gitlab.getPr(12345);
+      expect(pr).toMatchSnapshot();
+    });
+  });
+  describe('updatePr(prNo, title, body)', () => {
+    jest.resetAllMocks();
+    it('updates the PR', async () => {
+      await gitlab.updatePr();
+      expect(glGot.put.mock.calls.length).toEqual(1);
+    });
+  });
+  describe('mergePr(pr)', () => {
+    jest.resetAllMocks();
+    it('merges the PR', async () => {
+      await gitlab.mergePr({ number: 1 });
+      expect(glGot.put.mock.calls.length).toEqual(1);
+    });
+  });
+  describe('getFile(filePath, branchName)', () => {
+    it('gets the file with v4 by default', async () => {
+      glGot.mockReturnValueOnce({
+        body: {
+          content: 'foo',
+        },
+      });
+      const res = await gitlab.getFile('some-path', 'some-branch');
+      expect(res).toMatchSnapshot();
+      expect(glGot.mock.calls[0][0].indexOf('file_path')).toBe(-1);
+    });
+    it('gets the file with v3', async () => {
+      glGot.mockReturnValueOnce({
+        body: {},
+      });
+      glGot.mockReturnValueOnce({
+        body: {},
+      });
+      glGot.mockReturnValueOnce({
+        body: {},
+      });
+      glGot.mockReturnValueOnce({
+        body: {
+          content: 'foo',
+        },
+      });
+      const config = await gitlab.initRepo('some-repo', 'some-token');
+      expect(config).toMatchSnapshot();
+      const res = await gitlab.getFile('some-path', 'some-branch');
+      expect(res).toMatchSnapshot();
+      expect(glGot.mock.calls[3][0].indexOf('file_path')).not.toBe(-1);
+    });
+  });
+  describe('getFileContent(filePath, branchName)', () => {
+    it('gets the file', async () => {
+      glGot.mockReturnValueOnce({
+        body: {
+          content: 'foo',
+        },
+      });
+      const res = await gitlab.getFileContent('some-path', 'some-branch');
+      expect(res).toMatchSnapshot();
+    });
+    it('returns null for 404', async () => {
+      glGot.mockImplementationOnce(() => Promise.reject({ statusCode: 404 }));
+      const res = await gitlab.getFileContent('some-path', 'some-branch');
+      expect(res).toBe(null);
+    });
+    it('throws error for non-404', async () => {
+      glGot.mockImplementationOnce(() => Promise.reject({ statusCode: 403 }));
+      let e;
+      try {
+        await gitlab.getFileContent('some-path', 'some-branch');
+      } catch (err) {
+        e = err;
+      }
+      expect(e).toMatchSnapshot();
+    });
+  });
+  describe('getFileJson(filePath, branchName)', () => {
+    it('returns null for 404', async () => {
+      glGot.mockImplementationOnce(() => Promise.reject({ statusCode: 404 }));
+      const res = await gitlab.getFileJson('some-path', 'some-branch');
+      expect(res).toBe(null);
+    });
+  });
+  describe('createFile(branchName, filePath, fileContents, message)', () => {
+    it('creates file with v4', async () => {
+      await gitlab.createFile(
+        'some-branch',
+        'some-path',
+        'some-contents',
+        'some-message'
+      );
+      expect(glGot.post.mock.calls).toMatchSnapshot();
+      expect(glGot.post.mock.calls[0][1].body.file_path).not.toBeDefined();
+    });
+    it('creates file with v3', async () => {
+      glGot.mockReturnValueOnce({
+        body: {},
+      });
+      glGot.mockReturnValueOnce({
+        body: {},
+      });
+      glGot.mockReturnValueOnce({
+        body: {},
+      });
+      await gitlab.initRepo('some-repo', 'some-token');
+      await gitlab.createFile(
+        'some-branch',
+        'some-path',
+        'some-contents',
+        'some-message'
+      );
+      expect(glGot.post.mock.calls).toMatchSnapshot();
+      expect(glGot.post.mock.calls[0][1].body.file_path).toBeDefined();
+    });
+    describe('updateFile(branchName, filePath, fileContents, message)', () => {
+      it('creates file with v4', async () => {
+        await gitlab.updateFile(
+          'some-branch',
+          'some-path',
+          'some-contents',
+          'some-message'
+        );
+        expect(glGot.put.mock.calls).toMatchSnapshot();
+        expect(glGot.put.mock.calls[0][1].body.file_path).not.toBeDefined();
+      });
+      it('creates file with v3', async () => {
+        glGot.mockReturnValueOnce({
+          body: {},
+        });
+        glGot.mockReturnValueOnce({
+          body: {},
+        });
+        glGot.mockReturnValueOnce({
+          body: {},
+        });
+        await gitlab.initRepo('some-repo', 'some-token');
+        await gitlab.updateFile(
+          'some-branch',
+          'some-path',
+          'some-contents',
+          'some-message'
+        );
+        expect(glGot.put.mock.calls).toMatchSnapshot();
+        expect(glGot.put.mock.calls[0][1].body.file_path).toBeDefined();
+      });
+    });
+    describe('createBranch(branchName)', () => {
+      it('creates file with v4', async () => {
+        await gitlab.createBranch('some-branch');
+        expect(glGot.post.mock.calls).toMatchSnapshot();
+        expect(glGot.post.mock.calls[0][1].body.branch_name).not.toBeDefined();
+      });
+      it('creates file with v3', async () => {
+        glGot.mockReturnValueOnce({
+          body: {},
+        });
+        glGot.mockReturnValueOnce({
+          body: {},
+        });
+        glGot.mockReturnValueOnce({
+          body: {},
+        });
+        await gitlab.initRepo('some-repo', 'some-token');
+        await gitlab.createBranch('some-branch');
+        expect(glGot.post.mock.calls).toMatchSnapshot();
+        expect(glGot.post.mock.calls[0][1].body.branch_name).toBeDefined();
+      });
+    });
+    describe('commitFilesToBranch(branchName, files, message, parentBranch)', () => {
+      it('creates branch', async () => {
+        glGot.mockReturnValueOnce({ statusCode: 404 });
+        await gitlab.commitFilesToBranch(
+          'some-branch',
+          [],
+          'some-message',
+          'parent-branch'
+        );
+      });
+      it('does not create branch and updates file', async () => {
+        glGot.mockReturnValueOnce({ statusCode: 200 });
+        glGot.mockReturnValueOnce({
+          body: {
+            content: 'hello',
+          },
+        });
+        const file = {
+          name: 'foo',
+          contents: 'bar',
+        };
+        await gitlab.commitFilesToBranch(
+          'some-branch',
+          [file],
+          'some-message',
+          'parent-branch'
+        );
+      });
+      it('does not create branch and creates file', async () => {
+        glGot.mockReturnValueOnce({ statusCode: 200 });
+        glGot.mockReturnValueOnce(Promise.reject({ statusCode: 404 }));
+        const file = {
+          name: 'foo',
+          contents: 'bar',
+        };
+        await gitlab.commitFilesToBranch(
+          'some-branch',
+          [file],
+          'some-message',
+          'parent-branch'
+        );
+      });
+    });
+  });
 });