From e0fe2834ebe6d9678444c8a10b79ca093f944232 Mon Sep 17 00:00:00 2001
From: Jacopo <beschi.jacopo@gmail.com>
Date: Thu, 22 Dec 2016 07:21:40 +0100
Subject: [PATCH] New file from interface on existing branch

Now you can create a new file and select a target_branch != source_branch.
If the file that you want to create already exists on the target branch an error message is shown
A glDropdown is used to select and create a new branch instead of a text field.
---
 .../javascripts/blob/blob_file_dropzone.js    |   2 +-
 .../blob/create_branch_dropdown.js            |  88 ++++++++++
 .../blob/target_branch_dropdown.js            | 152 ++++++++++++++++++
 app/assets/javascripts/dispatcher.js          |  26 ++-
 app/assets/javascripts/main.js                |   2 +
 app/assets/javascripts/new_commit_form.js     |  12 +-
 app/assets/stylesheets/pages/projects.scss    |   3 +-
 app/controllers/projects/blob_controller.rb   |   7 +
 .../projects/branches_controller.rb           |  13 +-
 app/views/projects/commit/_change.html.haml   |   2 +-
 app/views/shared/_branch_switcher.html.haml   |   8 +
 app/views/shared/_new_commit_form.html.haml   |   2 +-
 .../blob/_branch_page_create.html.haml        |   8 +
 .../blob/_branch_page_default.html.haml       |  10 ++
 .../24501-new-file-existing-branch.yml        |   4 +
 features/steps/project/source/browse_files.rb |   5 +-
 .../projects/branches_controller_spec.rb      |  14 ++
 .../projects/blobs/user_create_spec.rb        | 107 ++++++++++++
 .../blob/create_branch_dropdown_spec.js       | 109 +++++++++++++
 .../blob/target_branch_dropdown_spec.js       | 121 ++++++++++++++
 .../fixtures/project_branches.json            |   5 +
 .../fixtures/target_branch_dropdown.html.haml |  28 ++++
 22 files changed, 710 insertions(+), 18 deletions(-)
 create mode 100644 app/assets/javascripts/blob/create_branch_dropdown.js
 create mode 100644 app/assets/javascripts/blob/target_branch_dropdown.js
 create mode 100644 app/views/shared/_branch_switcher.html.haml
 create mode 100644 app/views/shared/projects/blob/_branch_page_create.html.haml
 create mode 100644 app/views/shared/projects/blob/_branch_page_default.html.haml
 create mode 100644 changelogs/unreleased/24501-new-file-existing-branch.yml
 create mode 100644 spec/features/projects/blobs/user_create_spec.rb
 create mode 100644 spec/javascripts/blob/create_branch_dropdown_spec.js
 create mode 100644 spec/javascripts/blob/target_branch_dropdown_spec.js
 create mode 100644 spec/javascripts/fixtures/project_branches.json
 create mode 100644 spec/javascripts/fixtures/target_branch_dropdown.html.haml

diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 5f14ff40eee..8f6bf162d6e 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -36,7 +36,7 @@
             this.removeFile(file);
           });
           return this.on('sending', function(file, xhr, formData) {
-            formData.append('target_branch', form.find('.js-target-branch').val());
+            formData.append('target_branch', form.find('input[name="target_branch"]').val());
             formData.append('create_merge_request', form.find('.js-create-merge-request').val());
             formData.append('commit_message', form.find('.js-commit-message').val());
           });
