Commit 376dd3a3838622f73a444e5621257c831b284809

Authored by Dmitriy Zaporozhets
2 parents 2ee04b12 3083e5e4

Merge branch 'feature/create_file' of /home/git/repositories/gitlab/gitlabhq

app/contexts/base_context.rb
... ... @@ -17,4 +17,3 @@ class BaseContext
17 17 abilities.allowed?(object, action, subject)
18 18 end
19 19 end
20   -
... ...
app/contexts/files/base_context.rb 0 → 100644
... ... @@ -0,0 +1,31 @@
  1 +module Files
  2 + class BaseContext < ::BaseContext
  3 + attr_reader :ref, :path
  4 +
  5 + def initialize(project, user, params, ref, path = nil)
  6 + @project, @current_user, @params = project, user, params.dup
  7 + @ref = ref
  8 + @path = path
  9 + end
  10 +
  11 + private
  12 +
  13 + def error(message)
  14 + {
  15 + error: message,
  16 + status: :error
  17 + }
  18 + end
  19 +
  20 + def success
  21 + {
  22 + error: '',
  23 + status: :success
  24 + }
  25 + end
  26 +
  27 + def repository
  28 + project.repository
  29 + end
  30 + end
  31 +end
... ...
app/contexts/files/create_context.rb 0 → 100644
... ... @@ -0,0 +1,50 @@
  1 +module Files
  2 + class CreateContext < BaseContext
  3 + def execute
  4 + allowed = if project.protected_branch?(ref)
  5 + can?(current_user, :push_code_to_protected_branches, project)
  6 + else
  7 + can?(current_user, :push_code, project)
  8 + end
  9 +
  10 + unless allowed
  11 + return error("You are not allowed to create file in this branch")
  12 + end
  13 +
  14 + unless repository.branch_names.include?(ref)
  15 + return error("You can only create files if you are on top of a branch")
  16 + end
  17 +
  18 + file_name = params[:file_name]
  19 +
  20 + unless file_name =~ Gitlab::Regex.path_regex
  21 + return error("Your changes could not be commited, because file name contains not allowed characters")
  22 + end
  23 +
  24 + file_path = if path.blank?
  25 + file_name
  26 + else
  27 + File.join(path, file_name)
  28 + end
  29 +
  30 + blob = repository.blob_at(ref, file_path)
  31 +
  32 + if blob
  33 + return error("Your changes could not be commited, because file with such name exists")
  34 + end
  35 +
  36 + new_file_action = Gitlab::Satellite::NewFileAction.new(current_user, project, ref, path)
  37 + created_successfully = new_file_action.commit!(
  38 + params[:content],
  39 + params[:commit_message],
  40 + file_name,
  41 + )
  42 +
  43 + if created_successfully
  44 + success
  45 + else
  46 + error("Your changes could not be commited, because the file has been changed")
  47 + end
  48 + end
  49 + end
  50 +end
... ...
app/contexts/files/update_context.rb 0 → 100644
... ... @@ -0,0 +1,38 @@
  1 +module Files
  2 + class UpdateContext < BaseContext
  3 + def execute
  4 + allowed = if project.protected_branch?(ref)
  5 + can?(current_user, :push_code_to_protected_branches, project)
  6 + else
  7 + can?(current_user, :push_code, project)
  8 + end
  9 +
  10 + unless allowed
  11 + return error("You are not allowed to push into this branch")
  12 + end
  13 +
  14 + unless repository.branch_names.include?(ref)
  15 + return error("You can only create files if you are on top of a branch")
  16 + end
  17 +
  18 + blob = repository.blob_at(ref, path)
  19 +
  20 + unless blob
  21 + return error("You can only edit text files")
  22 + end
  23 +
  24 + new_file_action = Gitlab::Satellite::EditFileAction.new(current_user, project, ref, path)
  25 + created_successfully = new_file_action.commit!(
  26 + params[:content],
  27 + params[:commit_message],
  28 + params[:last_commit]
  29 + )
  30 +
  31 + if created_successfully
  32 + success
  33 + else
  34 + error("Your changes could not be commited, because the file has been changed")
  35 + end
  36 + end
  37 + end
  38 +end
... ...
app/controllers/projects/application_controller.rb
... ... @@ -23,4 +23,10 @@ class Projects::ApplicationController &lt; ApplicationController
23 23 'public_projects'
24 24 end
25 25 end
  26 +
  27 + def require_branch_head
  28 + unless @repository.branch_names.include?(@ref)
  29 + redirect_to project_tree_path(@project, @ref), notice: "This action is not allowed unless you are on top of a branch"
  30 + end
  31 + end
