Repository: OWASP/threat-dragon
Pull request #1181 (2025-01-29 14:41:43)

Feat: Add dialog for creating branches and indicators for protected branches
Summary:
This pull request introduces a dialog that allows users to create a new branch during the branch selection process. The newly created branch can then be used directly within the workflow.

Background:
Our repositories have protected branches, which means that users must manually create new branches in the repository before they can work on them. To improve usability, this feature enables branch creation directly from the UI, eliminating the need to navigate to the repository. In addition, the UI now shows which branches are protected and which are not, so users can see in advance if they can make changes to a selected branch.

Description for the changelog:
Added a dialog for creating new branches during branch selection and an indicator for protected branches.

Other info:
All corresponding translations for supported languages have been provided, but they were generated using translation software. Please review for accuracy and completeness.
Displaying protected branches in Bitbucket was unfortunately not straightforward to implement and is therefore not supported in this update.

Screenshot:
![image](https://github.com/user-attachments/assets/914c0eac-f49b-4e3a-830c-63f21fb9aecb)

![image](https://github.com/user-attachments/assets/f2cd1606-7506-4594-9355-c94115ec5cf0)



Patch:
diff --git a/td.server/src/config/routes.config.js b/td.server/src/config/routes.config.js
index d5a4c71f..878e5908 100644
--- a/td.server/src/config/routes.config.js
+++ b/td.server/src/config/routes.config.js
@@ -42,6 +42,8 @@ const routes = (router) => {
     router.get('/api/threatmodel/:organisation/:repo/:branch/models', threatmodelController.models);
     router.get('/api/threatmodel/:organisation/:repo/:branch/:model/data', threatmodelController.model);
 
+    router.post('/api/threatmodel/:organisation/:repo/:branch/createBranch', threatmodelController.createBranch);
+
     // removed because of security denial of service concerns (denial of models)
     //router.delete('/api/threatmodel/:organisation/:repo/:branch/:model', threatmodelController.deleteModel);
 
diff --git a/td.server/src/controllers/threatmodelcontroller.js b/td.server/src/controllers/threatmodelcontroller.js
index 2b1b78d7..2936d561 100644
--- a/td.server/src/controllers/threatmodelcontroller.js
+++ b/td.server/src/controllers/threatmodelcontroller.js
@@ -54,7 +54,11 @@ const branches = (req, res) => responseWrapper.sendResponseAsync(async () => {
     const headers = branchesResp[1];
     const pageLinks = branchesResp[2];
 
-    const branchNames = branches.map((x) => x.name);
+    const branchNames = branches.map((x) => ({
+        name: x.name,
+        // Protected branches are not so easy to determine from the API on Bitbucket
+        protected: x.protected||false
+    }));
 
     const pagination = getPagination(headers, pageLinks, repoInfo.page);
 
@@ -212,6 +216,26 @@ const organisation = (req, res) => {
     return res.status(200).send(organisation);
 };
 
+const createBranch = async (req, res) => {
+    const repository = repositories.get();
+
+    const branchInfo = {
+        organisation: req.params.organisation,
+        repo: req.params.repo,
+        branch: req.params.branch,
+        ref: req.body.refBranch
+    };
+    logger.debug(`API createBranch request: ${logger.transformToString(req)}`);
+
+    try {
+        const createBranchResp = await repository.createBranchAsync(branchInfo, req.provider.access_token);
+        return res.status(201).send(createBranchResp);
+    } catch (err) {
+        logger.error(err);
+        return serverError('Error creating branch', res, logger);
+    }
+};
+
 export default {
     branches,
     create,
@@ -220,5 +244,6 @@ export default {
     models,
     organisation,
     repos,
-    update
+    update,
+    createBranch
 };
diff --git a/td.server/src/repositories/bitbucketrepo.js b/td.server/src/repositories/bitbucketrepo.js
index a3ecf38a..53648f48 100644
--- a/td.server/src/repositories/bitbucketrepo.js
+++ b/td.server/src/repositories/bitbucketrepo.js
@@ -154,10 +154,28 @@ export const deleteAsync = async (modelInfo, accessToken) => {
     throw new Error(`Bitbucket deleteAsync is not implemented yet`);
 };
 
+export const createBranchAsync = (repoInfo, accessToken) => {
+    const workspace = env.get().config.BITBUCKET_WORKSPACE;
+
+    const client = getClient(accessToken);
+    const repo = getRepoFullName(repoInfo);
+    return client.refs.createBranch({
+        _body: {
+            name: repoInfo.branch,
+            target: {
+                hash: repoInfo.ref
+            }
+        },
+        repo_slug: repo,
+        workspace: workspace
+    });
+};
+
 const getRepoFullName = (info) => `${info.repo}`;
 const getModelPath = (modelInfo) => `${repoRootDirectory()}/${modelInfo.model}/${modelInfo.model}.json`;
 const getModelContent = (modelInfo) => JSON.stringify(modelInfo.body, null, '  ');
 
+
 export default {
     branchesAsync,
     createAsync,
@@ -168,5 +186,6 @@ export default {
     searchAsync,
     updateAsync,
     userAsync,
-    getClient
+    getClient,
+    createBranchAsync
 };
diff --git a/td.server/src/repositories/githubrepo.js b/td.server/src/repositories/githubrepo.js
index c7a513bc..29671b97 100644
--- a/td.server/src/repositories/githubrepo.js
+++ b/td.server/src/repositories/githubrepo.js
@@ -81,6 +81,14 @@ const deleteAsync = async (modelInfo, accessToken) => {
         );
 };
 
+const createBranchAsync = async (repoInfo, accessToken) => {
+    const client = getClient(accessToken);
+    const repo = getRepoFullName(repoInfo);
+    const resp = await client.repo(repo).refAsync(`heads/${repoInfo.ref}`);
+    const sha = resp[0].object.sha;
+    return client.repo(repo).createRefAsync(`refs/heads/${repoInfo.branch}`, sha);
+};
+
 const getRepoFullName = (info) => `${info.organisation}/${info.repo}`;
 const getModelPath = (modelInfo) => `${repoRootDirectory()}/${modelInfo.model}/${modelInfo.model}.json`;
 const getModelContent = (modelInfo) => JSON.stringify(modelInfo.body, null, '  ');
@@ -94,5 +102,6 @@ export default {
     reposAsync,
     searchAsync,
     updateAsync,
-    userAsync
+    userAsync,
+    createBranchAsync
 };
diff --git a/td.server/src/repositories/gitlabrepo.js b/td.server/src/repositories/gitlabrepo.js
index d1100819..26767264 100644
--- a/td.server/src/repositories/gitlabrepo.js
+++ b/td.server/src/repositories/gitlabrepo.js
@@ -100,11 +100,18 @@ export const deleteAsync = (modelInfo, accessToken) => getClient(accessToken).Re
         'Deleted by OWASP Threat Dragon',
     );
 
+export const createBranchAsync = (repoInfo, accessToken) => {
+    const client = getClient(accessToken);
+    const repo = getRepoFullName(repoInfo);
+    return client.Branches.create(repo, repoInfo.branch, repoInfo.ref);
+};
+
 const getRepoFullName = (info) => `${info.organisation}/${info.repo}`;
 const getModelPath = (modelInfo) => `${repoRootDirectory()}/${modelInfo.model}/${modelInfo.model}.json`;
 const getModelContent = (modelInfo) => JSON.stringify(modelInfo.body, null, '  ');
 
 export default {
+    createBranchAsync,
     branchesAsync,
     createAsync,
     deleteAsync,
@@ -113,5 +120,5 @@ export default {
     reposAsync,
     searchAsync,
     updateAsync,
-    userAsync
+    userAsync,
 };
diff --git a/td.vue/src/components/AddBranchDialog.vue b/td.vue/src/components/AddBranchDialog.vue
new file mode 100644
index 00000000..d8ed8b3b
--- /dev/null
+++ b/td.vue/src/components/AddBranchDialog.vue
@@ -0,0 +1,138 @@
+<template>
+    <b-modal
+        id="add-new-branch"
+        size="md"
+        ok-variant="primary"
+        header-bg-variant="primary"
+        header-text-variant="light"
+        :title="modalTitle"
+        visible
+        centered
+        @hide="closeDialog"
+        hide-footer
+    >
+        <form @submit.prevent="addBranch">
+            <b-row>
+                <b-col lg="12" class="pb-2">
+                    <b-form-group id="input-group-1" :label="$t('branch.name')" label-for="branchName">
+                        <b-form-input
+                            type="text"
+                            id="branchName"
+                            v-model="newBranchName"
+                            @input="validate"
+                            :state="isError"
+                            lazy-formatter
+                            trim
+                            required
+                        />
+                        <b-form-invalid-feedback :state="isError">
+                            {{ branchNameError }}
+                        </b-form-invalid-feedback>
+                    </b-form-group>
+                </b-col>
+            </b-row>
+            <b-row>
+                <b-col lg="12" class="pb-2">
+                    <b-form-group id="input-group-2" :label="$t('branch.refBranch')" label-for="refBranch">
+                        <b-form-select id="refBranch" v-model="refBranch" :options="branchNames" size="md"
+                                       required/>
+                    </b-form-group>
+                </b-col>
+            </b-row>
+        </form>
+        <hr/>
+        <div class="d-flex justify-content-end">
+            <b-overlay
+                :show="wait"
+                variant="light"
+                blur="true"
+                opacity="0.8"
+                spinner-small
+            >
+                <b-button variant="primary" type="submit" @click="addBranch" class="m-1">{{ $t('branch.add') }}</b-button>
+            </b-overlay>
+            <b-button variant="secondary" @click="closeDialog" class="m-1">{{ $t('branch.cancel') }}</b-button>
+        </div>
+    </b-modal>
+</template>
+<script>
+import branchActions from '@/store/actions/branch.js';
+
+export default {
+    name: 'AddBranchModal',
+    props: {
+        branches: {
+            type: Array,
+            validator: (value) => {
+                return value.every((branch) => {
+                    return typeof branch === 'string' || (branch.value && typeof branch.value === 'string');
+                });
+            },
+            required: true
+        }
+    },
+    data() {
+        return {
+            newBranchName: '',
+            refBranch: '',
+            modalTitle: this.$t('branch.addNew'),
+            branchNameError: '',
+            isError: null,
+            wait: false
+        };
+    },
+    computed: {
+        branchNames() {
+            return this.branches.map(branch => branch.value || branch);
+        }
+    },
+    mounted() {
+        this.refBranch = this.branchNames.slice(-1)[0];
+    },
+    watch: {
+        branches: function (newBranches) {
+            if (newBranches.length > 0) {
+                this.refBranch = this.branchNames.slice(-1)[0];
+            }
+        }
+    },
+    methods: {
+        closeDialog() {
+            this.$emit('close-dialog');
+        },
+        validate() {
+            if (this.newBranchName === '') {
+                this.branchNameError = this.$t('branch.nameRequired');
+                this.isError = false;
+            } else if (this.branchNames.includes(this.newBranchName)) {
+                this.branchNameError = this.$t('branch.nameExists');
+                this.isError = false;
+            } else {
+                this.branchNameError = '';
+                this.isError = true;
+            }
+        },
+        async addBranch() {
+            this.wait = true;
+            this.validate();
+            if (!this.isError) {
+                this.wait = false;
+                return;
+            }
+            this.$store.dispatch(branchActions.create, {branchName: this.newBranchName, refBranch: this.refBranch});
+
+            // sometimes the branch is not immediately available, so we wait for it (only for 30 seconds)
+            for (let i = 0; i < 30; i++) {
+                await this.$store.dispatch(branchActions.fetch, 1);
+                if (this.branchNames.includes(this.newBranchName)) {
+                    break;
+                }
+                await new Promise(resolve => setTimeout(resolve, 1000));
+            }
+
+            this.wait = false;
+            this.closeDialog();
+        }
+    }
+};
+</script>
diff --git a/td.vue/src/components/SelectionPage.vue b/td.vue/src/components/SelectionPage.vue
index de14ff39..edfbdcd0 100644
--- a/td.vue/src/components/SelectionPage.vue
+++ b/td.vue/src/components/SelectionPage.vue
@@ -49,7 +49,16 @@
                         :key="idx"
                         href="javascript:void(0)"
                         @click="onItemClick(item)">
-                        {{ isGoogleProvider ? item.name : item }}
+                        <span v-if="typeof item === 'string'">{{ item }}</span>
+                        <span v-else class="d-flex justify-content-between align-items-center">
+                            {{ item.value }}
+                            <font-awesome-icon
+                                v-if="item.icon"
+                                :icon="item.icon"
+                                v-b-tooltip.hover
+                                :title="$t(item.iconTooltip) || ''"
+                            ></font-awesome-icon>
+                        </span>
                     </b-list-group-item>
                 </b-list-group>
             </b-col>
@@ -59,7 +68,7 @@
             <b-col md=6 offset=3>
                 <div class="pagination">
                     <button @click="paginate(--pageRef)" :disabled="!pagePrev">Previous</button>
-                    <button class="btn" data-toggle="buttons" :disabled="true">{{pageRef}}</button>
+                    <button class="btn" data-toggle="buttons" :disabled="true">{{ pageRef }}</button>
                     <button @click="paginate(++pageRef)" :disabled="!pageNext">Next</button>
                 </div>
             </b-col>
@@ -91,7 +100,15 @@ export default {
             default: ''
         },
         items: {
-            required: true
+            required: true,
+            type: Array,
+            validator: (value) => {
+                return value.every((item) => {
+                    return typeof item === 'string' || (item.value && typeof item.value === 'string')
+                        && (!item.icon || typeof item.icon === 'string')
+                        && (!item.iconTooltip || (typeof item.iconTooltip === 'string' && item.icon));
+                });
+            }
         },
         page: {
             required: false,
@@ -123,7 +140,8 @@ export default {
         onEmptyStateClick: {
             required: false,
             type: Function,
-            default: () => {}
+            default: () => {
+            }
         },
         showBackItem: {
             required: false,
@@ -133,7 +151,8 @@ export default {
         onBackClick: {
             required: false,
             type: Function,
-            default: () => {}
+            default: () => {
+            }
         },
         isGoogleProvider: {
             required: false,
@@ -143,11 +162,14 @@ export default {
     },
     computed: {
         displayedItems: function () {
-            if (!this.filter) { return this.items; }
+            if (!this.filter) {
+                return this.items;
+            }
             if (this.$props.isGoogleProvider) {
                 return this.items.filter(x => x.name.toLowerCase().includes(this.filter.toLowerCase()));
             } else {
-                return this.items.filter(x => x.toLowerCase().includes(this.filter.toLowerCase()));
+                console.log(this.items);
+                return this.items.filter(x => (x.value || x).toLowerCase().includes(this.filter.toLowerCase()));
             }
         }
     }
diff --git a/td.vue/src/i18n/ar.js b/td.vue/src/i18n/ar.js
index 8f151484..419ffd64 100644
--- a/td.vue/src/i18n/ar.js
+++ b/td.vue/src/i18n/ar.js
@@ -83,7 +83,16 @@ const ara = {
     branch: {
         select: 'حدد فرعًا من',
         from: 'من القائمة أدناه أو',
-        chooseRepo: 'اختيار مستودع آخر'
+        or: 'أو',
+        chooseRepo: 'اختيار مستودع آخر',
+        protectedBranch: 'فرع محمي',
+        nameRequired: 'اسم الفرع مطلوب',
+        nameExists: 'اسم الفرع موجود بالفعل',
+        refBranch: 'الفرع المرجعي',
+        addNew: 'إضافة فرع جديد',
+        add: 'إضافة فرع',
+        cancel: 'إلغاء',
+        name: 'اسم الفرع',
     },
     threatmodelSelect: {
         select: 'حدد نموذج تهديد من',
diff --git a/td.vue/src/i18n/de.js b/td.vue/src/i18n/de.js
index 5a509374..13215c50 100644
--- a/td.vue/src/i18n/de.js
+++ b/td.vue/src/i18n/de.js
@@ -83,7 +83,16 @@ const deu = {
     branch: {
         select: 'Wähle einen Branch aus', //in line with Github wording https://docs.github.com/de/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-and-deleting-branches-within-your-repository
         from: 'aus folgender Liste aus',
-        chooseRepo: 'ein anderes Repository auswählen'
+        chooseRepo: 'ein anderes Repository auswählen',
+        or: 'oder',
+        addNew: 'füge einen neuen Branch hinzu',
+        protectedBranch: 'Geschützter Branch',
+        nameRequired: 'Branch Name ist erforderlich',
+        nameExists: 'Branch Name existiert bereits',
+        refBranch: 'Referenz Branch',
+        add: 'Branch hinzufügen',
+        cancel: 'Abbrechen',
+        name: 'Branch Name'
     },
     threatmodelSelect: {
         select: 'Ein Bedrohungsmodell auswählen aus',
diff --git a/td.vue/src/i18n/el.js b/td.vue/src/i18n/el.js
index 68bee0ff..043867d7 100644
--- a/td.vue/src/i18n/el.js
+++ b/td.vue/src/i18n/el.js
@@ -83,7 +83,16 @@ const ell = {
     branch: {
         select: 'Επιλέξτε ένα παρακλάδι (branch) από',
         from: 'από την παρακάτω λίστα ή',
-        chooseRepo: 'επιλέξτε ένα άλλο αποθετήριο'
+        chooseRepo: 'επιλέξτε ένα άλλο αποθετήριο',
+        or: 'ή',
+        addNew: 'να προσθέσετε έναν νέο κλάδο',
+        protectedBranch: 'Προστατευμένος κλάδος',
+        nameRequired: 'Το όνομα του κλάδου είναι υποχρεωτικό',
+        nameExists: 'Το όνομα του κλάδου υπάρχει ήδη σε αυτό το αποθετήριο',
+        refBranch: 'Κλάδος αναφοράς (Ref Branch)',
+        add: 'Προσθήκη υποκαταστήματος',
+        cancel: 'Ακύρωση',
+        name: 'Όνομα υποκαταστήματος',
     },
     threatmodelSelect: {
         select: 'Επιλέξτε ένα μοντέλο απειλών από',
diff --git a/td.vue/src/i18n/en.js b/td.vue/src/i18n/en.js
index ac6229f0..b5ba0c6b 100644
--- a/td.vue/src/i18n/en.js
+++ b/td.vue/src/i18n/en.js
@@ -83,7 +83,16 @@ const eng = {
     branch: {
         select: 'Select a branch from',
         from: 'from the list below or',
-        chooseRepo: 'choose another repo'
+        chooseRepo: 'choose another repo',
+        or: 'or',
+        addNew: 'add a new branch',
+        protectedBranch: 'Protected branch',
+        refBranch: 'Reference branch',
+        nameRequired: 'Branch name is required',
+        nameExists: 'Branch name already exists',
+        add: 'add branch',
+        cancel: 'Cancel',
+        name: 'branch name',
     },
     folder: {
         select: 'Select a',
diff --git a/td.vue/src/i18n/es.js b/td.vue/src/i18n/es.js
index e5e0141e..00a5c886 100644
--- a/td.vue/src/i18n/es.js
+++ b/td.vue/src/i18n/es.js
@@ -83,7 +83,15 @@ const spa = {
     branch: {
         select: 'Seleccione un branch',
         from: 'de la lista a continuación o',
-        chooseRepo: 'elija otro repositorio'
+        chooseRepo: 'elija otro repositorio',
+        addNew: 'o añadir una nueva rama',
+        protectedBranch: 'Rama protegida',
+        nameRequired: 'El nombre de la rama es obligatorio',
+        nameExists: 'El nombre de la rama ya existe',
+        refBranch: 'Rama de referencia',
+        add: 'Añadir rama',
+        cancel: 'Cancelar',
+        name: 'Nombre de la sucursal',
     },
     threatmodelSelect: {
         select: 'Seleccione un modelo de amenazas (threat model) ',
diff --git a/td.vue/src/i18n/fi.js b/td.vue/src/i18n/fi.js
index 26a48528..dc20fe9a 100644
--- a/td.vue/src/i18n/fi.js
+++ b/td.vue/src/i18n/fi.js
@@ -83,7 +83,16 @@ const fin = {
     branch: {
         select: 'Valitse haara',
         from: 'alla olevista, tai',
-        chooseRepo: 'valitse toinen arkisto'
+        chooseRepo: 'valitse toinen arkisto',
+        or: 'or',
+        addNew: 'lisätä uusi haara',
+        protectedBranch: 'Suojattu haara',
+        nameRequired: 'Haaran nimi vaaditaan',
+        nameExists: 'Haara on jo olemassa',
+        refBranch: 'Viitehaara',
+        add: 'Lisää haara',
+        cancel: 'Peruuta',
+        name: 'Sivuliikkeen nimi',
     },
     threatmodelSelect: {
         select: 'Valitse uhkamalli kohteesta',
@@ -369,7 +378,7 @@ const fin = {
             total: 'Uhkia yhteensä',
             mitigated: 'Hallittuja uhkia',
             notMitigated: 'Hallitsemattomia uhkia',
-            openCritical: 'Avoin / Kriittinen tärkeys', 
+            openCritical: 'Avoin / Kriittinen tärkeys',
             openHigh: 'Avoin / Korkea tärkeys',
             openMedium: 'Avoin / Keskitason tärkeys',
             openLow: 'Avoin / Alhainen tärkeys',
diff --git a/td.vue/src/i18n/fr.js b/td.vue/src/i18n/fr.js
index d7ce25f9..806fa3da 100644
--- a/td.vue/src/i18n/fr.js
+++ b/td.vue/src/i18n/fr.js
@@ -83,7 +83,16 @@ const fra = {
     branch: {
         select: 'Sélectionnez une branche parmi',
         from: 'parmi la liste ci-dessous ou',
-        chooseRepo: 'choisir un autre projet'
+        chooseRepo: 'choisir un autre projet',
+        or: 'ou',
+        addNew: 'ajouter une nouvelle branche',
+        protectedBranch: 'Branche protégée',
+        nameRequired: 'Le nom de la branche est requis',
+        nameExists: 'Le nom de la branche existe déjà',
+        refBranch: 'branche de référence',
+        add: 'Ajouter une branche',
+        cancel: 'Annuler',
+        name: 'Nom de la branche',
     },
     threatmodelSelect: {
         select: 'Sélectionnez un modèle de menace parmi',
diff --git a/td.vue/src/i18n/hi.js b/td.vue/src/i18n/hi.js
index 7ba2311c..4e7ac463 100644
--- a/td.vue/src/i18n/hi.js
+++ b/td.vue/src/i18n/hi.js
@@ -83,7 +83,16 @@ const hin = {
     branch: {
         select: 'से एक शाखा का चयन करें',
         from: 'नीचे दी गई सूची से या',
-        chooseRepo: 'एक और रेपो चुनें'
+        chooseRepo: 'एक और रेपो चुनें',
+        or: 'या',
+        addNew: 'नई शाखा जोड़ें',
+        protectedBranch: 'संरक्षित शाखा',
+        nameRequired: 'एक नाम आवश्यक है',
+        nameExists: 'एक शाखा इस नाम से पहले ही मौजूद है',
+        refBranch: 'आधार शाखा',
+        add: 'शाखा जोडे',
+        cancel: 'रद्द करें',
+        name: 'शाखा का नाम',
     },
     threatmodelSelect: {
         select: 'से एक थ्रेट मॉडल का चयन करें',
diff --git a/td.vue/src/i18n/id.js b/td.vue/src/i18n/id.js
index c49f10e4..798a4cbd 100644
--- a/td.vue/src/i18n/id.js
+++ b/td.vue/src/i18n/id.js
@@ -83,7 +83,16 @@ const id = {
     branch: {
         select: 'Pilih cabang dari',
         from: 'dari daftar di bawah atau',
-        chooseRepo: 'pilih repo lain'
+        chooseRepo: 'pilih repo lain',
+        or: 'atau',
+        addNew: 'tambahkan cabang baru',
+        protectedBranch: 'Cabang dilindungi',
+        nameRequired: 'Nama cabang diperlukan',
+        nameExists: 'Nama cabang sudah ada',
+        refBranch: 'Cabang Referensi',
+        add: 'Tambah cabang',
+        cancel: 'Batal',
+        name: 'Nama Cabang',
     },
     threatmodelSelect: {
         select: 'Pilih Model Ancaman dari',
diff --git a/td.vue/src/i18n/ja.js b/td.vue/src/i18n/ja.js
index 53bb43ee..7cf0e5b4 100644
--- a/td.vue/src/i18n/ja.js
+++ b/td.vue/src/i18n/ja.js
@@ -79,7 +79,16 @@ const jpn = {
     branch: {
         select: '次のものから',
         from: 'ブランチ名を選択してください',
-        chooseRepo: 'リポジトリの切り替え'
+        chooseRepo: 'リポジトリの切り替え',
+        or: 'または',
+        addNew: '新しいブランチを追加します。',
+        protectedBranch: '保護されたブランチ',
+        nameRequired: 'ブランチ名が必要です。',
+        nameExists: 'ブランチ名が既に存在します。',
+        refBranch: 'リファレンスブランチ',
+        add: 'ブランチを追加する\n',
+        cancel: '取り消し',
+        name: 'ブランチ名',
     },
     threatmodelSelect: {
         select: '次のものから',
diff --git a/td.vue/src/i18n/ms.js b/td.vue/src/i18n/ms.js
index 78059312..3787eda5 100644
--- a/td.vue/src/i18n/ms.js
+++ b/td.vue/src/i18n/ms.js
@@ -83,7 +83,16 @@ const ms = {
     branch: {
         select: 'Pilih cawangan dari',
         from: 'dari senarai di bawah atau',
-        chooseRepo: 'pilih repo lain'
+        chooseRepo: 'pilih repo lain',
+        or: 'atau',
+        addNew: 'tambah cawangan baru',
+        protectedBranch: 'Cawangan Dilindungi',
+        nameRequired: 'Nama cawangan diperlukan',
+        nameExists: 'Nama cawangan sudah wujud',
+        refBranch: 'Cawangan Rujukan',
+        add: 'tambah cawangan',
+        cancel: 'Batal',
+        nama: 'nama cawangan',
     },
     threatmodelSelect: {
         select: 'Pilih Model Ancaman dari',
diff --git a/td.vue/src/i18n/pt.js b/td.vue/src/i18n/pt.js
index a85bf6bf..87602304 100644
--- a/td.vue/src/i18n/pt.js
+++ b/td.vue/src/i18n/pt.js
@@ -83,7 +83,16 @@ const por = {
     branch: {
         select: 'Selecionar uma branch',
         from: 'da lista abaixo ou',
-        chooseRepo: 'escolher outro repositório'
+        chooseRepo: 'escolher outro repositório',
+        or: 'ou',
+        addNew: 'adicionar um novo branch',
+        protectedBranch: 'Branch protegida',
+        nameRequired: 'Nome da branch é obrigatório',
+        nameExists: 'Nome da branch já existe',
+        refBranch: 'Branch de referência',
+        add: 'Adicionar branch',
+        cancel: 'Cancelar',
+        name: 'Nome da branch',
     },
     threatmodelSelect: {
         select: 'Selecion a Threat Model from',
diff --git a/td.vue/src/i18n/ru.js b/td.vue/src/i18n/ru.js
index ffcc1a21..aac49444 100644
--- a/td.vue/src/i18n/ru.js
+++ b/td.vue/src/i18n/ru.js
@@ -83,7 +83,16 @@ const rus = {
     branch: {
         select: 'Select a branch from',
         from: 'from the list below or',
-        chooseRepo: 'choose another repo'
+        chooseRepo: 'choose another repo',
+        or: 'or',
+        addNew: 'add a new branch',
+        protectedBranch: 'Protected branch',
+        nameRequired: 'Branch name is required',
+        nameExists: 'Branch name already exists',
+        refBranch: 'Reference branch',
+        add: 'add branch',
+        cancel: 'Cancel',
+        name: 'branch name',
     },
     threatmodelSelect: {
         select: 'Select a Threat Model from',
diff --git a/td.vue/src/i18n/uk.js b/td.vue/src/i18n/uk.js
index 26c430e2..267bd8b1 100644
--- a/td.vue/src/i18n/uk.js
+++ b/td.vue/src/i18n/uk.js
@@ -83,7 +83,16 @@ const ukr = {
     branch: {
         select: 'Select a branch from',
         from: 'from the list below or',
-        chooseRepo: 'choose another repo'
+        chooseRepo: 'choose another repo',
+        or: 'or',
+        addNew: 'add a new branch',
+        protectedBranch: 'Protected branch',
+        nameRequired: 'Branch name is required',
+        nameExists: 'Branch name already exists',
+        refBranch: 'Reference branch',
+        add: 'add branch',
+        cancel: 'Cancel',
+        name: 'branch name',
     },
     threatmodelSelect: {
         select: 'Select a Threat Model from',
diff --git a/td.vue/src/i18n/zh.js b/td.vue/src/i18n/zh.js
index fe749793..52dc456e 100644
--- a/td.vue/src/i18n/zh.js
+++ b/td.vue/src/i18n/zh.js
@@ -83,7 +83,16 @@ const zho = {
     branch: {
         select: '选择一个分支',
         from: '从下面列表或',
-        chooseRepo: '选择另一个源'
+        chooseRepo: '选择另一个源',
+        or: '或者',
+        addNew: '添加新分支',
+        protectedBranch: '受保护的分支',
+        nameRequired: '分支名称是必需的',
+        nameExists: '分支名称已存在',
+        refBranch: '参考分支',
+        add: '添加分支',
+        cancel: '取消',
+        name: '分支名称',
     },
     threatmodelSelect: {
         select: '从中选择一个威胁模型',
diff --git a/td.vue/src/plugins/fontawesome-vue.js b/td.vue/src/plugins/fontawesome-vue.js
index 8d95eab6..9c954904 100644
--- a/td.vue/src/plugins/fontawesome-vue.js
+++ b/td.vue/src/plugins/fontawesome-vue.js
@@ -31,10 +31,10 @@ import {
     faPrint,
     faProjectDiagram,
     faDiagramProject,
+    faLock
 } from '@fortawesome/free-solid-svg-icons';
 
 import {faBitbucket, faGithub, faGitlab, faVuejs, faGoogle, faGoogleDrive} from '@fortawesome/free-brands-svg-icons';
-
 // Add icons to the library for use
 library.add(
     faSignOutAlt,
@@ -69,7 +69,8 @@ library.add(
     faProjectDiagram,
     faDiagramProject,
     faGoogle,
-    faGoogleDrive
+    faGoogleDrive,
+    faLock
 );
 
 Vue.component('font-awesome-icon', FontAwesomeIcon);
diff --git a/td.vue/src/service/api/threatmodelApi.js b/td.vue/src/service/api/threatmodelApi.js
index 7261300c..0ee616ad 100644
--- a/td.vue/src/service/api/threatmodelApi.js
+++ b/td.vue/src/service/api/threatmodelApi.js
@@ -65,18 +65,47 @@ const modelAsync = (fullRepoName, branch, model) => {
     return api.getAsync(`${resource}/${encodedOrg}/${encodedRepo}/${encodedBranch}/${encodedModel}/data`);
 };
 
+/**
+ * create Model
+ * @param fullRepoName
+ * @param branch
+ * @param modelName
+ * @param threatModel
+ * @returns {Promise<*>}
+ */
 const createAsync = (fullRepoName, branch, modelName, threatModel) => {
     const { org, repo } = extractRepoParts(fullRepoName);
     const [ encodedOrg, encodedRepo, encodedBranch, encodedModelName ] = encodeUrlComponents(org, repo, branch, modelName);
     return api.postAsync(`${resource}/${encodedOrg}/${encodedRepo}/${encodedBranch}/${encodedModelName}/create`, threatModel);
 };
 
+/**
+ * Updates the given model
+ * @param fullRepoName
+ * @param branch
+ * @param modelName
+ * @param threatModel
+ * @returns {Promise<*>}
+ */
 const updateAsync = (fullRepoName, branch, modelName, threatModel) => {
     const { org, repo } = extractRepoParts(fullRepoName);
     const [ encodedOrg, encodedRepo, encodedBranch, encodedModelName ] = encodeUrlComponents(org, repo, branch, modelName);
     return api.putAsync(`${resource}/${encodedOrg}/${encodedRepo}/${encodedBranch}/${encodedModelName}/update`, threatModel);
 };
 
+/**
+ * Creates a new branch on the given repository
+ * @param fullRepoName
+ * @param branchName
+ * @param refBranch - the branch to base the new branch on
+ * @returns {Promise<*>}
+ */
+const createBranchAsync = (fullRepoName, branchName, refBranch) => {
+    const { org, repo } = extractRepoParts(fullRepoName);
+    const [ encodedOrg, encodedRepo, encodedBranchName, encodedRefBranch ] = encodeUrlComponents(org, repo, branchName, refBranch);
+    return api.postAsync(`${resource}/${encodedOrg}/${encodedRepo}/${encodedBranchName}/createBranch`, { refBranch: encodedRefBranch });
+};
+
 export default {
     branchesAsync,
     createAsync,
@@ -84,5 +113,6 @@ export default {
     modelsAsync,
     organisationAsync,
     reposAsync,
-    updateAsync
+    updateAsync,
+    createBranchAsync
 };
diff --git a/td.vue/src/store/actions/branch.js b/td.vue/src/store/actions/branch.js
index 5f39766c..dbe25e84 100644
--- a/td.vue/src/store/actions/branch.js
+++ b/td.vue/src/store/actions/branch.js
@@ -1,9 +1,11 @@
 export const BRANCH_CLEAR = 'BRANCH_CLEAR';
 export const BRANCH_FETCH = 'BRANCH_FETCH';
 export const BRANCH_SELECTED = 'BRANCH_SELECTED';
+export const BRANCH_CREATE = 'BRANCH_CREATE';
 
 export default {
     clear: BRANCH_CLEAR,
     fetch: BRANCH_FETCH,
-    selected: BRANCH_SELECTED
+    selected: BRANCH_SELECTED,
+    create: BRANCH_CREATE
 };
diff --git a/td.vue/src/store/modules/branch.js b/td.vue/src/store/modules/branch.js
index 8d28d997..dee768d6 100644
--- a/td.vue/src/store/modules/branch.js
+++ b/td.vue/src/store/modules/branch.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
 
 import {
     BRANCH_CLEAR,
+    BRANCH_CREATE,
     BRANCH_FETCH,
     BRANCH_SELECTED
 } from '../actions/branch.js';
@@ -28,14 +29,18 @@ const actions = {
     [BRANCH_FETCH]: async ({ commit, dispatch, rootState }, page = 1) => {
         dispatch(BRANCH_CLEAR);
         const resp = await threatmodelApi.branchesAsync(rootState.repo.selected, page);
-        commit(BRANCH_FETCH, { 
+        commit(BRANCH_FETCH, {
             'branches': resp.data.branches,
             'page': resp.data.pagination.page,
             'pageNext': resp.data.pagination.next,
             'pagePrev': resp.data.pagination.prev
         });
     },
-    [BRANCH_SELECTED]: ({ commit }, branch) => commit(BRANCH_SELECTED, branch)
+    [BRANCH_SELECTED]: ({ commit }, branch) => commit(BRANCH_SELECTED, branch),
+    [BRANCH_CREATE]: async ({ dispatch, rootState }, {branchName, refBranch}) => {
+        await threatmodelApi.createBranchAsync(rootState.repo.selected, branchName, refBranch);
+        await dispatch(BRANCH_FETCH);
+    }
 };
 
 const mutations = {
diff --git a/td.vue/src/views/git/BranchAccess.vue b/td.vue/src/views/git/BranchAccess.vue
index 3fdfb53f..986278c4 100644
--- a/td.vue/src/views/git/BranchAccess.vue
+++ b/td.vue/src/views/git/BranchAccess.vue
@@ -19,6 +19,13 @@
         <a href="javascript:void(0)" id="return-to-repo" @click="selectRepoClick">
             {{ $t('branch.chooseRepo') }}
         </a>
+        {{ $t('branch.or') }}
+        <a href="javascript:void(0)" id="new-branch" @click="toggleNewBranchDialog()">{{ $t('branch.addNew') }}</a>
+
+        <add-branch-modal
+            v-if="showNewBranchDialog"
+            :branches="branches"
+            @close-dialog="toggleNewBranchDialog()"/>
     </td-selection-page>
 </template>
 
@@ -30,21 +37,37 @@ import { getProviderType } from '@/service/provider/providers.js';
 import providerActions from '@/store/actions/provider.js';
 import repoActions from '@/store/actions/repository.js';
 import TdSelectionPage from '@/components/SelectionPage.vue';
+import AddBranchModal from '@/components/AddBranchDialog.vue';
 
 export default {
     name: 'BranchAccess',
     components: {
+        AddBranchModal,
         TdSelectionPage
     },
+    data() {
+        return {
+            showNewBranchDialog: false,
+        };
+    },
     computed: mapState({
-        branches: (state) => state.branch.all,
+        branches: (state) => state.branch.all.map((branch) => {
+            if(branch['protected']){
+                return {
+                    value: branch.name,
+                    icon: 'lock',
+                    iconTooltip: 'branch.protectedBranch',
+                };
+            }
+            return branch.name;
+        }),
         provider: (state) => state.provider.selected,
         providerType: (state) => getProviderType(state.provider.selected),
         providerUri: (state) => state.provider.providerUri,
         repoName: (state) => state.repo.selected,
         page: (state) => Number(state.branch.page),
         pageNext: (state) => state.branch.pageNext,
-        pagePrev: (state) => state.branch.pagePrev
+        pagePrev: (state) => state.branch.pagePrev,
     }),
     mounted() {
         if (this.provider !== this.$route.params.provider) {
@@ -74,6 +97,9 @@ export default {
         },
         paginate(page) {
             this.$store.dispatch(branchActions.fetch, page);
+        },
+        toggleNewBranchDialog(){
+            this.showNewBranchDialog = !this.showNewBranchDialog;
         }
     }
 };


Test patch:
diff --git a/td.server/test/repositories/bitbucketrepo.spec.js b/td.server/test/repositories/bitbucketrepo.spec.js
index 4c045802..9cd9bef3 100644
--- a/td.server/test/repositories/bitbucketrepo.spec.js
+++ b/td.server/test/repositories/bitbucketrepo.spec.js
@@ -9,7 +9,7 @@ import {getClient, userAsync} from '../../src/repositories/bitbucketrepo.js';
 
 describe('repositories/bitbucketrepo.js', () => {
     const workspace = 'threat-workspace';
-    const repoPath = 'ThreatDragonModels'
+    const repoPath = 'ThreatDragonModels';
 
     const info = {
         body: {
@@ -19,6 +19,8 @@ describe('repositories/bitbucketrepo.js', () => {
         branch: 'testBranch',
         organisation: 'test org',
         page: 'testPage',
+        repo: 'test repo',
+        ref: 'testRef',
         listBranches: {
             workspace: workspace,
             repo_slug: 'repoInfo.repo',
@@ -68,7 +70,8 @@ describe('repositories/bitbucketrepo.js', () => {
                     values: [
                         {full_name: 'Threat-Workspace/Repo1'},
                         {full_name: 'Threat-Workspace/Repo2'},
-                        {full_name: 'Threat-Workspace/Repo3'},]
+                        {full_name: 'Threat-Workspace/Repo3'},
+]
                 }
             })),
             getBranch: sinon.stub().returns(Promise.resolve({data: {target: {hash: info.readInfo.commit}}})),
@@ -82,6 +85,9 @@ describe('repositories/bitbucketrepo.js', () => {
         users: {
             getAuthedUser: sinon.stub().returns(Promise.resolve({data: {id: 1, username: 'Test User'}})),
         },
+        refs:{
+            createBranch: sinon.stub().returns(Promise.resolve({data: {values: []}})),
+        }
     };
 
     const clientOptions = {
@@ -124,7 +130,7 @@ describe('repositories/bitbucketrepo.js', () => {
         });
 
         it('creates the bitbucket client', () => {
-            threatModelRepository.userAsync(accessToken)
+            threatModelRepository.userAsync(accessToken);
             sinon.stub(env, 'get').returns({config: {BITBUCKET_WORKSPACE: workspace}});
             expect(BitbucketClientWrapper.getClient).to.have.been.calledWith(clientOptions);
 
@@ -146,7 +152,7 @@ describe('repositories/bitbucketrepo.js', () => {
         });
 
         // Setup the transformed data
-        let transformedReposData = [
+        const transformedReposData = [
             {full_name: 'repo1'},
             {full_name: 'repo2'},
             {full_name: 'repo3'},
@@ -163,7 +169,7 @@ describe('repositories/bitbucketrepo.js', () => {
     });
 
     describe('branchesAsync', () => {
-        let repoInfo = {page: info.listBranches.page, repo: info.listBranches.repo_slug}
+        const repoInfo = {page: info.listBranches.page, repo: info.listBranches.repo_slug};
 
         beforeEach(async () => {
             sinon.stub(env, 'get').returns({config: {BITBUCKET_WORKSPACE: workspace}});
@@ -181,13 +187,15 @@ describe('repositories/bitbucketrepo.js', () => {
     });
 
     describe('modelsAsync', () => {
-        let branchInfo = {branch: info.branchInfo.name, repo: info.branchInfo.repo_slug};
+        const branchInfo = {branch: info.branchInfo.name, repo: info.branchInfo.repo_slug};
 
         beforeEach(async () => {
             sinon.stub(env, 'get').returns({config: {BITBUCKET_WORKSPACE: workspace, BITBUCKET_REPO_ROOT_DIRECTORY: repoPath}});
-            sinon.stub(mockClient.source, 'read').returns(Promise.resolve({data: {values: [{path: 'ThreatDragonModels/model1'},
+            sinon.stub(mockClient.source, 'read').returns(Promise.resolve({data: {values: [
+{path: 'ThreatDragonModels/model1'},
                         {path: 'ThreatDragonModels/model2'},
-                        {path: 'ThreatDragonModels/model3'}]}}));
+                        {path: 'ThreatDragonModels/model3'}
+]}}));
             await threatModelRepository.modelsAsync(branchInfo, accessToken);
         });
 
@@ -222,7 +230,7 @@ describe('repositories/bitbucketrepo.js', () => {
     });
 
     describe('modelAsync', () => {
-        let modelInfo = {branch: info.modelInfo.name, repo: info.modelInfo.repo_slug, model: info.modelInfo.model};
+        const modelInfo = {branch: info.modelInfo.name, repo: info.modelInfo.repo_slug, model: info.modelInfo.model};
 
         beforeEach(async () => {
             sinon.stub(env, 'get').returns({config: {BITBUCKET_WORKSPACE: workspace, BITBUCKET_REPO_ROOT_DIRECTORY: repoPath}});
@@ -245,14 +253,14 @@ describe('repositories/bitbucketrepo.js', () => {
     });
 
     describe('createAsync', () => {
-        let createAysncInfo = {
+        const createAysncInfo = {
             branch: info.createAsync.name,
             repo: info.createAsync.repo_slug,
             model: info.createAsync.model,
             body: info.createAsync.body
         };
 
-        let createFileCommitInfo = {
+        const createFileCommitInfo = {
             'ThreatDragonModels/my model/my model.json': JSON.stringify(createAysncInfo.body, null, '  '),
             repo_slug: createAysncInfo.repo,
             files: 'ThreatDragonModels/my model/my model.json',
@@ -280,14 +288,14 @@ describe('repositories/bitbucketrepo.js', () => {
 
     describe('updateAsync', () => {
 
-        let updateAysncInfo = {
+        const updateAysncInfo = {
             branch: info.createAsync.name,
             repo: info.createAsync.repo_slug,
             model: info.createAsync.model,
             body: info.createAsync.body
         };
 
-        let updateFileCommitInfo = {
+        const updateFileCommitInfo = {
             'ThreatDragonModels/my model/my model.json': JSON.stringify(updateAysncInfo.body, null, '  '),
             repo_slug: updateAysncInfo.repo,
             files: 'ThreatDragonModels/my model/my model.json',
@@ -324,6 +332,21 @@ describe('repositories/bitbucketrepo.js', () => {
 
     });
 
+    describe('create branch', () => {
+        beforeEach(async () => {
+            sinon.stub(env, 'get').returns({config: {BITBUCKET_WORKSPACE: workspace, BITBUCKET_REPO_ROOT_DIRECTORY: repoPath}});
+            await threatModelRepository.createBranchAsync(info, accessToken);
+        });
+        it('creates a new branch', () => {
+            expect(mockClient.refs.createBranch).to.have.been.calledWith(
+                {
+                    _body: { name: info.branch, target: { hash: info.ref } },
+                    repo_slug: info.repo,
+                    workspace: workspace
+                }
+            );
+        });
+    });
 
 });
 
diff --git a/td.server/test/repositories/githubrepo.spec.js b/td.server/test/repositories/githubrepo.spec.js
index adf6d675..d184e726 100644
--- a/td.server/test/repositories/githubrepo.spec.js
+++ b/td.server/test/repositories/githubrepo.spec.js
@@ -1,4 +1,4 @@
-import { expect } from 'chai';
+import {expect} from 'chai';
 import github from 'octonode';
 import sinon from 'sinon';
 
@@ -6,271 +6,297 @@ import env from '../../src/env/Env.js';
 import threatModelRepository from '../../src/repositories/githubrepo.js';
 
 describe('repositories/githubrepo.js', () => {
-    const info = {
-        body: {
-            content: 'test content',
-            id: 1
-        },
-        branch: 'testBranch',
-        organisation: 'test org',
-        page: 'testPage',
-        repo: 'test repo'
-    };
-    const content = [{
-        sha: 'asdf'
-    }];
-    const accessToken = 'access token';
-    const repoFullName = `${info.organisation}/${info.repo}`;
-    const modelContent = JSON.stringify(info.body, null, '  ');
-    const modelPath = `ThreatDragonModels/${info.model}/${info.model}.json`;
-    
-    const mockClient = {
-        me: () => {},
-        infoAsync: () => Promise.resolve([{}, {}]),
-        reposAsync: () => {},
-        repo: () => {}
-    };
-    
-    const mockRepo = {
-        branchesAsync: () => {},
-        contentsAsync: () => {},
-        createContentsAsync: () => {},
-        deleteContentsAsync: () => {},
-        updateContentsAsync: () => {}
-    };
-
-    beforeEach(() => {
-        sinon.stub(github, 'client').returns(mockClient);
-        sinon.stub(mockClient, 'repo').returns(mockRepo);
-        sinon.stub(mockRepo, 'contentsAsync').resolves(content);
-    });
-
-    describe('userAsync', () => {
-        beforeEach(async () => {
-            sinon.stub(mockClient, 'me').returns(mockClient);
-            sinon.spy(mockClient, 'infoAsync');
-            await threatModelRepository.userAsync(accessToken);
-        });
-
-        it('creates the github client', () => {
-            expect(github.client).to.have.been.calledWith(accessToken);
-        });
-
-        it('calls me', () => {
-            expect(mockClient.me).to.have.been.calledOnce;
-        });
-
-        it('calls infoAsync', () => {
-            expect(mockClient.infoAsync).to.have.been.calledOnce;
-        });
-    });
-
-        describe('with enterprise hostname', () => {
-        const enterpriseHostname = 'fakehost';
-
-        beforeEach(async () => {
-            sinon.stub(env, 'get').returns({ config: { GITHUB_ENTERPRISE_HOSTNAME: enterpriseHostname }});
-            sinon.stub(mockClient, 'me').returns(mockClient);
-            await threatModelRepository.reposAsync(info.page, accessToken);
-        });
-
-        it('creates the github client with the enterprise hostname', () => {
-            expect(github.client).to.have.been.calledWith(accessToken, { hostname: `${enterpriseHostname}/api/v3` });
-        });
-    });
-
-    describe('with enterprise hostname and port', () => {
-        const enterpriseHostname = 'fakehost';
-        const enterprisePort = '8443';
-
-        beforeEach(async () => {
-            sinon.stub(env, 'get').returns({ config: {
-                GITHUB_ENTERPRISE_HOSTNAME: enterpriseHostname,
-                GITHUB_ENTERPRISE_PORT: enterprisePort
-            }});
-            sinon.stub(mockClient, 'me').returns(mockClient);
-            await threatModelRepository.reposAsync(info.page, accessToken);
-        });
-
-        it('creates the github client with the enterprise hostname and port', () => {
-            expect(github.client).to.have.been.calledWith(accessToken, {
-                hostname: `${enterpriseHostname}/api/v3`,
-                port: 8443
-            });
-        });
-    });
-
-    describe('with enterprise hostname, port, and protocol', () => {
-        const enterpriseHostname = 'fakehost';
-        const enterprisePort = '8443';
-        const enterpriseProtocol = 'https';
-
-        beforeEach(async () => {
-            sinon.stub(env, 'get').returns({ config: {
-                GITHUB_ENTERPRISE_HOSTNAME: enterpriseHostname,
-                GITHUB_ENTERPRISE_PORT: enterprisePort,
-                GITHUB_ENTERPRISE_PROTOCOL: enterpriseProtocol
-            }});
-            sinon.stub(mockClient, 'me').returns(mockClient);
-            await threatModelRepository.reposAsync(info.page, accessToken);
-        });
-
-        it('creates the github client with the enterprise hostname, port and protocol', () => {
-            expect(github.client).to.have.been.calledWith(accessToken, {
-                hostname: `${enterpriseHostname}/api/v3`,
-                port: 8443,
-                protocol: enterpriseProtocol
-            });
-        });
-    });
-
-    describe('reposAsync', () => {
-        beforeEach(async () => {
-            sinon.stub(mockClient, 'me').returns(mockClient);
-            sinon.spy(mockClient, 'reposAsync');
-            await threatModelRepository.reposAsync(info.page, accessToken);
-        });
-
-        it('creates the github client', () => {
-            expect(github.client).to.have.been.calledWith(accessToken);
-        });
-
-        it('calls me', () => {
-            expect(mockClient.me).to.have.been.calledOnce;
-        });
-
-        it('calls reposAsync', () => {
-            expect(mockClient.reposAsync).to.have.been.calledWith(info.page);
-        });
-    });
-    
-    describe('branchesAsync', () => {
-        beforeEach(async () => {
-            sinon.stub(mockRepo, 'branchesAsync').resolves();
-            await threatModelRepository.branchesAsync(info, accessToken);
-        });
-
-        it('creates the client', () => {
-            expect(github.client).to.have.been.calledWith(accessToken);
-        });
-
-        it('gets the repo', () => {
-            expect(mockClient.repo).to.have.been.calledWith(repoFullName);
-        });
-
-        it('gets the branches for the repo', () => {
-            expect(mockRepo.branchesAsync).to.have.been.calledWith(info.page);
-        });
-    });
-
-    describe('modelsAsync', () => {
-        beforeEach(async () => {
-            await threatModelRepository.modelsAsync(info, accessToken);
-        });
-
-        it('should create the github client', () => {
-            expect(github.client).to.have.been.calledWith(accessToken);
-        });
-
-        it('should get the repo', () => {
-            expect(mockClient.repo).to.have.been.calledWith(repoFullName);
-        });
-
-        it('should get the contents', () => {
-            expect(mockRepo.contentsAsync).to.have.been
-                .calledWith('ThreatDragonModels', info.branch);
-        });
-    });
-    
-    describe('modelAsync', () => {
-        beforeEach(async () => {
-            await threatModelRepository.modelAsync(info, accessToken);
-        });
-
-        it('should create the github client', () => {
-            expect(github.client).to.have.been.calledWith(accessToken);
-        });
-
-        it('should get the repo', () => {
-            expect(mockClient.repo).to.have.been.calledWith(repoFullName);
-        });
-
-        it('should get the contents', () => {
-            expect(mockRepo.contentsAsync).to.have.been.calledWith(modelPath, info.branch);
-        });
-    });
-
-    describe('createAsync', () => {
-        beforeEach(async () => {
-            sinon.stub(mockRepo, 'createContentsAsync').resolves();
-            await threatModelRepository.createAsync(info, accessToken);
-        });
-
-        it('should create the github client', () => {
-            expect(github.client).to.have.been.calledWith(accessToken);
-        });
-
-        it('should get the repo', () => {
-            expect(mockClient.repo).to.have.been.calledWith(repoFullName);
-        });
-
-        it('should get the contents', () => {
-            expect(mockRepo.createContentsAsync).to.have.been.calledWith(
-                modelPath,
-                'Created by OWASP Threat Dragon',
-                modelContent,
-                info.branch
-            );
-        });
-    });
-
-    describe('updateAsync', () => {
-        beforeEach(async () => {
-            sinon.stub(mockRepo, 'updateContentsAsync');
-            await threatModelRepository.updateAsync(info, accessToken);
-        });
-
-        it('creates a github client', () => {
-            expect(github.client).to.have.been.calledWith(accessToken);
-        });
-
-        it('calls client.repo', () => {
-            expect(mockClient.repo).to.have.been.calledWith(`${info.organisation}/${info.repo}`);
-        });
-
-        it('calls repo.updateContents', () => {
-            expect(mockRepo.updateContentsAsync).to.have.been.calledWith(
-                modelPath,
-                'Updated by OWASP Threat Dragon',
-                JSON.stringify(info.body, null, '  '),
-                content[0].sha,
-                info.branch
-            );
-        });
-    });
-
-
-    describe('deleteAsync', () => {
-        beforeEach(async () => {
-            sinon.stub(mockRepo, 'deleteContentsAsync').resolves();
-            await threatModelRepository.deleteAsync(info, accessToken);
-        });
-
-        it('creates a github client', () => {
-            expect(github.client).to.have.been.calledWith(accessToken);
-        });
-
-        it('calls client.repo', () => {
-            expect(mockClient.repo).to.have.been.calledWith(`${info.organisation}/${info.repo}`);
-        });
-
-        it('calls repo.deleteContents', () => {
-            expect(mockRepo.deleteContentsAsync).to.have.been.calledWith(
-                modelPath,
-                'Deleted by OWASP Threat Dragon',
-                content[0].sha,
-                info.branch
-            );
-        });
+  const info = {
+    body: {
+      content: 'test content',
+      id: 1
+    },
+    branch: 'testBranch',
+    organisation: 'test org',
+    page: 'testPage',
+    repo: 'test repo'
+  };
+  const content = [
+    {
+      sha: 'asdf'
+    }
+  ];
+  const accessToken = 'access token';
+  const repoFullName = `${info.organisation}/${info.repo}`;
+  const modelContent = JSON.stringify(info.body, null, '  ');
+  const modelPath = `ThreatDragonModels/${info.model}/${info.model}.json`;
+
+  const mockClient = {
+    me: () => {
+    },
+    infoAsync: () => Promise.resolve([{}, {}]),
+    reposAsync: () => {
+    },
+    repo: () => {
+    }
+  };
+
+  const mockRepo = {
+    branchesAsync: () => {
+    },
+    contentsAsync: () => {
+    },
+    createContentsAsync: () => {
+    },
+    deleteContentsAsync: () => {
+    },
+    updateContentsAsync: () => {
+    },
+    createRefAsync: () => {
+    },
+    refAsync: () => [{object: content[0]}]
+  };
+
+  beforeEach(() => {
+    sinon.stub(github, 'client').returns(mockClient);
+    sinon.stub(mockClient, 'repo').returns(mockRepo);
+    sinon.stub(mockRepo, 'contentsAsync').resolves(content);
+  });
+
+  describe('userAsync', () => {
+    beforeEach(async () => {
+      sinon.stub(mockClient, 'me').returns(mockClient);
+      sinon.spy(mockClient, 'infoAsync');
+      await threatModelRepository.userAsync(accessToken);
     });
+
+    it('creates the github client', () => {
+      expect(github.client).to.have.been.calledWith(accessToken);
+    });
+
+    it('calls me', () => {
+      expect(mockClient.me).to.have.been.calledOnce;
+    });
+
+    it('calls infoAsync', () => {
+      expect(mockClient.infoAsync).to.have.been.calledOnce;
+    });
+  });
+
+  describe('with enterprise hostname', () => {
+    const enterpriseHostname = 'fakehost';
+
+    beforeEach(async () => {
+      sinon.stub(env, 'get').returns({config: {GITHUB_ENTERPRISE_HOSTNAME: enterpriseHostname}});
+      sinon.stub(mockClient, 'me').returns(mockClient);
+      await threatModelRepository.reposAsync(info.page, accessToken);
+    });
+
+    it('creates the github client with the enterprise hostname', () => {
+      expect(github.client).to.have.been.calledWith(accessToken, {hostname: `${enterpriseHostname}/api/v3`});
+    });
+  });
+
+  describe('with enterprise hostname and port', () => {
+    const enterpriseHostname = 'fakehost';
+    const enterprisePort = '8443';
+
+    beforeEach(async () => {
+      sinon.stub(env, 'get').returns({
+        config: {
+          GITHUB_ENTERPRISE_HOSTNAME: enterpriseHostname,
+          GITHUB_ENTERPRISE_PORT: enterprisePort
+        }
+      });
+      sinon.stub(mockClient, 'me').returns(mockClient);
+      await threatModelRepository.reposAsync(info.page, accessToken);
+    });
+
+    it('creates the github client with the enterprise hostname and port', () => {
+      expect(github.client).to.have.been.calledWith(accessToken, {
+        hostname: `${enterpriseHostname}/api/v3`,
+        port: 8443
+      });
+    });
+  });
+
+  describe('with enterprise hostname, port, and protocol', () => {
+    const enterpriseHostname = 'fakehost';
+    const enterprisePort = '8443';
+    const enterpriseProtocol = 'https';
+
+    beforeEach(async () => {
+      sinon.stub(env, 'get').returns({
+        config: {
+          GITHUB_ENTERPRISE_HOSTNAME: enterpriseHostname,
+          GITHUB_ENTERPRISE_PORT: enterprisePort,
+          GITHUB_ENTERPRISE_PROTOCOL: enterpriseProtocol
+        }
+      });
+      sinon.stub(mockClient, 'me').returns(mockClient);
+      await threatModelRepository.reposAsync(info.page, accessToken);
+    });
+
+    it('creates the github client with the enterprise hostname, port and protocol', () => {
+      expect(github.client).to.have.been.calledWith(accessToken, {
+        hostname: `${enterpriseHostname}/api/v3`,
+        port: 8443,
+        protocol: enterpriseProtocol
+      });
+    });
+  });
+
+  describe('reposAsync', () => {
+    beforeEach(async () => {
+      sinon.stub(mockClient, 'me').returns(mockClient);
+      sinon.spy(mockClient, 'reposAsync');
+      await threatModelRepository.reposAsync(info.page, accessToken);
+    });
+
+    it('creates the github client', () => {
+      expect(github.client).to.have.been.calledWith(accessToken);
+    });
+
+    it('calls me', () => {
+      expect(mockClient.me).to.have.been.calledOnce;
+    });
+
+    it('calls reposAsync', () => {
+      expect(mockClient.reposAsync).to.have.been.calledWith(info.page);
+    });
+  });
+
+  describe('branchesAsync', () => {
+    beforeEach(async () => {
+      sinon.stub(mockRepo, 'branchesAsync').resolves();
+      await threatModelRepository.branchesAsync(info, accessToken);
+    });
+
+    it('creates the client', () => {
+      expect(github.client).to.have.been.calledWith(accessToken);
+    });
+
+    it('gets the repo', () => {
+      expect(mockClient.repo).to.have.been.calledWith(repoFullName);
+    });
+
+    it('gets the branches for the repo', () => {
+      expect(mockRepo.branchesAsync).to.have.been.calledWith(info.page);
+    });
+  });
+
+  describe('modelsAsync', () => {
+    beforeEach(async () => {
+      await threatModelRepository.modelsAsync(info, accessToken);
+    });
+
+    it('should create the github client', () => {
+      expect(github.client).to.have.been.calledWith(accessToken);
+    });
+
+    it('should get the repo', () => {
+      expect(mockClient.repo).to.have.been.calledWith(repoFullName);
+    });
+
+    it('should get the contents', () => {
+      expect(mockRepo.contentsAsync).to.have.been.calledWith('ThreatDragonModels', info.branch);
+    });
+  });
+
+  describe('modelAsync', () => {
+    beforeEach(async () => {
+      await threatModelRepository.modelAsync(info, accessToken);
+    });
+
+    it('should create the github client', () => {
+      expect(github.client).to.have.been.calledWith(accessToken);
+    });
+
+    it('should get the repo', () => {
+      expect(mockClient.repo).to.have.been.calledWith(repoFullName);
+    });
+
+    it('should get the contents', () => {
+      expect(mockRepo.contentsAsync).to.have.been.calledWith(modelPath, info.branch);
+    });
+  });
+
+  describe('createAsync', () => {
+    beforeEach(async () => {
+      sinon.stub(mockRepo, 'createContentsAsync').resolves();
+      await threatModelRepository.createAsync(info, accessToken);
+    });
+
+    it('should create the github client', () => {
+      expect(github.client).to.have.been.calledWith(accessToken);
+    });
+
+    it('should get the repo', () => {
+      expect(mockClient.repo).to.have.been.calledWith(repoFullName);
+    });
+
+    it('should get the contents', () => {
+      expect(mockRepo.createContentsAsync).to.have.been.calledWith(
+        modelPath,
+        'Created by OWASP Threat Dragon',
+        modelContent,
+        info.branch
+      );
+    });
+  });
+
+  describe('updateAsync', () => {
+    beforeEach(async () => {
+      sinon.stub(mockRepo, 'updateContentsAsync');
+      await threatModelRepository.updateAsync(info, accessToken);
+    });
+
+    it('creates a github client', () => {
+      expect(github.client).to.have.been.calledWith(accessToken);
+    });
+
+    it('calls client.repo', () => {
+      expect(mockClient.repo).to.have.been.calledWith(`${info.organisation}/${info.repo}`);
+    });
+
+    it('calls repo.updateContents', () => {
+      expect(mockRepo.updateContentsAsync).to.have.been.calledWith(
+        modelPath,
+        'Updated by OWASP Threat Dragon',
+        JSON.stringify(info.body, null, '  '),
+        content[0].sha,
+        info.branch
+      );
+    });
+  });
+
+
+  describe('deleteAsync', () => {
+    beforeEach(async () => {
+      sinon.stub(mockRepo, 'deleteContentsAsync').resolves();
+      await threatModelRepository.deleteAsync(info, accessToken);
+    });
+
+    it('creates a github client', () => {
+      expect(github.client).to.have.been.calledWith(accessToken);
+    });
+
+    it('calls client.repo', () => {
+      expect(mockClient.repo).to.have.been.calledWith(`${info.organisation}/${info.repo}`);
+    });
+
+    it('calls repo.deleteContents', () => {
+      expect(mockRepo.deleteContentsAsync).to.have.been.calledWith(
+        modelPath,
+        'Deleted by OWASP Threat Dragon',
+        content[0].sha,
+        info.branch
+      );
+    });
+  });
+
+  describe('create branch', () => {
+    beforeEach(async () => {
+      sinon.stub(mockRepo, 'createRefAsync').resolves();
+      await threatModelRepository.createBranchAsync(info, accessToken);
+    });
+    it('create a new branch', () => {
+      expect(mockRepo.createRefAsync).to.have.been.calledWith(`refs/heads/${info.branch}`, content[0].sha);
+    });
+  });
 });
diff --git a/td.server/test/repositories/gitlabrepo.spec.js b/td.server/test/repositories/gitlabrepo.spec.js
index 764411f6..2333f96c 100644
--- a/td.server/test/repositories/gitlabrepo.spec.js
+++ b/td.server/test/repositories/gitlabrepo.spec.js
@@ -9,19 +9,19 @@ import {getClient, userAsync} from '../../src/repositories/gitlabrepo.js';
 
 describe('repositories/gitlabrepo.js', () => {
     const workspace = 'threat-workspace';
-    const repoPath = 'ThreatDragonModels'
+    const repoPath = 'ThreatDragonModels';
 
     const info = {
         body: {
             content: 'test content',
             id: 1
         },
+        ref: 'main',
         branch: 'testBranch',
         organisation: 'test org',
         page: 'testPage',
-        listBranches: { page: 'repoInfo.page', showExpanded: true }
-        ,
-        branchInfo: { path: repoPath, ref: 'main' } ,
+        listBranches: { page: 'repoInfo.page', showExpanded: true },        
+        branchInfo: { path: repoPath, ref: 'main' },
         readInfo: {
             path: repoPath,
             workspace: workspace,
@@ -63,7 +63,8 @@ describe('repositories/gitlabrepo.js', () => {
                         [
                             {path_with_namespace: 'Threat-Workspace/Repo1'},
                             {path_with_namespace: 'Threat-Workspace/Repo2'},
-                            {path_with_namespace: 'Threat-Workspace/Repo3'},],
+                            {path_with_namespace: 'Threat-Workspace/Repo3'},
+],
                     paginationInfo: {next: null, previous: null}
                 }
             )),
@@ -75,6 +76,9 @@ describe('repositories/gitlabrepo.js', () => {
                 data: [],
                 paginationInfo: {next: 3, previous: 1}
 
+            })),
+            create: sinon.stub().returns(Promise.resolve({
+
             })),
         },
         Repositories : {
@@ -140,7 +144,7 @@ describe('repositories/gitlabrepo.js', () => {
         });
 
         it('creates the gitlab client', () => {
-             threatModelRepository.userAsync(accessToken)
+             threatModelRepository.userAsync(accessToken);
             sinon.stub(env, 'get').returns({config: {}});
             expect(GitlabClientWrapper.getClient).to.have.been.calledWith(clientOptions.auth);
 
@@ -168,7 +172,7 @@ describe('repositories/gitlabrepo.js', () => {
         });
 
         // Setup the transformed data
-        let transformedReposData = [
+        const transformedReposData = [
             {full_name: 'Threat-Workspace/Repo1', path_with_namespace: 'Threat-Workspace/Repo1'},
             {full_name: 'Threat-Workspace/Repo2', path_with_namespace: 'Threat-Workspace/Repo2'},
             {full_name: 'Threat-Workspace/Repo3', path_with_namespace: 'Threat-Workspace/Repo3'},
@@ -185,7 +189,7 @@ describe('repositories/gitlabrepo.js', () => {
     });
 
     describe('branchesAsync', () => {
-        let repoInfo = {page: info.listBranches.page, repo: "repo", organisation: "org"}
+        const repoInfo = {page: info.listBranches.page, repo: "repo", organisation: "org"};
         beforeEach(async () => {
             sinon.stub(env, 'get').returns({config: {GITLAB_WORKSPACE: workspace}});
             await threatModelRepository.branchesAsync(repoInfo, accessToken);
@@ -202,7 +206,7 @@ describe('repositories/gitlabrepo.js', () => {
     });
 
     describe('modelsAsync', () => {
-        let branchInfo = {branch: "main", repo: "repo"};
+        const branchInfo = {branch: "main", repo: "repo"};
 
         beforeEach(async () => {
             sinon.stub(env, 'get').returns({config: {GITLAB_WORKSPACE: workspace, GITLAB_REPO_ROOT_DIRECTORY: repoPath}});
@@ -219,7 +223,7 @@ describe('repositories/gitlabrepo.js', () => {
     });
 
     describe('modelAsync', () => {
-        let modelInfo = {page: info.listBranches.page, repo: "repo", organisation: "org", model: "model"}
+        const modelInfo = {page: info.listBranches.page, repo: "repo", organisation: "org", model: "model"};
 
         beforeEach(async () => {
             sinon.stub(env, 'get').returns({config: {GITLAB_WORKSPACE: workspace, GITLAB_REPO_ROOT_DIRECTORY: repoPath}});
@@ -237,7 +241,7 @@ describe('repositories/gitlabrepo.js', () => {
     });
 
     describe('createAsync', () => {
-        let createAysncInfo = {
+        const createAysncInfo = {
             branch: info.createAsync.name,
             repo: info.createAsync.repo_slug,
             model: info.createAsync.model,
@@ -269,7 +273,7 @@ describe('repositories/gitlabrepo.js', () => {
 
     describe('updateAsync', () => {
 
-        let updateAysncInfo = {
+        const updateAysncInfo = {
             branch: info.createAsync.name,
             repo: info.createAsync.repo_slug,
             model: info.createAsync.model,
@@ -310,6 +314,13 @@ describe('repositories/gitlabrepo.js', () => {
 
     });
 
-
-});
-
+    describe('create branch', () => {
+        beforeEach(async () => {
+            sinon.stub(env, 'get').returns({config: {GITLAB_WORKSPACE: workspace}});
+            await threatModelRepository.createBranchAsync(info, accessToken);
+        });
+        it('create a new branch', () => {
+            expect(mockClient.Branches.create).to.have.been.calledWith(`${info.organisation}/${info.repo}`, info.branch, info.ref);
+        });
+    });
+});
\ No newline at end of file
diff --git a/td.vue/tests/unit/components/addBranchDialog.spec.js b/td.vue/tests/unit/components/addBranchDialog.spec.js
new file mode 100644
index 00000000..8c96c5be
--- /dev/null
+++ b/td.vue/tests/unit/components/addBranchDialog.spec.js
@@ -0,0 +1,133 @@
+import { BootstrapVue, BModal, BFormInput, BFormSelect, BButton } from 'bootstrap-vue';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import AddBranchDialog from '@/components/AddBranchDialog.vue';
+
+describe('components/AddBranchDialog.vue', () => {
+    let localVue, wrapper;
+
+    beforeEach(() => {
+        localVue = createLocalVue();
+        localVue.use(BootstrapVue);
+    });
+
+    describe('with data', () => {
+        const branches = ['main', 'develop', 'feature'];
+        let closeDialog;
+
+        beforeEach(() => {
+            closeDialog = jest.fn();
+            wrapper = shallowMount(AddBranchDialog, {
+                localVue,
+                propsData: {
+                    branches
+                },
+                mocks: {
+                    $t: key => key
+                }
+            });
+            wrapper.vm.closeDialog = closeDialog;
+        });
+
+        it('displays the modal', () => {
+            expect(wrapper.findComponent(BModal).exists()).toBe(true);
+        });
+
+        it('displays the branch name input', () => {
+            expect(wrapper.findComponent(BFormInput).exists()).toBe(true);
+        });
+
+        it('displays the reference branch select', () => {
+            expect(wrapper.findComponent(BFormSelect).exists()).toBe(true);
+        });
+
+        it('displays the add button', () => {
+            expect(wrapper.findAllComponents(BButton).at(0).text()).toBe('branch.add');
+        });
+
+        it('displays the cancel button', () => {
+            expect(wrapper.findAllComponents(BButton).at(1).text()).toBe('branch.cancel');
+        });
+
+        it('calls closeDialog on cancel button click', async () => {
+            await wrapper.findAllComponents(BButton).at(1).trigger('click');
+            expect(closeDialog).toHaveBeenCalled();
+        });
+    });
+
+    describe('validation', () => {
+        const branches = ['develop', 'feature', 'main'];
+
+        beforeEach(() => {
+            wrapper = shallowMount(AddBranchDialog, {
+                localVue,
+                propsData: {
+                    branches
+                },
+                mocks: {
+                    $t: key => key
+                }
+            });
+        });
+
+        it('shows an error if branch name is empty', async () => {
+            await wrapper.setData({ newBranchName: '' });
+            wrapper.vm.validate();
+            expect(wrapper.vm.branchNameError).toBe('branch.nameRequired');
+        });
+
+        it('shows an error if branch name already exists', async () => {
+            await wrapper.setData({ newBranchName: 'main' });
+            wrapper.vm.validate();
+            expect(wrapper.vm.branchNameError).toBe('branch.nameExists');
+        });
+
+        it('does not show an error if branch name is valid', async () => {
+            await wrapper.setData({ newBranchName: 'new-branch' });
+            wrapper.vm.validate();
+            expect(wrapper.vm.branchNameError).toBe('');
+        });
+    });
+
+    describe('addBranch', () => {
+        let closeDialog, dispatch;
+
+        beforeEach(() => {
+            const branches = ['main', 'develop', 'feature'];
+            closeDialog = jest.fn();
+            dispatch = jest.fn((branchActions, {branchName}) => {
+                if(branchActions === 'BRANCH_CREATE')
+                    branches.push(branchName);
+            });
+            wrapper = shallowMount(AddBranchDialog, {
+                localVue,
+                propsData: {
+                    branches
+                },
+                mocks: {
+                    $t: key => key,
+                    $store: {
+                        dispatch
+                    }
+                },
+                data() {
+                    return {
+                        newBranchName:  'new-branch',
+                        refBranch: 'main'
+                    };
+                }
+            });
+            wrapper.vm.closeDialog = closeDialog;
+        });
+
+        it('dispatches the create action with correct payload', async () => {
+            await wrapper.vm.addBranch();
+            expect(wrapper.vm.branches).toContain('new-branch');
+        });
+
+        it('closes the dialog after adding the branch', async () => {
+            await wrapper.setData({ newBranchName: 'new-branch', refBranch: 'main'});
+            await wrapper.vm.addBranch();
+            expect(closeDialog).toHaveBeenCalled();
+        });
+    });
+});
diff --git a/td.vue/tests/unit/components/selectionPage.spec.js b/td.vue/tests/unit/components/selectionPage.spec.js
index f59a6e01..1723312b 100644
--- a/td.vue/tests/unit/components/selectionPage.spec.js
+++ b/td.vue/tests/unit/components/selectionPage.spec.js
@@ -12,7 +12,11 @@ describe('components/SelectionPage.vue', () => {
     });
 
     describe('with data', () => {
-        const items = [ 'one', 'two', 'three', 'four' ];
+        const items = [ 'one', 'two', 'three', 'four', ({
+            value: 'five',
+            icon: 'lock',
+            iconTooltip: 'foobar',
+        }) ];
         let onItemClick;
 
         beforeEach(() => {
@@ -41,7 +45,7 @@ describe('components/SelectionPage.vue', () => {
         it('displays the items', () => {
             expect(wrapper.findAllComponents(BListGroupItem).at(1).text()).toEqual(items[1]);
         });
-        
+
         it('filters items based on the search bar', async () => {
             await wrapper.setData({ filter: 'FOUR' });
             expect(wrapper.findComponent(BListGroupItem).text()).toEqual('four');
@@ -51,6 +55,12 @@ describe('components/SelectionPage.vue', () => {
             await wrapper.findComponent(BListGroupItem).trigger('click');
             expect(onItemClick).toHaveBeenCalledTimes(1);
         });
+
+        it('display the icon only on the last item', () => {
+            expect(wrapper.findAllComponents(BListGroupItem).at(3).find('b-icon-stub').exists()).toBeFalsy();
+            expect(wrapper.findAllComponents(BListGroupItem).at(3).find('span').text()).toEqual(items[3]);
+            expect(wrapper.findAllComponents(BListGroupItem).at(4).html()).toMatch(`<font-awesome-icon icon="${items[4].icon}" title="${items[4].iconTooltip}"></font-awesome-icon>`);
+        });
     });
 
     describe('empty state with action', () => {
@@ -73,7 +83,7 @@ describe('components/SelectionPage.vue', () => {
                 }
             });
         });
-        
+
         it('shows the empty state', () => {
             expect(wrapper.findComponent(BListGroupItem).text()).toEqual(emptyStateText);
         });
diff --git a/td.vue/tests/unit/views/branchAccess.spec.js b/td.vue/tests/unit/views/branchAccess.spec.js
index 82498797..d6f7869a 100644
--- a/td.vue/tests/unit/views/branchAccess.spec.js
+++ b/td.vue/tests/unit/views/branchAccess.spec.js
@@ -6,6 +6,7 @@ import { BRANCH_FETCH, BRANCH_SELECTED } from '@/store/actions/branch.js';
 import { PROVIDER_SELECTED } from '@/store/actions/provider.js';
 import { REPOSITORY_CLEAR, REPOSITORY_SELECTED } from '@/store/actions/repository.js';
 import TdSelectionPage from '@/components/SelectionPage.vue';
+import AddBranchDialog from '@/components/AddBranchDialog.vue';
 
 
 describe('views/BranchAccess.vue', () => {
@@ -42,7 +43,7 @@ describe('views/BranchAccess.vue', () => {
             },
             branch: {
                 selected: 'someBranch',
-                all: ['b1', 'b2', 'b3'],
+                all: [{ name: 'b1', protected: true }, { name: 'b2', protected: false }, { name: 'b3', protected: false }],
                 page: 1,
                 pageNext: true,
                 pagePrev: false
@@ -52,7 +53,7 @@ describe('views/BranchAccess.vue', () => {
             }
         },
         actions: {
-            [BRANCH_FETCH]: () => { },
+            [BRANCH_FETCH]: () =>  Promise.resolve(getMockStore().state.branch.all),
             [BRANCH_SELECTED]: () => { },
             [PROVIDER_SELECTED]: () => { },
             [REPOSITORY_CLEAR]: () => { },
@@ -80,7 +81,7 @@ describe('views/BranchAccess.vue', () => {
             });
             expect(mockStore.dispatch).toHaveBeenCalledWith(REPOSITORY_SELECTED, 'fakeRepoBad');
         });
-        
+
         it('fetches the branches', () => {
             getLocalVue({
                 params: {
@@ -89,6 +90,14 @@ describe('views/BranchAccess.vue', () => {
                 }
             });
             expect(mockStore.dispatch).toHaveBeenCalledWith(BRANCH_FETCH, 1);
+            expect(wrapper.vm.branches).toEqual([
+                {
+                    value: 'b1',
+                    icon: 'lock',
+                    iconTooltip: 'branch.protectedBranch'
+                },
+                'b2','b3'
+            ]);
         });
     });
 
@@ -178,4 +187,21 @@ describe('views/BranchAccess.vue', () => {
             expect(mockRouter.push).toHaveBeenCalledWith({ name: 'gitNewThreatModel', params: routeParams });
         });
     });
+
+    describe('open add branch dialog', () => {
+        beforeEach(() => {
+            getLocalVue({
+                params: {
+                    provider: mockStore.state.provider.selected,
+                    repository: mockStore.state.repo.selected
+                }
+            });
+            wrapper.vm.toggleNewBranchDialog();
+        });
+
+        it('sets the dialog to open', () => {
+            expect(wrapper.vm.showNewBranchDialog).toEqual(true);
+            expect(wrapper.findComponent(AddBranchDialog).exists()).toEqual(true);
+        });
+    });
 });