From bd2b6f59445919cdcef627f7f1b1fca5d402168b Mon Sep 17 00:00:00 2001
From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
Date: Tue, 5 Nov 2013 10:28:49 +0200
Subject: [PATCH] New feature: Create file from UI

Now you are able to create a new file in repository from your browser.
You are not allowed to create a file if file with same name already
exists in the repo.

Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
---
 .../projects/new_tree_controller.rb           | 65 +++++++++++++++++++
 app/views/layouts/nav/_project.html.haml      |  2 +-
 app/views/projects/new_tree/show.html.haml    | 47 ++++++++++++++
 app/views/projects/tree/_tree.html.haml       |  5 ++
 config/routes.rb                              | 20 +++---
 .../satellite/{ => files}/edit_file_action.rb | 19 +-----
 lib/gitlab/satellite/files/file_action.rb     | 20 ++++++
 lib/gitlab/satellite/files/new_file_action.rb | 44 +++++++++++++
 8 files changed, 196 insertions(+), 26 deletions(-)
 create mode 100644 app/controllers/projects/new_tree_controller.rb
 create mode 100644 app/views/projects/new_tree/show.html.haml
 rename lib/gitlab/satellite/{ => files}/edit_file_action.rb (77%)
 create mode 100644 lib/gitlab/satellite/files/file_action.rb
 create mode 100644 lib/gitlab/satellite/files/new_file_action.rb

diff --git a/app/controllers/projects/new_tree_controller.rb b/app/controllers/projects/new_tree_controller.rb
new file mode 100644
index 00000000000..9003f2df5fb
--- /dev/null
+++ b/app/controllers/projects/new_tree_controller.rb
@@ -0,0 +1,65 @@
+class Projects::NewTreeController < Projects::ApplicationController
+  include ExtractsPath
+
+  # Authorize
+  before_filter :authorize_read_project!
+  before_filter :authorize_code_access!
+  before_filter :require_non_empty_project
+
+  before_filter :create_requirements, only: [:show, :update]
+
+  def show
+  end
+
+  def update
+    file_name = params[:file_name]
+
+    unless file_name =~ Gitlab::Regex.path_regex
+      flash[:notice] = "Your changes could not be commited, because file name contains not allowed characters"
+      render :show and return
+    end
+
+    file_path = if @path.blank?
+                  file_name
+                else
+                  File.join(@path, file_name)
+                end
+
+    blob = @repository.blob_at(@commit.id, file_path)
+
+    if blob
+      flash[:notice] = "Your changes could not be commited, because file with such name exists"
+      render :show and return
+    end
+
+    new_file_action = Gitlab::Satellite::NewFileAction.new(current_user, @project, @ref, @path)
+    updated_successfully = new_file_action.commit!(
+      params[:content],
+      params[:commit_message],
+      file_name,
+    )
+
+    if updated_successfully
+      redirect_to project_blob_path(@project, File.join(@id, params[:file_name])), notice: "Your changes have been successfully commited"
+    else
+      flash[:notice] = "Your changes could not be commited, because the file has been changed"
+      render :show
+    end
+  end
+
+  private
+
+  def create_requirements
+    allowed = if project.protected_branch? @ref
+                can?(current_user, :push_code_to_protected_branches, project)
+              else
+                can?(current_user, :push_code, project)
+              end
+
+    return access_denied! unless allowed
+
+    unless @repository.branch_names.include?(@ref)
+      redirect_to project_blob_path(@project, @id), notice: "You can only create files if you are on top of a branch"
+    end
+  end
+end
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index e9d535f6972..1f70cf17987 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -4,7 +4,7 @@
       %i.icon-home
 
   - if project_nav_tab? :files
-    = nav_link(controller: %w(tree blob blame edit_tree)) do
+    = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do
       = link_to 'Files', project_tree_path(@project, @ref || @repository.root_ref)
 
   - if project_nav_tab? :commits