26 32 end
... ...
app/controllers/projects/base_tree_controller.rb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +class Projects::BaseTreeController < Projects::ApplicationController
  2 + include ExtractsPath
  3 +
  4 + before_filter :authorize_read_project!
  5 + before_filter :authorize_code_access!
  6 + before_filter :require_non_empty_project
  7 +end
  8 +
... ...
app/controllers/projects/edit_tree_controller.rb
1   -# Controller for edit a repository's file
2   -class Projects::EditTreeController < Projects::ApplicationController
3   - include ExtractsPath
4   -
5   - # Authorize
6   - before_filter :authorize_read_project!
7   - before_filter :authorize_code_access!
8   - before_filter :require_non_empty_project
9   -
10   - before_filter :edit_requirements, only: [:show, :update]
  1 +class Projects::EditTreeController < Projects::BaseTreeController
  2 + before_filter :require_branch_head
  3 + before_filter :blob
11 4  
12 5 def show
13 6 @last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha
14 7 end
15 8  
16 9 def update
17   - edit_file_action = Gitlab::Satellite::EditFileAction.new(current_user, @project, @ref, @path)
18   - updated_successfully = edit_file_action.commit!(
19   - params[:content],
20   - params[:commit_message],
21   - params[:last_commit]
22   - )
  10 + result = Files::UpdateContext.new(@project, current_user, params, @ref, @path).execute
23 11  
24   - if updated_successfully
25   - redirect_to project_blob_path(@project, @id), notice: "Your changes have been successfully commited"
  12 + if result[:status] == :success
  13 + flash[:notice] = "Your changes have been successfully commited"
  14 + redirect_to project_blob_path(@project, @id)
26 15 else
27   - flash[:notice] = "Your changes could not be commited, because the file has been changed"
  16 + flash[:alert] = result[:error]
28 17 render :show
29 18 end
30 19 end
31 20  
32 21 private
33 22  
34   - def edit_requirements
35   - @blob = @repository.blob_at(@commit.id, @path)
36   -
37   - unless @blob
38   - redirect_to project_blob_path(@project, @id), notice: "You can only edit text files"
39   - end
40   -
41   - allowed = if project.protected_branch? @ref
42   - can?(current_user, :push_code_to_protected_branches, project)
43   - else
44   - can?(current_user, :push_code, project)
45   - end
46   -
47   - return access_denied! unless allowed
48   -
49   - unless @repository.branch_names.include?(@ref)
50   - redirect_to project_blob_path(@project, @id), notice: "You can only edit this file if you are on top of a branch"
51   - end
  23 + def blob
  24 + @blob ||= @repository.blob_at(@commit.id, @path)
52 25 end
53 26 end
... ...
app/controllers/projects/new_tree_controller.rb 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +class Projects::NewTreeController < Projects::BaseTreeController
  2 + before_filter :require_branch_head
  3 +
  4 + def show
  5 + end
  6 +
  7 + def update
  8 + result = Files::CreateContext.new(@project, current_user, params, @ref, @path).execute
  9 +
  10 + if result[:status] == :success
  11 + flash[:notice] = "Your changes have been successfully commited"
  12 + redirect_to project_blob_path(@project, File.join(@id, params[:file_name]))
  13 + else
  14 + flash[:alert] = result[:error]
  15 + render :show
  16 + end
  17 + end
  18 +end
... ...
app/controllers/projects/tree_controller.rb
1 1 # Controller for viewing a repository's file structure
2   -class Projects::TreeController < Projects::ApplicationController
3   - include ExtractsPath
4   -
5   - # Authorize
6   - before_filter :authorize_read_project!
7   - before_filter :authorize_code_access!
8   - before_filter :require_non_empty_project
9   -
  2 +class Projects::TreeController < Projects::BaseTreeController
10 3 def show
11 4 return not_found! if tree.entries.empty?
12 5  
... ...
app/views/layouts/nav/_project.html.haml
... ... @@ -4,7 +4,7 @@
4 4 %i.icon-home
5 5  
6 6 - if project_nav_tab? :files
7   - = nav_link(controller: %w(tree blob blame edit_tree)) do
  7 + = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do
