Commit 909aee04 authored by Timo Furrer's avatar Timo Furrer Committed by Vasilii Iakliushin

Support executable flag in repository files API

This change adds support for a `execute_filemode` field in the
[Repository Files
API](https://docs.gitlab.com/ee/api/repository_files.html)
endpoints, much like it's already available in the
[Commits API](https://docs.gitlab.com/ee/api/commits.html).

The driver for this change is the `gitlab_repository_file` resource in
the Terraform GitLab Provider. This resource allows to manage a file
using the Repository Files API, but wasn't supporting to control the
executable flag on those files.

Changelog: added
MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83499
parent d74aea9c
......@@ -8,6 +8,7 @@ class Blob < SimpleDelegator
include BlobActiveModel
MODE_SYMLINK = '120000' # The STRING 120000 is the git-reported octal filemode for a symlink
MODE_EXECUTABLE = '100755' # The STRING 100755 is the git-reported octal filemode for an executable file
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
......@@ -181,6 +182,10 @@ class Blob < SimpleDelegator
mode == MODE_SYMLINK
end
def executable?
mode == MODE_EXECUTABLE
end
def extension
@extension ||= extname.downcase.delete('.')
end
......
......@@ -789,6 +789,12 @@ class Repository
def create_file(user, path, content, **options)
options[:actions] = [{ action: :create, file_path: path, content: content }]
execute_filemode = options.delete(:execute_filemode)
unless execute_filemode.nil?
options[:actions].push({ action: :chmod, file_path: path, execute_filemode: execute_filemode })
end
multi_action(user, **options)
end
......@@ -798,6 +804,12 @@ class Repository
options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }]
execute_filemode = options.delete(:execute_filemode)
unless execute_filemode.nil?
options[:actions].push({ action: :chmod, file_path: path, execute_filemode: execute_filemode })
end
multi_action(user, **options)
end
......
......@@ -19,6 +19,8 @@ module Files
@file_content = params[:file_content]
@file_content = Base64.decode64(@file_content) if params[:file_content_encoding] == 'base64'
@execute_filemode = params[:execute_filemode]
end
def file_has_changed?(path, commit_id)
......
......@@ -22,7 +22,8 @@ module Files
author_email: @author_email,
author_name: @author_name,
start_project: @start_project,
start_branch_name: @start_branch)
start_branch_name: @start_branch,
execute_filemode: @execute_filemode)
end
end
end
......@@ -10,7 +10,8 @@ module Files
author_email: @author_email,
author_name: @author_name,
start_project: @start_project,
start_branch_name: @start_branch)
start_branch_name: @start_branch,
execute_filemode: @execute_filemode)
end
private
......
......@@ -23,6 +23,8 @@ in the following table.
## Get file from repository
> The `execute_filemode` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83499) in GitLab 14.10.
Allows you to receive information about file in repository like name, size,
content. File content is Base64 encoded. This endpoint can be accessed
without authentication if the repository is publicly accessible.
......@@ -54,7 +56,8 @@ Example response:
"ref": "master",
"blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83",
"commit_id": "d5a3ff139356ce33e37e73add446f16869741b50",
"last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
"last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
"execute_filemode": false
}
```
......@@ -85,6 +88,7 @@ X-Gitlab-File-Path: app/models/key.rb
X-Gitlab-Last-Commit-Id: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
X-Gitlab-Ref: master
X-Gitlab-Size: 1476
X-Gitlab-Execute-Filemode: false
...
```
......@@ -155,6 +159,7 @@ X-Gitlab-File-Path: path/to/file.rb
X-Gitlab-Last-Commit-Id: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
X-Gitlab-Ref: master
X-Gitlab-Size: 1476
X-Gitlab-Execute-Filemode: false
...
```
......@@ -179,6 +184,8 @@ Like [Get file from repository](repository_files.md#get-file-from-repository) yo
## Create new file in repository
> The `execute_filemode` parameter was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83499) in GitLab 14.10.
This allows you to create a single file. For creating multiple files with a single request see the [commits API](commits.md#create-a-commit-with-multiple-files-and-actions).
```plaintext
......@@ -196,6 +203,7 @@ POST /projects/:id/repository/files/:file_path
| `author_name` | string | no | The commit author's name. |
| `content` | string | yes | The file's content. |
| `commit_message` | string | yes | The commit message. |
| `execute_filemode` | boolean | no | Enables or disables the `execute` flag on the file. Can be `true` or `false`. |
```shell
curl --request POST --header 'PRIVATE-TOKEN: <your_access_token>' \
......@@ -216,6 +224,8 @@ Example response:
## Update existing file in repository
> The `execute_filemode` parameter was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83499) in GitLab 14.10.
This allows you to update a single file. For updating multiple files with a single request see the [commits API](commits.md#create-a-commit-with-multiple-files-and-actions).
```plaintext
......@@ -234,6 +244,7 @@ PUT /projects/:id/repository/files/:file_path
| `content` | string | yes | The file's content. |
| `commit_message` | string | yes | The commit message. |
| `last_commit_id` | string | no | Last known file commit ID. |
| `execute_filemode` | boolean | no | Enables or disables the `execute` flag on the file. Can be `true` or `false`. |
```shell
curl --request PUT --header 'PRIVATE-TOKEN: <your_access_token>' \
......
......@@ -24,7 +24,8 @@ module API
file_content_encoding: attrs[:encoding],
author_email: attrs[:author_email],
author_name: attrs[:author_name],
last_commit_sha: attrs[:last_commit_id]
last_commit_sha: attrs[:last_commit_id],
execute_filemode: attrs[:execute_filemode]
}
end
......@@ -65,7 +66,8 @@ module API
ref: params[:ref],
blob_id: @blob.id,
commit_id: @commit.id,
last_commit_id: @repo.last_commit_id_for_path(@commit.sha, params[:file_path], literal_pathspec: true)
last_commit_id: @repo.last_commit_id_for_path(@commit.sha, params[:file_path], literal_pathspec: true),
execute_filemode: @blob.executable?
}
end
......@@ -83,6 +85,7 @@ module API
requires :content, type: String, desc: 'File content'
optional :encoding, type: String, values: %w[base64], desc: 'File encoding'
optional :last_commit_id, type: String, desc: 'Last known commit id for this file'
optional :execute_filemode, type: Boolean, desc: 'Enable / Disable the executable flag on the file path'
end
end
......
......@@ -229,6 +229,20 @@ RSpec.describe Blob do
end
end
describe '#executable?' do
it 'is true for executables' do
executable_blob = fake_blob(path: 'file', mode: '100755')
expect(executable_blob.executable?).to eq true
end
it 'is false for non-executables' do
non_executable_blob = fake_blob(path: 'file', mode: '100655')
expect(non_executable_blob.executable?).to eq false
end
end
describe '#extension' do
it 'returns the extension' do
blob = fake_blob(path: 'file.md')
......
......@@ -9,6 +9,7 @@ RSpec.describe API::Files do
let!(:project) { create(:project, :repository, namespace: user.namespace ) }
let(:guest) { create(:user) { |u| project.add_guest(u) } }
let(:file_path) { "files%2Fruby%2Fpopen%2Erb" }
let(:executable_file_path) { "files%2Fexecutables%2Fls" }
let(:rouge_file_path) { "%2e%2e%2f" }
let(:absolute_path) { "%2Fetc%2Fpasswd.rb" }
let(:invalid_file_message) { 'file_path should be a valid file path' }
......@@ -18,6 +19,12 @@ RSpec.describe API::Files do
}
end
let(:executable_ref_params) do
{
ref: 'with-executables'
}
end
let(:author_email) { 'user@example.org' }
let(:author_name) { 'John Doe' }
......@@ -219,9 +226,26 @@ RSpec.describe API::Files do
expect(json_response['file_name']).to eq('popen.rb')
expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
expect(json_response['content_sha256']).to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
expect(json_response['execute_filemode']).to eq(false)
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
end
context 'for executable file' do
it 'returns file attributes as json' do
get api(route(executable_file_path), api_user, **options), params: executable_ref_params
aggregate_failures 'testing response' do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['file_path']).to eq(CGI.unescape(executable_file_path))
expect(json_response['file_name']).to eq('ls')
expect(json_response['last_commit_id']).to eq('6b8dc4a827797aa025ff6b8f425e583858a10d4f')
expect(json_response['content_sha256']).to eq('2c74b1181ef780dfb692c030d3a0df6e0b624135c38a9344e56b9f80007b6191')
expect(json_response['execute_filemode']).to eq(true)
expect(Base64.decode64(json_response['content']).lines.first).to eq("#!/bin/sh\n")
end
end
end
it 'returns json when file has txt extension' do
file_path = "bar%2Fbranch-test.txt"
......@@ -386,6 +410,23 @@ RSpec.describe API::Files do
expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
expect(response.headers['X-Gitlab-Content-Sha256'])
.to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
expect(response.headers['X-Gitlab-Execute-Filemode']).to eq("false")
end
context 'for executable file' do
it 'returns file attributes in headers' do
head api(route(executable_file_path) + '/blame', current_user), params: executable_ref_params
aggregate_failures 'testing response' do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(executable_file_path))
expect(response.headers['X-Gitlab-File-Name']).to eq('ls')
expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('6b8dc4a827797aa025ff6b8f425e583858a10d4f')
expect(response.headers['X-Gitlab-Content-Sha256'])
.to eq('2c74b1181ef780dfb692c030d3a0df6e0b624135c38a9344e56b9f80007b6191')
expect(response.headers['X-Gitlab-Execute-Filemode']).to eq("true")
end
end
end
it 'returns 400 when file path is invalid' do
......@@ -642,6 +683,15 @@ RSpec.describe API::Files do
}
end
let(:executable_params) do
{
branch: "master",
content: "puts 8",
commit_message: "Added newfile",
execute_filemode: true
}
end
it 'returns 400 when file path is invalid' do
post api(route(rouge_file_path), user), params: params
......@@ -661,6 +711,18 @@ RSpec.describe API::Files do
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
expect(last_commit.author_name).to eq(user.name)
expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(false)
end
it "creates a new executable file in project repo" do
post api(route(file_path), user), params: executable_params
expect(response).to have_gitlab_http_status(:created)
expect(json_response["file_path"]).to eq(CGI.unescape(file_path))
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
expect(last_commit.author_name).to eq(user.name)
expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(true)
end
it "returns a 400 bad request if no mandatory params given" do
......@@ -820,6 +882,44 @@ RSpec.describe API::Files do
expect(last_commit.author_name).to eq(author_name)
end
end
context 'when specifying the execute_filemode' do
let(:executable_params) do
{
branch: 'master',
content: 'puts 8',
commit_message: 'Changed file',
execute_filemode: true
}
end
let(:non_executable_params) do
{
branch: 'with-executables',
content: 'puts 8',
commit_message: 'Changed file',
execute_filemode: false
}
end
it 'updates to executable file mode' do
put api(route(file_path), user), params: executable_params
aggregate_failures 'testing response' do
expect(response).to have_gitlab_http_status(:ok)
expect(project.repository.blob_at_branch(executable_params[:branch], CGI.unescape(file_path)).executable?).to eq(true)
end
end
it 'updates to non-executable file mode' do
put api(route(executable_file_path), user), params: non_executable_params
aggregate_failures 'testing response' do
expect(response).to have_gitlab_http_status(:ok)
expect(project.repository.blob_at_branch(non_executable_params[:branch], CGI.unescape(executable_file_path)).executable?).to eq(false)
end
end
end
end
describe "DELETE /projects/:id/repository/files" do
......
......@@ -81,7 +81,8 @@ module TestEnv
'compare-with-merge-head-target' => '2f1e176',
'trailers' => 'f0a5ed6',
'add_commit_with_5mb_subject' => '8cf8e80',
'blame-on-renamed' => '32c33da'
'blame-on-renamed' => '32c33da',
'with-executables' => '6b8dc4a'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment