diff --git a/reviewboard/static/rb/js/admin/repositoryform.es6.js b/reviewboard/static/rb/js/admin/repositoryform.es6.js
deleted file mode 100644
index 6c79e30f04613b1c235c1aa052c25ca4e9bc540e..0000000000000000000000000000000000000000
--- a/reviewboard/static/rb/js/admin/repositoryform.es6.js
+++ /dev/null
@@ -1,452 +0,0 @@
-(function() {
-
-
-const prevTypes = {};
-const origRepoTypes = [];
-const powerPackTemplate = dedent`
-    <h3>${gettext('Power Pack Required')}</h3>
-    <p>
-    ${gettext('<span class="power-pack-advert-hosting-type"></span> support is available with <a href="https://www.reviewboard.org/powerpack/">Power Pack</a>, an extension which also offers powerful reports, document review, and more.')}
-    </p>
-`;
-const gerritPluginRequiredTemplate = dedent`
-    <h3>
-    ${gettext('Plugin Required')}
-    </h3>
-    <p>
-    ${interpolate(
-        gettext('The <code>gerrit-reviewboard</code> plugin is required for Gerrit integration. See the <a href="%s" target="_blank">instructions</a> for installing the plugin on your server.'),
-        [MANUAL_URL + 'admin/configuration/repositories/gerrit/'])}
-    </p>
-`;
-
-
-function updatePlanEl($row, $plan, serviceType, isFake) {
-    const planTypes = HOSTING_SERVICES[serviceType].plans;
-    const selectedPlan = $plan.val();
-
-    $plan.empty();
-
-    if (planTypes.length === 1 || isFake) {
-        $row.hide();
-    } else {
-        for (let i = 0; i < planTypes.length; i++) {
-            const planType = planTypes[i];
-            const opt = $('<option/>')
-                .val(planType.type)
-                .text(planType.label)
-                .appendTo($plan);
-
-            if (planType.type === selectedPlan) {
-                opt.prop('selected', true);
-            }
-        }
-
-        $row.show();
-    }
-
-    $plan.triggerHandler('change');
-}
-
-
-function updateHostingForm($hostingType, formPrefix, $plan, $forms) {
-    const formID = `#${formPrefix}-${$hostingType.val()}-${$plan.val() || 'default'}`;
-
-    $forms.hide();
-    $(formID).show();
-}
-
-
-function updateRepositoryType() {
-    const hostingType = $('#id_hosting_type').val();
-    const newRepoTypes = (hostingType === 'custom'
-                          ? []
-                          : HOSTING_SERVICES[hostingType].scmtools);
-    const $repoTypes = $('#id_tool');
-    const currentRepoType = $repoTypes.val();
-
-    $repoTypes.empty();
-
-    origRepoTypes.forEach(repoType => {
-        if (newRepoTypes.length === 0 ||
-            newRepoTypes.indexOf(repoType.text) !== -1 ||
-            newRepoTypes.indexOf(repoType.value) !== -1) {
-            $('<option/>')
-                .text(repoType.text)
-                .val(repoType.value)
-                .appendTo($repoTypes);
-
-            if (repoType.value === currentRepoType) {
-                $repoTypes.val(currentRepoType);
-            }
-        }
-    });
-
-    $repoTypes.triggerHandler('change');
-}
-
-
-function updateAccountList() {
-    const hostingType = $('#id_hosting_type').val();
-    const $hostingAccount = $('#id_hosting_account');
-    const $authForm = $('#hosting-auth-form-' + hostingType);
-    const hostingInfo = HOSTING_SERVICES[hostingType];
-    const accounts = hostingInfo.accounts;
-    const selectedAccount = parseInt($hostingAccount.val(), 10);
-    let foundSelected = false;
-
-    /* Rebuild the list of accounts. */
-    $hostingAccount.find('option[value!=""]').remove();
-
-    if (hostingInfo.needs_two_factor_auth_code ||
-        $authForm.find('.errorlist').length > 0) {
-        /*
-         * The first one will be selected automatically, which
-         * we want. Don't select any below.
-         */
-        foundSelected = true;
-    }
-
-    accounts.forEach(account => {
-        let text = account.username;
-
-        if (account.hosting_url) {
-            text += ` (${account.hosting_url})`;
-        }
-
-        const $opt = $('<option/>')
-            .val(account.pk)
-            .text(text)
-            .data('account', account)
-            .appendTo($hostingAccount);
-
-        if (account.pk === selectedAccount || !foundSelected) {
-            $opt.prop('selected', true);
-            foundSelected = true;
-            $hostingAccount.triggerHandler('change');
-        }
-    });
-}
-
-
-$(document).ready(function() {
-    const $hostingType = $('#id_hosting_type');
-    const $hostingAuthForms = $('.hosting-auth-form');
-    const $hostingRepoForms = $('.hosting-repo-form');
-    const $hostingAccount = $('#id_hosting_account');
-    const $hostingAccountRow = $('#row-hosting_account');
-    const $hostingAccountRelink = $('<p/>')
-        .text(gettext('The authentication requirements for this account have changed. You will need to re-authenticate.'))
-        .addClass('errornote')
-        .hide()
-        .appendTo($hostingAccountRow);
-    const $scmtoolAuthForms = $('.scmtool-auth-form');
-    const $scmtoolRepoForms = $('.scmtool-repo-form');
-    const $associateSshKeyFieldset =
-        $('#row-associate_ssh_key').parents('fieldset');
-    const $associateSshKey = $('#id_associate_ssh_key');
-    const associateSshKeyDisabled = $associateSshKey.prop('disabled');
-    const $bugTrackerUseHosting = $('#id_bug_tracker_use_hosting');
-    const $bugTrackerUseHostingRow = $('#row-bug_tracker_use_hosting');
-    const $bugTrackerType = $('#id_bug_tracker_type');
-    const $bugTrackerHostingURLRow = $('#row-bug_tracker_hosting_url');
-    const $bugTrackerTypeRow = $('#row-bug_tracker_type');
-    const $bugTrackerPlan = $('#id_bug_tracker_plan');
-    const $bugTrackerPlanRow = $('#row-bug_tracker_plan');
-    const $bugTrackerURLRow = $('#row-bug_tracker');
-    const $bugTrackerUsernameRow =
-        $('#row-bug_tracker_hosting_account_username');
-    const $repoPlanRow = $('#row-repository_plan');
-    const $repoPlan = $('#id_repository_plan');
-    const $publicAccess = $('#id_public');
-    const $tool = $('#id_tool');
-    const $toolRow = $('#row-tool');
-    const $showSshKey = $('#show-ssh-key-link');
-    const $publicKeyPopup = $('#ssh-public-key-popup');
-    const $bugTrackerForms = $('.bug-tracker-form');
-    const $submitButtons = $('input[type="submit"]');
-    const $editHostingCredentials = $('#repo-edit-hosting-credentials');
-    const $editHostingCredentialsLabel =
-        $('#repo-edit-hosting-credentials-label');
-    const $forceAuth = $('#id_force_authorize');
-    const $hostingPowerPackAdvert = $('<div class="powerpack-advert" />')
-        .html(powerPackTemplate)
-        .hide()
-        .appendTo($hostingType.closest('fieldset'));
-    const $toolPowerPackAdvert = $('<div class="powerpack-advert" />')
-        .html(powerPackTemplate)
-        .hide()
-        .appendTo($tool.closest('fieldset'));
-    const $gerritPluginInfo = $('<div class="gerrit-plugin-advert" />')
-        .html(gerritPluginRequiredTemplate)
-        .hide()
-        .appendTo($('#row-hosting_type'));
-
-    prevTypes.bug_tracker_type = 'none';
-    prevTypes.hosting_type = 'custom';
-    prevTypes.tool = 'none';
-
-    $tool.find('option').each((i, el) => {
-        const $repoType = $(el);
-
-        origRepoTypes.push({
-            value: $repoType.val(),
-            text: $repoType.text(),
-        });
-    });
-
-    $bugTrackerUseHosting
-        .change(function() {
-            if (this.checked) {
-                $bugTrackerTypeRow.hide();
-                $bugTrackerPlanRow.hide();
-                $bugTrackerUsernameRow.hide();
-                $bugTrackerURLRow.hide();
-                $bugTrackerForms.hide();
-            } else {
-                $bugTrackerTypeRow.show();
-                $bugTrackerType.triggerHandler('change');
-            }
-        })
-        .triggerHandler('change');
-
-    $repoPlan.change(() => updateHostingForm($hostingType, 'repo-form-hosting',
-                                             $repoPlan, $hostingRepoForms));
-
-    $bugTrackerPlan.change(() => {
-        const plan = $bugTrackerPlan.val() || 'default';
-        const bugTrackerType = $bugTrackerType.val();
-        const planInfo = HOSTING_SERVICES[bugTrackerType].planInfo[plan];
-
-        updateHostingForm($bugTrackerType, 'bug-tracker-form-hosting',
-                          $bugTrackerPlan, $bugTrackerForms);
-
-        $bugTrackerUsernameRow.setVisible(
-            planInfo.bug_tracker_requires_username);
-    });
-
-    $hostingType
-        .change(() => {
-            const hostingType = $hostingType.val();
-            const isCustom = (hostingType === 'custom');
-            const isFake = (!isCustom &&
-                            HOSTING_SERVICES[hostingType].fake === true);
-
-            updateRepositoryType();
-
-            $gerritPluginInfo.toggle(hostingType === 'gerrit');
-
-            if (isCustom) {
-                $repoPlanRow.hide();
-            } else {
-                $scmtoolAuthForms.hide();
-                $scmtoolRepoForms.hide();
-
-                updatePlanEl($repoPlanRow, $repoPlan, hostingType, isFake);
-            }
-
-            $repoPlan.triggerHandler('change');
-
-            if (isCustom ||
-                isFake ||
-                !HOSTING_SERVICES[hostingType].supports_bug_trackers) {
-                $bugTrackerUseHostingRow.hide();
-                $bugTrackerUseHosting
-                    .prop({
-                        disabled: true,
-                        checked: false,
-                    })
-                    .triggerHandler('change');
-            } else {
-                $bugTrackerUseHosting.prop('disabled', false);
-                $bugTrackerUseHostingRow.show();
-            }
-
-            if (isCustom ||
-                !HOSTING_SERVICES[hostingType].supports_ssh_key_association) {
-                $associateSshKeyFieldset.hide();
-                $associateSshKey.prop({
-                    disabled: true,
-                    checked: false,
-                });
-            } else {
-                /*
-                 * Always use the original state of the checkbox (i.e. the
-                 * state on page load)
-                 */
-                $associateSshKey.prop('disabled', associateSshKeyDisabled);
-                $associateSshKeyFieldset.show();
-            }
-
-            if (isFake) {
-                $hostingPowerPackAdvert
-                    .find('.power-pack-advert-hosting-type')
-                    .text($hostingType.find(':selected').text());
-            }
-
-            $hostingAccountRow.setVisible(!isFake);
-            $toolRow.setVisible(!isFake);
-
-            $hostingPowerPackAdvert.setVisible(isFake);
-            $submitButtons.prop('disabled', isFake);
-
-            if (!isCustom) {
-                updateAccountList();
-            }
-        })
-        .triggerHandler('change');
-
-    $([$hostingType[0], $hostingAccount[0]])
-        .change(() => {
-            $hostingAuthForms.hide();
-            $hostingAccountRelink.hide();
-            $editHostingCredentials
-                .hide()
-                .val(gettext('Edit credentials'));
-            $forceAuth.val('false');
-
-            const hostingType = $hostingType.val();
-
-            if (hostingType === 'custom') {
-                $hostingAccountRow.hide();
-            } else {
-                const hostingInfo = HOSTING_SERVICES[hostingType];
-
-                if (hostingInfo.fake !== true) {
-                    $hostingAccountRow.show();
-
-                    const $authForm = $('#hosting-auth-form-' + hostingType);
-
-                    /*
-                     * Hide any fields required for 2FA unless explicitly
-                     * needed.
-                     */
-                    $authForm.find('[data-required-for-2fa]').closest('.form-row')
-                        .setVisible(hostingInfo.needs_two_factor_auth_code);
-
-                    if ($hostingAccount.val() === '') {
-                        /* Present fields for linking a new account. */
-                        $authForm.show();
-                    } else if (hostingInfo.needs_two_factor_auth_code) {
-                        /*
-                         * The user needs to enter a 2FA code. We need to
-                         * show the auth form, and ensure we will be forcing
-                         * authentication.
-                         */
-                        $forceAuth.val('true');
-                        $authForm.show();
-                    } else {
-                        /* An existing linked account has been selected. */
-                        const selectedIndex = $hostingAccount[0].selectedIndex;
-                        const $selectedOption =
-                            $($hostingAccount[0].options[selectedIndex]);
-                        const account = $selectedOption.data('account');
-
-                        if (account.is_authorized &&
-                            $authForm.find('.errorlist').length === 0) {
-                            $editHostingCredentials.show();
-                        } else {
-                            $authForm.show();
-                            $hostingAccountRelink.show();
-                        }
-                    }
-                }
-            }
-        })
-        .triggerHandler('change');
-
-    $tool
-        .change(() => {
-            if ($hostingType.val() === 'custom') {
-                const scmtoolID = $tool.val();
-                const $authForm = $('#auth-form-scm-' + scmtoolID);
-                const $repoForm = $('#repo-form-scm-' + scmtoolID);
-
-                const toolInfo = TOOLS_INFO[scmtoolID];
-                const isFake = (toolInfo.fake === true);
-
-                if (isFake) {
-                    $toolPowerPackAdvert
-                        .find('.power-pack-advert-hosting-type')
-                        .text(toolInfo.name);
-                }
-
-                $scmtoolAuthForms.hide();
-                $scmtoolRepoForms.hide();
-
-                $authForm.setVisible(!isFake);
-                $repoForm.setVisible(!isFake);
-                $toolPowerPackAdvert.setVisible(isFake);
-                $submitButtons.prop('disabled', isFake);
-            }
-        })
-        .triggerHandler('change');
-
-    $bugTrackerType
-        .change(() => {
-            $bugTrackerForms.hide();
-
-            const bugTrackerType = $bugTrackerType.val();
-
-            if (bugTrackerType === 'custom' || bugTrackerType === 'none') {
-                $bugTrackerHostingURLRow.hide();
-                $bugTrackerPlanRow.hide();
-                $bugTrackerUsernameRow.hide();
-            }
-
-            if (bugTrackerType === 'custom') {
-                $bugTrackerURLRow.show();
-            } else if (bugTrackerType === 'none') {
-                $bugTrackerURLRow.hide();
-            } else {
-                $bugTrackerURLRow.hide();
-                updatePlanEl($bugTrackerPlanRow, $bugTrackerPlan,
-                             bugTrackerType, false);
-
-                $bugTrackerHostingURLRow.setVisible(
-                    HOSTING_SERVICES[bugTrackerType].self_hosted);
-            }
-        })
-        .triggerHandler('change');
-
-    $publicAccess
-        .change(function() {
-            var visible = !this.checked;
-            $('#row-users').setVisible(visible);
-            $('#row-review_groups').setVisible(visible);
-        })
-        .triggerHandler('change');
-
-    $showSshKey.on('click', () => {
-        if ($publicKeyPopup.is(':visible')) {
-            $showSshKey.text(gettext('Show your SSH public key'));
-            $publicKeyPopup.hide();
-        } else {
-            $showSshKey.text(gettext('Hide your SSH public key'));
-            $publicKeyPopup.show();
-        }
-
-        return false;
-    });
-
-    $editHostingCredentials.click(() => {
-        let $authForm = $('#hosting-auth-form-' + $hostingType.val());
-
-        if ($forceAuth.val() === 'true') {
-            $editHostingCredentialsLabel.text(gettext('Edit credentials'));
-            $authForm.hide();
-            $forceAuth.val('false');
-        } else {
-            $editHostingCredentialsLabel.text(
-                gettext('Cancel editing credentials'));
-            $authForm = $('#hosting-auth-form-' + $hostingType.val()).show();
-            $authForm.show();
-            $forceAuth.val('true');
-        }
-
-        return false;
-    });
-});
-
-
-})();
diff --git a/reviewboard/static/rb/js/admin/views/repositoryFormView.es6.js b/reviewboard/static/rb/js/admin/views/repositoryFormView.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..e1f98f89b875116c43138c1479866433255904dc
--- /dev/null
+++ b/reviewboard/static/rb/js/admin/views/repositoryFormView.es6.js
@@ -0,0 +1,733 @@
+/**
+ * Manages UI for the repository configuration form.
+ *
+ * Presently, this provides basic management of the display of subforms, rows,
+ * fields, and information panels when configuring a repository, based on the
+ * state loaded in the page.
+ *
+ * Version Added:
+ *     6.0
+ */
+RB.RepositoryFormView = Backbone.View.extend({
+    events: {
+        'change #id_bug_tracker_plan': '_onBugTrackerPlanChanged',
+        'change #id_bug_tracker_type': '_onBugTrackerTypeChanged',
+        'change #id_bug_tracker_use_hosting': '_onBugTrackerUseHostingChanged',
+        'change #id_hosting_account': '_onHostingAccountChanged',
+        'change #id_hosting_type': '_onHostingTypeChanged',
+        'change #id_public': '_onPublicAccessChanged',
+        'change #id_repository_plan': '_onRepositoryPlanChanged',
+        'change #id_tool': '_onRepositoryToolChanged',
+        'click #repo-edit-hosting-credentials-label':
+            '_onEditHostingCredentialsClicked',
+        'click #show-ssh-key-link': '_onShowSSHKeyClicked',
+    },
+
+    _powerPackTemplate: dedent`
+        <h3>${gettext('Power Pack Required')}</h3>
+        <p>
+        ${gettext('<span class="power-pack-advert-hosting-type"></span> support is available with <a href="https://www.reviewboard.org/powerpack/">Power Pack</a>, an extension which also offers powerful reports, document review, and more.')}
+        </p>
+    `,
+
+    _gerritPluginRequiredTemplate: dedent`
+        <h3>
+        ${gettext('Plugin Required')}
+        </h3>
+        <p>
+        ${interpolate(
+            gettext('The <code>gerrit-reviewboard</code> plugin is required for Gerrit integration. See the <a href="%s" target="_blank">instructions</a> for installing the plugin on your server.'),
+            [MANUAL_URL + 'admin/configuration/repositories/gerrit/'])}
+        </p>
+    `,
+
+    /**
+     * Initialize the form.
+     */
+    initialize() {
+        this._origRepoTypes = [];
+    },
+
+    /**
+     * Render the form.
+     *
+     * This will load aspects of the page from the DOM and calculate some
+     * initial state, including which parts of the page should be displayed or
+     * hidden.
+     *
+     * Returns:
+     *     RB.RepositoryFormView:
+     *     This instance, for chaining.
+     */
+    render() {
+        /* Common UI */
+        this._$submitButtons = this.$('input[type="submit"]');
+        console.assert(this._$submitButtons.length > 0);
+
+        /* Subforms */
+        this._$scmtoolAuthForms = this.$('.scmtool-auth-form');
+        this._$scmtoolRepoForms = this.$('.scmtool-repo-form');
+        this._$hostingAuthForms = this.$('.hosting-auth-form');
+        this._$hostingRepoForms = this.$('.hosting-repo-form');
+        this._$bugTrackerForms = this.$('.bug-tracker-form');
+
+        console.assert(this._$scmtoolAuthForms.length > 0);
+        console.assert(this._$scmtoolRepoForms.length > 0);
+        console.assert(this._$hostingAuthForms.length > 0);
+        console.assert(this._$hostingRepoForms.length > 0);
+        console.assert(this._$bugTrackerForms.length > 0);
+
+        /* Common configuration */
+        this._$hostingType = this.$('#id_hosting_type');
+        console.assert(this._$hostingType.length === 1);
+
+        /* Repository configuration */
+        this._$tool = this.$('#id_tool');
+        this._$toolRow = this.$('#row-tool');
+
+        console.assert(this._$hostingType.length === 1);
+        console.assert(this._$toolRow.length === 1);
+
+        /* Hosting service configuration */
+        this._$hostingAccount = this.$('#id_hosting_account');
+        this._$hostingAccountRow = this.$('#row-hosting_account');
+        this._$repoPlanRow = this.$('#row-repository_plan');
+        this._$repoPlan = this.$('#id_repository_plan');
+        this._$editHostingCredentials =
+            this.$('#repo-edit-hosting-credentials');
+        this._$editHostingCredentialsLabel =
+            this.$('#repo-edit-hosting-credentials-label');
+        this._$forceAuth = this.$('#id_force_authorize');
+
+        console.assert(this._$hostingAccount.length === 1);
+        console.assert(this._$hostingAccountRow.length === 1);
+        console.assert(this._$repoPlanRow.length === 1);
+        console.assert(this._$repoPlan.length === 1);
+        console.assert(this._$editHostingCredentials.length === 1);
+        console.assert(this._$editHostingCredentialsLabel.length === 1);
+        console.assert(this._$forceAuth.length === 1);
+
+        this._$hostingAccountRelink = $('<p/>')
+            .text(_`
+                The authentication requirements for this account have
+                changed. You will need to re-authenticate.
+            `)
+            .addClass('errornote')
+            .hide()
+            .appendTo(this._$hostingAccountRow);
+
+        /* SSH configuration and information */
+        this._$associateSSHKeyFieldset =
+            this.$('#row-associate_ssh_key').parents('fieldset');
+        this._$associateSSHKey = this.$('#id_associate_ssh_key');
+        this._$showSSHKey = this.$('#show-ssh-key-link');
+        this._$publicKeyPopup = this.$('#ssh-public-key-popup');
+
+        console.assert(this._$associateSSHKeyFieldset.length === 1);
+        console.assert(this._$associateSSHKey.length === 1);
+        console.assert(this._$showSSHKey.length === 1);
+        console.assert(this._$publicKeyPopup.length === 1);
+
+        /* Bug tracker configuration */
+        this._$bugTrackerUseHosting = this.$('#id_bug_tracker_use_hosting');
+        this._$bugTrackerUseHostingRow =
+            this.$('#row-bug_tracker_use_hosting');
+        this._$bugTrackerType = this.$('#id_bug_tracker_type');
+        this._$bugTrackerHostingURLRow =
+            this.$('#row-bug_tracker_hosting_url');
+        this._$bugTrackerTypeRow = this.$('#row-bug_tracker_type');
+        this._$bugTrackerPlan = this.$('#id_bug_tracker_plan');
+        this._$bugTrackerPlanRow = this.$('#row-bug_tracker_plan');
+        this._$bugTrackerURLRow = this.$('#row-bug_tracker');
+        this._$bugTrackerUsernameRow =
+            this.$('#row-bug_tracker_hosting_account_username');
+
+        console.assert(this._$bugTrackerUseHosting.length === 1);
+        console.assert(this._$bugTrackerUseHostingRow.length === 1);
+        console.assert(this._$bugTrackerType.length === 1);
+        console.assert(this._$bugTrackerHostingURLRow.length === 1);
+        console.assert(this._$bugTrackerTypeRow.length === 1);
+        console.assert(this._$bugTrackerPlan.length === 1);
+        console.assert(this._$bugTrackerPlanRow.length === 1);
+        console.assert(this._$bugTrackerURLRow.length === 1);
+        console.assert(this._$bugTrackerUsernameRow.length === 1);
+
+        /* Access control configuration */
+        this._$publicAccess = this.$('#id_public');
+        this._$users = this.$('#row-users');
+        this._$reviewGroups = this.$('#row-review_groups');
+
+        console.assert(this._$publicAccess.length === 1);
+        console.assert(this._$users.length === 1);
+        console.assert(this._$reviewGroups.length === 1);
+
+        /* Service-specific information display */
+        this._$hostingPowerPackAdvert = $('<div class="powerpack-advert" />')
+            .html(this._powerPackTemplate)
+            .hide()
+            .appendTo(this._$hostingType.closest('fieldset'));
+        this._$toolPowerPackAdvert = $('<div class="powerpack-advert" />')
+            .html(this._powerPackTemplate)
+            .hide()
+            .appendTo(this._$tool.closest('fieldset'));
+        this._$gerritPluginInfo = $('<div class="gerrit-plugin-advert" />')
+            .html(this._gerritPluginRequiredTemplate)
+            .hide()
+            .appendTo(this.$('#row-hosting_type'));
+
+        /* Set the initial state based on the populated form data. */
+        this._associateSSHKeyDisabled =
+            this._$associateSSHKey.prop('disabled');
+
+        this._$tool.find('option').each((i, el) => {
+            const $repoType = $(el);
+
+            this._origRepoTypes.push({
+                value: $repoType.val(),
+                text: $repoType.text(),
+            });
+        });
+
+        this._onHostingTypeChanged();
+        this._onRepositoryToolChanged();
+        this._onBugTrackerTypeChanged();
+        this._onBugTrackerUseHostingChanged();
+        this._onPublicAccessChanged();
+
+        return this;
+    },
+
+    /**
+     * Update the list of hosting service accounts.
+     *
+     * This will display only accounts that match the currently-selected
+     * hosting service.
+     *
+     * If possible, the selected account will be preserved across rebuilds.
+     */
+    _updateHostingAccountList() {
+        const hostingType = this._$hostingType.val();
+        const $hostingAccount = this._$hostingAccount;
+        const $authForm = $(`#hosting-auth-form-${hostingType}`);
+        const hostingInfo = HOSTING_SERVICES[hostingType];
+        const accounts = hostingInfo.accounts;
+        const selectedAccount = parseInt($hostingAccount.val(), 10);
+        let foundSelected = false;
+
+        /* Rebuild the list of accounts. */
+        $hostingAccount.find('option[value!=""]').remove();
+
+        if (hostingInfo.needs_two_factor_auth_code ||
+            $authForm.find('.errorlist').length > 0) {
+            /*
+             * The first one will be selected automatically, which
+             * we want. Don't select any below.
+             */
+            foundSelected = true;
+        }
+
+        accounts.forEach(account => {
+            const username = account.get('username');
+            const hostingURL = account.get('hostingURL');
+
+            const $opt = $('<option/>')
+                .val(account.pk)
+                .text(hostingURL
+                      ? `${username} (${hostingURL})`
+                      : username)
+                .data('account', account)
+                .appendTo($hostingAccount);
+
+            if (account.pk === selectedAccount || !foundSelected) {
+                $opt.prop('selected', true);
+                foundSelected = true;
+                $hostingAccount.triggerHandler('change');
+            }
+        });
+    },
+
+    /**
+     * Update the list of available tools for repositories.
+     *
+     * If selecting a custom repository, this will display all available
+     * repository types.
+     *
+     * If selecting a hosting service, then only tools pertaining to that
+     * hosting service will be shown.
+     *
+     * If possible, the selected tool will be preserved across rebuilds.
+     */
+    _updateRepositoryToolList() {
+        const hostingType = this._$hostingType.val();
+        const newRepoTypes = (hostingType === 'custom'
+                              ? []
+                              : HOSTING_SERVICES[hostingType].scmtools);
+        const $tool = this._$tool;
+        const currentRepoType = $tool.val();
+
+        $tool.empty();
+
+        this._origRepoTypes.forEach(repoType => {
+            if (newRepoTypes.length === 0 ||
+                newRepoTypes.indexOf(repoType.text) !== -1 ||
+                newRepoTypes.indexOf(repoType.value) !== -1) {
+                $('<option/>')
+                    .text(repoType.text)
+                    .val(repoType.value)
+                    .appendTo($tool);
+
+                if (repoType.value === currentRepoType) {
+                    $tool.val(currentRepoType);
+                }
+            }
+        });
+
+        $tool.triggerHandler('change');
+    },
+
+    /**
+     * Update a list of available hosting service plans.
+     *
+     * This will display only plans that match the currently-selected
+     * hosting service.
+     *
+     * If the service doesn't support multiple plans, the row for the field
+     * will be hidden.
+     *
+     * If possible, the selected plan will be preserved across rebuilds.
+     *
+     * This can be used with either the hosting service or bug tracker plan
+     * rows/fields.
+     *
+     * Args:
+     *     $row (jQuery):
+     *         The row containing the plan field.
+     *
+     *     $plan (jQuery):
+     *         The plan field to update.
+     *
+     *     serviceType (string):
+     *         The hosting service type ID.
+     *
+     *     isFake (boolean):
+     *         Whether this hosting service represents a "fake" service entry
+     *         for a service requiring additional support.
+     */
+    _updateHostingServicePlanList($row, $plan, serviceType, isFake) {
+        const planTypes = HOSTING_SERVICES[serviceType].plans;
+        const selectedPlan = $plan.val();
+
+        $plan.empty();
+
+        if (planTypes.length === 1 || isFake) {
+            $row.hide();
+        } else {
+            for (const planType of planTypes) {
+                const opt = $('<option/>')
+                    .val(planType.type)
+                    .text(planType.label)
+                    .appendTo($plan);
+
+                if (planType.type === selectedPlan) {
+                    opt.prop('selected', true);
+                }
+            }
+
+            $row.show();
+        }
+
+        $plan.triggerHandler('change');
+    },
+
+    /**
+     * Update the visibility of a hosting service plan form.
+     *
+     * This will hide all relevant forms, and then show the form for the
+     * given hosting service and plan.
+     *
+     * This can be used with either the hosting service or bug tracker plan
+     * rows/fields.
+     *
+     * Args:
+     *     $hostingType (jQuery):
+     *         The hosting type field.
+     *
+     *     formPrefix (string):
+     *         The prefix for the form ID.
+     *
+     *     $plan (jQuery):
+     *         The hosting service plan field.
+     *
+     *     $forms (jQuery):
+     *         All relevant forms to hide.
+     */
+    _updateVisibleHostingForm($hostingType, formPrefix, $plan, $forms) {
+        const hostingType = $hostingType.val();
+        const plan = $plan.val() || 'default';
+        const formID = `#${formPrefix}-${hostingType}-${plan}`;
+
+        $forms.hide();
+        this.$(formID).show();
+    },
+
+    /**
+     * Update authentication forms for the selected hosting service type.
+     *
+     * If configuring a custom repository, this will hide the hosting service
+     * account row.
+     *
+     * If configuring a hosting service, this will show the account selector
+     * row, and any fields or forms required to satisfy any authentication
+     * requirements.
+     */
+    _updateAuthFormsForHostingType() {
+        this._$hostingAuthForms.hide();
+        this._$hostingAccountRelink.hide();
+        this._$editHostingCredentials
+            .hide()
+            .val(_`Edit credentials`);
+        this._$forceAuth.prop('checked', false);
+
+        const hostingType = this._$hostingType.val();
+
+        if (hostingType === 'custom') {
+            this._$hostingAccountRow.hide();
+        } else {
+            const hostingInfo = HOSTING_SERVICES[hostingType];
+
+            if (hostingInfo.fake) {
+                return;
+            }
+
+            this._$hostingAccountRow.show();
+
+            const $authForm = this.$(`#hosting-auth-form-${hostingType}`);
+
+            /*
+             * Hide any fields required for 2FA unless explicitly
+             * needed.
+             */
+            $authForm.find('[data-required-for-2fa]').closest('.form-row')
+                .setVisible(hostingInfo.needs_two_factor_auth_code);
+
+            if (this._$hostingAccount.val() === '') {
+                /* Present fields for linking a new account. */
+                $authForm.show();
+            } else if (hostingInfo.needs_two_factor_auth_code) {
+                /*
+                 * The user needs to enter a 2FA code. We need to
+                 * show the auth form, and ensure we will be forcing
+                 * authentication.
+                 */
+                this._$forceAuth.prop('checked', true);
+                $authForm.show();
+            } else {
+                /* An existing linked account has been selected. */
+                const selectedIndex =
+                    this._$hostingAccount[0].selectedIndex;
+                const $selectedOption =
+                    $(this._$hostingAccount[0].options[selectedIndex]);
+                const account = $selectedOption.data('account');
+
+                if (account.is_authorized &&
+                    $authForm.find('.errorlist').length === 0) {
+                    this._$editHostingCredentials.show();
+                } else {
+                    $authForm.show();
+                    this._$hostingAccountRelink.show();
+                }
+            }
+        }
+    },
+
+    /**
+     * Handler for changes to the selected bug tracker plan.
+     *
+     * This will show or hide bug tracker configuration fields dependent on
+     * the selected plan.
+    */
+    _onBugTrackerPlanChanged() {
+        const plan = this._$bugTrackerPlan.val() || 'default';
+        const bugTrackerType = this._$bugTrackerType.val();
+        const planInfo = HOSTING_SERVICES[bugTrackerType].planInfo[plan];
+
+        this._updateVisibleHostingForm(this._$bugTrackerType,
+                                       'bug-tracker-form-hosting',
+                                       this._$bugTrackerPlan,
+                                       this._$bugTrackerForms);
+
+        this._$bugTrackerUsernameRow.setVisible(
+            planInfo.bug_tracker_requires_username);
+    },
+
+    /**
+     * Handler for changes to the selected bug tracker type.
+     *
+     * This will show or hide bug tracker configuration fields dependent on
+     * the selected type.
+     */
+    _onBugTrackerTypeChanged() {
+        this._$bugTrackerForms.hide();
+
+        const bugTrackerType = this._$bugTrackerType.val();
+
+        if (bugTrackerType === 'custom' || bugTrackerType === 'none') {
+            this._$bugTrackerHostingURLRow.hide();
+            this._$bugTrackerPlanRow.hide();
+            this._$bugTrackerUsernameRow.hide();
+        }
+
+        if (bugTrackerType === 'custom') {
+            this._$bugTrackerURLRow.show();
+        } else if (bugTrackerType === 'none') {
+            this._$bugTrackerURLRow.hide();
+        } else {
+            this._$bugTrackerURLRow.hide();
+            this._updateHostingServicePlanList(this._$bugTrackerPlanRow,
+                                               this._$bugTrackerPlan,
+                                               bugTrackerType,
+                                               false);
+
+            this._$bugTrackerHostingURLRow.setVisible(
+                HOSTING_SERVICES[bugTrackerType].self_hosted);
+        }
+    },
+
+    /**
+     * Handler for changes to the Use Hosting Service's Bug Tracker field.
+     *
+     * This will show or hide the bug tracker fields relevant to the value
+     * of this field.
+     */
+    _onBugTrackerUseHostingChanged() {
+        if (this._$bugTrackerUseHosting[0].checked) {
+            this._$bugTrackerTypeRow.hide();
+            this._$bugTrackerPlanRow.hide();
+            this._$bugTrackerUsernameRow.hide();
+            this._$bugTrackerURLRow.hide();
+            this._$bugTrackerForms.hide();
+        } else {
+            this._$bugTrackerTypeRow.show();
+            this._$bugTrackerType.trigger('change');
+        }
+    },
+
+    /**
+     * Handler for changes to the selected hosting service account.
+     *
+     * This will update all the authentication forms based on the account.
+     */
+    _onHostingAccountChanged() {
+        this._updateAuthFormsForHostingType();
+    },
+
+    /**
+     * Handler for changes to the selected hosting service type.
+     *
+     * If selecting a custom repository, all hosting service forms will be
+     * hidden, and basic repository forms shown.
+     *
+     * If selecting a hosting service, the basic repository forms will be
+     * hidden, and hosting service forms shown.
+     *
+     * This will also impact options for bug trackers and SSH key display.
+     */
+    _onHostingTypeChanged() {
+        const hostingType = this._$hostingType.val();
+        const hostingInfo = HOSTING_SERVICES[hostingType];
+        const isCustom = (hostingType === 'custom');
+        const isFake = (!isCustom && hostingInfo.fake === true);
+
+        this._updateRepositoryToolList();
+
+        this._$gerritPluginInfo.toggle(hostingType === 'gerrit');
+
+        if (isCustom) {
+            this._$repoPlanRow.hide();
+        } else {
+            this._$scmtoolAuthForms.hide();
+            this._$scmtoolRepoForms.hide();
+
+            this._updateHostingServicePlanList(this._$repoPlanRow,
+                                               this._$repoPlan,
+                                               hostingType,
+                                               isFake);
+        }
+
+        this._$repoPlan.trigger('change');
+
+        if (isCustom ||
+            isFake ||
+            !hostingInfo.supports_bug_trackers) {
+            this._$bugTrackerUseHostingRow.hide();
+            this._$bugTrackerUseHosting
+                .prop({
+                    checked: false,
+                    disabled: true,
+                })
+                .trigger('change');
+        } else {
+            this._$bugTrackerUseHosting.prop('disabled', false);
+            this._$bugTrackerUseHostingRow.show();
+        }
+
+        if (isCustom || !hostingInfo.supports_ssh_key_association) {
+            this._$associateSSHKeyFieldset.hide();
+            this._$associateSSHKey.prop({
+                checked: false,
+                disabled: true,
+            });
+        } else {
+            /*
+             * Always use the original state of the checkbox (i.e. the
+             * state on page load)
+             */
+            this._$associateSSHKey.prop('disabled',
+                                        this._associateSSHKeyDisabled);
+            this._$associateSSHKeyFieldset.show();
+        }
+
+        if (isFake) {
+            this._$hostingPowerPackAdvert
+                .find('.power-pack-advert-hosting-type')
+                .text($hostingType.find(':selected').text());
+        }
+
+        this._$hostingAccountRow.setVisible(!isFake);
+        this._$toolRow.setVisible(!isFake);
+
+        this._$hostingPowerPackAdvert.setVisible(isFake);
+        this._$submitButtons.prop('disabled', isFake);
+
+        if (!isCustom) {
+            this._updateHostingAccountList();
+        }
+
+        this._updateAuthFormsForHostingType();
+    },
+
+    /**
+     * Handler for changes to the Publicly Accessible checkbox.
+     *
+     * This will show or hide the user and review group ACL lists, depending
+     * on the value of the checkbox.
+     */
+    _onPublicAccessChanged() {
+        const visible = !this._$publicAccess[0].checked;
+
+        this._$users.setVisible(visible);
+        this._$reviewGroups.setVisible(visible);
+    },
+
+    /**
+     * Handler for changes to a repository plan for a hosting service.
+     *
+     * This will update the display of any relevant repository-related fields
+     * for the hosting service plan.
+     */
+    _onRepositoryPlanChanged() {
+        this._updateVisibleHostingForm(this._$hostingType,
+                                       'repo-form-hosting',
+                                       this._$repoPlan,
+                                       this._$hostingRepoForms);
+    },
+
+    /**
+     * Handler for changes to a repository's tool field.
+     *
+     * If configuring a custom repository not backed by a hosting service,
+     * this will hide or show any authentication or repository subforms
+     * relevant to the tool.
+     */
+    _onRepositoryToolChanged() {
+        if (this._$hostingType.val() !== 'custom') {
+            return;
+        }
+
+        const scmtoolID = this._$tool.val();
+        const $authForm = $(`#auth-form-scm-${scmtoolID}`);
+        const $repoForm = $(`#repo-form-scm-${scmtoolID}`);
+
+        const toolInfo = TOOLS_INFO[scmtoolID];
+        const isFake = (toolInfo.fake === true);
+
+        if (isFake) {
+            this._$toolPowerPackAdvert
+                .find('.power-pack-advert-hosting-type')
+                .text(toolInfo.name);
+        }
+
+        this._$scmtoolAuthForms.hide();
+        this._$scmtoolRepoForms.hide();
+
+        $authForm.setVisible(!isFake);
+        $repoForm.setVisible(!isFake);
+        this._$toolPowerPackAdvert.setVisible(isFake);
+        this._$submitButtons.prop('disabled', isFake);
+    },
+
+    /**
+     * Handler for when the Edit Credentials button is clicked.
+     *
+     * This will toggle the display of the Edit Credentials part of the
+     * authentication form.
+     *
+     * Args:
+     *     e (jQuery.Event):
+     *         The click event.
+     */
+    _onEditHostingCredentialsClicked(e) {
+        e.preventDefault();
+        e.stopPropagation();
+
+        const hostingType = this._$hostingType.val();
+        const $authForm = $(`#hosting-auth-form-${hostingType}`);
+        let showForm;
+        let forceAuth;
+        let labelText;
+
+        if (this._$forceAuth.prop('checked')) {
+            labelText = `Edit credentials`;
+            showForm = false;
+            forceAuth = false;
+        } else {
+            labelText = `Cancel editing credentials`;
+            showForm = true;
+            forceAuth = true;
+        }
+
+        this._$editHostingCredentialsLabel.text(labelText);
+        this._$forceAuth.prop('checked', forceAuth);
+        $authForm.setVisible(showForm);
+    },
+
+    /**
+     * Handler for when the Show SSH Key button is clicked.
+     *
+     * This will toggle the display of the configured SSH key.
+     *
+     * Args:
+     *     e (jQuery.Event):
+     *         The click event.
+     */
+    _onShowSSHKeyClicked(e) {
+        e.preventDefault();
+        e.stopPropagation();
+
+        const $showSSHKey = this._$showSSHKey;
+        const $popup = this._$publicKeyPopup;
+        let text;
+        let showPopup;
+
+        if ($popup.is(':visible')) {
+            text = _`Show your SSH public key`;
+            showPopup = false;
+        } else {
+            text = _`Hide your SSH public key`;
+            showPopup = true;
+        }
+
+        $showSSHKey.text(text);
+        $popup.setVisible(showPopup);
+    },
+});
diff --git a/reviewboard/staticbundles.py b/reviewboard/staticbundles.py
index 774484f73d5ce8141b4be5d7bd50769ec6c36cbd..e548077784d6f9d90996bd1f8d0f227f72d87a09 100644
--- a/reviewboard/staticbundles.py
+++ b/reviewboard/staticbundles.py
@@ -424,10 +424,9 @@ PIPELINE_JAVASCRIPT = dict({
         ),
         'output_filename': 'rb/js/admin.min.js',
     },
-    'repositoryform': {
+    'admin-repository-form': {
         'source_filenames': (
-            # Legacy JavaScript
-            'rb/js/admin/repositoryform.es6.js',
+            'rb/js/admin/views/repositoryFormView.es6.js',
         ),
         'output_filename': 'rb/js/repositoryform.min.js',
     },
diff --git a/reviewboard/templates/admin/scmtools/repository/change_form.html b/reviewboard/templates/admin/scmtools/repository/change_form.html
index ee65b2dd7cbade1b5c1f0769aaa32806ef990a96..b33103e628fccc2ea792a64dd9c8edce7f9cf9a1 100644
--- a/reviewboard/templates/admin/scmtools/repository/change_form.html
+++ b/reviewboard/templates/admin/scmtools/repository/change_form.html
@@ -3,10 +3,17 @@
 
 {% block scripts-post %}
 {{block.super}}
-{%  javascript 'repositoryform' %}
+{%  javascript 'admin-repository-form' %}
 
 <script>
 {% include "admin/repository_fields.js" with form=adminform.form %}
+
+$(document).ready(function() {
+    var view = new RB.RepositoryFormView({
+        el: $('#repository_form')
+    });
+    view.render();
+});
 </script>
 {% endblock scripts-post %}
 
