debian-mirror-gitlab/spec/controllers/projects/commit_controller_spec.rb

619 lines
20 KiB
Ruby
Raw Permalink Normal View History

2019-07-31 22:56:46 +05:30
# frozen_string_literal: true
2016-08-24 12:49:21 +05:30
require 'spec_helper'
2016-04-02 18:10:28 +05:30
2023-05-27 22:25:52 +05:30
RSpec.describe Projects::CommitController, feature_category: :source_code_management do
2021-04-29 21:17:54 +05:30
include ProjectForksHelper
2019-12-21 20:55:43 +05:30
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:commit) { project.commit("master") }
2016-08-24 12:49:21 +05:30
let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
2019-03-02 22:35:43 +05:30
let(:master_pickable_commit) { project.commit(master_pickable_sha) }
2021-04-17 20:07:23 +05:30
let(:pipeline) { create(:ci_pipeline, project: project, ref: project.default_branch, sha: commit.sha, status: :running) }
let(:build) { create(:ci_build, pipeline: pipeline, status: :running) }
2016-08-24 12:49:21 +05:30
before do
sign_in(user)
2018-11-18 11:00:15 +05:30
project.add_maintainer(user)
2016-08-24 12:49:21 +05:30
end
2016-04-02 18:10:28 +05:30
describe 'GET show' do
render_views
2016-08-24 12:49:21 +05:30
def go(extra_params = {})
params = {
2017-08-17 22:00:37 +05:30
namespace_id: project.namespace,
project_id: project
2016-08-24 12:49:21 +05:30
}
2019-02-15 15:39:39 +05:30
get :show, params: params.merge(extra_params)
2016-08-24 12:49:21 +05:30
end
2016-04-02 18:10:28 +05:30
context 'with valid id' do
it 'responds with 200' do
2016-08-24 12:49:21 +05:30
go(id: commit.id)
2016-04-02 18:10:28 +05:30
expect(response).to be_ok
2023-04-23 21:23:45 +05:30
expect(assigns(:ref)).to eq commit.id
2016-04-02 18:10:28 +05:30
end
2021-04-17 20:07:23 +05:30
context 'when a pipeline job is running' do
before do
build.run
end
it 'defines last pipeline information' do
go(id: commit.id)
expect(assigns(:last_pipeline)).to have_attributes(id: pipeline.id, status: 'running')
expect(assigns(:last_pipeline_stages)).not_to be_empty
end
end
2016-04-02 18:10:28 +05:30
end
context 'with invalid id' do
it 'responds with 404' do
2016-08-24 12:49:21 +05:30
go(id: commit.id.reverse)
2016-04-02 18:10:28 +05:30
expect(response).to be_not_found
2023-04-23 21:23:45 +05:30
expect(assigns(:ref)).to be_nil
2016-04-02 18:10:28 +05:30
end
end
2022-06-21 17:19:12 +05:30
context 'with valid page' do
it 'responds with 200' do
go(id: commit.id, page: 1)
expect(response).to be_ok
end
end
context 'with invalid page' do
it 'does not return an error' do
go(id: commit.id, page: ['invalid'])
expect(response).to be_ok
end
end
it 'handles binary files' do
2016-08-24 12:49:21 +05:30
go(id: TestEnv::BRANCH_SHA['binary-encoding'], format: 'html')
2019-12-04 20:38:33 +05:30
expect(response).to be_successful
2016-08-24 12:49:21 +05:30
end
shared_examples "export as" do |format|
2016-09-13 17:45:13 +05:30
it "does generally work" do
2016-08-24 12:49:21 +05:30
go(id: commit.id, format: format)
2019-12-04 20:38:33 +05:30
expect(response).to be_successful
2016-08-24 12:49:21 +05:30
end
2016-09-13 17:45:13 +05:30
it "generates it" do
2016-08-24 12:49:21 +05:30
expect_any_instance_of(Commit).to receive(:"to_#{format}")
go(id: commit.id, format: format)
end
2016-09-13 17:45:13 +05:30
it "renders it" do
2016-08-24 12:49:21 +05:30
go(id: commit.id, format: format)
expect(response.body).to eq(commit.send(:"to_#{format}"))
end
2016-09-13 17:45:13 +05:30
it "does not escape Html" do
2017-09-10 17:25:29 +05:30
allow_any_instance_of(Commit).to receive(:"to_#{format}")
.and_return('HTML entities &<>" ')
2016-08-24 12:49:21 +05:30
go(id: commit.id, format: format)
expect(response.body).not_to include('&amp;')
expect(response.body).not_to include('&gt;')
expect(response.body).not_to include('&lt;')
expect(response.body).not_to include('&quot;')
end
end
describe "as diff" do
2018-11-08 19:23:39 +05:30
it "triggers workhorse to serve the request" do
go(id: commit.id, format: :diff)
2016-08-24 12:49:21 +05:30
2018-11-08 19:23:39 +05:30
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
2016-08-24 12:49:21 +05:30
end
end
describe "as patch" do
2016-09-13 17:45:13 +05:30
it "contains a git diff" do
2018-11-08 19:23:39 +05:30
go(id: commit.id, format: :patch)
2016-08-24 12:49:21 +05:30
2018-11-08 19:23:39 +05:30
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:")
2016-08-24 12:49:21 +05:30
end
end
context 'commit that removes a submodule' do
render_views
let(:fork_project) { create(:forked_project_with_submodules, visibility_level: 20) }
let(:commit) { fork_project.commit('remove-submodule') }
it 'renders it' do
2023-05-27 22:25:52 +05:30
get :show, params: { namespace_id: fork_project.namespace, project_id: fork_project, id: commit.id }
2016-08-24 12:49:21 +05:30
2019-12-04 20:38:33 +05:30
expect(response).to be_successful
2016-08-24 12:49:21 +05:30
end
end
2018-03-17 18:26:18 +05:30
context 'in the context of a merge_request' do
let(:merge_request) { create(:merge_request, source_project: project) }
let(:commit) { merge_request.commits.first }
it 'prepare diff notes in the context of the merge request' do
go(id: commit.id, merge_request_iid: merge_request.iid)
expect(assigns(:new_diff_note_attrs)).to eq({
2023-05-27 22:25:52 +05:30
noteable_type: 'MergeRequest',
noteable_id: merge_request.id,
commit_id: commit.id
})
2018-03-17 18:26:18 +05:30
expect(response).to be_ok
end
end
2016-08-24 12:49:21 +05:30
end
2018-03-17 18:26:18 +05:30
describe 'GET branches' do
it 'contains branch and tags information' do
commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
2023-05-27 22:25:52 +05:30
get :branches, params: { namespace_id: project.namespace, project_id: project, id: commit.id }
2018-03-17 18:26:18 +05:30
expect(assigns(:branches)).to include('master', 'feature_conflict')
expect(assigns(:branches_limit_exceeded)).to be_falsey
expect(assigns(:tags)).to include('v1.1.0')
expect(assigns(:tags_limit_exceeded)).to be_falsey
end
it 'returns :limit_exceeded when number of branches/tags reach a threshhold' do
2016-11-03 12:29:30 +05:30
commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
2018-03-17 18:26:18 +05:30
allow_any_instance_of(Repository).to receive(:branch_count).and_return(1001)
allow_any_instance_of(Repository).to receive(:tag_count).and_return(1001)
2016-11-03 12:29:30 +05:30
2023-05-27 22:25:52 +05:30
get :branches, params: { namespace_id: project.namespace, project_id: project, id: commit.id }
2018-03-17 18:26:18 +05:30
expect(assigns(:branches)).to eq([])
expect(assigns(:branches_limit_exceeded)).to be_truthy
expect(assigns(:tags)).to eq([])
expect(assigns(:tags_limit_exceeded)).to be_truthy
2016-08-24 12:49:21 +05:30
end
2022-04-04 11:22:00 +05:30
context 'when commit is not found' do
it 'responds with 404' do
get(:branches, params: {
namespace_id: project.namespace,
project_id: project,
id: '11111111111111111111111111111111111111'
})
expect(response).to be_not_found
end
end
2016-08-24 12:49:21 +05:30
end
describe 'POST revert' do
context 'when target branch is not provided' do
2016-09-13 17:45:13 +05:30
it 'renders the 404 page' do
2023-05-27 22:25:52 +05:30
post :revert, params: { namespace_id: project.namespace, project_id: project, id: commit.id }
2016-08-24 12:49:21 +05:30
2019-12-04 20:38:33 +05:30
expect(response).not_to be_successful
2020-03-13 15:44:24 +05:30
expect(response).to have_gitlab_http_status(:not_found)
2016-08-24 12:49:21 +05:30
end
end
2022-06-21 17:19:12 +05:30
context 'when the revert commit is missing' do
it 'renders the 404 page' do
2023-05-27 22:25:52 +05:30
post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: '1234567890' }
2022-06-21 17:19:12 +05:30
expect(response).not_to be_successful
expect(response).to have_gitlab_http_status(:not_found)
end
end
2016-08-24 12:49:21 +05:30
context 'when the revert was successful' do
2016-09-13 17:45:13 +05:30
it 'redirects to the commits page' do
2023-05-27 22:25:52 +05:30
post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: commit.id }
2016-08-24 12:49:21 +05:30
2017-09-10 17:25:29 +05:30
expect(response).to redirect_to project_commits_path(project, 'master')
2016-08-24 12:49:21 +05:30
expect(flash[:notice]).to eq('The commit has been successfully reverted.')
end
end
context 'when the revert failed' do
before do
2023-05-27 22:25:52 +05:30
post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: commit.id }
2016-08-24 12:49:21 +05:30
end
2016-09-13 17:45:13 +05:30
it 'redirects to the commit page' do
2016-08-24 12:49:21 +05:30
# Reverting a commit that has been already reverted.
2023-05-27 22:25:52 +05:30
post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: commit.id }
2016-08-24 12:49:21 +05:30
2017-09-10 17:25:29 +05:30
expect(response).to redirect_to project_commit_path(project, commit.id)
2023-05-27 22:25:52 +05:30
expect(flash[:alert]).to match('Commit revert failed:')
end
end
context 'in the context of a merge_request' do
let(:merge_request) { create(:merge_request, :merged, source_project: project) }
let(:repository) { project.repository }
before do
merge_commit_id = repository.merge(user,
merge_request.diff_head_sha,
merge_request,
'Test message')
repository.commit(merge_commit_id)
merge_request.update!(merge_commit_sha: merge_commit_id)
end
context 'when the revert was successful' do
it 'redirects to the merge request page' do
post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: merge_request.merge_commit_sha }
expect(response).to redirect_to project_merge_request_path(project, merge_request)
expect(flash[:notice]).to eq('The merge request has been successfully reverted.')
end
end
context 'when the revert failed' do
before do
post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: merge_request.merge_commit_sha }
end
it 'redirects to the merge request page' do
# Reverting a merge request that has been already reverted.
post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: merge_request.merge_commit_sha }
expect(response).to redirect_to project_merge_request_path(project, merge_request)
expect(flash[:alert]).to match('Merge request revert failed:')
end
2016-08-24 12:49:21 +05:30
end
end
end
describe 'POST cherry_pick' do
context 'when target branch is not provided' do
2016-09-13 17:45:13 +05:30
it 'renders the 404 page' do
2023-05-27 22:25:52 +05:30
post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, id: master_pickable_commit.id }
2016-08-24 12:49:21 +05:30
2019-12-04 20:38:33 +05:30
expect(response).not_to be_successful
2020-03-13 15:44:24 +05:30
expect(response).to have_gitlab_http_status(:not_found)
2016-08-24 12:49:21 +05:30
end
end
2022-06-21 17:19:12 +05:30
context 'when the cherry-pick commit is missing' do
it 'renders the 404 page' do
2023-05-27 22:25:52 +05:30
post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: '1234567890' }
2022-06-21 17:19:12 +05:30
expect(response).not_to be_successful
expect(response).to have_gitlab_http_status(:not_found)
end
end
2016-08-24 12:49:21 +05:30
context 'when the cherry-pick was successful' do
2016-09-13 17:45:13 +05:30
it 'redirects to the commits page' do
2023-05-27 22:25:52 +05:30
post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: master_pickable_commit.id }
2016-08-24 12:49:21 +05:30
2017-09-10 17:25:29 +05:30
expect(response).to redirect_to project_commits_path(project, 'master')
2018-11-20 20:47:30 +05:30
expect(flash[:notice]).to eq('The commit has been successfully cherry-picked into master.')
2016-08-24 12:49:21 +05:30
end
end
2016-08-24 12:49:21 +05:30
context 'when the cherry_pick failed' do
before do
2023-05-27 22:25:52 +05:30
post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: master_pickable_commit.id }
2016-08-24 12:49:21 +05:30
end
2016-09-13 17:45:13 +05:30
it 'redirects to the commit page' do
2016-08-24 12:49:21 +05:30
# Cherry-picking a commit that has been already cherry-picked.
2023-05-27 22:25:52 +05:30
post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: master_pickable_commit.id }
2016-08-24 12:49:21 +05:30
2017-09-10 17:25:29 +05:30
expect(response).to redirect_to project_commit_path(project, master_pickable_commit.id)
2023-05-27 22:25:52 +05:30
expect(flash[:alert]).to match('Commit cherry-pick failed:')
end
end
context 'in the context of a merge_request' do
let(:merge_request) { create(:merge_request, :merged, source_project: project) }
let(:repository) { project.repository }
before do
merge_commit_id = repository.merge(user,
merge_request.diff_head_sha,
merge_request,
'Test message')
repository.commit(merge_commit_id)
merge_request.update!(merge_commit_sha: merge_commit_id)
end
context 'when the cherry_pick was successful' do
it 'redirects to the merge request page' do
post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'merge-test', id: merge_request.merge_commit_sha }
expect(response).to redirect_to project_merge_request_path(project, merge_request)
expect(flash[:notice]).to eq('The merge request has been successfully cherry-picked into merge-test.')
end
end
context 'when the cherry_pick failed' do
before do
post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'merge-test', id: merge_request.merge_commit_sha }
end
it 'redirects to the merge request page' do
# Reverting a merge request that has been already cherry-picked.
post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'merge-test', id: merge_request.merge_commit_sha }
expect(response).to redirect_to project_merge_request_path(project, merge_request)
expect(flash[:alert]).to match('Merge request cherry-pick failed:')
end
2016-08-24 12:49:21 +05:30
end
end
2021-04-29 21:17:54 +05:30
context 'when a project has a fork' do
let(:project) { create(:project, :repository) }
let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) }
let(:target_project) { project }
let(:create_merge_request) { nil }
def send_request
2023-05-27 22:25:52 +05:30
post :cherry_pick, params: {
namespace_id: forked_project.namespace,
project_id: forked_project,
target_project_id: target_project.id,
start_branch: 'feature',
id: forked_project.commit.id,
create_merge_request: create_merge_request
}
2021-04-29 21:17:54 +05:30
end
def merge_request_url(source_project, branch)
project_new_merge_request_path(
source_project,
merge_request: {
target_project_id: project.id,
source_branch: branch,
target_branch: 'feature'
}
)
end
before do
forked_project.add_maintainer(user)
end
it 'successfully cherry picks a commit from fork to upstream project' do
send_request
expect(response).to redirect_to project_commits_path(project, 'feature')
expect(flash[:notice]).to eq('The commit has been successfully cherry-picked into feature.')
expect(project.commit('feature').message).to include(forked_project.commit.id)
end
context 'when the cherry pick is performed via merge request' do
let(:create_merge_request) { true }
it 'successfully cherry picks a commit from fork to a cherry pick branch' do
branch = forked_project.commit.cherry_pick_branch_name
send_request
expect(response).to redirect_to merge_request_url(project, branch)
expect(flash[:notice]).to start_with("The commit has been successfully cherry-picked into #{branch}")
expect(project.commit(branch).message).to include(forked_project.commit.id)
end
end
context 'when a user cannot push to upstream project' do
let(:create_merge_request) { true }
before do
project.add_reporter(user)
end
it 'cherry picks a commit to the fork' do
branch = forked_project.commit.cherry_pick_branch_name
send_request
expect(response).to redirect_to merge_request_url(forked_project, branch)
expect(flash[:notice]).to start_with("The commit has been successfully cherry-picked into #{branch}")
expect(project.commit('feature').message).not_to include(forked_project.commit.id)
expect(forked_project.commit(branch).message).to include(forked_project.commit.id)
end
end
context 'when a user do not have access to the target project' do
let(:target_project) { create(:project, :private) }
it 'cherry picks a commit to the fork' do
send_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
2016-08-24 12:49:21 +05:30
end
2023-06-20 00:43:36 +05:30
describe 'GET #diff_files' do
subject(:send_request) { get :diff_files, params: params }
let(:format) { :html }
let(:params) do
{
namespace_id: project.namespace,
project_id: project,
id: commit.id,
format: format
}
end
it 'renders diff files' do
send_request
expect(assigns(:diffs)).to be_a(Gitlab::Diff::FileCollection::Commit)
expect(assigns(:environment)).to be_nil
end
context 'when format is not html' do
let(:format) { :json }
it 'returns 404 page' do
send_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
2016-08-24 12:49:21 +05:30
describe 'GET diff_for_path' do
def diff_for_path(extra_params = {})
params = {
2017-08-17 22:00:37 +05:30
namespace_id: project.namespace,
project_id: project
2016-08-24 12:49:21 +05:30
}
2019-02-15 15:39:39 +05:30
get :diff_for_path, params: params.merge(extra_params)
2016-08-24 12:49:21 +05:30
end
let(:existing_path) { '.gitmodules' }
2016-11-03 12:29:30 +05:30
let(:commit2) { project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
2016-08-24 12:49:21 +05:30
context 'when the commit exists' do
context 'when the user has access to the project' do
context 'when the path exists in the diff' do
it 'enables diff notes' do
2016-11-03 12:29:30 +05:30
diff_for_path(id: commit2.id, old_path: existing_path, new_path: existing_path)
2016-08-24 12:49:21 +05:30
expect(assigns(:diff_notes_disabled)).to be_falsey
2023-05-27 22:25:52 +05:30
expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'Commit', commit_id: commit2.id)
2016-08-24 12:49:21 +05:30
end
it 'only renders the diffs for the path given' do
2016-09-13 17:45:13 +05:30
expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
meth.call(diffs)
2016-08-24 12:49:21 +05:30
end
2016-11-03 12:29:30 +05:30
diff_for_path(id: commit2.id, old_path: existing_path, new_path: existing_path)
2016-08-24 12:49:21 +05:30
end
end
context 'when the path does not exist in the diff' do
2017-09-10 17:25:29 +05:30
before do
diff_for_path(id: commit.id, old_path: existing_path.succ, new_path: existing_path.succ)
end
2016-08-24 12:49:21 +05:30
it 'returns a 404' do
2020-03-13 15:44:24 +05:30
expect(response).to have_gitlab_http_status(:not_found)
2016-08-24 12:49:21 +05:30
end
end
end
context 'when the user does not have access to the project' do
before do
project.team.truncate
diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
2020-03-13 15:44:24 +05:30
expect(response).to have_gitlab_http_status(:not_found)
2016-08-24 12:49:21 +05:30
end
end
end
context 'when the commit does not exist' do
2017-09-10 17:25:29 +05:30
before do
2018-03-17 18:26:18 +05:30
diff_for_path(id: commit.id.reverse, old_path: existing_path, new_path: existing_path)
2017-09-10 17:25:29 +05:30
end
2016-08-24 12:49:21 +05:30
it 'returns a 404' do
2020-03-13 15:44:24 +05:30
expect(response).to have_gitlab_http_status(:not_found)
2016-08-24 12:49:21 +05:30
end
2016-04-02 18:10:28 +05:30
end
end
2017-08-17 22:00:37 +05:30
describe 'GET pipelines' do
def get_pipelines(extra_params = {})
params = {
namespace_id: project.namespace,
project_id: project
}
2019-02-15 15:39:39 +05:30
get :pipelines, params: params.merge(extra_params)
2017-08-17 22:00:37 +05:30
end
context 'when the commit exists' do
context 'when the commit has pipelines' do
before do
2021-04-17 20:07:23 +05:30
build.run
2017-08-17 22:00:37 +05:30
end
context 'when rendering a HTML format' do
2021-04-17 20:07:23 +05:30
before do
2017-08-17 22:00:37 +05:30
get_pipelines(id: commit.id)
2021-04-17 20:07:23 +05:30
end
2017-08-17 22:00:37 +05:30
2021-04-17 20:07:23 +05:30
it 'shows pipelines' do
2017-08-17 22:00:37 +05:30
expect(response).to be_ok
end
2021-04-17 20:07:23 +05:30
it 'defines last pipeline information' do
expect(assigns(:last_pipeline)).to have_attributes(id: pipeline.id, status: 'running')
expect(assigns(:last_pipeline_stages)).not_to be_empty
end
2017-08-17 22:00:37 +05:30
end
context 'when rendering a JSON format' do
2021-09-30 23:02:18 +05:30
it 'responds with serialized pipelines', :aggregate_failures do
2017-08-17 22:00:37 +05:30
get_pipelines(id: commit.id, format: :json)
expect(response).to be_ok
2019-09-30 21:07:59 +05:30
expect(json_response['pipelines']).not_to be_empty
expect(json_response['count']['all']).to eq 1
2018-12-13 13:39:08 +05:30
expect(response).to include_pagination_headers
2017-08-17 22:00:37 +05:30
end
2021-09-30 23:02:18 +05:30
context 'with pagination' do
let!(:extra_pipeline) { create(:ci_pipeline, project: project, ref: project.default_branch, sha: commit.sha, status: :running) }
it 'paginates the result when ref is blank' do
allow(Ci::Pipeline).to receive(:default_per_page).and_return(1)
get_pipelines(id: commit.id, format: :json)
expect(json_response['pipelines'].count).to eq(1)
end
it 'paginates the result when ref is present' do
allow(Ci::Pipeline).to receive(:default_per_page).and_return(1)
get_pipelines(id: commit.id, ref: project.default_branch, format: :json)
expect(json_response['pipelines'].count).to eq(1)
end
end
2017-08-17 22:00:37 +05:30
end
end
end
context 'when the commit does not exist' do
before do
get_pipelines(id: 'e7a412c8da9f6d0081a633a4a402dde1c4694ebd')
end
it 'returns a 404' do
2020-03-13 15:44:24 +05:30
expect(response).to have_gitlab_http_status(:not_found)
2017-08-17 22:00:37 +05:30
end
end
end
2016-04-02 18:10:28 +05:30
end