Commit 736d36d8 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 5426ca99
...@@ -9,9 +9,12 @@ ...@@ -9,9 +9,12 @@
<!-- Outline the tasks with issues that you need evaluate as a part of the implementation issue --> <!-- Outline the tasks with issues that you need evaluate as a part of the implementation issue -->
- [ ] Add task - [ ] Determine feasibility of the feature
- [ ] Add task - [ ] Create issue for implementation or update existing implementation issue description with implementation proposal
- [ ] Add task - [ ] Set weight on implementation issue
- [ ] If weight is greater than 5, break issue into smaller issues
- [ ] Add task
- [ ] Add task
### Risks and Implementation Considerations ### Risks and Implementation Considerations
......
/* global ace */ /* global ace */
import Editor from '~/editor/editor_lite';
import $ from 'jquery';
import setupCollapsibleInputs from './collapsible_input'; import setupCollapsibleInputs from './collapsible_input';
export default () => { let editor;
const editor = ace.edit('editor');
const initAce = () => {
editor = ace.edit('editor');
const form = document.querySelector('.snippet-form-holder form');
const content = document.querySelector('.snippet-file-content');
form.addEventListener('submit', () => {
content.value = editor.getValue();
});
};
$('.snippet-form-holder form').on('submit', () => { const initMonaco = () => {
$('.snippet-file-content').val(editor.getValue()); const editorEl = document.getElementById('editor');
const contentEl = document.querySelector('.snippet-file-content');
const fileNameEl = document.querySelector('.snippet-file-name');
const form = document.querySelector('.snippet-form-holder form');
editor = new Editor();
editor.createInstance({
el: editorEl,
blobPath: fileNameEl.value,
blobContent: contentEl.value,
}); });
fileNameEl.addEventListener('change', () => {
editor.updateModelLanguage(fileNameEl.value);
});
form.addEventListener('submit', () => {
contentEl.value = editor.getValue();
});
};
export const initEditor = () => {
if (window?.gon?.features?.monacoSnippets) {
initMonaco();
} else {
initAce();
}
setupCollapsibleInputs(); setupCollapsibleInputs();
}; };
export default () => {
initEditor();
};
...@@ -229,7 +229,14 @@ class Deployment < ApplicationRecord ...@@ -229,7 +229,14 @@ class Deployment < ApplicationRecord
end end
def link_merge_requests(relation) def link_merge_requests(relation)
select = relation.select(['merge_requests.id', id]).to_sql # NOTE: relation.select will perform column deduplication,
# when id == environment_id it will outputs 2 columns instead of 3
# i.e.:
# MergeRequest.select(1, 2).to_sql #=> SELECT 1, 2 FROM "merge_requests"
# MergeRequest.select(1, 1).to_sql #=> SELECT 1 FROM "merge_requests"
select = relation.select('merge_requests.id',
"#{id} as deployment_id",
"#{environment_id} as environment_id").to_sql
# We don't use `Gitlab::Database.bulk_insert` here so that we don't need to # We don't use `Gitlab::Database.bulk_insert` here so that we don't need to
# first pluck lots of IDs into memory. # first pluck lots of IDs into memory.
...@@ -238,7 +245,7 @@ class Deployment < ApplicationRecord ...@@ -238,7 +245,7 @@ class Deployment < ApplicationRecord
# for the same deployment, only inserting any missing merge requests. # for the same deployment, only inserting any missing merge requests.
DeploymentMergeRequest.connection.execute(<<~SQL) DeploymentMergeRequest.connection.execute(<<~SQL)
INSERT INTO #{DeploymentMergeRequest.table_name} INSERT INTO #{DeploymentMergeRequest.table_name}
(merge_request_id, deployment_id) (merge_request_id, deployment_id, environment_id)
#{select} #{select}
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
SQL SQL
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
.js-file-title.file-title-flex-parent .js-file-title.file-title-flex-parent
= f.text_field :file_name, placeholder: s_("Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby"), class: 'form-control snippet-file-name qa-snippet-file-name' = f.text_field :file_name, placeholder: s_("Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby"), class: 'form-control snippet-file-name qa-snippet-file-name'
.file-content.code .file-content.code
%pre#editor= @snippet.content %pre#editor{ data: { 'editor-loading': true } }= @snippet.content
= f.hidden_field :content, class: 'snippet-file-content' = f.hidden_field :content, class: 'snippet-file-content'
.form-group .form-group
......
---
title: Replaced ACE with Monaco editor for Snippets
merge_request: 25465
author:
type: added
---
title: Include full path to an upload in api response
merge_request: 23500
author: briankabiro
type: other
---
title: Don't track MR deployment multiple times
merge_request: 25537
author:
type: fixed
# frozen_string_literal: true
class AddEnvironmentIdToDeploymentMergeRequests < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :deployment_merge_requests, :environment_id, :integer, null: true
end
end
# frozen_string_literal: true
class AddEnvironmentIdFkToDeploymentMergeRequests < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key :deployment_merge_requests, :environments, column: :environment_id, on_delete: :cascade
end
def down
remove_foreign_key_if_exists :deployment_merge_requests, column: :environment_id
end
end
# frozen_string_literal: true
class AddEnvironmentIdMergeRequestIdUniqIdxToDeploymentMergeRequests < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :deployment_merge_requests, [:environment_id, :merge_request_id], unique: true, name: 'idx_environment_merge_requests_unique_index'
end
def down
remove_concurrent_index_by_name :deployment_merge_requests, 'idx_environment_merge_requests_unique_index'
end
end
...@@ -1372,7 +1372,9 @@ ActiveRecord::Schema.define(version: 2020_02_26_162723) do ...@@ -1372,7 +1372,9 @@ ActiveRecord::Schema.define(version: 2020_02_26_162723) do
create_table "deployment_merge_requests", id: false, force: :cascade do |t| create_table "deployment_merge_requests", id: false, force: :cascade do |t|
t.integer "deployment_id", null: false t.integer "deployment_id", null: false
t.integer "merge_request_id", null: false t.integer "merge_request_id", null: false
t.integer "environment_id"
t.index ["deployment_id", "merge_request_id"], name: "idx_deployment_merge_requests_unique_index", unique: true t.index ["deployment_id", "merge_request_id"], name: "idx_deployment_merge_requests_unique_index", unique: true
t.index ["environment_id", "merge_request_id"], name: "idx_environment_merge_requests_unique_index", unique: true
t.index ["merge_request_id"], name: "index_deployment_merge_requests_on_merge_request_id" t.index ["merge_request_id"], name: "index_deployment_merge_requests_on_merge_request_id"
end end
...@@ -4719,6 +4721,7 @@ ActiveRecord::Schema.define(version: 2020_02_26_162723) do ...@@ -4719,6 +4721,7 @@ ActiveRecord::Schema.define(version: 2020_02_26_162723) do
add_foreign_key "deployment_clusters", "clusters", on_delete: :cascade add_foreign_key "deployment_clusters", "clusters", on_delete: :cascade
add_foreign_key "deployment_clusters", "deployments", on_delete: :cascade add_foreign_key "deployment_clusters", "deployments", on_delete: :cascade
add_foreign_key "deployment_merge_requests", "deployments", on_delete: :cascade add_foreign_key "deployment_merge_requests", "deployments", on_delete: :cascade
add_foreign_key "deployment_merge_requests", "environments", name: "fk_a064ff4453", on_delete: :cascade
add_foreign_key "deployment_merge_requests", "merge_requests", on_delete: :cascade add_foreign_key "deployment_merge_requests", "merge_requests", on_delete: :cascade
add_foreign_key "deployments", "clusters", name: "fk_289bba3222", on_delete: :nullify add_foreign_key "deployments", "clusters", name: "fk_289bba3222", on_delete: :nullify
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
......
...@@ -1836,11 +1836,12 @@ Returned object: ...@@ -1836,11 +1836,12 @@ Returned object:
{ {
"alt": "dk", "alt": "dk",
"url": "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png", "url": "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png",
"full_path": "/namespace1/project1/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png",
"markdown": "![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)" "markdown": "![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)"
} }
``` ```
>**Note**: The returned `url` is relative to the project path. >**Note**: The returned `url` is relative to the project path. The returned `full_path` is the absolute path to the file.
In Markdown contexts, the link is automatically expanded when the format in In Markdown contexts, the link is automatically expanded when the format in
`markdown` is used. `markdown` is used.
......
...@@ -175,6 +175,21 @@ are loaded dynamically with webpack. ...@@ -175,6 +175,21 @@ are loaded dynamically with webpack.
Do not use `innerHTML`, `append()` or `html()` to set content. It opens up too many Do not use `innerHTML`, `append()` or `html()` to set content. It opens up too many
vulnerabilities. vulnerabilities.
## Avoid single-line conditional statements
Indentation is important when scanning code as it gives a quick indication of the existence of branches, loops, and return points.
This can help to quickly understand the control flow.
```javascript
// bad
if (isThingNull) return '';
// good
if (isThingNull) {
return '';
}
```
## ESLint ## ESLint
ESLint behaviour can be found in our [tooling guide](../tooling.md). ESLint behaviour can be found in our [tooling guide](../tooling.md).
......
...@@ -37,6 +37,8 @@ Design Management requires that projects are using ...@@ -37,6 +37,8 @@ Design Management requires that projects are using
[hashed storage](../../../administration/repository_storage_types.md#hashed-storage) [hashed storage](../../../administration/repository_storage_types.md#hashed-storage)
(the default storage type since v10.0). (the default storage type since v10.0).
If the requirements are not met, the **Designs** tab displays a message to the user.
### Feature Flags ### Feature Flags
- Reference Parsing - Reference Parsing
......
# frozen_string_literal: true
module API
module Entities
class ProjectUpload < Grape::Entity
include Gitlab::Routing
expose :markdown_name, as: :alt
expose :secure_url, as: :url
expose :full_path do |uploader|
show_project_uploads_path(
uploader.model,
uploader.secret,
uploader.filename
)
end
expose :markdown_link, as: :markdown
end
end
end
...@@ -494,7 +494,9 @@ module API ...@@ -494,7 +494,9 @@ module API
requires :file, type: File, desc: 'The file to be uploaded' # rubocop:disable Scalability/FileUploads requires :file, type: File, desc: 'The file to be uploaded' # rubocop:disable Scalability/FileUploads
end end
post ":id/uploads" do post ":id/uploads" do
UploadService.new(user_project, params[:file]).execute.to_h upload = UploadService.new(user_project, params[:file]).execute
present upload, with: Entities::ProjectUpload
end end
desc 'Get the users list of a project' do desc 'Get the users list of a project' do
......
...@@ -1293,6 +1293,15 @@ msgstr "" ...@@ -1293,6 +1293,15 @@ msgstr ""
msgid "AdminArea|Stopping jobs failed" msgid "AdminArea|Stopping jobs failed"
msgstr "" msgstr ""
msgid "AdminArea|Users statistics"
msgstr ""
msgid "AdminArea|Users total"
msgstr ""
msgid "AdminArea|Users with highest role"
msgstr ""
msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running." msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
msgstr "" msgstr ""
...@@ -6669,6 +6678,9 @@ msgstr "" ...@@ -6669,6 +6678,9 @@ msgstr ""
msgid "DesignManagement|The one place for your designs" msgid "DesignManagement|The one place for your designs"
msgstr "" msgstr ""
msgid "DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance."
msgstr ""
msgid "DesignManagement|Upload and view the latest designs for this issue. Consistent and easy to find, so everyone is up to date." msgid "DesignManagement|Upload and view the latest designs for this issue. Consistent and easy to find, so everyone is up to date."
msgstr "" msgstr ""
...@@ -19358,6 +19370,9 @@ msgstr "" ...@@ -19358,6 +19370,9 @@ msgstr ""
msgid "The number of times an upload record could not find its file" msgid "The number of times an upload record could not find its file"
msgstr "" msgstr ""
msgid "The one place for your designs"
msgstr ""
msgid "The passphrase required to decrypt the private key. This is optional and the value is encrypted at rest." msgid "The passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
msgstr "" msgstr ""
......
...@@ -52,7 +52,7 @@ module QA ...@@ -52,7 +52,7 @@ module QA
private private
def text_area def text_area
find('#editor>textarea', visible: false) find('#editor textarea', visible: false)
end end
end end
end end
......
...@@ -2,11 +2,10 @@ ...@@ -2,11 +2,10 @@
require 'spec_helper' require 'spec_helper'
describe 'Projects > Snippets > Create Snippet', :js do shared_examples_for 'snippet editor' do
include DropzoneHelper before do
stub_feature_flags(monaco_snippets: flag)
let_it_be(:user) { create(:user) } end
let_it_be(:project) { create(:project, :public) }
def description_field def description_field
find('.js-description-input').find('input,textarea') find('.js-description-input').find('input,textarea')
...@@ -20,7 +19,8 @@ describe 'Projects > Snippets > Create Snippet', :js do ...@@ -20,7 +19,8 @@ describe 'Projects > Snippets > Create Snippet', :js do
fill_in 'project_snippet_description', with: 'My Snippet **Description**' fill_in 'project_snippet_description', with: 'My Snippet **Description**'
page.within('.file-editor') do page.within('.file-editor') do
find('.ace_text-input', visible: false).send_keys('Hello World!') el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false)
el.send_keys 'Hello World!'
end end
end end
...@@ -33,6 +33,7 @@ describe 'Projects > Snippets > Create Snippet', :js do ...@@ -33,6 +33,7 @@ describe 'Projects > Snippets > Create Snippet', :js do
visit project_snippets_path(project) visit project_snippets_path(project)
click_on('New snippet') click_on('New snippet')
wait_for_requests
end end
it 'shows collapsible description input' do it 'shows collapsible description input' do
...@@ -111,3 +112,22 @@ describe 'Projects > Snippets > Create Snippet', :js do ...@@ -111,3 +112,22 @@ describe 'Projects > Snippets > Create Snippet', :js do
end end
end end
end end
describe 'Projects > Snippets > Create Snippet', :js do
include DropzoneHelper
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
context 'when using Monaco' do
it_behaves_like "snippet editor" do
let(:flag) { true }
end
end
context 'when using ACE' do
it_behaves_like "snippet editor" do
let(:flag) { false }
end
end
end
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe 'User creates snippet', :js do shared_examples_for 'snippet editor' do
let(:user) { create(:user) }
def description_field def description_field
find('.js-description-input').find('input,textarea') find('.js-description-input').find('input,textarea')
end end
...@@ -12,6 +10,7 @@ describe 'User creates snippet', :js do ...@@ -12,6 +10,7 @@ describe 'User creates snippet', :js do
before do before do
stub_feature_flags(allow_possible_spam: false) stub_feature_flags(allow_possible_spam: false)
stub_feature_flags(snippets_vue: false) stub_feature_flags(snippets_vue: false)
stub_feature_flags(monaco_snippets: flag)
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
Gitlab::CurrentSettings.update!( Gitlab::CurrentSettings.update!(
...@@ -33,7 +32,8 @@ describe 'User creates snippet', :js do ...@@ -33,7 +32,8 @@ describe 'User creates snippet', :js do
find('#personal_snippet_visibility_level_20').set(true) find('#personal_snippet_visibility_level_20').set(true)
page.within('.file-editor') do page.within('.file-editor') do
find('.ace_text-input', visible: false).send_keys 'Hello World!' el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false)
el.send_keys 'Hello World!'
end end
end end
...@@ -80,3 +80,19 @@ describe 'User creates snippet', :js do ...@@ -80,3 +80,19 @@ describe 'User creates snippet', :js do
end end
end end
end end
describe 'User creates snippet', :js do
let_it_be(:user) { create(:user) }
context 'when using Monaco' do
it_behaves_like "snippet editor" do
let(:flag) { true }
end
end
context 'when using ACE' do
it_behaves_like "snippet editor" do
let(:flag) { false }
end
end
end
...@@ -2,13 +2,10 @@ ...@@ -2,13 +2,10 @@
require 'spec_helper' require 'spec_helper'
describe 'User creates snippet', :js do shared_examples_for 'snippet editor' do
include DropzoneHelper
let(:user) { create(:user) }
before do before do
stub_feature_flags(snippets_vue: false) stub_feature_flags(snippets_vue: false)
stub_feature_flags(monaco_snippets: flag)
sign_in(user) sign_in(user)
visit new_snippet_path visit new_snippet_path
end end
...@@ -25,7 +22,8 @@ describe 'User creates snippet', :js do ...@@ -25,7 +22,8 @@ describe 'User creates snippet', :js do
fill_in 'personal_snippet_description', with: 'My Snippet **Description**' fill_in 'personal_snippet_description', with: 'My Snippet **Description**'
page.within('.file-editor') do page.within('.file-editor') do
find('.ace_text-input', visible: false).send_keys 'Hello World!' el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false)
el.send_keys 'Hello World!'
end end
end end
...@@ -109,7 +107,8 @@ describe 'User creates snippet', :js do ...@@ -109,7 +107,8 @@ describe 'User creates snippet', :js do
fill_in 'personal_snippet_title', with: 'My Snippet Title' fill_in 'personal_snippet_title', with: 'My Snippet Title'
page.within('.file-editor') do page.within('.file-editor') do
find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name' find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name'
find('.ace_text-input', visible: false).send_keys 'Hello World!' el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false)
el.send_keys 'Hello World!'
end end
click_button 'Create snippet' click_button 'Create snippet'
...@@ -120,3 +119,21 @@ describe 'User creates snippet', :js do ...@@ -120,3 +119,21 @@ describe 'User creates snippet', :js do
expect(page).to have_content('Hello World!') expect(page).to have_content('Hello World!')
end end
end end
describe 'User creates snippet', :js do
include DropzoneHelper
let_it_be(:user) { create(:user) }
context 'when using Monaco' do
it_behaves_like "snippet editor" do
let(:flag) { true }
end
end
context 'when using ACE' do
it_behaves_like "snippet editor" do
let(:flag) { false }
end
end
end
import Editor from '~/editor/editor_lite';
import { initEditor } from '~/snippet/snippet_bundle';
import { setHTMLFixture } from 'helpers/fixtures';
jest.mock('~/editor/editor_lite', () => jest.fn());
describe('Snippet editor', () => {
describe('Monaco editor for Snippets', () => {
let oldGon;
let editorEl;
let contentEl;
let fileNameEl;
let form;
const mockName = 'foo.bar';
const mockContent = 'Foo Bar';
const updatedMockContent = 'New Foo Bar';
const mockEditor = {
createInstance: jest.fn(),
updateModelLanguage: jest.fn(),
getValue: jest.fn().mockReturnValueOnce(updatedMockContent),
};
Editor.mockImplementation(() => mockEditor);
function setUpFixture(name, content) {
setHTMLFixture(`
<div class="snippet-form-holder">
<form>
<input class="snippet-file-name" type="text" value="${name}">
<input class="snippet-file-content" type="hidden" value="${content}">
<pre id="editor"></pre>
</form>
</div>
`);
}
function bootstrap(name = '', content = '') {
setUpFixture(name, content);
editorEl = document.getElementById('editor');
contentEl = document.querySelector('.snippet-file-content');
fileNameEl = document.querySelector('.snippet-file-name');
form = document.querySelector('.snippet-form-holder form');
initEditor();
}
function createEvent(name) {
return new Event(name, {
view: window,
bubbles: true,
cancelable: true,
});
}
beforeEach(() => {
oldGon = window.gon;
window.gon = { features: { monacoSnippets: true } };
bootstrap(mockName, mockContent);
});
afterEach(() => {
window.gon = oldGon;
});
it('correctly initializes Editor', () => {
expect(mockEditor.createInstance).toHaveBeenCalledWith({
el: editorEl,
blobPath: mockName,
blobContent: mockContent,
});
});
it('listens to file name changes and updates syntax highlighting of code', () => {
expect(mockEditor.updateModelLanguage).not.toHaveBeenCalled();
const event = createEvent('change');
fileNameEl.value = updatedMockContent;
fileNameEl.dispatchEvent(event);
expect(mockEditor.updateModelLanguage).toHaveBeenCalledWith(updatedMockContent);
});
it('listens to form submit event and populates the hidden field with most recent version of the content', () => {
expect(contentEl.value).toBe(mockContent);
const event = createEvent('submit');
form.dispatchEvent(event);
expect(contentEl.value).toBe(updatedMockContent);
});
});
});
...@@ -195,6 +195,7 @@ describe Gitlab::Profiler do ...@@ -195,6 +195,7 @@ describe Gitlab::Profiler do
describe '.print_by_total_time' do describe '.print_by_total_time' do
let(:stdout) { StringIO.new } let(:stdout) { StringIO.new }
let(:regexp) { /^\s+\d+\.\d+\s+(\d+\.\d+)/ }
let(:output) do let(:output) do
stdout.rewind stdout.rewind
...@@ -202,6 +203,8 @@ describe Gitlab::Profiler do ...@@ -202,6 +203,8 @@ describe Gitlab::Profiler do
end end
let_it_be(:result) do let_it_be(:result) do
Thread.new { sleep 1 }
RubyProf.profile do RubyProf.profile do
sleep 0.1 sleep 0.1
1.to_s 1.to_s
...@@ -212,23 +215,22 @@ describe Gitlab::Profiler do ...@@ -212,23 +215,22 @@ describe Gitlab::Profiler do
stub_const('STDOUT', stdout) stub_const('STDOUT', stdout)
end end
it 'prints a profile result sorted by total time', quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/206907' do it 'prints a profile result sorted by total time' do
described_class.print_by_total_time(result) described_class.print_by_total_time(result)
total_times =
output
.scan(/^\s+\d+\.\d+\s+(\d+\.\d+)/)
.map { |(total)| total.to_f }
expect(output).to include('Kernel#sleep') expect(output).to include('Kernel#sleep')
if total_times != total_times.sort.reverse thread_profiles = output.split('Sort by: total_time').select { |x| x =~ regexp }
warn "Profiler test failed, output is:"
warn output
end
expect(total_times).to eq(total_times.sort.reverse) thread_profiles.each do |profile|
expect(total_times).not_to eq(total_times.uniq) total_times =
profile
.scan(regexp)
.map { |(total)| total.to_f }
expect(total_times).to eq(total_times.sort.reverse)
expect(total_times).not_to eq(total_times.uniq)
end
end end
it 'accepts a max_percent option' do it 'accepts a max_percent option' do
......
...@@ -1250,6 +1250,8 @@ describe API::Projects do ...@@ -1250,6 +1250,8 @@ describe API::Projects do
expect(json_response['alt']).to eq("dk") expect(json_response['alt']).to eq("dk")
expect(json_response['url']).to start_with("/uploads/") expect(json_response['url']).to start_with("/uploads/")
expect(json_response['url']).to end_with("/dk.png") expect(json_response['url']).to end_with("/dk.png")
expect(json_response['full_path']).to start_with("/#{project.namespace.path}/#{project.path}/uploads")
end end
end end
......
...@@ -133,6 +133,34 @@ describe Deployments::LinkMergeRequestsService do ...@@ -133,6 +133,34 @@ describe Deployments::LinkMergeRequestsService do
expect(deploy.merge_requests).to include(mr1, picked_mr) expect(deploy.merge_requests).to include(mr1, picked_mr)
end end
it "doesn't link the same merge_request twice" do
create(:merge_request, :merged, merge_commit_sha: mr1_merge_commit_sha,
source_project: project)
picked_mr = create(:merge_request, :merged, merge_commit_sha: '123abc',
source_project: project)
# the first MR includes c1c67abba which is a cherry-pick of the fake picked_mr merge request
create(:track_mr_picking_note, noteable: picked_mr, project: project, commit_id: 'c1c67abbaf91f624347bb3ae96eabe3a1b742478')
environment = create(:environment, project: project)
old_deploy =
create(:deployment, :success, project: project, environment: environment)
# manually linking all the MRs to the old_deploy
old_deploy.link_merge_requests(project.merge_requests)
deploy =
create(:deployment, :success, project: project, environment: environment)
described_class.new(deploy).link_merge_requests_for_range(
first_deployment_sha,
mr1_merge_commit_sha
)
expect(deploy.merge_requests).to be_empty
end
context 'when :track_mr_picking feature flag is disabled' do context 'when :track_mr_picking feature flag is disabled' do
before do before do
stub_feature_flags(track_mr_picking: false) stub_feature_flags(track_mr_picking: false)
......
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