8 8 = link_to 'Files', project_tree_path(@project, @ref || @repository.root_ref)
9 9  
10 10 - if project_nav_tab? :commits
... ...
app/views/projects/new_tree/show.html.haml 0 → 100644
... ... @@ -0,0 +1,47 @@
  1 +%h3.page-title New file
  2 +%hr
  3 +.file-editor
  4 + = form_tag(project_new_tree_path(@project, @id), method: :put, class: "form-horizontal") do
  5 + .control-group.commit_message-group
  6 + = label_tag 'file_name', class: "control-label" do
  7 + File name
  8 + .controls
  9 + %span.monospace= @path[-1] == "/" ? @path : @path + "/"
  10 + &nbsp;
  11 + = text_field_tag 'file_name', params[:file_name], placeholder: "sample.rb", required: true
  12 + %span
  13 + &nbsp;
  14 + on
  15 + %span.label-branch= @ref
  16 +
  17 + .control-group.commit_message-group
  18 + = label_tag 'commit_message', class: "control-label" do
  19 + Commit message
  20 + .controls
  21 + = text_area_tag 'commit_message', params[:commit_message], placeholder: "Added new file", required: true, rows: 3
  22 +
  23 + .file-holder
  24 + .file-title
  25 + %i.icon-file
  26 + .file-content.code
  27 + %pre#editor= params[:content]
  28 +
  29 + .form-actions
  30 + = hidden_field_tag 'content', '', id: "file-content"
  31 + .commit-button-annotation
  32 + = button_tag "Commit changes", class: 'btn commit-btn js-commit-button btn-create'
  33 + .message
  34 + to branch
  35 + %strong= @ref
  36 + = link_to "Cancel", project_tree_path(@project, @id), class: "btn btn-cancel", confirm: leave_edit_message
  37 +
  38 +:javascript
  39 + ace.config.set("modePath", gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}/ace-src-noconflict")
  40 + var editor = ace.edit("editor");
  41 +
  42 + disableButtonIfEmptyField("#commit_message", ".js-commit-button");
  43 +
  44 + $(".js-commit-button").click(function(){
  45 + $("#file-content").val(editor.getValue());
  46 + $(".file-editor form").submit();
  47 + });
... ...
app/views/projects/tree/_tree.html.haml
... ... @@ -10,6 +10,12 @@
10 10 = link_to truncate(title, length: 40), project_tree_path(@project, path)
11 11 - else
12 12 = link_to title, '#'
  13 + - if @repository.branch_names.include?(@ref)
  14 + \/
  15 + %li
  16 + = link_to project_new_tree_path(@project, @id), title: 'New file', id: 'new-file-link' do
  17 + %small
  18 + %i.icon-plus.light
13 19  
14 20 %div#tree-content-holder.tree-content-holder
15 21 %table#tree-slider{class: "table_#{@hex_path} tree-table" }
... ...
config/routes.rb
... ... @@ -166,16 +166,18 @@ Gitlab::Application.routes.draw do
166 166 end
167 167  
168 168 scope module: :projects do
169   - resources :blob, only: [:show], constraints: {id: /.+/}
170   - resources :raw, only: [:show], constraints: {id: /.+/}
171   - resources :tree, only: [:show], constraints: {id: /.+/, format: /(html|js)/ }
172   - resources :edit_tree, only: [:show, :update], constraints: {id: /.+/}, path: 'edit'
173   - resources :commit, only: [:show], constraints: {id: /[[:alnum:]]{6,40}/}
174   - resources :commits, only: [:show], constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/}
175   - resources :compare, only: [:index, :create]
176   - resources :blame, only: [:show], constraints: {id: /.+/}
  169 + resources :blob, only: [:show], constraints: {id: /.+/}
  170 + resources :raw, only: [:show], constraints: {id: /.+/}
  171 + resources :tree, only: [:show], constraints: {id: /.+/, format: /(html|js)/ }
  172 + resources :edit_tree, only: [:show, :update], constraints: {id: /.+/}, path: 'edit'
  173 + resources :new_tree, only: [:show, :update], constraints: {id: /.+/}, path: 'new'
  174 + resources :commit, only: [:show], constraints: {id: /[[:alnum:]]{6,40}/}
  175 + resources :commits, only: [:show], constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/}
  176 + resources :compare, only: [:index, :create]
  177 + resources :blame, only: [:show], constraints: {id: /.+/}
177 178 resources :network, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/}
178   - resources :graphs, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/}
  179 + resources :graphs, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/}
  180 +
179 181 match "/compare/:from...:to" => "compare#show", as: "compare", via: [:get, :post], constraints: {from: /.+/, to: /.+/}
180 182  
181 183 resources :snippets, constraints: {id: /\d+/} do
... ...
features/project/source/browse_files.feature
... ... @@ -20,6 +20,10 @@ Feature: Project Browse files
20 20 And I click link "raw"
21 21 Then I should see raw file content
22 22  
  23 + Scenario: I can create file
  24 + Given I click on "new file" link in repo
  25 + Then I can see new file page
  26 +
23 27 @javascript
24 28 Scenario: I can edit file
25 29 Given I click on "Gemfile.lock" file in repo
... ...
features/steps/project/project_browse_files.rb
... ... @@ -3,42 +3,51 @@ class ProjectBrowseFiles &lt; Spinach::FeatureSteps
3 3 include SharedProject
4 4 include SharedPaths
5 5  
6   - Then 'I should see files from repository' do
  6 + step 'I should see files from repository' do
7 7 page.should have_content "app"
8 8 page.should have_content "history"
9 9 page.should have_content "Gemfile"
10 10 end
11 11  
12   - Then 'I should see files from repository for "8470d70"' do
  12 + step 'I should see files from repository for "8470d70"' do
13 13 current_path.should == project_tree_path(@project, "8470d70")
14 14 page.should have_content "app"
15 15 page.should have_content "history"
16 16 page.should have_content "Gemfile"
17 17 end
18 18  
19   - Given 'I click on "Gemfile.lock" file in repo' do
  19 + step 'I click on "Gemfile.lock" file in repo' do
20 20 click_link "Gemfile.lock"
21 21 end
22 22  
23   - Then 'I should see it content' do
  23 + step 'I should see it content' do
24 24 page.should have_content "DEPENDENCIES"
25 25 end
26 26  
27   - And 'I click link "raw"' do
  27 + step 'I click link "raw"' do
28 28 click_link "raw"
29 29 end
30 30  
31   - Then 'I should see raw file content' do
  31 + step 'I should see raw file content' do
32 32 page.source.should == ValidCommit::BLOB_FILE
33 33 end
34 34  
35   - Given 'I click button "edit"' do
  35 + step 'I click button "edit"' do
36 36 click_link 'edit'
37 37 end
38 38  
39   - Then 'I can edit code' do
  39 + step 'I can edit code' do
40 40 page.execute_script('editor.setValue("GitlabFileEditor")')
41 41 page.evaluate_script('editor.getValue()').should == "GitlabFileEditor"
42 42 end
43 43  
  44 + step 'I click on "new file" link in repo' do
  45 + click_link 'new-file-link'
  46 + end
  47 +
  48 + step 'I can see new file page' do
  49 + page.should have_content "New file"
  50 + page.should have_content "File name"
  51 + page.should have_content "Commit message"
  52 + end