diff --git a/app/views/projects/new_tree/show.html.haml b/app/views/projects/new_tree/show.html.haml
new file mode 100644
index 00000000000..4837b8e3b82
--- /dev/null
+++ b/app/views/projects/new_tree/show.html.haml
@@ -0,0 +1,47 @@
+%h3.page-title New file
+%hr
+.file-editor
+  = form_tag(project_new_tree_path(@project, @id), method: :put, class: "form-horizontal") do
+    .control-group.commit_message-group
+      = label_tag 'file_name', class: "control-label" do
+        File name
+      .controls
+        %span.monospace= @path[-1] == "/" ? @path : @path + "/"
+        &nbsp;
+        = text_field_tag 'file_name', '', placeholder: "sample.rb", required: true
+        %span
+          &nbsp;
+          on
+          %span.label-branch= @ref
+
+    .control-group.commit_message-group
+      = label_tag 'commit_message', class: "control-label" do
+        Commit message
+      .controls
+        = text_area_tag 'commit_message', '', placeholder: "Added new file", required: true, rows: 3
+
+    .file-holder
+      .file-title
+        %i.icon-file
+      .file-content.code
+        %pre#editor= ""
+
+    .form-actions
+      = hidden_field_tag 'content', '', id: "file-content"
+      .commit-button-annotation
+        = button_tag "Commit changes", class: 'btn commit-btn js-commit-button btn-create'
+        .message
+          to branch
+          %strong= @ref
+      = link_to "Cancel", project_tree_path(@project, @id), class: "btn btn-cancel", confirm: leave_edit_message
+
+:javascript
+  ace.config.set("modePath", gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}/ace-src-noconflict")
+  var editor = ace.edit("editor");
+
+  disableButtonIfEmptyField("#commit_message", ".js-commit-button");
+
+  $(".js-commit-button").click(function(){
+    $("#file-content").val(editor.getValue());
+    $(".file-editor form").submit();
+  });
diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml
index eadfd33bd3c..7a41089a3f2 100644
--- a/app/views/projects/tree/_tree.html.haml
+++ b/app/views/projects/tree/_tree.html.haml
@@ -10,6 +10,11 @@
         = link_to truncate(title, length: 40), project_tree_path(@project, path)
       - else
         = link_to title, '#'
+  \/
+  %li
+    = link_to project_new_tree_path(@project, @id) do
+      %small
+        %i.icon-plus.light
 
 %div#tree-content-holder.tree-content-holder
   %table#tree-slider{class: "table_#{@hex_path} tree-table" }
diff --git a/config/routes.rb b/config/routes.rb
index 78f75d11835..8f1758394b6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -166,16 +166,18 @@ Gitlab::Application.routes.draw do
     end
 
     scope module: :projects do
-      resources :blob,    only: [:show], constraints: {id: /.+/}
-      resources :raw,    only: [:show], constraints: {id: /.+/}
-      resources :tree,    only: [:show], constraints: {id: /.+/, format: /(html|js)/ }
-      resources :edit_tree,    only: [:show, :update], constraints: {id: /.+/}, path: 'edit'
-      resources :commit,  only: [:show], constraints: {id: /[[:alnum:]]{6,40}/}
-      resources :commits, only: [:show], constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/}
-      resources :compare, only: [:index, :create]
-      resources :blame,   only: [:show], constraints: {id: /.+/}
+      resources :blob,      only: [:show], constraints: {id: /.+/}
+      resources :raw,       only: [:show], constraints: {id: /.+/}
+      resources :tree,      only: [:show], constraints: {id: /.+/, format: /(html|js)/ }
+      resources :edit_tree, only: [:show, :update], constraints: {id: /.+/}, path: 'edit'
+      resources :new_tree,  only: [:show, :update], constraints: {id: /.+/}, path: 'new'
+      resources :commit,    only: [:show], constraints: {id: /[[:alnum:]]{6,40}/}
+      resources :commits,   only: [:show], constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/}
+      resources :compare,   only: [:index, :create]
+      resources :blame,     only: [:show], constraints: {id: /.+/}
       resources :network,   only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/}
