summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDouwe Maan <douwe@gitlab.com>2017-12-21 09:22:32 +0000
committerDouwe Maan <douwe@gitlab.com>2017-12-21 09:22:32 +0000
commita24b6dbfca310ffa605f87eb376679c3c627e688 (patch)
treed8941fbf4e79ecb91a0b7b8d046076e4ffad833a
parent0a8fa500f06707ec14a044bdc652aaf0920bf55b (diff)
parentc927e57466b6c705891f09c95f566259d8e1ec0e (diff)
downloadgitlab-ce-a24b6dbfca310ffa605f87eb376679c3c627e688.tar.gz
Merge branch '38356-add-last_commit_sha-to-the-commit-api' into 'master'
Add new last_commit_id param for create commit endpoint Closes #38356 See merge request gitlab-org/gitlab-ce!15922
-rw-r--r--app/services/files/base_service.rb14
-rw-r--r--app/services/files/delete_service.rb10
-rw-r--r--app/services/files/multi_service.rb13
-rw-r--r--app/services/files/update_service.rb21
-rw-r--r--changelogs/unreleased/15922-validate-file-status-when-commiting-multiple-files.yml5
-rw-r--r--doc/api/commits.md1
-rw-r--r--doc/api/repository_files.md1
-rw-r--r--spec/services/files/delete_service_spec.rb64
-rw-r--r--spec/services/files/multi_service_spec.rb144
9 files changed, 253 insertions, 20 deletions
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 38231f66009..8d4b9f14780 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -1,11 +1,14 @@
module Files
class BaseService < Commits::CreateService
+ FileChangedError = Class.new(StandardError)
+
def initialize(*args)
super
@author_email = params[:author_email]
@author_name = params[:author_name]
@commit_message = params[:commit_message]
+ @last_commit_sha = params[:last_commit_sha]
@file_path = params[:file_path]
@previous_path = params[:previous_path]
@@ -13,5 +16,16 @@ module Files
@file_content = params[:file_content]
@file_content = Base64.decode64(@file_content) if params[:file_content_encoding] == 'base64'
end
+
+ def file_has_changed?(path, commit_id)
+ return false unless commit_id
+
+ last_commit = Gitlab::Git::Commit
+ .last_for_path(@start_project.repository, @start_branch, path)
+
+ return false unless last_commit
+
+ last_commit.sha != commit_id
+ end
end
end
diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb
index 7952e5c95d4..32a57484d4e 100644
--- a/app/services/files/delete_service.rb
+++ b/app/services/files/delete_service.rb
@@ -11,5 +11,15 @@ module Files
start_project: @start_project,
start_branch_name: @start_branch)
end
+
+ private
+
+ def validate!
+ super
+
+ if file_has_changed?(@file_path, @last_commit_sha)
+ raise FileChangedError, "You are attempting to delete a file that has been previously updated."
+ end
+ end
end
end
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index bfacc462847..98a3e83c130 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -1,5 +1,7 @@
module Files
class MultiService < Files::BaseService
+ UPDATE_FILE_ACTIONS = %w(update move delete).freeze
+
def create_commit!
repository.multi_action(
user: current_user,
@@ -20,6 +22,7 @@ module Files
params[:actions].each do |action|
validate_action!(action)
+ validate_file_status!(action)
end
end
@@ -28,5 +31,15 @@ module Files
raise_error("Unknown action '#{action[:action]}'")
end
end
+
+ def validate_file_status!(action)
+ return unless UPDATE_FILE_ACTIONS.include?(action[:action])
+
+ file_path = action[:previous_path] || action[:file_path]
+
+ if file_has_changed?(file_path, action[:last_commit_id])
+ raise_error("The file has changed since you started editing it: #{file_path}")
+ end
+ end
end
end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index bcca1386bed..1902d1cea72 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -1,13 +1,5 @@
module Files
class UpdateService < Files::BaseService
- FileChangedError = Class.new(StandardError)
-
- def initialize(*args)
- super
-
- @last_commit_sha = params[:last_commit_sha]
- end
-
def create_commit!
repository.update_file(current_user, @file_path, @file_content,
message: @commit_message,
@@ -21,21 +13,10 @@ module Files
private
- def file_has_changed?
- return false unless @last_commit_sha && last_commit
-
- @last_commit_sha != last_commit.sha
- end
-
- def last_commit
- @last_commit ||= Gitlab::Git::Commit
- .last_for_path(@start_project.repository, @start_branch, @file_path)
- end
-
def validate!
super
- if file_has_changed?
+ if file_has_changed?(@file_path, @last_commit_sha)
raise FileChangedError, "You are attempting to update a file that has changed since you started editing it."
end
end
diff --git a/changelogs/unreleased/15922-validate-file-status-when-commiting-multiple-files.yml b/changelogs/unreleased/15922-validate-file-status-when-commiting-multiple-files.yml
new file mode 100644
index 00000000000..db2bd6e692b
--- /dev/null
+++ b/changelogs/unreleased/15922-validate-file-status-when-commiting-multiple-files.yml
@@ -0,0 +1,5 @@
+---
+title: 'Validate file status when commiting multiple files'
+merge_request: 15922
+author:
+type: added
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 5a4a8d888b3..c9b72d4a1dd 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -84,6 +84,7 @@ POST /projects/:id/repository/commits
| `previous_path` | string | no | Original full path to the file being moved. Ex. `lib/class1.rb` |
| `content` | string | no | File content, required for all except `delete`. Optional for `move` |
| `encoding` | string | no | `text` or `base64`. `text` is default. |
+| `last_commit_id` | string | no | Last known file commit id. Will be only considered in update, move and delete actions. |
```bash
PAYLOAD=$(cat << 'JSON'
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index c517a38a8ba..a1a0b1b756c 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -151,3 +151,4 @@ Parameters:
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
- `commit_message` (required) - Commit message
+- `last_commit_id` (optional) - Last known file commit id
diff --git a/spec/services/files/delete_service_spec.rb b/spec/services/files/delete_service_spec.rb
new file mode 100644
index 00000000000..e9f8f0efe6b
--- /dev/null
+++ b/spec/services/files/delete_service_spec.rb
@@ -0,0 +1,64 @@
+require "spec_helper"
+
+describe Files::DeleteService do
+ subject { described_class.new(project, user, commit_params) }
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:file_path) { 'files/ruby/popen.rb' }
+ let(:branch_name) { project.default_branch }
+ let(:last_commit_sha) { nil }
+
+ let(:commit_params) do
+ {
+ file_path: file_path,
+ commit_message: "Delete File",
+ last_commit_sha: last_commit_sha,
+ start_project: project,
+ start_branch: project.default_branch,
+ branch_name: branch_name
+ }
+ end
+
+ shared_examples 'successfully deletes the file' do
+ it 'returns a hash with the :success status' do
+ results = subject.execute
+
+ expect(results[:status]).to match(:success)
+ end
+
+ it 'deletes the file' do
+ subject.execute
+
+ blob = project.repository.blob_at_branch(project.default_branch, file_path)
+
+ expect(blob).to be_nil
+ end
+ end
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe "#execute" do
+ context "when the file's last commit sha does not match the supplied last_commit_sha" do
+ let(:last_commit_sha) { "foo" }
+
+ it "returns a hash with the correct error message and a :error status " do
+ expect { subject.execute }
+ .to raise_error(Files::UpdateService::FileChangedError,
+ "You are attempting to delete a file that has been previously updated.")
+ end
+ end
+
+ context "when the file's last commit sha does match the supplied last_commit_sha" do
+ let(:last_commit_sha) { Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch, file_path).sha }
+
+ it_behaves_like 'successfully deletes the file'
+ end
+
+ context "when the last_commit_sha is not supplied" do
+ it_behaves_like 'successfully deletes the file'
+ end
+ end
+end
diff --git a/spec/services/files/multi_service_spec.rb b/spec/services/files/multi_service_spec.rb
new file mode 100644
index 00000000000..085a28d267f
--- /dev/null
+++ b/spec/services/files/multi_service_spec.rb
@@ -0,0 +1,144 @@
+require "spec_helper"
+
+describe Files::MultiService do
+ subject { described_class.new(project, user, commit_params) }
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:branch_name) { project.default_branch }
+ let(:original_file_path) { 'files/ruby/popen.rb' }
+ let(:new_file_path) { 'files/ruby/popen.rb' }
+ let(:action) { 'update' }
+
+ let!(:original_commit_id) do
+ Gitlab::Git::Commit.last_for_path(project.repository, branch_name, original_file_path).sha
+ end
+
+ let(:actions) do
+ [
+ {
+ action: action,
+ file_path: new_file_path,
+ previous_path: original_file_path,
+ content: 'New content',
+ last_commit_id: original_commit_id
+ }
+ ]
+ end
+
+ let(:commit_params) do
+ {
+ commit_message: "Update File",
+ branch_name: branch_name,
+ start_branch: branch_name,
+ actions: actions
+ }
+ end
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe '#execute' do
+ context 'with a valid action' do
+ it 'returns a hash with the :success status ' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:success)
+ end
+ end
+
+ context 'with an invalid action' do
+ let(:action) { 'rename' }
+
+ it 'returns a hash with the :error status ' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:error)
+ expect(results[:message]).to match(/Unknown action/)
+ end
+ end
+
+ describe 'Updating files' do
+ context 'when the file has been previously updated' do
+ before do
+ update_file(original_file_path)
+ end
+
+ it 'rejects the commit' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:error)
+ expect(results[:message]).to match(new_file_path)
+ end
+ end
+
+ context 'when the file have not been modified' do
+ it 'accepts the commit' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:success)
+ end
+ end
+ end
+
+ context 'when moving a file' do
+ let(:action) { 'move' }
+ let(:new_file_path) { 'files/ruby/new_popen.rb' }
+
+ context 'when original file has been updated' do
+ before do
+ update_file(original_file_path)
+ end
+
+ it 'rejects the commit' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:error)
+ expect(results[:message]).to match(original_file_path)
+ end
+ end
+
+ context 'when original file have not been updated' do
+ it 'moves the file' do
+ results = subject.execute
+ blob = project.repository.blob_at_branch(branch_name, new_file_path)
+
+ expect(results[:status]).to eq(:success)
+ expect(blob).to be_present
+ end
+ end
+ end
+
+ context 'when file status validation is skipped' do
+ let(:action) { 'create' }
+ let(:new_file_path) { 'files/ruby/new_file.rb' }
+
+ it 'does not check the last commit' do
+ expect(Gitlab::Git::Commit).not_to receive(:last_for_path)
+
+ subject.execute
+ end
+
+ it 'creates the file' do
+ subject.execute
+
+ blob = project.repository.blob_at_branch(branch_name, new_file_path)
+
+ expect(blob).to be_present
+ end
+ end
+ end
+
+ def update_file(path)
+ params = {
+ file_path: path,
+ start_branch: branch_name,
+ branch_name: branch_name,
+ commit_message: 'Update file',
+ file_content: 'New content'
+ }
+
+ Files::UpdateService.new(project, user, params).execute
+ end
+end