44 53 end
... ...
lib/gitlab/satellite/edit_file_action.rb
... ... @@ -1,57 +0,0 @@
1   -module Gitlab
2   - module Satellite
3   - # GitLab server-side file update and commit
4   - class EditFileAction < Action
5   - attr_accessor :file_path, :ref
6   -
7   - def initialize(user, project, ref, file_path)
8   - super user, project, git_timeout: 10.seconds
9   - @file_path = file_path
10   - @ref = ref
11   - end
12   -
13   - # Updates the files content and creates a new commit for it
14   - #
15   - # Returns false if the ref has been updated while editing the file
16   - # Returns false if committing the change fails
17   - # Returns false if pushing from the satellite to Gitolite failed or was rejected
18   - # Returns true otherwise
19   - def commit!(content, commit_message, last_commit)
20   - return false unless can_edit?(last_commit)
21   -
22   - in_locked_and_timed_satellite do |repo|
23   - prepare_satellite!(repo)
24   -
25   - # create target branch in satellite at the corresponding commit from Gitolite
26   - repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}")
27   -
28   - # update the file in the satellite's working dir
29   - file_path_in_satellite = File.join(repo.working_dir, file_path)
30   - File.open(file_path_in_satellite, 'w') { |f| f.write(content) }
31   -
32   - # commit the changes
33   - # will raise CommandFailed when commit fails
34   - repo.git.commit(raise: true, timeout: true, a: true, m: commit_message)
35   -
36   -
37   - # push commit back to Gitolite
38   - # will raise CommandFailed when push fails
39   - repo.git.push({raise: true, timeout: true}, :origin, ref)
40   -
41   - # everything worked
42   - true
43   - end
44   - rescue Grit::Git::CommandFailed => ex
45   - Gitlab::GitLogger.error(ex.message)
46   - false
47   - end
48   -
49   - protected
50   -
51   - def can_edit?(last_commit)
52   - current_last_commit = Gitlab::Git::Commit.last_for_path(@project.repository, ref, file_path).sha
53   - last_commit == current_last_commit
54   - end
55   - end
56   - end
57   -end
lib/gitlab/satellite/files/edit_file_action.rb 0 → 100644
... ... @@ -0,0 +1,44 @@
  1 +require_relative 'file_action'
  2 +
  3 +module Gitlab
  4 + module Satellite
  5 + # GitLab server-side file update and commit
  6 + class EditFileAction < FileAction
  7 + # Updates the files content and creates a new commit for it
  8 + #
  9 + # Returns false if the ref has been updated while editing the file
  10 + # Returns false if committing the change fails
  11 + # Returns false if pushing from the satellite to Gitolite failed or was rejected
  12 + # Returns true otherwise
  13 + def commit!(content, commit_message, last_commit)
  14 + return false unless can_edit?(last_commit)
  15 +
  16 + in_locked_and_timed_satellite do |repo|
  17 + prepare_satellite!(repo)
  18 +
  19 + # create target branch in satellite at the corresponding commit from Gitolite
  20 + repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}")
  21 +
  22 + # update the file in the satellite's working dir
  23 + file_path_in_satellite = File.join(repo.working_dir, file_path)
  24 + File.open(file_path_in_satellite, 'w') { |f| f.write(content) }
  25 +
  26 + # commit the changes
  27 + # will raise CommandFailed when commit fails
  28 + repo.git.commit(raise: true, timeout: true, a: true, m: commit_message)
  29 +
  30 +
  31 + # push commit back to Gitolite
  32 + # will raise CommandFailed when push fails
  33 + repo.git.push({raise: true, timeout: true}, :origin, ref)
  34 +
  35 + # everything worked
  36 + true
  37 + end
  38 + rescue Grit::Git::CommandFailed => ex
  39 + Gitlab::GitLogger.error(ex.message)
  40 + false
  41 + end
  42 + end
  43 + end
  44 +end
... ...
lib/gitlab/satellite/files/file_action.rb 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +module Gitlab
  2 + module Satellite
  3 + class FileAction < Action
  4 + attr_accessor :file_path, :ref
  5 +
  6 + def initialize(user, project, ref, file_path)
  7 + super user, project, git_timeout: 10.seconds
  8 + @file_path = file_path
  9 + @ref = ref
  10 + end
  11 +
  12 + protected
  13 +
  14 + def can_edit?(last_commit)
  15 + current_last_commit = Gitlab::Git::Commit.last_for_path(@project.repository, ref, file_path).sha
  16 + last_commit == current_last_commit
  17 + end
  18 + end
  19 + end
  20 +end
... ...
lib/gitlab/satellite/files/new_file_action.rb 0 → 100644
... ... @@ -0,0 +1,44 @@
  1 +require_relative 'file_action'
  2 +
  3 +module Gitlab
  4 + module Satellite
  5 + class NewFileAction < FileAction
  6 + # Updates the files content and creates a new commit for it
  7 + #
  8 + # Returns false if the ref has been updated while editing the file
  9 + # Returns false if committing the change fails
  10 + # Returns false if pushing from the satellite to Gitolite failed or was rejected
  11 + # Returns true otherwise
  12 + def commit!(content, commit_message, file_name)
  13 + in_locked_and_timed_satellite do |repo|
  14 + prepare_satellite!(repo)
  15 +
  16 + # create target branch in satellite at the corresponding commit from Gitolite
  17 + repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}")
  18 +
  19 + # update the file in the satellite's working dir
  20 + file_path_in_satellite = File.join(repo.working_dir, file_path, file_name)
  21 + File.open(file_path_in_satellite, 'w') { |f| f.write(content) }
  22 +
  23 + # add new file
  24 + repo.add(file_path_in_satellite)
  25 +
  26 + # commit the changes
  27 + # will raise CommandFailed when commit fails
  28 + repo.git.commit(raise: true, timeout: true, a: true, m: commit_message)
  29 +
  30 +
  31 + # push commit back to Gitolite
  32 + # will raise CommandFailed when push fails
  33 + repo.git.push({raise: true, timeout: true}, :origin, ref)
  34 +
  35 + # everything worked
  36 + true
  37 + end
  38 + rescue Grit::Git::CommandFailed => ex
  39 + Gitlab::GitLogger.error(ex.message)
  40 + false
  41 + end
  42 + end
  43 + end
  44 +end
... ...