-      resources :graphs, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/}
+      resources :graphs,    only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/}
+
       match "/compare/:from...:to" => "compare#show", as: "compare", via: [:get, :post], constraints: {from: /.+/, to: /.+/}
 
         resources :snippets, constraints: {id: /\d+/} do
diff --git a/lib/gitlab/satellite/edit_file_action.rb b/lib/gitlab/satellite/files/edit_file_action.rb
similarity index 77%
rename from lib/gitlab/satellite/edit_file_action.rb
rename to lib/gitlab/satellite/files/edit_file_action.rb
index d793d0ba8dc..72e12fb077c 100644
--- a/lib/gitlab/satellite/edit_file_action.rb
+++ b/lib/gitlab/satellite/files/edit_file_action.rb
@@ -1,15 +1,9 @@
+require_relative 'file_action'
+
 module Gitlab
   module Satellite
     # GitLab server-side file update and commit
-    class EditFileAction < Action
-      attr_accessor :file_path, :ref
-
-      def initialize(user, project, ref, file_path)
-        super user, project, git_timeout: 10.seconds
-        @file_path = file_path
-        @ref = ref
-      end
-
+    class EditFileAction < FileAction
       # Updates the files content and creates a new commit for it
       #
       # Returns false if the ref has been updated while editing the file
@@ -45,13 +39,6 @@ module Gitlab
         Gitlab::GitLogger.error(ex.message)
         false
       end
-
-      protected
-
-      def can_edit?(last_commit)
-        current_last_commit = Gitlab::Git::Commit.last_for_path(@project.repository, ref, file_path).sha
-        last_commit == current_last_commit
-      end
     end
   end
 end
diff --git a/lib/gitlab/satellite/files/file_action.rb b/lib/gitlab/satellite/files/file_action.rb
new file mode 100644
index 00000000000..4ac53c2cd5a
--- /dev/null
+++ b/lib/gitlab/satellite/files/file_action.rb
@@ -0,0 +1,20 @@
+module Gitlab
+  module Satellite
+    class FileAction < Action
+      attr_accessor :file_path, :ref
+
+      def initialize(user, project, ref, file_path)
+        super user, project, git_timeout: 10.seconds
+        @file_path = file_path
+        @ref = ref
+      end
+
+      protected
+
+      def can_edit?(last_commit)
+        current_last_commit = Gitlab::Git::Commit.last_for_path(@project.repository, ref, file_path).sha
+        last_commit == current_last_commit
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/satellite/files/new_file_action.rb b/lib/gitlab/satellite/files/new_file_action.rb
new file mode 100644
index 00000000000..9fe5a38eb80
--- /dev/null
+++ b/lib/gitlab/satellite/files/new_file_action.rb
@@ -0,0 +1,44 @@
+require_relative 'file_action'
+
+module Gitlab
+  module Satellite
+    class NewFileAction < FileAction
+      # Updates the files content and creates a new commit for it
+      #
+      # Returns false if the ref has been updated while editing the file
+      # Returns false if committing the change fails
+      # Returns false if pushing from the satellite to Gitolite failed or was rejected
+      # Returns true otherwise
+      def commit!(content, commit_message, file_name)
+        in_locked_and_timed_satellite do |repo|
+          prepare_satellite!(repo)
+
+          # create target branch in satellite at the corresponding commit from Gitolite
+          repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}")
+
+          # update the file in the satellite's working dir
+          file_path_in_satellite = File.join(repo.working_dir, file_path, file_name)
+          File.open(file_path_in_satellite, 'w') { |f| f.write(content) }
+
+          # add new file
+          repo.add(file_path_in_satellite)
+
+          # commit the changes
+          # will raise CommandFailed when commit fails
+          repo.git.commit(raise: true, timeout: true, a: true, m: commit_message)
+
+
+          # push commit back to Gitolite
+          # will raise CommandFailed when push fails
+          repo.git.push({raise: true, timeout: true}, :origin, ref)
+
+          # everything worked
+          true
+        end
+      rescue Grit::Git::CommandFailed => ex
+        Gitlab::GitLogger.error(ex.message)
+        false
+      end
+    end
+  end
+end
-- 
GitLab