1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
|
# frozen_string_literal: true
require 'mime/types'
module API
class Repositories < ::API::Base
include PaginationParams
content_type :txt, 'text/plain'
helpers ::API::Helpers::HeadersHelpers
helpers do
params :release_params do
requires :version,
type: String,
regexp: Gitlab::Regex.unbounded_semver_regex,
desc: 'The version of the release, using the semantic versioning format',
documentation: { example: '1.0.0' }
optional :from,
type: String,
desc: 'The first commit in the range of commits to use for the changelog',
documentation: { example: 'ed899a2f4b50b4370feeea94676502b42383c746' }
optional :to,
type: String,
desc: 'The last commit in the range of commits to use for the changelog',
documentation: { example: '6104942438c14ec7bd21c6cd5bd995272b3faff6' }
optional :date,
type: DateTime,
desc: 'The date and time of the release',
documentation: { type: 'dateTime', example: '2021-09-20T11:50:22.001+00:00' }
optional :trailer,
type: String,
desc: 'The Git trailer to use for determining if commits are to be included in the changelog',
default: ::Repositories::ChangelogService::DEFAULT_TRAILER,
documentation: { example: 'Changelog' }
end
end
before { authorize! :read_code, user_project }
feature_category :source_code_management
params do
requires :id, types: [String, Integer],
desc: 'The ID or URL-encoded path of the project',
documentation: { example: 1 }
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
helpers do
include Gitlab::RepositoryArchiveRateLimiter
def handle_project_member_errors(errors)
if errors[:project_access].any?
error!(errors[:project_access], 422)
end
not_found!
end
def assign_blob_vars!(limit:)
authorize! :read_code, user_project
@repo = user_project.repository
begin
@blob = Gitlab::Git::Blob.raw(@repo, params[:sha], limit: limit)
rescue StandardError
not_found! 'Blob'
end
not_found! 'Blob' unless @blob
end
def fetch_target_project(current_user, user_project, params)
return user_project unless params[:from_project_id].present?
MergeRequestTargetProjectFinder
.new(current_user: current_user, source_project: user_project, project_feature: :repository)
.execute(include_routes: true).find_by_id(params[:from_project_id])
end
def compare_cache_key(current_user, user_project, target_project, params)
[
user_project,
target_project,
current_user,
:repository_compare,
target_project.repository.commit(params[:from]),
user_project.repository.commit(params[:to]),
params
]
end
end
desc 'Get a project repository tree' do
success Entities::TreeObject
end
params do
optional :ref, type: String,
desc: 'The name of a repository branch or tag, if not given the default branch is used',
documentation: { example: 'main' }
optional :path, type: String, desc: 'The path of the tree', documentation: { example: 'files/html' }
optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree'
use :pagination
optional :pagination, type: String, values: %w(legacy keyset none), default: 'legacy', desc: 'Specify the pagination method ("none" is only valid if "recursive" is true)'
given pagination: ->(value) { value == 'keyset' } do
optional :page_token, type: String,
desc: 'Record from which to start the keyset pagination',
documentation: { example: 'a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba' }
end
given pagination: ->(value) { value == 'none' } do
given recursive: ->(value) { value == false } do
validates([:pagination], except_values: { value: 'none', message: 'cannot be "none" unless "recursive" is true' })
end
end
end
get ':id/repository/tree', urgency: :low do
tree_finder = ::Repositories::TreeFinder.new(user_project, declared_params(include_missing: false))
not_found!("Tree") unless tree_finder.commit_exists?
tree = Gitlab::Pagination::GitalyKeysetPager.new(self, user_project).paginate(tree_finder)
present tree, with: Entities::TreeObject
end
desc 'Get raw blob contents from the repository'
params do
requires :sha, type: String,
desc: 'The commit hash', documentation: { example: '7d70e02340bac451f281cecf0a980907974bd8be' }
end
get ':id/repository/blobs/:sha/raw' do
# Load metadata enough to ask Workhorse to load the whole blob
assign_blob_vars!(limit: 0)
no_cache_headers
send_git_blob @repo, @blob
end
desc 'Get a blob from the repository'
params do
requires :sha, type: String,
desc: 'The commit hash', documentation: { example: '7d70e02340bac451f281cecf0a980907974bd8be' }
end
get ':id/repository/blobs/:sha' do
assign_blob_vars!(limit: -1)
{
size: @blob.size,
encoding: "base64",
content: Base64.strict_encode64(@blob.data),
sha: @blob.id
}
end
desc 'Get an archive of the repository'
params do
optional :sha, type: String,
desc: 'The commit sha of the archive to be downloaded',
documentation: { example: '7d70e02340bac451f281cecf0a980907974bd8be' }
optional :format, type: String, desc: 'The archive format', documentation: { example: 'tar.gz' }
optional :path, type: String,
desc: 'Subfolder of the repository to be downloaded', documentation: { example: 'files/archives' }
end
get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do
check_archive_rate_limit!(current_user, user_project) do
render_api_error!({ error: _('This archive has been requested too many times. Try again later.') }, 429)
end
not_acceptable! if Gitlab::HotlinkingDetector.intercept_hotlinking?(request)
send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true, path: params[:path]
rescue StandardError
not_found!('File')
end
desc 'Compare two branches, tags, or commits' do
success Entities::Compare
end
params do
requires :from, type: String,
desc: 'The commit, branch name, or tag name to start comparison',
documentation: { example: 'main' }
requires :to, type: String,
desc: 'The commit, branch name, or tag name to stop comparison',
documentation: { example: 'feature' }
optional :from_project_id, type: Integer, desc: 'The project to compare from', documentation: { example: 1 }
optional :straight, type: Boolean, desc: 'Comparison method, `true` for direct comparison between `from` and `to` (`from`..`to`), `false` to compare using merge base (`from`...`to`)', default: false
end
get ':id/repository/compare', urgency: :low do
target_project = fetch_target_project(current_user, user_project, params)
if target_project.blank?
render_api_error!("Target project id:#{params[:from_project_id]} is not a fork of project id:#{params[:id]}", 400)
end
unless can?(current_user, :read_code, target_project)
forbidden!("You don't have access to this fork's parent project")
end
cache_key = compare_cache_key(current_user, user_project, target_project, declared_params)
cache_action(cache_key, expires_in: 1.minute) do
compare = CompareService.new(user_project, params[:to]).execute(target_project, params[:from], straight: params[:straight])
if compare
present compare, with: Entities::Compare, current_user: current_user
else
not_found!("Ref")
end
end
end
desc 'Get repository contributors' do
success Entities::Contributor
end
params do
use :pagination
optional :order_by, type: String, values: %w[email name commits], default: 'commits', desc: 'Return contributors ordered by `name` or `email` or `commits`'
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
end
get ':id/repository/contributors' do
contributors = ::Kaminari.paginate_array(user_project.repository.contributors(order_by: params[:order_by], sort: params[:sort]))
present paginate(contributors), with: Entities::Contributor
rescue StandardError
not_found!
end
desc 'Get the common ancestor between commits' do
success Entities::Commit
end
params do
requires :refs, type: Array[String],
coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
desc: 'The refs to find the common ancestor of, multiple refs can be passed',
documentation: { example: 'main' }
end
get ':id/repository/merge_base' do
refs = params[:refs]
if refs.size < 2
render_api_error!('Provide at least 2 refs', 400)
end
merge_base = Gitlab::Git::MergeBase.new(user_project.repository, refs)
if merge_base.unknown_refs.any?
ref_noun = 'ref'.pluralize(merge_base.unknown_refs.size)
message = "Could not find #{ref_noun}: #{merge_base.unknown_refs.join(', ')}"
render_api_error!(message, 400)
end
if merge_base.commit
present merge_base.commit, with: Entities::Commit
else
not_found!("Merge Base")
end
end
desc 'Generates a changelog section for a release and returns it' do
detail 'This feature was introduced in GitLab 14.6'
success Entities::Changelog
end
params do
use :release_params
optional :config_file,
type: String,
documentation: { example: '.gitlab/changelog_config.yml' },
desc: "The file path to the configuration file as stored in the project's Git repository. Defaults to '.gitlab/changelog_config.yml'"
end
get ':id/repository/changelog' do
service = ::Repositories::ChangelogService.new(
user_project,
current_user,
**declared_params(include_missing: false)
)
changelog = service.execute(commit_to_changelog: false)
present changelog, with: Entities::Changelog
rescue Gitlab::Changelog::Error => ex
render_api_error!("Failed to generate the changelog: #{ex.message}", 422)
end
desc 'Generates a changelog section for a release and commits it in a changelog file' do
detail 'This feature was introduced in GitLab 13.9'
success code: 200
end
params do
use :release_params
optional :branch,
type: String,
desc: 'The branch to commit the changelog changes to',
documentation: { example: 'main' }
optional :config_file,
type: String,
documentation: { example: '.gitlab/changelog_config.yml' },
desc: "The file path to the configuration file as stored in the project's Git repository. Defaults to '.gitlab/changelog_config.yml'"
optional :file,
type: String,
desc: 'The file to commit the changelog changes to',
default: ::Repositories::ChangelogService::DEFAULT_FILE,
documentation: { example: 'CHANGELOG.md' }
optional :message,
type: String,
desc: 'The commit message to use when committing the changelog',
documentation: { example: 'Initial commit' }
end
post ':id/repository/changelog' do
branch = params[:branch] || user_project.default_branch_or_main
access = Gitlab::UserAccess.new(current_user, container: user_project)
unless access.can_push_to_branch?(branch)
forbidden!("You are not allowed to commit a changelog on this branch")
end
service = ::Repositories::ChangelogService.new(
user_project,
current_user,
**declared_params(include_missing: false)
)
service.execute(commit_to_changelog: true)
status(200)
rescue Gitlab::Changelog::Error => ex
render_api_error!("Failed to generate the changelog: #{ex.message}", 422)
end
end
end
end
API::Repositories.prepend_mod
|