diff --git a/app/assets/javascripts/blob/create_branch_dropdown.js b/app/assets/javascripts/blob/create_branch_dropdown.js
new file mode 100644
index 00000000000..95517f51b1c
--- /dev/null
+++ b/app/assets/javascripts/blob/create_branch_dropdown.js
@@ -0,0 +1,88 @@
+class CreateBranchDropdown {
+  constructor(el, targetBranchDropdown) {
+    this.targetBranchDropdown = targetBranchDropdown;
+    this.el = el;
+    this.dropdownBack = this.el.closest('.dropdown').querySelector('.dropdown-menu-back');
+    this.cancelButton = this.el.querySelector('.js-cancel-branch-btn');
+    this.newBranchField = this.el.querySelector('#new_branch_name');
+    this.newBranchCreateButton = this.el.querySelector('.js-new-branch-btn');
+
+    this.newBranchCreateButton.setAttribute('disabled', '');
+
+    this.addBindings();
+    this.cleanupWrapper = this.cleanup.bind(this);
+    document.addEventListener('beforeunload', this.cleanupWrapper);
+  }
+
+  cleanup() {
+    this.cleanBindings();
+    document.removeEventListener('beforeunload', this.cleanupWrapper);
+  }
+
+  cleanBindings() {
+    this.newBranchField.removeEventListener('keyup', this.enableBranchCreateButtonWrapper);
+    this.newBranchField.removeEventListener('change', this.enableBranchCreateButtonWrapper);
+    this.newBranchField.removeEventListener('keydown', this.handleNewBranchKeydownWrapper);
+    this.dropdownBack.removeEventListener('click', this.resetFormWrapper);
+    this.cancelButton.removeEventListener('click', this.handleCancelClickWrapper);
+    this.newBranchCreateButton.removeEventListener('click', this.createBranchWrapper);
+  }
+
+  addBindings() {
+    this.enableBranchCreateButtonWrapper = this.enableBranchCreateButton.bind(this);
+    this.handleNewBranchKeydownWrapper = this.handleNewBranchKeydown.bind(this);
+    this.resetFormWrapper = this.resetForm.bind(this);
+    this.handleCancelClickWrapper = this.handleCancelClick.bind(this);
+    this.createBranchWrapper = this.createBranch.bind(this);
+
+    this.newBranchField.addEventListener('keyup', this.enableBranchCreateButtonWrapper);
+    this.newBranchField.addEventListener('change', this.enableBranchCreateButtonWrapper);
+    this.newBranchField.addEventListener('keydown', this.handleNewBranchKeydownWrapper);
+    this.dropdownBack.addEventListener('click', this.resetFormWrapper);
+    this.cancelButton.addEventListener('click', this.handleCancelClickWrapper);
+    this.newBranchCreateButton.addEventListener('click', this.createBranchWrapper);
+  }
+
+  handleCancelClick(e) {
+    e.preventDefault();
+    e.stopPropagation();
+
+    this.resetForm();
+    this.dropdownBack.click();
+  }
+
+  handleNewBranchKeydown(e) {
+    const keyCode = e.which;
+    const ENTER_KEYCODE = 13;
+    if (keyCode === ENTER_KEYCODE) {
+      this.createBranch(e);
+    }
+  }
+
+  enableBranchCreateButton() {
+    if (this.newBranchField.value !== '') {
+      this.newBranchCreateButton.removeAttribute('disabled');
+    } else {
+      this.newBranchCreateButton.setAttribute('disabled', '');
+    }
+  }
+
+  resetForm() {
+    this.newBranchField.value = '';
+    this.enableBranchCreateButtonWrapper();
+  }
+
+  createBranch(e) {
+    e.preventDefault();
+
+    if (this.newBranchCreateButton.getAttribute('disabled') === '') {
+      return;
+    }
+    const newBranchName = this.newBranchField.value;
+    this.targetBranchDropdown.setNewBranch(newBranchName);
+    this.resetForm();
+  }
+}
+
+window.gl = window.gl || {};
+gl.CreateBranchDropdown = CreateBranchDropdown;
diff --git a/app/assets/javascripts/blob/target_branch_dropdown.js b/app/assets/javascripts/blob/target_branch_dropdown.js
new file mode 100644
index 00000000000..216f069ef71
--- /dev/null
+++ b/app/assets/javascripts/blob/target_branch_dropdown.js
@@ -0,0 +1,152 @@
+/* eslint-disable class-methods-use-this */
+const SELECT_ITEM_MSG = 'Select';
+
+class TargetBranchDropDown {
+  constructor(dropdown) {
+    this.dropdown = dropdown;
+    this.$dropdown = $(dropdown);
+    this.fieldName = this.dropdown.getAttribute('data-field-name');
+    this.form = this.dropdown.closest('form');
+    this.createDropdown();
+  }
+
+  static bootstrap() {
+    const dropdowns = document.querySelectorAll('.js-project-branches-dropdown');
+    [].forEach.call(dropdowns, dropdown => new TargetBranchDropDown(dropdown));
+  }
+
+  createDropdown() {
+    const self = this;
+    this.$dropdown.glDropdown({
+      selectable: true,
+      filterable: true,
+      search: {
+        fields: ['title'],
+      },
+      data: (term, callback) => $.ajax({
+        url: self.dropdown.getAttribute('data-refs-url'),
+        data: {
+          ref: self.dropdown.getAttribute('data-ref'),
+          show_all: true,
+        },
+        dataType: 'json',
+      }).done(refs => callback(self.dropdownData(refs))),
+      toggleLabel(item, el) {
+        if (el.is('.is-active')) {
+          return item.text;
+        }
+        return SELECT_ITEM_MSG;
+      },
+      clicked(item, el, e) {
+        e.preventDefault();
+        self.onClick.call(self);
+      },
+      fieldName: self.fieldName,
+    });
+    return new gl.CreateBranchDropdown(this.form.querySelector('.dropdown-new-branch'), this);
+  }
+
+  onClick() {
+    this.enableSubmit();
+    this.$dropdown.trigger('change.branch');
+  }
+
+  enableSubmit() {
+    const submitBtn = this.form.querySelector('[type="submit"]');
+    if (this.branchInput && this.branchInput.value) {
+      submitBtn.removeAttribute('disabled');
+    } else {
+      submitBtn.setAttribute('disabled', '');
+    }
+  }
+
+  dropdownData(refs) {
+    const branchList = this.dropdownItems(refs);
+    this.cachedRefs = refs;
+    this.addDefaultBranch(branchList);
+    this.addNewBranch(branchList);
+    return { Branches: branchList };
+  }
+
+  dropdownItems(refs) {
+    return refs.map(this.dropdownItem);
+  }
+
+  dropdownItem(ref) {
+    return { id: ref, text: ref, title: ref };
+  }
+
+  addDefaultBranch(branchList) {
+    // when no branch is selected do nothing
+    if (!this.branchInput) {
+      return;
+    }
+
+    const branchInputVal = this.branchInput.value;
+    const currentBranchIndex = this.searchBranch(branchList, branchInputVal);
+
+    if (currentBranchIndex === -1) {
+      this.unshiftBranch(branchList, this.dropdownItem(branchInputVal));
+    }
+  }
+
+  addNewBranch(branchList) {
+    if (this.newBranch) {
+      this.unshiftBranch(branchList, this.newBranch);
+    }
+  }
+
+  searchBranch(branchList, branchName) {
+    return _.findIndex(branchList, el => branchName === el.id);
+  }
+
+  unshiftBranch(branchList, branch) {
+    const branchIndex = this.searchBranch(branchList, branch.id);
+
+    if (branchIndex === -1) {
+      branchList.unshift(branch);
+    }
+  }
+
+  setNewBranch(newBranchName) {
+    this.newBranch = this.dropdownItem(newBranchName);
+    this.refreshData();
+    this.selectBranch(this.searchBranch(this.glDropdown.fullData.Branches, newBranchName));
+  }
+
+  refreshData() {
+    this.glDropdown.fullData = this.dropdownData(this.cachedRefs);
+    this.clearFilter();
+  }
+
+  clearFilter() {
+    // apply an empty filter in order to refresh the data
+    this.glDropdown.filter.filter('');
+    this.dropdown.closest('.dropdown').querySelector('.dropdown-page-one .dropdown-input-field').value = '';
+  }
+
+  selectBranch(index) {
+    const branch = this.dropdown.closest('.dropdown').querySelectorAll('li a')[index];
+
+    if (!branch.classList.contains('is-active')) {
+      branch.click();
+    } else {
+      this.closeDropdown();
+    }
+  }
+
+  closeDropdown() {
+    this.dropdown.closest('.dropdown').querySelector('.dropdown-menu-close').click();
+  }
+
+  get branchInput() {
+    return this.form.querySelector(`input[name="${this.fieldName}"]`);
+  }
+
+  get glDropdown() {
+    return this.$dropdown.data('glDropdown');
+  }
+}
+
+window.gl = window.gl || {};
+gl.TargetBranchDropDown = TargetBranchDropDown;
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 7b9b9123c31..5739a28699f 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -59,7 +59,7 @@ const UserCallout = require('./user_callout');
     }
 
     Dispatcher.prototype.initPageScripts = function() {
-      var page, path, shortcut_handler;
+      var page, path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl;
       page = $('body').attr('data-page');
       if (!page) {
         return false;
@@ -245,16 +245,36 @@ const UserCallout = require('./user_callout');
         case 'projects:tree:show':
           shortcut_handler = new ShortcutsNavigation();
           new TreeView();
+          gl.TargetBranchDropDown.bootstrap();
           break;
         case 'projects:find_file:show':
           shortcut_handler = true;
           break;
+        case 'projects:blob:new':
+          gl.TargetBranchDropDown.bootstrap();
+          break;
+        case 'projects:blob:create':
+          gl.TargetBranchDropDown.bootstrap();
+          break;
         case 'projects:blob:show':
+          gl.TargetBranchDropDown.bootstrap();
+          new LineHighlighter();
+          shortcut_handler = new ShortcutsNavigation();
+          fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
+          fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+          new ShortcutsBlob({
+            skipResetBindings: true,
+            fileBlobPermalinkUrl,
+          });
+          break;
+        case 'projects:blob:edit':
+          gl.TargetBranchDropDown.bootstrap();
+          break;
         case 'projects:blame:show':
           new LineHighlighter();
           shortcut_handler = new ShortcutsNavigation();
-          const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
-          const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+          fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
+          fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
           new ShortcutsBlob({
             skipResetBindings: true,
             fileBlobPermalinkUrl,
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 689a6c3a93a..604ed91627a 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -66,6 +66,8 @@ import './blob/blob_gitignore_selectors';
 import './blob/blob_license_selector';
 import './blob/blob_license_selectors';
 import './blob/template_selector';
+import './blob/create_branch_dropdown';
+import './blob/target_branch_dropdown';
 
 // templates
 import './templates/issuable_template_selector';
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 747f693726e..ad36f08840d 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -3,19 +3,23 @@
   var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
 
   this.NewCommitForm = (function() {
-    function NewCommitForm(form) {
+    function NewCommitForm(form, targetBranchName = 'target_branch') {
+      this.form = form;
+      this.targetBranchName = targetBranchName;
       this.renderDestination = bind(this.renderDestination, this);
-      this.newBranch = form.find('.js-target-branch');
+      this.targetBranchDropdown = form.find('button.js-target-branch');
       this.originalBranch = form.find('.js-original-branch');
       this.createMergeRequest = form.find('.js-create-merge-request');
       this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
+      this.targetBranchDropdown.on('change.branch', this.renderDestination);
       this.renderDestination();
-      this.newBranch.keyup(this.renderDestination);
     }
 
     NewCommitForm.prototype.renderDestination = function() {
       var different;
-      different = this.newBranch.val() !== this.originalBranch.val();
+      var targetBranch = this.form.find(`input[name="${this.targetBranchName}"]`);
+
+      different = targetBranch.val() !== this.originalBranch.val();
       if (different) {
         this.createMergeRequestContainer.show();
         if (!this.wasDifferent) {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 4914933430f..efa47be9a73 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -795,7 +795,8 @@ pre.light-well {
 }
 
 .project-refs-form .dropdown-menu,
-.dropdown-menu-projects {
+.dropdown-menu-projects,
+.dropdown-menu-branches {
   width: 300px;
 
   @media (min-width: $screen-sm-min) {
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 21ed0660762..52fc67d162c 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -23,6 +23,8 @@ class Projects::BlobController < Projects::ApplicationController
   end
 
   def create
+    update_ref
+
     create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
                                         success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) },
                                         failure_view: :new,
@@ -87,6 +89,11 @@ class Projects::BlobController < Projects::ApplicationController
 
   private
 
+  def update_ref
+    branch_exists = @repository.find_branch(@target_branch)
+    @ref = @target_branch if branch_exists
+  end
+
   def blob
     @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
 
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index c40f9b7f75f..22714d9c5a4 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -10,15 +10,16 @@ class Projects::BranchesController < Projects::ApplicationController
   def index
     @sort = params[:sort].presence || sort_value_name
     @branches = BranchesFinder.new(@repository, params).execute
-    @branches = Kaminari.paginate_array(@branches).page(params[:page])
 
-    @max_commits = @branches.reduce(0) do |memo, branch|
-      diverging_commit_counts = repository.diverging_commit_counts(branch)
-      [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
-    end
+    @branches = Kaminari.paginate_array(@branches).page(params[:page]) unless params[:show_all].present?
 
     respond_to do |format|
-      format.html
+      format.html do
+        @max_commits = @branches.reduce(0) do |memo, branch|
+          diverging_commit_counts = repository.diverging_commit_counts(branch)
+          [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
+        end
+      end
       format.json do
         render json: @branches.map(&:name)
       end
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 2ebd4f9069a..b5f67cae341 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -37,4 +37,4 @@
                 = commit_in_fork_help
 
 :javascript
-  new NewCommitForm($('.js-#{type}-form'))
+  new NewCommitForm($('.js-#{type}-form'), 'start_branch')
diff --git a/app/views/shared/_branch_switcher.html.haml b/app/views/shared/_branch_switcher.html.haml
new file mode 100644
index 00000000000..7799aff6b5b
--- /dev/null
+++ b/app/views/shared/_branch_switcher.html.haml
@@ -0,0 +1,8 @@
+- dropdown_toggle_text = @target_branch || tree_edit_branch
+= hidden_field_tag 'target_branch', dropdown_toggle_text
+
+.dropdown
+  = dropdown_toggle dropdown_toggle_text, { toggle: 'dropdown', selected: dropdown_toggle_text, field_name: 'target_branch', form_id: '.js-edit-blob-form', refs_url: namespace_project_branches_path(@project.namespace, @project) }, { toggle_class: 'js-project-branches-dropdown js-target-branch' }
+  .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging.dropdown-menu-branches
+    = render partial: 'shared/projects/blob/branch_page_default'
+    = render partial: 'shared/projects/blob/branch_page_create'
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
index 0c8ac48bb58..3ac5e15d1c4 100644
--- a/app/views/shared/_new_commit_form.html.haml
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -7,7 +7,7 @@
     .form-group.branch
       = label_tag 'target_branch', 'Target branch', class: 'control-label'
       .col-sm-10
-        = text_field_tag 'target_branch', @target_branch || tree_edit_branch, required: true, class: "form-control js-target-branch"
+        = render 'shared/branch_switcher'
 
         .js-create-merge-request-container
           .checkbox
diff --git a/app/views/shared/projects/blob/_branch_page_create.html.haml b/app/views/shared/projects/blob/_branch_page_create.html.haml
new file mode 100644
index 00000000000..c279a0d8846
--- /dev/null
+++ b/app/views/shared/projects/blob/_branch_page_create.html.haml
@@ -0,0 +1,8 @@
+.dropdown-page-two.dropdown-new-branch
+  = dropdown_title('Create new branch', back: true)
+  = dropdown_content do
+    %input#new_branch_name.default-dropdown-input.append-bottom-10{ type: "text", placeholder: "Name new branch" }
+      %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" }
+        Create
+      %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" }
+        Cancel
diff --git a/app/views/shared/projects/blob/_branch_page_default.html.haml b/app/views/shared/projects/blob/_branch_page_default.html.haml
new file mode 100644
index 00000000000..9bf78d10878
--- /dev/null
+++ b/app/views/shared/projects/blob/_branch_page_default.html.haml
@@ -0,0 +1,10 @@
+.dropdown-page-one
+  = dropdown_title "Select branch"
+  = dropdown_filter "Search branches"
+  = dropdown_content
+  = dropdown_loading
+  = dropdown_footer do
+    %ul.dropdown-footer-list
+      %li
+        %a.create-new-branch.dropdown-toggle-page{ href: "#" }
+          Create new branch
diff --git a/changelogs/unreleased/24501-new-file-existing-branch.yml b/changelogs/unreleased/24501-new-file-existing-branch.yml
new file mode 100644
index 00000000000..31c66b2a978
--- /dev/null
+++ b/changelogs/unreleased/24501-new-file-existing-branch.yml
@@ -0,0 +1,4 @@
+---
+title: New file from interface on existing branch
+merge_request: 8427
+author: Jacopo Beschi @jacopo-beschi
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index f18adcadcce..e84cd9193da 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -82,7 +82,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
   end
 
   step 'I fill the new branch name' do
-    fill_in :target_branch, with: 'new_branch_name', visible: true
+    first('button.js-target-branch', visible: true).click
+    first('.create-new-branch', visible: true).click
+    first('#new_branch_name', visible: true).set('new_branch_name')
+    first('.js-new-branch-btn', visible: true).click
   end
 
   step 'I fill the new file name with an illegal name' do
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 298a7ff179c..d20e7368086 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -266,5 +266,19 @@ describe Projects::BranchesController do
         expect(parsed_response.first).to eq 'master'
       end
     end
+
+    context 'show_all = true' do
+      it 'returns all the branches name' do
+        get :index,
+            namespace_id: project.namespace,
+            project_id: project,
+            format: :json,
+            show_all: true
+
+        parsed_response = JSON.parse(response.body)
+
+        expect(parsed_response.length).to eq(project.repository.branches.count)
+      end
+    end
   end
 end
diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb
new file mode 100644
index 00000000000..03d08c12612
--- /dev/null
+++ b/spec/features/projects/blobs/user_create_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+feature 'New blob creation', feature: true, js: true do
+  include WaitForAjax
+
+  given(:user) { create(:user) }
+  given(:role) { :developer }
+  given(:project) { create(:project) }
+  given(:content) { 'class NextFeature\nend\n' }
+
+  background do
+    login_as(user)
+    project.team << [user, role]
+    visit namespace_project_new_blob_path(project.namespace, project, 'master')
+  end
+
+  def edit_file
+    wait_for_ajax
+    fill_in 'file_name', with: 'feature.rb'
+    execute_script("ace.edit('editor').setValue('#{content}')")
+  end
+
+  def select_branch_index(index)
+    first('button.js-target-branch').click
+    wait_for_ajax
+    all('a[data-group="Branches"]')[index].click
+  end
+
+  def create_new_branch(name)
+    first('button.js-target-branch').click
+    click_link 'Create new branch'
+    fill_in 'new_branch_name', with: name
+    click_button 'Create'
+  end
+
+  def commit_file
+    click_button 'Commit Changes'
+  end
+
+  context 'with default target branch' do
+    background do
+      edit_file
+      commit_file
+    end
+
+    scenario 'creates the blob in the default branch' do
+      expect(page).to have_content 'master'
+      expect(page).to have_content 'successfully created'
+      expect(page).to have_content 'NextFeature'
+    end
+  end
+
+  context 'with different target branch' do
+    background do
+      edit_file
+      select_branch_index(0)
+      commit_file
+    end
+
+    scenario 'creates the blob in the different branch' do
+      expect(page).to have_content 'test'
+      expect(page).to have_content 'successfully created'
+    end
+  end
+
+  context 'with a new target branch' do
+    given(:new_branch_name) { 'new-feature' }
+
+    background do
+      edit_file
+      create_new_branch(new_branch_name)
+      commit_file
+    end
+
+    scenario 'creates the blob in the new branch' do
+      expect(page).to have_content new_branch_name
+      expect(page).to have_content 'successfully created'
+    end
+    scenario 'returns you to the mr' do
+      expect(page).to have_content 'New Merge Request'
+      expect(page).to have_content "From #{new_branch_name} into master"
+      expect(page).to have_content 'Add new file'
+    end
+  end
+
+  context 'the file already exist in the source branch' do
+    background do
+      Files::CreateService.new(
+        project,
+        user,
+        start_branch: 'master',
+        target_branch: 'master',
+        commit_message: 'Create file',
+        file_path: 'feature.rb',
+        file_content: content
+      ).execute
+      edit_file
+      commit_file
+    end
+
+    scenario 'shows error message' do
+      expect(page).to have_content('Your changes could not be committed because a file with the same name already exists')
+      expect(page).to have_content('New File')
+      expect(page).to have_content('NextFeature')
+    end
+  end
+end
diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js
new file mode 100644
index 00000000000..dafb43761e0
--- /dev/null
+++ b/spec/javascripts/blob/create_branch_dropdown_spec.js
@@ -0,0 +1,109 @@
+require('jquery');
+require('~/extensions/jquery.js');
+require('~/gl_dropdown');
+require('~/lib/utils/type_utility');
+require('~/blob/create_branch_dropdown');
+require('~/blob/target_branch_dropdown');
+
+describe('CreateBranchDropdown', () => {
+  const fixtureTemplate = 'static/target_branch_dropdown.html.raw';
+  // selectors
+  const createBranchSel = '.js-new-branch-btn';
+  const backBtnSel = '.dropdown-menu-back';
+  const cancelBtnSel = '.js-cancel-branch-btn';
+  const branchNameSel = '#new_branch_name';
+  const branchName = 'new_name';
+  let dropdown;
+
+  function createDropdown() {
+    const dropdownEl = document.querySelector('.js-project-branches-dropdown');
+    const projectBranches = getJSONFixture('project_branches.json');
+    dropdown = new gl.TargetBranchDropDown(dropdownEl);
+    dropdown.cachedRefs = projectBranches;
+    return dropdown;
+  }
+
+  function createBranchBtn() {
+    return document.querySelector(createBranchSel);
+  }
+
+  function backBtn() {
+    return document.querySelector(backBtnSel);
+  }
+
+  function cancelBtn() {
+    return document.querySelector(cancelBtnSel);
+  }
+
+  function branchNameEl() {
+    return document.querySelector(branchNameSel);
+  }
+
+  function changeBranchName(text) {
+    branchNameEl().value = text;
+    branchNameEl().dispatchEvent(new Event('change'));
+  }
+
+  preloadFixtures(fixtureTemplate);
+
+  beforeEach(() => {
+    loadFixtures(fixtureTemplate);
+    createDropdown();
+  });
+
+  it('disable submit when branch name is empty', () => {
+    expect(createBranchBtn()).toBeDisabled();
+  });
+
+  it('enable submit when branch name is present', () => {
+    changeBranchName(branchName);
+
+    expect(createBranchBtn()).not.toBeDisabled();
+  });
+
+  it('resets the form when cancel btn is clicked and triggers dropdownback', () => {
+    const spyBackEvent = spyOnEvent(backBtnSel, 'click');
+    changeBranchName(branchName);
+
+    cancelBtn().click();
+
+    expect(branchNameEl()).toHaveValue('');
+    expect(spyBackEvent).toHaveBeenTriggered();
+  });
+
+  it('resets the form when back btn is clicked', () => {
+    changeBranchName(branchName);
+
+    backBtn().click();
+
+    expect(branchNameEl()).toHaveValue('');
+  });
+
+  describe('new branch creation', () => {
+    beforeEach(() => {
+      changeBranchName(branchName);
+    });
+    it('sets the new branch name and updates the dropdown', () => {
+      spyOn(dropdown, 'setNewBranch');
+
+      createBranchBtn().click();
+
+      expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName);
+    });
+
+    it('resets the form', () => {
+      createBranchBtn().click();
+
+      expect(branchNameEl()).toHaveValue('');
+    });
+
+    it('is triggered with enter keypress', () => {
+      spyOn(dropdown, 'setNewBranch');
+      const enterEvent = new Event('keydown');
+      enterEvent.which = 13;
+      branchNameEl().dispatchEvent(enterEvent);
+
+      expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName);
+    });
+  });
+});
diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js
new file mode 100644
index 00000000000..6f3eb4cc7eb
--- /dev/null
+++ b/spec/javascripts/blob/target_branch_dropdown_spec.js
@@ -0,0 +1,121 @@
+require('jquery');
+require('~/extensions/jquery.js');
+require('~/gl_dropdown');
+require('~/lib/utils/type_utility');
+require('~/blob/create_branch_dropdown');
+require('~/blob/target_branch_dropdown');
+
+describe('TargetBranchDropdown', () => {
+  const fixtureTemplate = 'static/target_branch_dropdown.html.raw';
+  let dropdown;
+
+  function createDropdown() {
+    const projectBranches = getJSONFixture('project_branches.json');
+    const dropdownEl = document.querySelector('.js-project-branches-dropdown');
+    dropdown = new gl.TargetBranchDropDown(dropdownEl);
+    dropdown.cachedRefs = projectBranches;
+    dropdown.refreshData();
+    return dropdown;
+  }
+
+  function submitBtn() {
+    return document.querySelector('button[type="submit"]');
+  }
+
+  function searchField() {
+    return document.querySelector('.dropdown-page-one .dropdown-input-field');
+  }
+
+  function element() {
+    return document.querySelectorAll('div.dropdown-content li a');
+  }
+
+  function elementAtIndex(index) {
+    return element()[index];
+  }
+
+  function clickElementAtIndex(index) {
+    elementAtIndex(index).click();
+  }
+
+  preloadFixtures(fixtureTemplate);
+
+  beforeEach(() => {
+    loadFixtures(fixtureTemplate);
+    createDropdown();
+  });
+
+  it('disable submit when branch is not selected', () => {
+    document.querySelector('input[name="target_branch"]').value = null;
+    clickElementAtIndex(1);
+
+    expect(submitBtn().getAttribute('disabled')).toEqual('');
+  });
+
+  it('enable submit when a branch is selected', () => {
+    clickElementAtIndex(1);
+
+    expect(submitBtn().getAttribute('disabled')).toBe(null);
+  });
+
+  it('triggers change.branch event on a branch click', () => {
+    spyOnEvent(dropdown.$dropdown, 'change.branch');
+    clickElementAtIndex(0);
+
+    expect('change.branch').toHaveBeenTriggeredOn(dropdown.$dropdown);
+  });
+
+  describe('#dropdownData', () => {
+    it('cache the refs', () => {
+      const refs = dropdown.cachedRefs;
+      dropdown.cachedRefs = null;
+
+      dropdown.dropdownData(refs);
+
+      expect(dropdown.cachedRefs).toEqual(refs);
+    });
+
+    it('returns the Branches with the newBranch and defaultBranch', () => {
+      const refs = dropdown.cachedRefs;
+      dropdown.branchInput.value = 'master';
+      dropdown.newBranch = { id: 'new_branch', text: 'new_branch', title: 'new_branch' };
+
+      const branches = dropdown.dropdownData(refs).Branches;
+
+      expect(branches.length).toEqual(4);
+      expect(branches[0]).toEqual(dropdown.newBranch);
+      expect(branches[1]).toEqual({ id: 'master', text: 'master', title: 'master' });
+      expect(branches[2]).toEqual({ id: 'development', text: 'development', title: 'development' });
+      expect(branches[3]).toEqual({ id: 'staging', text: 'staging', title: 'staging' });
+    });
+  });
+
+  describe('#setNewBranch', () => {
+    it('adds the new branch and select it', () => {
+      const branchName = 'new_branch';
+
+      dropdown.setNewBranch(branchName);
+
+      expect(elementAtIndex(0)).toHaveClass('is-active');
+      expect(elementAtIndex(0)).toContainHtml(branchName);
+    });
+
+    it("doesn't add a new branch if already exists in the list", () => {
+      const branchName = elementAtIndex(0).text;
+      const initialLength = element().length;
+
+      dropdown.setNewBranch(branchName);
+
+      expect(element().length).toEqual(initialLength);
+    });
+
+    it('clears the search filter', () => {
+      const branchName = elementAtIndex(0).text;
+      searchField().value = 'searching';
+
+      dropdown.setNewBranch(branchName);
+
+      expect(searchField().value).toEqual('');
+    });
+  });
+});
diff --git a/spec/javascripts/fixtures/project_branches.json b/spec/javascripts/fixtures/project_branches.json
new file mode 100644
index 00000000000..a96a4c0c095
--- /dev/null
+++ b/spec/javascripts/fixtures/project_branches.json
@@ -0,0 +1,5 @@
+[
+  "master",
+  "development",
+  "staging"
+]
diff --git a/spec/javascripts/fixtures/target_branch_dropdown.html.haml b/spec/javascripts/fixtures/target_branch_dropdown.html.haml
new file mode 100644
index 00000000000..821fb7940a0
--- /dev/null
+++ b/spec/javascripts/fixtures/target_branch_dropdown.html.haml
@@ -0,0 +1,28 @@
+%form.js-edit-blob-form
+  %input{type: 'hidden', name: 'target_branch', value: 'master'}
+  %div
+    .dropdown
+      %button.dropdown-menu-toggle.js-project-branches-dropdown.js-target-branch{type: 'button', data: {toggle: 'dropdown', selected: 'master', field_name: 'target_branch', form_id: '.js-edit-blob-form'}}
+      .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging
+        .dropdown-page-one
+          .dropdown-title 'Select branch'
+          .dropdown-input
+            %input.dropdown-input-field{type: 'search', value: ''}
+            %i.fa.fa-search.dropdown-input-search
+            %i.fa.fa-times-dropdown-input-clear.js-dropdown-input-clear{role: 'button'}
+          .dropdown-content
+          .dropdown-footer
+            %ul.dropdown-footer-list
+              %li
+                %a.create-new-branch.dropdown-toggle-page{href: "#"}
+                  Create new branch
+        .dropdown-page-two.dropdown-new-branch
+          %button.dropdown-title-button.dropdown-menu-back{type: 'button'}
+          .dropdown_title 'Create new branch'
+          .dropdown_content
+            %input#new_branch_name.default-dropdown-input{ type: "text", placeholder: "Name new branch" }
+              %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" }
+                Create
+              %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" }
+                Cancel
+  %button{type: 'submit'}
-- 
GitLab