summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js7
-rw-r--r--app/assets/javascripts/droplab/keyboard.js2
-rw-r--r--app/assets/javascripts/droplab/plugins/ajax_filter.js3
-rw-r--r--app/assets/javascripts/environments/components/environment.vue95
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue100
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js17
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js3
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js6
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js8
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js3
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/controllers/projects/environments_controller.rb2
-rw-r--r--app/controllers/projects/variables_controller.rb3
-rw-r--r--app/helpers/avatars_helper.rb20
-rw-r--r--app/helpers/notes_helper.rb2
-rw-r--r--app/models/ci/build.rb34
-rw-r--r--app/models/ci/variable.rb5
-rw-r--r--app/models/deployment.rb5
-rw-r--r--app/models/diff_discussion.rb1
-rw-r--r--app/models/diff_note.rb18
-rw-r--r--app/models/discussion.rb6
-rw-r--r--app/models/environment.rb16
-rw-r--r--app/models/merge_request.rb22
-rw-r--r--app/models/merge_request_diff.rb2
-rw-r--r--app/models/project.rb13
-rw-r--r--app/models/user.rb2
-rw-r--r--app/services/ci/create_pipeline_service.rb9
-rw-r--r--app/services/discussions/update_diff_position_service.rb41
-rw-r--r--app/services/gravatar_service.rb21
-rw-r--r--app/services/merge_requests/create_service.rb9
-rw-r--r--app/services/notes/diff_position_update_service.rb33
-rw-r--r--app/uploaders/artifact_uploader.rb28
-rw-r--r--app/uploaders/gitlab_uploader.rb6
-rw-r--r--app/validators/dynamic_path_validator.rb2
-rw-r--r--app/views/discussions/_jump_to_next.html.haml4
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml2
-rw-r--r--app/views/projects/registry/repositories/index.html.haml72
-rw-r--r--app/views/projects/variables/_content.html.haml5
-rw-r--r--app/views/projects/variables/_form.html.haml9
-rw-r--r--app/views/projects/variables/_table.html.haml3
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml29
-rw-r--r--app/views/shared/issuable/_user_dropdown_item.html.haml11
-rw-r--r--changelogs/unreleased/24196-protected-variables.yml5
-rw-r--r--changelogs/unreleased/30651-improve-container-registry-description.yml4
-rw-r--r--changelogs/unreleased/31644-make-cookie-sessions-unique.yml4
-rw-r--r--changelogs/unreleased/aliyun-backup-provider.yml4
-rw-r--r--changelogs/unreleased/dm-comment-on-mr-commit-discussion.yml4
-rw-r--r--changelogs/unreleased/dm-fix-jump-button.yml4
-rw-r--r--changelogs/unreleased/dm-gravatar-username.yml4
-rw-r--r--changelogs/unreleased/migrate-artifacts-to-a-new-path.yml4
-rw-r--r--changelogs/unreleased/winh-current-user-filter.yml4
-rw-r--r--changelogs/unreleased/zj-realtime-env-list.yml4
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--config/initializers/session_store.rb8
-rw-r--r--db/migrate/20170524161101_add_protected_to_ci_variables.rb15
-rw-r--r--db/post_migrate/20170523083112_migrate_old_artifacts.rb72
-rw-r--r--db/schema.rb3
-rw-r--r--doc/api/build_variables.md28
-rw-r--r--doc/ci/variables/README.md24
-rw-r--r--doc/customization/libravatar.md4
-rw-r--r--doc/development/i18n_guide.md3
-rw-r--r--doc/raketasks/backup_restore.md2
-rw-r--r--doc/user/project/container_registry.md2
-rw-r--r--doc/user/project/img/container_registry_panel.pngbin32310 -> 0 bytes
-rw-r--r--lib/api/entities.rb1
-rw-r--r--lib/api/helpers.rb10
-rw-r--r--lib/api/jobs.rb10
-rw-r--r--lib/api/runner.rb11
-rw-r--r--lib/api/v3/builds.rb10
-rw-r--r--lib/api/variables.rb4
-rw-r--r--lib/backup/artifacts.rb2
-rw-r--r--lib/ci/api/builds.rb8
-rw-r--r--lib/gitlab/encoding_helper.rb62
-rw-r--r--lib/gitlab/etag_caching/router.rb8
-rw-r--r--lib/gitlab/git/blame.rb2
-rw-r--r--lib/gitlab/git/blob.rb2
-rw-r--r--lib/gitlab/git/commit.rb2
-rw-r--r--lib/gitlab/git/diff.rb2
-rw-r--r--lib/gitlab/git/encoding_helper.rb64
-rw-r--r--lib/gitlab/git/ref.rb2
-rw-r--r--lib/gitlab/git/tree.rb2
-rw-r--r--lib/gitlab/utils.rb8
-rw-r--r--lib/tasks/gettext.rake8
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb5
-rw-r--r--spec/factories/ci/variables.rb4
-rw-r--r--spec/features/container_registry_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb19
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb19
-rw-r--r--spec/features/merge_requests/discussion_spec.rb41
-rw-r--r--spec/features/variables_spec.rb48
-rw-r--r--spec/helpers/avatars_helper_spec.rb101
-rw-r--r--spec/javascripts/droplab/plugins/ajax_filter_spec.js72
-rw-r--r--spec/javascripts/environments/environments_store_spec.js9
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb (renamed from spec/lib/gitlab/git/encoding_helper_spec.rb)4
-rw-r--r--spec/lib/gitlab/etag_caching/router_spec.rb11
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb2
-rw-r--r--spec/lib/gitlab/utils_spec.rb11
-rw-r--r--spec/migrations/migrate_old_artifacts_spec.rb117
-rw-r--r--spec/models/ci/build_spec.rb68
-rw-r--r--spec/models/ci/variable_spec.rb33
-rw-r--r--spec/models/deployment_spec.rb13
-rw-r--r--spec/models/diff_note_spec.rb27
-rw-r--r--spec/models/environment_spec.rb22
-rw-r--r--spec/models/merge_request_spec.rb10
-rw-r--r--spec/models/project_spec.rb84
-rw-r--r--spec/requests/api/variables_spec.rb7
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb5
-rw-r--r--spec/services/discussions/update_diff_position_service_spec.rb (renamed from spec/services/notes/diff_position_update_service_spec.rb)28
-rw-r--r--spec/services/gravatar_service_spec.rb20
-rw-r--r--spec/uploaders/artifact_uploader_spec.rb38
-rw-r--r--spec/uploaders/gitlab_uploader_spec.rb56
114 files changed, 1559 insertions, 431 deletions
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 2d6c0bcf19c..ab0fa336dd0 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-5.0.4
+5.0.5
diff --git a/Gemfile b/Gemfile
index dce2e4ba94e..c49b60ffc23 100644
--- a/Gemfile
+++ b/Gemfile
@@ -97,6 +97,7 @@ gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1'
+gem 'fog-aliyun', '~> 0.1.0'
# for Google storage
gem 'google-api-client', '~> 0.8.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index f0728a358fa..f8adfec6143 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -213,6 +213,11 @@ GEM
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
+ fog-aliyun (0.1.0)
+ fog-core (~> 1.27)
+ fog-json (~> 1.0)
+ ipaddress (~> 0.8)
+ xml-simple (~> 1.1)
fog-aws (0.13.0)
fog-core (~> 1.38)
fog-json (~> 1.0)
@@ -913,6 +918,7 @@ DEPENDENCIES
flay (~> 2.8.0)
flipper (~> 0.10.2)
flipper-active_record (~> 0.10.2)
+ fog-aliyun (~> 0.1.0)
fog-aws (~> 0.9)
fog-core (~> 1.44)
fog-google (~> 0.5)
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index 8a0fd3bb4a7..37ddca29e71 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -16,6 +16,13 @@ const JumpToDiscussion = Vue.extend({
};
},
computed: {
+ buttonText: function () {
+ if (this.discussionId) {
+ return 'Jump to next unresolved discussion';
+ } else {
+ return 'Jump to first unresolved discussion';
+ }
+ },
allResolved: function () {
return this.unresolvedDiscussionCount === 0;
},
diff --git a/app/assets/javascripts/droplab/keyboard.js b/app/assets/javascripts/droplab/keyboard.js
index 36740a430e1..02f1b805ce4 100644
--- a/app/assets/javascripts/droplab/keyboard.js
+++ b/app/assets/javascripts/droplab/keyboard.js
@@ -8,7 +8,7 @@ const Keyboard = function () {
var isUpArrow = false;
var isDownArrow = false;
var removeHighlight = function removeHighlight(list) {
- var itemElements = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0);
+ var itemElements = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider):not(.hidden)'), 0);
var listItems = [];
for(var i = 0; i < itemElements.length; i++) {
var listItem = itemElements[i];
diff --git a/app/assets/javascripts/droplab/plugins/ajax_filter.js b/app/assets/javascripts/droplab/plugins/ajax_filter.js
index a5427417031..1db20227a16 100644
--- a/app/assets/javascripts/droplab/plugins/ajax_filter.js
+++ b/app/assets/javascripts/droplab/plugins/ajax_filter.js
@@ -63,6 +63,9 @@ const AjaxFilter = {
return AjaxCache.retrieve(url)
.then((data) => {
this._loadData(data, config);
+ if (config.onLoadingFinished) {
+ config.onLoadingFinished(data);
+ }
})
.catch(config.onError);
},
diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue
index d4e13f3c84a..86d8fe89010 100644
--- a/app/assets/javascripts/environments/components/environment.vue
+++ b/app/assets/javascripts/environments/components/environment.vue
@@ -1,5 +1,6 @@
<script>
/* global Flash */
+import Visibility from 'visibilityjs';
import EnvironmentsService from '../services/environments_service';
import environmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
@@ -7,6 +8,8 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import '../../lib/utils/common_utils';
import eventHub from '../event_hub';
+import Poll from '../../lib/utils/poll';
+import environmentsMixin from '../mixins/environments_mixin';
export default {
@@ -16,6 +19,10 @@ export default {
loadingIcon,
},
+ mixins: [
+ environmentsMixin,
+ ],
+
data() {
const environmentsData = document.querySelector('#environments-list-view').dataset;
const store = new EnvironmentsStore();
@@ -35,6 +42,7 @@ export default {
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
+ isMakingRequest: false,
// Pagination Properties,
paginationInformation: {},
@@ -65,17 +73,43 @@ export default {
* Toggles loading property.
*/
created() {
+ const scope = gl.utils.getParameterByName('scope') || this.visibility;
+ const page = gl.utils.getParameterByName('page') || this.pageNumber;
+
this.service = new EnvironmentsService(this.endpoint);
- this.fetchEnvironments();
+ const poll = new Poll({
+ resource: this.service,
+ method: 'get',
+ data: { scope, page },
+ successCallback: this.successCallback,
+ errorCallback: this.errorCallback,
+ notificationCallback: (isMakingRequest) => {
+ this.isMakingRequest = isMakingRequest;
+
+ // We need to verify if any folder is open to also fecth it
+ this.openFolders = this.store.getOpenFolders();
+ },
+ });
+
+ if (!Visibility.hidden()) {
+ this.isLoading = true;
+ poll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ poll.restart();
+ } else {
+ poll.stop();
+ }
+ });
- eventHub.$on('refreshEnvironments', this.fetchEnvironments);
eventHub.$on('toggleFolder', this.toggleFolder);
eventHub.$on('postAction', this.postAction);
},
beforeDestroyed() {
- eventHub.$off('refreshEnvironments');
eventHub.$off('toggleFolder');
eventHub.$off('postAction');
},
@@ -104,29 +138,13 @@ export default {
fetchEnvironments() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
- const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
+ const page = gl.utils.getParameterByName('page') || this.pageNumber;
this.isLoading = true;
- return this.service.get(scope, pageNumber)
- .then(resp => ({
- headers: resp.headers,
- body: resp.json(),
- }))
- .then((response) => {
- this.store.storeAvailableCount(response.body.available_count);
- this.store.storeStoppedCount(response.body.stopped_count);
- this.store.storeEnvironments(response.body.environments);
- this.store.setPagination(response.headers);
- })
- .then(() => {
- this.isLoading = false;
- })
- .catch(() => {
- this.isLoading = false;
- // eslint-disable-next-line no-new
- new Flash('An error occurred while fetching the environments.');
- });
+ return this.service.get({ scope, page })
+ .then(this.successCallback)
+ .catch(this.errorCallback);
},
fetchChildEnvironments(folder, folderUrl) {
@@ -146,9 +164,34 @@ export default {
},
postAction(endpoint) {
- this.service.postAction(endpoint)
- .then(() => this.fetchEnvironments())
- .catch(() => new Flash('An error occured while making the request.'));
+ if (!this.isMakingRequest) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => this.fetchEnvironments())
+ .catch(() => new Flash('An error occured while making the request.'));
+ }
+ },
+
+ successCallback(resp) {
+ this.saveData(resp);
+
+ // If folders are open while polling we need to open them again
+ if (this.openFolders.length) {
+ this.openFolders.map((folder) => {
+ // TODO - Move this to the backend
+ const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`;
+
+ this.store.updateFolder(folder, 'isOpen', true);
+ return this.fetchChildEnvironments(folder, folderUrl);
+ });
+ }
+ },
+
+ errorCallback() {
+ this.isLoading = false;
+ // eslint-disable-next-line no-new
+ new Flash('An error occurred while fetching the environments.');
},
},
};
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index bd161c8a379..925503a01c4 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -1,12 +1,15 @@
<script>
/* global Flash */
+import Visibility from 'visibilityjs';
import EnvironmentsService from '../services/environments_service';
import environmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
+import Poll from '../../lib/utils/poll';
+import eventHub from '../event_hub';
+import environmentsMixin from '../mixins/environments_mixin';
import '../../lib/utils/common_utils';
-import '../../vue_shared/vue_resource_interceptor';
export default {
components: {
@@ -15,6 +18,10 @@ export default {
loadingIcon,
},
+ mixins: [
+ environmentsMixin,
+ ],
+
data() {
const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
const store = new EnvironmentsStore();
@@ -76,33 +83,39 @@ export default {
*/
created() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
- const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
-
- const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
-
- this.service = new EnvironmentsService(endpoint);
-
- this.isLoading = true;
-
- return this.service.get()
- .then(resp => ({
- headers: resp.headers,
- body: resp.json(),
- }))
- .then((response) => {
- this.store.storeAvailableCount(response.body.available_count);
- this.store.storeStoppedCount(response.body.stopped_count);
- this.store.storeEnvironments(response.body.environments);
- this.store.setPagination(response.headers);
- })
- .then(() => {
- this.isLoading = false;
- })
- .catch(() => {
- this.isLoading = false;
- // eslint-disable-next-line no-new
- new Flash('An error occurred while fetching the environments.', 'alert');
- });
+ const page = gl.utils.getParameterByName('page') || this.pageNumber;
+
+ this.service = new EnvironmentsService(this.endpoint);
+
+ const poll = new Poll({
+ resource: this.service,
+ method: 'get',
+ data: { scope, page },
+ successCallback: this.successCallback,
+ errorCallback: this.errorCallback,
+ notificationCallback: (isMakingRequest) => {
+ this.isMakingRequest = isMakingRequest;
+ },
+ });
+
+ if (!Visibility.hidden()) {
+ this.isLoading = true;
+ poll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ poll.restart();
+ } else {
+ poll.stop();
+ }
+ });
+
+ eventHub.$on('postAction', this.postAction);
+ },
+
+ beforeDestroyed() {
+ eventHub.$off('postAction');
},
methods: {
@@ -117,6 +130,37 @@ export default {
gl.utils.visitUrl(param);
return param;
},
+
+ fetchEnvironments() {
+ const scope = gl.utils.getParameterByName('scope') || this.visibility;
+ const page = gl.utils.getParameterByName('page') || this.pageNumber;
+
+ this.isLoading = true;
+
+ return this.service.get({ scope, page })
+ .then(this.successCallback)
+ .catch(this.errorCallback);
+ },
+
+ successCallback(resp) {
+ this.saveData(resp);
+ },
+
+ errorCallback() {
+ this.isLoading = false;
+ // eslint-disable-next-line no-new
+ new Flash('An error occurred while fetching the environments.');
+ },
+
+ postAction(endpoint) {
+ if (!this.isMakingRequest) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => this.fetchEnvironments())
+ .catch(() => new Flash('An error occured while making the request.'));
+ }
+ },
},
};
</script>
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
new file mode 100644
index 00000000000..25b24fbd6dc
--- /dev/null
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -0,0 +1,17 @@
+export default {
+ methods: {
+ saveData(resp) {
+ const response = {
+ headers: resp.headers,
+ body: resp.json(),
+ };
+
+ this.isLoading = false;
+
+ this.store.storeAvailableCount(response.body.available_count);
+ this.store.storeStoppedCount(response.body.stopped_count);
+ this.store.storeEnvironments(response.body.environments);
+ this.store.setPagination(response.headers);
+ },
+ },
+};
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 8adb53ea86d..03ab74b3338 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -10,7 +10,8 @@ export default class EnvironmentsService {
this.folderResults = 3;
}
- get(scope, page) {
+ get(options = {}) {
+ const { scope, page } = options;
return this.environments.get({ scope, page });
}
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 158e7922e3c..8a2f6a473de 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -153,4 +153,10 @@ export default class EnvironmentsStore {
return updatedEnvironments;
}
+ getOpenFolders() {
+ const environments = this.state.environments;
+
+ return environments.filter(env => env.isFolder && env.isOpen);
+ }
+
}
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 6b4338ca1d6..65c1b2050ac 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -18,6 +18,9 @@ class DropdownUser extends gl.FilteredSearchDropdown {
},
searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate,
+ onLoadingFinished: () => {
+ this.hideCurrentUser();
+ },
onError() {
/* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.');
@@ -28,6 +31,11 @@ class DropdownUser extends gl.FilteredSearchDropdown {
this.tokenKeys = tokenKeys;
}
+ hideCurrentUser() {
+ const currenUserItem = this.dropdown.querySelector('.js-current-user');
+ currenUserItem.classList.add('hidden');
+ }
+
itemClicked(e) {
super.itemClicked(e,
selected => selected.querySelector('.dropdown-light-content').innerText.trim());
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index b9d2fc25c39..3328ff9cc23 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -66,7 +66,8 @@ w.gl.utils.removeParamQueryString = function(url, param) {
})()).join('&');
};
w.gl.utils.removeParams = (params) => {
- const url = new URL(window.location.href);
+ const url = document.createElement('a');
+ url.href = window.location.href;
params.forEach((param) => {
url.search = w.gl.utils.removeParamQueryString(url.search, param);
});
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index cec3b54d567..10881987038 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -4,7 +4,7 @@
padding: 0;
&::before {
- @include notes-media('max', $screen-xs-max) {
+ @include notes-media('max', $screen-xs-min) {
background: none;
}
}
@@ -30,7 +30,7 @@
.timeline-entry-inner {
position: relative;
- @include notes-media('max', $screen-xs-max) {
+ @include notes-media('max', $screen-xs-min) {
.timeline-icon {
display: none;
}
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index efe83776834..4630f451445 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -15,6 +15,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
+ Gitlab::PollingInterval.set_header(response, interval: 3_000)
+
render json: {
environments: EnvironmentSerializer
.new(project: @project, current_user: @current_user)
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index a4d1b1ee69b..0953eecaeb5 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -42,6 +42,7 @@ class Projects::VariablesController < Projects::ApplicationController
private
def project_params
- params.require(:variable).permit([:id, :key, :value, :_destroy])
+ params.require(:variable)
+ .permit([:id, :key, :value, :protected, :_destroy])
end
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index b7e0ff8ecd0..bbe7f3c8fb4 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -8,18 +8,28 @@ module AvatarsHelper
}))
end
- def user_avatar(options = {})
+ def user_avatar_without_link(options = {})
avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name]
css_class = options[:css_class] || ''
-
- avatar = image_tag(
- avatar_icon(options[:user] || options[:user_email], avatar_size),
+ avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
+ data_attributes = { container: 'body' }
+
+ if options[:lazy]
+ data_attributes[:src] = avatar_url
+ end
+
+ image_tag(
+ options[:lazy] ? '' : avatar_url,
class: "avatar has-tooltip s#{avatar_size} #{css_class}",
alt: "#{user_name}'s avatar",
title: user_name,
- data: { container: 'body' }
+ data: data_attributes
)
+ end
+
+ def user_avatar(options = {})
+ avatar = user_avatar_without_link(options)
if options[:user]
link_to(avatar, user_path(options[:user]))
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 375110b77e2..3d4802290b5 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -50,7 +50,7 @@ module NotesHelper
def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user
- data = { discussion_id: discussion.id, line_type: line_type }
+ data = { discussion_id: discussion.reply_id, line_type: line_type }
button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
data: data, title: 'Add a reply'
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 0000ecc5bbf..58dfdd87652 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -191,7 +191,7 @@ module Ci
variables += project.deployment_variables if has_environment?
variables += yaml_variables
variables += user_variables
- variables += project.secret_variables
+ variables += project.secret_variables_for(ref).map(&:to_runner_variable)
variables += trigger_request.user_variables if trigger_request
variables
end
@@ -255,38 +255,6 @@ module Ci
Time.now - updated_at > 15.minutes.to_i
end
- ##
- # Deprecated
- #
- # This contains a hotfix for CI build data integrity, see #4246
- #
- # This method is used by `ArtifactUploader` to create a store_dir.
- # Warning: Uploader uses it after AND before file has been stored.
- #
- # This method returns old path to artifacts only if it already exists.
- #
- def artifacts_path
- # We need the project even if it's soft deleted, because whenever
- # we're really deleting the project, we'll also delete the builds,
- # and in order to delete the builds, we need to know where to find
- # the artifacts, which is depending on the data of the project.
- # We need to retain the project in this case.
- the_project = project || unscoped_project
-
- old = File.join(created_at.utc.strftime('%Y_%m'),
- the_project.ci_id.to_s,
- id.to_s)
-
- old_store = File.join(ArtifactUploader.artifacts_path, old)
- return old if the_project.ci_id && File.directory?(old_store)
-
- File.join(
- created_at.utc.strftime('%Y_%m'),
- the_project.id.to_s,
- id.to_s
- )
- end
-
def valid_token?(token)
self.token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 6c6586110c5..f235260208f 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -12,11 +12,16 @@ module Ci
message: "can contain only letters, digits and '_'." }
scope :order_key_asc, -> { reorder(key: :asc) }
+ scope :unprotected, -> { where(protected: false) }
attr_encrypted :value,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
+
+ def to_runner_variable
+ { key: key, value: value, public: false }
+ end
end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 216cec751e3..304179c0a97 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -12,6 +12,7 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true
after_create :create_ref
+ after_create :invalidate_cache
def commit
project.commit(sha)
@@ -33,6 +34,10 @@ class Deployment < ActiveRecord::Base
project.repository.create_ref(ref, ref_path)
end
+ def invalidate_cache
+ environment.expire_etag_cache
+ end
+
def manual_actions
@manual_actions ||= deployable.try(:other_actions)
end
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index 800574d8be0..07c4846e2ac 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -10,6 +10,7 @@ class DiffDiscussion < Discussion
delegate :position,
:original_position,
+ :change_position,
to: :first_note
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 6cd0502069a..20ef1378500 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -95,13 +95,21 @@ class DiffNote < Note
return if active?
- Notes::DiffPositionUpdateService.new(
- self.project,
- nil,
+ tracer = Gitlab::Diff::PositionTracer.new(
+ project: self.project,
old_diff_refs: self.position.diff_refs,
- new_diff_refs: noteable.diff_refs,
+ new_diff_refs: self.noteable.diff_refs,
paths: self.position.paths
- ).execute(self)
+ )
+
+ result = tracer.trace(self.position)
+ return unless result
+
+ if result[:outdated]
+ self.change_position = result[:position]
+ else
+ self.position = result[:position]
+ end
end
def verify_supported
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 9b32d573387..d1cec7613af 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -85,6 +85,12 @@ class Discussion
first_note.discussion_id(context_noteable)
end
+ def reply_id
+ # To reply to this discussion, we need the actual discussion_id from the database,
+ # not the potentially overwritten one based on the noteable.
+ first_note.discussion_id
+ end
+
alias_method :to_param, :id
def diff_discussion?
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 61572d8d69a..6211a5c1e63 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -57,6 +57,10 @@ class Environment < ActiveRecord::Base
state :available
state :stopped
+
+ after_transition do |environment|
+ environment.expire_etag_cache
+ end
end
def predefined_variables
@@ -196,6 +200,18 @@ class Environment < ActiveRecord::Base
[external_url, public_path].join('/')
end
+ def expire_etag_cache
+ Gitlab::EtagCaching::Store.new.tap do |store|
+ store.touch(etag_cache_key)
+ end
+ end
+
+ def etag_cache_key
+ Gitlab::Routing.url_helpers.namespace_project_environments_path(
+ project.namespace,
+ project)
+ end
+
private
# Slugifying a name may remove the uniqueness guarantee afforded by it being
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 4bccda8a732..dd155252ad5 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -421,7 +421,7 @@ class MergeRequest < ActiveRecord::Base
MergeRequests::MergeRequestDiffCacheService.new.execute(self)
new_diff_refs = self.diff_refs
- update_diff_notes_positions(
+ update_diff_discussion_positions(
old_diff_refs: old_diff_refs,
new_diff_refs: new_diff_refs,
current_user: current_user
@@ -853,19 +853,18 @@ class MergeRequest < ActiveRecord::Base
diff_refs && diff_refs.complete?
end
- def update_diff_notes_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
+ def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
return unless has_complete_diff_refs?
return if new_diff_refs == old_diff_refs
- active_diff_notes = self.notes.new_diff_notes.select do |note|
- note.active?(old_diff_refs)
+ active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
+ discussion.active?(old_diff_refs)
end
+ return if active_diff_discussions.empty?
- return if active_diff_notes.empty?
+ paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
- paths = active_diff_notes.flat_map { |n| n.diff_file.paths }.uniq
-
- service = Notes::DiffPositionUpdateService.new(
+ service = Discussions::UpdateDiffPositionService.new(
self.project,
current_user,
old_diff_refs: old_diff_refs,
@@ -873,11 +872,8 @@ class MergeRequest < ActiveRecord::Base
paths: paths
)
- transaction do
- active_diff_notes.each do |note|
- service.execute(note)
- Gitlab::Timeless.timeless(note, &:save)
- end
+ active_diff_discussions.each do |discussion|
+ service.execute(discussion)
end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 1c2f335f95e..99dd2130188 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -1,7 +1,7 @@
class MergeRequestDiff < ActiveRecord::Base
include Sortable
include Importable
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
diff --git a/app/models/project.rb b/app/models/project.rb
index 7cb79e3249d..446329557d5 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1245,12 +1245,19 @@ class Project < ActiveRecord::Base
variables
end
- def secret_variables
- variables.map do |variable|
- { key: variable.key, value: variable.value, public: false }
+ def secret_variables_for(ref)
+ if protected_for?(ref)
+ variables
+ else
+ variables.unprotected
end
end
+ def protected_for?(ref)
+ ProtectedBranch.protected?(self, ref) ||
+ ProtectedTag.protected?(self, ref)
+ end
+
def deployment_variables
return [] unless deployment_service
diff --git a/app/models/user.rb b/app/models/user.rb
index 8114d0ff88e..32048da6c6f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -781,7 +781,7 @@ class User < ActiveRecord::Base
def avatar_url(size: nil, scale: 2, **args)
# We use avatar_path instead of overriding avatar_url because of carrierwave.
# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
- avatar_path(args) || GravatarService.new.execute(email, size, scale)
+ avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
end
def all_emails
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 8227a78a650..13baa63220d 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -63,13 +63,10 @@ module Ci
private
def update_merge_requests_head_pipeline
- merge_requests = MergeRequest.where(source_branch: @pipeline.ref, source_project: @pipeline.project)
+ return unless pipeline.latest?
- merge_requests = merge_requests.select do |mr|
- mr.diff_head_sha == @pipeline.sha
- end
-
- MergeRequest.where(id: merge_requests).update_all(head_pipeline_id: @pipeline.id)
+ MergeRequest.where(source_project: @pipeline.project, source_branch: @pipeline.ref).
+ update_all(head_pipeline_id: @pipeline.id)
end
def skip_ci?
diff --git a/app/services/discussions/update_diff_position_service.rb b/app/services/discussions/update_diff_position_service.rb
new file mode 100644
index 00000000000..1ef8d9edbe1
--- /dev/null
+++ b/app/services/discussions/update_diff_position_service.rb
@@ -0,0 +1,41 @@
+module Discussions
+ class UpdateDiffPositionService < BaseService
+ def execute(discussion)
+ result = tracer.trace(discussion.position)
+ return unless result
+
+ position = result[:position]
+ outdated = result[:outdated]
+
+ discussion.notes.each do |note|
+ if outdated
+ note.change_position = position
+ else
+ note.position = position
+ note.change_position = nil
+ end
+ end
+
+ Note.transaction do
+ discussion.notes.each do |note|
+ Gitlab::Timeless.timeless(note, &:save)
+ end
+
+ if outdated && current_user
+ SystemNoteService.diff_discussion_outdated(discussion, project, current_user, position)
+ end
+ end
+ end
+
+ private
+
+ def tracer
+ @tracer ||= Gitlab::Diff::PositionTracer.new(
+ project: project,
+ old_diff_refs: params[:old_diff_refs],
+ new_diff_refs: params[:new_diff_refs],
+ paths: params[:paths]
+ )
+ end
+ end
+end
diff --git a/app/services/gravatar_service.rb b/app/services/gravatar_service.rb
index 433ecc2df32..e77e08aa380 100644
--- a/app/services/gravatar_service.rb
+++ b/app/services/gravatar_service.rb
@@ -1,15 +1,20 @@
class GravatarService
include Gitlab::CurrentSettings
- def execute(email, size = nil, scale = 2)
- if current_application_settings.gravatar_enabled? && email.present?
- size = 40 if size.nil? || size <= 0
+ def execute(email, size = nil, scale = 2, username: nil)
+ return unless current_application_settings.gravatar_enabled?
- sprintf gravatar_url,
- hash: Digest::MD5.hexdigest(email.strip.downcase),
- size: size * scale,
- email: email.strip
- end
+ identifier = email.presence || username.presence
+ return unless identifier
+
+ hash = Digest::MD5.hexdigest(identifier.strip.downcase)
+ size = 40 unless size && size > 0
+
+ sprintf gravatar_url,
+ hash: hash,
+ size: size * scale,
+ email: ERB::Util.url_encode(email&.strip || ''),
+ username: ERB::Util.url_encode(username&.strip || '')
end
def gitlab_config
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index fbf171f705e..71d37797bb4 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -30,15 +30,12 @@ module MergeRequests
def head_pipeline_for(merge_request)
return unless merge_request.source_project
- sha = merge_request.source_branch_head&.id
-
+ sha = merge_request.source_branch_sha
return unless sha
- pipelines =
- Ci::Pipeline.where(ref: merge_request.source_branch, project_id: merge_request.source_project.id, sha: sha).
- order(id: :desc)
+ pipelines = merge_request.source_project.pipelines.where(ref: merge_request.source_branch, sha: sha)
- pipelines.first
+ pipelines.order(id: :desc).first
end
end
end
diff --git a/app/services/notes/diff_position_update_service.rb b/app/services/notes/diff_position_update_service.rb
deleted file mode 100644
index eff7b287269..00000000000
--- a/app/services/notes/diff_position_update_service.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module Notes
- class DiffPositionUpdateService < BaseService
- def execute(note)
- results = tracer.trace(note.position)
- return unless results
-
- position = results[:position]
- outdated = results[:outdated]
-
- if outdated
- note.change_position = position
-
- if note.persisted? && current_user
- SystemNoteService.diff_discussion_outdated(note.to_discussion, project, current_user, position)
- end
- else
- note.position = position
- note.change_position = nil
- end
- end
-
- private
-
- def tracer
- @tracer ||= Gitlab::Diff::PositionTracer.new(
- project: project,
- old_diff_refs: params[:old_diff_refs],
- new_diff_refs: params[:new_diff_refs],
- paths: params[:paths]
- )
- end
- end
-end
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index 3e36ec91205..3bc0408f557 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -1,33 +1,35 @@
class ArtifactUploader < GitlabUploader
storage :file
- attr_accessor :build, :field
+ attr_reader :job, :field
- def self.artifacts_path
+ def self.local_artifacts_store
Gitlab.config.artifacts.path
end
def self.artifacts_upload_path
- File.join(self.artifacts_path, 'tmp/uploads/')
+ File.join(self.local_artifacts_store, 'tmp/uploads/')
end
- def self.artifacts_cache_path
- File.join(self.artifacts_path, 'tmp/cache/')
- end
-
- def initialize(build, field)
- @build, @field = build, field
+ def initialize(job, field)
+ @job, @field = job, field
end
def store_dir
- File.join(self.class.artifacts_path, @build.artifacts_path)
+ default_local_path
end
def cache_dir
- File.join(self.class.artifacts_cache_path, @build.artifacts_path)
+ File.join(self.class.local_artifacts_store, 'tmp/cache')
+ end
+
+ private
+
+ def default_local_path
+ File.join(self.class.local_artifacts_store, default_path)
end
- def filename
- file.try(:filename)
+ def default_path
+ File.join(job.created_at.utc.strftime('%Y_%m'), job.project_id.to_s, job.id.to_s)
end
end
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index e0a6c9b4067..02afddb8c6a 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -10,7 +10,11 @@ class GitlabUploader < CarrierWave::Uploader::Base
delegate :base_dir, to: :class
def file_storage?
- self.class.storage == CarrierWave::Storage::File
+ storage.is_a?(CarrierWave::Storage::File)
+ end
+
+ def file_cache_storage?
+ cache_storage.is_a?(CarrierWave::Storage::File)
end
# Reduce disk IO
diff --git a/app/validators/dynamic_path_validator.rb b/app/validators/dynamic_path_validator.rb
index a9b76c7c960..27ac60637fd 100644
--- a/app/validators/dynamic_path_validator.rb
+++ b/app/validators/dynamic_path_validator.rb
@@ -6,7 +6,7 @@
# Values are checked for formatting and exclusion from a list of illegal path
# names.
class DynamicPathValidator < ActiveModel::EachValidator
- extend Gitlab::Git::EncodingHelper
+ extend Gitlab::EncodingHelper
class << self
def valid_user_path?(path)
diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml
index 69bd416c4de..3db509f24a5 100644
--- a/app/views/discussions/_jump_to_next.html.haml
+++ b/app/views/discussions/_jump_to_next.html.haml
@@ -3,7 +3,7 @@
%jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" }
.btn-group{ role: "group", "v-show" => "!allResolved", "v-if" => "showButton" }
%button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
- title: "Jump to next unresolved discussion",
- "aria-label" => "Jump to next unresolved discussion",
+ ":title" => "buttonText",
+ ":aria-label" => "buttonText",
data: { container: "body" } }
= custom_icon("next_discussion")
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index 1b1910b5c0f..3b17daeb6da 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -42,7 +42,7 @@
= f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
= f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
%p.help-block
- Per job in minutes. If a job passes this threshold, it will be marked as failed.
+ Per job in minutes. If a job passes this threshold, it will be marked as failed
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index be128e92fa7..5661af01302 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -1,26 +1,60 @@
- page_title "Container Registry"
-%hr
-
-%ul.content-list
- %li.light.prepend-top-default
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ = page_title
%p
- A 'container image' is a snapshot of a container.
- You can host your container images with GitLab.
- %br
- To start using container images hosted on GitLab you first need to login:
- %pre
- %code
+ With the Docker Container Registry integrated into GitLab, every project
+ can have its own space to store its Docker images.
+ %p.append-bottom-0
+ = succeed '.' do
+ Learn more about
+ = link_to 'Container Registry', help_page_path('user/project/container_registry'), target: '_blank'
+
+ .col-lg-9
+ .panel.panel-default
+ .panel-heading
+ %h4.panel-title
+ How to use the Container Registry
+ .panel-body
+ %p
+ First log in to GitLab&rsquo;s Container Registry using your GitLab username
+ and password. If you have
+ = link_to '2FA enabled', help_page_path('user/profile/account/two_factor_authentication'), target: '_blank'
+ you need to use a
+ = succeed ':' do
+ = link_to 'personal access token', help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank'
+ %pre
docker login #{Gitlab.config.registry.host_port}
- %br
- Then you are free to create and upload a container image with build and push commands:
- %pre
- docker build -t #{escape_once(@project.container_registry_url)}/image .
%br
- docker push #{escape_once(@project.container_registry_url)}/image
+ %p
+ Once you log in, you&rsquo;re free to create and upload a container image
+ using the common
+ %code build
+ and
+ %code push
+ commands:
+ %pre
+ :plain
+ docker build -t #{escape_once(@project.container_registry_url)} .
+ docker push #{escape_once(@project.container_registry_url)}
- - if @images.blank?
- .nothing-here-block No container image repositories in Container Registry for this project.
+ %hr
+ %h5.prepend-top-default
+ Use different image names
+ %p.light
+ GitLab supports up to 3 levels of image names. The following
+ examples of images are valid for your project:
+ %pre
+ :plain
+ #{escape_once(@project.container_registry_url)}:tag
+ #{escape_once(@project.container_registry_url)}/optional-image-name:tag
+ #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
- - else
- = render partial: 'image', collection: @images
+ - if @images.blank?
+ %p.settings-message.text-center.append-bottom-default
+ No container images stored for this project. Add one by following the
+ instructions above.
+ - else
+ = render partial: 'image', collection: @images
diff --git a/app/views/projects/variables/_content.html.haml b/app/views/projects/variables/_content.html.haml
index 06477aba103..98f618ca3b8 100644
--- a/app/views/projects/variables/_content.html.haml
+++ b/app/views/projects/variables/_content.html.haml
@@ -1,7 +1,8 @@
%h4.prepend-top-0
- Secret Variables
+ Secret variables
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank'
%p
- These variables will be set to environment by the runner.
+ These variables will be set to environment by the runner, and could be protected by exposing only to protected branches or tags.
%p
So you can use them for passwords, secret keys or whatever you want.
%p
diff --git a/app/views/projects/variables/_form.html.haml b/app/views/projects/variables/_form.html.haml
index 1ae86d258af..0a70a301cb4 100644
--- a/app/views/projects/variables/_form.html.haml
+++ b/app/views/projects/variables/_form.html.haml
@@ -7,4 +7,13 @@
.form-group
= f.label :value, "Value", class: "label-light"
= f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE"
+ .form-group
+ .checkbox
+ = f.label :protected do
+ = f.check_box :protected
+ %strong Protected
+ .help-block
+ This variable will be passed only to pipelines running on protected branches and tags
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'protected-secret-variables'), target: '_blank'
+
= f.submit btn_text, class: "btn btn-save"
diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml
index 0ce597dcf21..59cd3c4b592 100644
--- a/app/views/projects/variables/_table.html.haml
+++ b/app/views/projects/variables/_table.html.haml
@@ -3,10 +3,12 @@
%colgroup
%col
%col
+ %col
%col{ width: 100 }
%thead
%th Key
%th Value
+ %th Protected
%th
%tbody
- @project.variables.order_key_asc.each do |variable|
@@ -14,6 +16,7 @@
%tr
%td.variable-key= variable.key
%td.variable-value{ "data-value" => variable.value }******
+ %td.variable-protected= Gitlab::Utils.boolean_to_yes_no(variable.protected)
%td.variable-menu
= link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do
%span.sr-only
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index f8d755b6961..a9a4792faae 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -46,30 +46,27 @@
%span.js-filter-tag.dropdown-light-content
{{tag}}
#js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu
+ - if current_user
+ %ul{ data: { dropdown: true } }
+ = render 'shared/issuable/user_dropdown_item',
+ user: current_user
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item
- %button.btn.btn-link.dropdown-user
- %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } }
- .dropdown-user-details
- %span
- {{name}}
- %span.dropdown-light-content
- @{{username}}
+ = render 'shared/issuable/user_dropdown_item',
+ user: User.new(username: '{{username}}', name: '{{name}}'),
+ avatar: { lazy: true, url: '{{avatar_url}}' }
#js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
No Assignee
%li.divider
+ - if current_user
+ = render 'shared/issuable/user_dropdown_item',
+ user: current_user
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item
- %button.btn.btn-link.dropdown-user
- %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } }
- .dropdown-user-details
- %span
- {{name}}
- %span.dropdown-light-content
- @{{username}}
+ = render 'shared/issuable/user_dropdown_item',
+ user: User.new(username: '{{username}}', name: '{{name}}'),
+ avatar: { lazy: true, url: '{{avatar_url}}' }
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
diff --git a/app/views/shared/issuable/_user_dropdown_item.html.haml b/app/views/shared/issuable/_user_dropdown_item.html.haml
new file mode 100644
index 00000000000..a82c01c6dc2
--- /dev/null
+++ b/app/views/shared/issuable/_user_dropdown_item.html.haml
@@ -0,0 +1,11 @@
+- user = local_assigns.fetch(:user)
+- avatar = local_assigns.fetch(:avatar, { })
+
+%li.filter-dropdown-item{ class: ('js-current-user' if user == current_user) }
+ %button.btn.btn-link.dropdown-user{ type: :button }
+ = user_avatar_without_link(user: user, lazy: avatar[:lazy], url: avatar[:url], size: 30)
+ .dropdown-user-details
+ %span
+ = user.name
+ %span.dropdown-light-content
+ = user.to_reference
diff --git a/changelogs/unreleased/24196-protected-variables.yml b/changelogs/unreleased/24196-protected-variables.yml
new file mode 100644
index 00000000000..71567a9d794
--- /dev/null
+++ b/changelogs/unreleased/24196-protected-variables.yml
@@ -0,0 +1,5 @@
+---
+title: Add protected variables which would only be passed to protected branches or
+ protected tags
+merge_request: 11688
+author:
diff --git a/changelogs/unreleased/30651-improve-container-registry-description.yml b/changelogs/unreleased/30651-improve-container-registry-description.yml
new file mode 100644
index 00000000000..0157c9885bc
--- /dev/null
+++ b/changelogs/unreleased/30651-improve-container-registry-description.yml
@@ -0,0 +1,4 @@
+---
+title: Add changelog for improved Registry description
+merge_request: 11816
+author:
diff --git a/changelogs/unreleased/31644-make-cookie-sessions-unique.yml b/changelogs/unreleased/31644-make-cookie-sessions-unique.yml
new file mode 100644
index 00000000000..e9a6a32cf70
--- /dev/null
+++ b/changelogs/unreleased/31644-make-cookie-sessions-unique.yml
@@ -0,0 +1,4 @@
+---
+title: Update session cookie key name to be unique to instance in development
+merge_request:
+author:
diff --git a/changelogs/unreleased/aliyun-backup-provider.yml b/changelogs/unreleased/aliyun-backup-provider.yml
new file mode 100644
index 00000000000..e7505e44a59
--- /dev/null
+++ b/changelogs/unreleased/aliyun-backup-provider.yml
@@ -0,0 +1,4 @@
+---
+title: Add Aliyun OSS as the backup storage provider
+merge_request: 9721
+author: Yuanfei Zhu
diff --git a/changelogs/unreleased/dm-comment-on-mr-commit-discussion.yml b/changelogs/unreleased/dm-comment-on-mr-commit-discussion.yml
new file mode 100644
index 00000000000..50db66c89ba
--- /dev/null
+++ b/changelogs/unreleased/dm-comment-on-mr-commit-discussion.yml
@@ -0,0 +1,4 @@
+---
+title: Fix replying to a commit discussion displayed in the context of an MR
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-fix-jump-button.yml b/changelogs/unreleased/dm-fix-jump-button.yml
new file mode 100644
index 00000000000..4cde354fa28
--- /dev/null
+++ b/changelogs/unreleased/dm-fix-jump-button.yml
@@ -0,0 +1,4 @@
+---
+title: Fix title of discussion jump button at top of page
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-gravatar-username.yml b/changelogs/unreleased/dm-gravatar-username.yml
new file mode 100644
index 00000000000..d50455061ec
--- /dev/null
+++ b/changelogs/unreleased/dm-gravatar-username.yml
@@ -0,0 +1,4 @@
+---
+title: Add username parameter to gravatar URL
+merge_request:
+author:
diff --git a/changelogs/unreleased/migrate-artifacts-to-a-new-path.yml b/changelogs/unreleased/migrate-artifacts-to-a-new-path.yml
new file mode 100644
index 00000000000..bd022a3a91b
--- /dev/null
+++ b/changelogs/unreleased/migrate-artifacts-to-a-new-path.yml
@@ -0,0 +1,4 @@
+---
+title: Migrate artifacts to a new path
+merge_request:
+author:
diff --git a/changelogs/unreleased/winh-current-user-filter.yml b/changelogs/unreleased/winh-current-user-filter.yml
new file mode 100644
index 00000000000..e5409827b31
--- /dev/null
+++ b/changelogs/unreleased/winh-current-user-filter.yml
@@ -0,0 +1,4 @@
+---
+title: Show current user immediately in issuable filters
+merge_request: 11630
+author:
diff --git a/changelogs/unreleased/zj-realtime-env-list.yml b/changelogs/unreleased/zj-realtime-env-list.yml
new file mode 100644
index 00000000000..6460d17edc9
--- /dev/null
+++ b/changelogs/unreleased/zj-realtime-env-list.yml
@@ -0,0 +1,4 @@
+---
+title: Make environment table realtime
+merge_request: 11333
+author:
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 6c1c1f8c041..d2aeb66ebf6 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -169,7 +169,7 @@ production: &base
## Gravatar
## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
gravatar:
- # gravatar urls: possible placeholders: %{hash} %{size} %{email}
+ # gravatar urls: possible placeholders: %{hash} %{size} %{email} %{username}
# plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
# ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 70be2617cab..8919f7640fe 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -10,6 +10,12 @@ rescue
Settings.gitlab['session_expire_delay'] ||= 10080
end
+cookie_key = if Rails.env.development?
+ "_gitlab_session_#{Digest::SHA256.hexdigest(Rails.root.to_s)}"
+ else
+ "_gitlab_session"
+ end
+
if Rails.env.test?
Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
else
@@ -19,7 +25,7 @@ else
Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks.
servers: redis_config,
- key: '_gitlab_session',
+ key: cookie_key,
secure: Gitlab.config.gitlab.https,
httponly: true,
expires_in: Settings.gitlab['session_expire_delay'] * 60,
diff --git a/db/migrate/20170524161101_add_protected_to_ci_variables.rb b/db/migrate/20170524161101_add_protected_to_ci_variables.rb
new file mode 100644
index 00000000000..99d4861e889
--- /dev/null
+++ b/db/migrate/20170524161101_add_protected_to_ci_variables.rb
@@ -0,0 +1,15 @@
+class AddProtectedToCiVariables < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:ci_variables, :protected, :boolean, default: false)
+ end
+
+ def down
+ remove_column(:ci_variables, :protected)
+ end
+end
diff --git a/db/post_migrate/20170523083112_migrate_old_artifacts.rb b/db/post_migrate/20170523083112_migrate_old_artifacts.rb
new file mode 100644
index 00000000000..f2690bd0017
--- /dev/null
+++ b/db/post_migrate/20170523083112_migrate_old_artifacts.rb
@@ -0,0 +1,72 @@
+class MigrateOldArtifacts < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ # This uses special heuristic to find potential candidates for data migration
+ # Read more about this here: https://gitlab.com/gitlab-org/gitlab-ce/issues/32036#note_30422345
+
+ def up
+ builds_with_artifacts.find_each do |build|
+ build.migrate_artifacts!
+ end
+ end
+
+ def down
+ end
+
+ private
+
+ def builds_with_artifacts
+ Build.with_artifacts
+ .joins('JOIN projects ON projects.id = ci_builds.project_id')
+ .where('ci_builds.id < ?', min_id)
+ .where('projects.ci_id IS NOT NULL')
+ .select('id', 'created_at', 'project_id', 'projects.ci_id AS ci_id')
+ end
+
+ def min_id
+ Build.joins('JOIN projects ON projects.id = ci_builds.project_id')
+ .where('projects.ci_id IS NULL')
+ .pluck('coalesce(min(ci_builds.id), 0)')
+ .first
+ end
+
+ class Build < ActiveRecord::Base
+ self.table_name = 'ci_builds'
+
+ scope :with_artifacts, -> { where.not(artifacts_file: [nil, '']) }
+
+ def migrate_artifacts!
+ return unless File.exist?(source_artifacts_path)
+ return if File.exist?(target_artifacts_path)
+
+ ensure_target_path
+
+ FileUtils.move(source_artifacts_path, target_artifacts_path)
+ end
+
+ private
+
+ def source_artifacts_path
+ @source_artifacts_path ||=
+ File.join(Gitlab.config.artifacts.path,
+ created_at.utc.strftime('%Y_%m'),
+ ci_id.to_s, id.to_s)
+ end
+
+ def target_artifacts_path
+ @target_artifacts_path ||=
+ File.join(Gitlab.config.artifacts.path,
+ created_at.utc.strftime('%Y_%m'),
+ project_id.to_s, id.to_s)
+ end
+
+ def ensure_target_path
+ directory = File.dirname(target_artifacts_path)
+ FileUtils.mkdir_p(directory) unless Dir.exist?(directory)
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index bac8f95ce3b..fa1c5dc15c4 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -356,6 +356,7 @@ ActiveRecord::Schema.define(version: 20170525174156) do
t.string "encrypted_value_salt"
t.string "encrypted_value_iv"
t.integer "project_id", null: false
+ t.boolean "protected", default: false, null: false
end
add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
@@ -1492,4 +1493,4 @@ ActiveRecord::Schema.define(version: 20170525174156) do
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade
-end \ No newline at end of file
+end
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
index 2aaf1c93705..d4f00256ed3 100644
--- a/doc/api/build_variables.md
+++ b/doc/api/build_variables.md
@@ -61,11 +61,12 @@ Create a new build variable.
POST /projects/:id/variables
```
-| Attribute | Type | required | Description |
-|-----------|---------|----------|-----------------------|
-| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
-| `value` | string | yes | The `value` of a variable |
+| Attribute | Type | required | Description |
+|-------------|---------|----------|-----------------------|
+| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
+| `value` | string | yes | The `value` of a variable |
+| `protected` | boolean | no | Whether the variable is protected |
```
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
@@ -74,7 +75,8 @@ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitl
```json
{
"key": "NEW_VARIABLE",
- "value": "new value"
+ "value": "new value",
+ "protected": false
}
```
@@ -86,11 +88,12 @@ Update a project's build variable.
PUT /projects/:id/variables/:key
```
-| Attribute | Type | required | Description |
-|-----------|---------|----------|-------------------------|
-| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `key` | string | yes | The `key` of a variable |
-| `value` | string | yes | The `value` of a variable |
+| Attribute | Type | required | Description |
+|-------------|---------|----------|-------------------------|
+| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `key` | string | yes | The `key` of a variable |
+| `value` | string | yes | The `value` of a variable |
+| `protected` | boolean | no | Whether the variable is protected |
```
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
@@ -99,7 +102,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitla
```json
{
"key": "NEW_VARIABLE",
- "value": "updated value"
+ "value": "updated value",
+ "protected": true
}
```
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 0d4d08106f8..76ad7c564a3 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -10,7 +10,7 @@ The variables can be overwritten and they take precedence over each other in
this order:
1. [Trigger variables][triggers] (take precedence over all)
-1. [Secret variables](#secret-variables)
+1. [Secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables)
1. YAML-defined [job-level variables](../yaml/README.md#job-variables)
1. YAML-defined [global variables](../yaml/README.md#variables)
1. [Deployment variables](#deployment-variables)
@@ -153,9 +153,25 @@ storing things like passwords, secret keys and credentials.
Secret variables can be added by going to your project's
**Settings âž” Pipelines**, then finding the section called
-**Secret Variables**.
+**Secret variables**.
-Once you set them, they will be available for all subsequent jobs.
+Once you set them, they will be available for all subsequent pipelines.
+
+## Protected secret variables
+
+>**Notes:**
+This feature requires GitLab 9.3 or higher.
+
+Secret variables could be protected. Whenever a secret variable is
+protected, it would only be securely passed to pipelines running on the
+[protected branches] or [protected tags]. The other pipelines would not get any
+protected variables.
+
+Protected variables can be added by going to your project's
+**Settings âž” Pipelines**, then finding the section called
+**Secret variables**, and check *Protected*.
+
+Once you set them, they will be available for all subsequent pipelines.
## Deployment variables
@@ -385,3 +401,5 @@ export CI_REGISTRY_PASSWORD="longalfanumstring"
[runner]: https://docs.gitlab.com/runner/
[triggered]: ../triggers/README.md
[triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger
+[protected branches]: ../../user/project/protected_branches.md
+[protected tags]: ../../user/project/protected_tags.md
diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md
index c46ce2ee203..9bd22d3966d 100644
--- a/doc/customization/libravatar.md
+++ b/doc/customization/libravatar.md
@@ -16,7 +16,7 @@ the configuration options as follows:
```yml
gravatar:
enabled: true
- # gravatar URLs: possible placeholders: %{hash} %{size} %{email}
+ # gravatar URLs: possible placeholders: %{hash} %{size} %{email} %{username}
plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon"
```
@@ -25,7 +25,7 @@ the configuration options as follows:
```yml
gravatar:
enabled: true
- # gravatar URLs: possible placeholders: %{hash} %{size} %{email}
+ # gravatar URLs: possible placeholders: %{hash} %{size} %{email} %{username}
ssl_url: "https://seccdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon"
```
diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md
index 735345bd126..bfb0779fbfa 100644
--- a/doc/development/i18n_guide.md
+++ b/doc/development/i18n_guide.md
@@ -233,8 +233,7 @@ Let's suppose you want to add translations for a new language, let's say French.
containing the translations:
```sh
- bundle exec rake gettext:pack
- bundle exec rake gettext:po_to_json
+ bundle exec rake gettext:compile
```
1. In order to see the translated content we need to change our preferred language
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 5be6053b76e..855f437cd73 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -133,7 +133,7 @@ It uses the [Fog library](http://fog.io/) to perform the upload.
In the example below we use Amazon S3 for storage, but Fog also lets you use
[other storage providers](http://fog.io/storage/). GitLab
[imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88)
-for AWS, Google, OpenStack Swift and Rackspace as well. A local driver is
+for AWS, Google, OpenStack Swift, Rackspace and Aliyun as well. A local driver is
[also available](#uploading-to-locally-mounted-shares).
For omnibus packages, add the following to `/etc/gitlab/gitlab.rb`:
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 6a2ca7fb428..3cbb0b5196d 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -95,8 +95,6 @@ and click **Registry** in the project menu.
This view will show you all tags in your project and will easily allow you to
delete them.
-![Container Registry panel](img/container_registry_panel.png)
-
## Build and push images using GitLab CI
> **Note:**
diff --git a/doc/user/project/img/container_registry_panel.png b/doc/user/project/img/container_registry_panel.png
deleted file mode 100644
index e4c9ecbb25b..00000000000
--- a/doc/user/project/img/container_registry_panel.png
+++ /dev/null
Binary files differ
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index e3258fdcffe..31da85e9917 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -675,6 +675,7 @@ module API
class Variable < Grape::Entity
expose :key, :value
+ expose :protected?, as: :protected
end
class Pipeline < PipelineBasic
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index d61450f8258..81f6fc3201d 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -311,6 +311,16 @@ module API
end
end
+ def present_artifacts!(artifacts_file)
+ return not_found! unless artifacts_file.exists?
+
+ if artifacts_file.file_storage?
+ present_file!(artifacts_file.path, artifacts_file.filename)
+ else
+ redirect_to(artifacts_file.url)
+ end
+ end
+
private
def private_token
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 0223957fde1..8a67de10bca 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -224,16 +224,6 @@ module API
find_build(id) || not_found!
end
- def present_artifacts!(artifacts_file)
- if !artifacts_file.file_storage?
- redirect_to(build.artifacts_file.url)
- elsif artifacts_file.exists?
- present_file!(artifacts_file.path, artifacts_file.filename)
- else
- not_found!
- end
- end
-
def filter_builds(builds, scope)
return builds if scope.nil? || scope.empty?
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 6fbb02cb3aa..3fd0536dadd 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -241,16 +241,7 @@ module API
get '/:id/artifacts' do
job = authenticate_job!
- artifacts_file = job.artifacts_file
- unless artifacts_file.file_storage?
- return redirect_to job.artifacts_file.url
- end
-
- unless artifacts_file.exists?
- not_found!
- end
-
- present_file!(artifacts_file.path, artifacts_file.filename)
+ present_artifacts!(job.artifacts_file)
end
end
end
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
index 21935922414..93ad9eb26b8 100644
--- a/lib/api/v3/builds.rb
+++ b/lib/api/v3/builds.rb
@@ -225,16 +225,6 @@ module API
find_build(id) || not_found!
end
- def present_artifacts!(artifacts_file)
- if !artifacts_file.file_storage?
- redirect_to(build.artifacts_file.url)
- elsif artifacts_file.exists?
- present_file!(artifacts_file.path, artifacts_file.filename)
- else
- not_found!
- end
- end
-
def filter_builds(builds, scope)
return builds if scope.nil? || scope.empty?
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index 5acde41551b..381c4ef50b0 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -42,6 +42,7 @@ module API
params do
requires :key, type: String, desc: 'The key of the variable'
requires :value, type: String, desc: 'The value of the variable'
+ optional :protected, type: String, desc: 'Whether the variable is protected'
end
post ':id/variables' do
variable = user_project.variables.create(declared(params, include_parent_namespaces: false).to_h)
@@ -59,13 +60,14 @@ module API
params do
optional :key, type: String, desc: 'The key of the variable'
optional :value, type: String, desc: 'The value of the variable'
+ optional :protected, type: String, desc: 'Whether the variable is protected'
end
put ':id/variables/:key' do
variable = user_project.variables.find_by(key: params[:key])
return not_found!('Variable') unless variable
- if variable.update(value: params[:value])
+ if variable.update(declared_params(include_missing: false).except(:key))
present variable, with: Entities::Variable
else
render_validation_error!(variable)
diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb
index 51fa3867e67..1f4bda6f588 100644
--- a/lib/backup/artifacts.rb
+++ b/lib/backup/artifacts.rb
@@ -3,7 +3,7 @@ require 'backup/files'
module Backup
class Artifacts < Files
def initialize
- super('artifacts', ArtifactUploader.artifacts_path)
+ super('artifacts', ArtifactUploader.local_artifacts_store)
end
def create_files_dir
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 67b269b330c..2285ef241d7 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -187,14 +187,14 @@ module Ci
build = authenticate_build!
artifacts_file = build.artifacts_file
- unless artifacts_file.file_storage?
- return redirect_to build.artifacts_file.url
- end
-
unless artifacts_file.exists?
not_found!
end
+ unless artifacts_file.file_storage?
+ return redirect_to build.artifacts_file.url
+ end
+
present_file!(artifacts_file.path, artifacts_file.filename)
end
diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb
new file mode 100644
index 00000000000..dbe28e6bb93
--- /dev/null
+++ b/lib/gitlab/encoding_helper.rb
@@ -0,0 +1,62 @@
+module Gitlab
+ module EncodingHelper
+ extend self
+
+ # This threshold is carefully tweaked to prevent usage of encodings detected
+ # by CharlockHolmes with low confidence. If CharlockHolmes confidence is low,
+ # we're better off sticking with utf8 encoding.
+ # Reason: git diff can return strings with invalid utf8 byte sequences if it
+ # truncates a diff in the middle of a multibyte character. In this case
+ # CharlockHolmes will try to guess the encoding and will likely suggest an
+ # obscure encoding with low confidence.
+ # There is a lot more info with this merge request:
+ # https://gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193
+ ENCODING_CONFIDENCE_THRESHOLD = 40
+
+ def encode!(message)
+ return nil unless message.respond_to? :force_encoding
+
+ # if message is utf-8 encoding, just return it
+ message.force_encoding("UTF-8")
+ return message if message.valid_encoding?
+
+ # return message if message type is binary
+ detect = CharlockHolmes::EncodingDetector.detect(message)
+ return message.force_encoding("BINARY") if detect && detect[:type] == :binary
+
+ # force detected encoding if we have sufficient confidence.
+ if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
+ message.force_encoding(detect[:encoding])
+ end
+
+ # encode and clean the bad chars
+ message.replace clean(message)
+ rescue
+ encoding = detect ? detect[:encoding] : "unknown"
+ "--broken encoding: #{encoding}"
+ end
+
+ def encode_utf8(message)
+ detect = CharlockHolmes::EncodingDetector.detect(message)
+ if detect
+ begin
+ CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
+ rescue ArgumentError => e
+ Rails.logger.warn("Ignoring error converting #{detect[:encoding]} into UTF8: #{e.message}")
+
+ ''
+ end
+ else
+ clean(message)
+ end
+ end
+
+ private
+
+ def clean(message)
+ message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
+ .encode("UTF-8")
+ .gsub("\0".encode("UTF-8"), "")
+ end
+ end
+end
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
index d137cc1bae6..2f9d8bfc266 100644
--- a/lib/gitlab/etag_caching/router.rb
+++ b/lib/gitlab/etag_caching/router.rb
@@ -9,9 +9,11 @@ module Gitlab
# - Ending in `noteable/issue/<id>/notes` for the `issue_notes` route
# - Ending in `issues/id`/realtime_changes` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes
- commit pipelines merge_requests new].freeze
+ commit pipelines merge_requests new
+ environments].freeze
RESERVED_WORDS = Gitlab::PathRegex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS.map(&Regexp.method(:escape)))
+
ROUTES = [
Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/noteable/issue/\d+/notes\z),
@@ -40,6 +42,10 @@ module Gitlab
Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/pipelines/\d+\.json\z),
'project_pipeline'
+ ),
+ Gitlab::EtagCaching::Router::Route.new(
+ %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/environments\.json\z),
+ 'environments'
)
].freeze
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
index 58193391926..66829a03c2e 100644
--- a/lib/gitlab/git/blame.rb
+++ b/lib/gitlab/git/blame.rb
@@ -1,7 +1,7 @@
module Gitlab
module Git
class Blame
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
attr_reader :lines, :blames
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 6b0a66365a7..d60e607b02b 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -2,7 +2,7 @@ module Gitlab
module Git
class Blob
include Linguist::BlobHelper
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
# This number is the maximum amount of data that we want to display to
# the user. We load as much as we can for encoding detection
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 297531db4cc..bb04731f08c 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -2,7 +2,7 @@
module Gitlab
module Git
class Commit
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
attr_accessor :raw_commit, :head, :refs
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index ccccca96595..0594ac8e213 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -3,7 +3,7 @@ module Gitlab
module Git
class Diff
TimeoutError = Class.new(StandardError)
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
# Diff properties
attr_accessor :old_path, :new_path, :a_mode, :b_mode, :diff
diff --git a/lib/gitlab/git/encoding_helper.rb b/lib/gitlab/git/encoding_helper.rb
deleted file mode 100644
index f918074cb14..00000000000
--- a/lib/gitlab/git/encoding_helper.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-module Gitlab
- module Git
- module EncodingHelper
- extend self
-
- # This threshold is carefully tweaked to prevent usage of encodings detected
- # by CharlockHolmes with low confidence. If CharlockHolmes confidence is low,
- # we're better off sticking with utf8 encoding.
- # Reason: git diff can return strings with invalid utf8 byte sequences if it
- # truncates a diff in the middle of a multibyte character. In this case
- # CharlockHolmes will try to guess the encoding and will likely suggest an
- # obscure encoding with low confidence.
- # There is a lot more info with this merge request:
- # https://gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193
- ENCODING_CONFIDENCE_THRESHOLD = 40
-
- def encode!(message)
- return nil unless message.respond_to? :force_encoding
-
- # if message is utf-8 encoding, just return it
- message.force_encoding("UTF-8")
- return message if message.valid_encoding?
-
- # return message if message type is binary
- detect = CharlockHolmes::EncodingDetector.detect(message)
- return message.force_encoding("BINARY") if detect && detect[:type] == :binary
-
- # force detected encoding if we have sufficient confidence.
- if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
- message.force_encoding(detect[:encoding])
- end
-
- # encode and clean the bad chars
- message.replace clean(message)
- rescue
- encoding = detect ? detect[:encoding] : "unknown"
- "--broken encoding: #{encoding}"
- end
-
- def encode_utf8(message)
- detect = CharlockHolmes::EncodingDetector.detect(message)
- if detect
- begin
- CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
- rescue ArgumentError => e
- Rails.logger.warn("Ignoring error converting #{detect[:encoding]} into UTF8: #{e.message}")
-
- ''
- end
- else
- clean(message)
- end
- end
-
- private
-
- def clean(message)
- message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
- .encode("UTF-8")
- .gsub("\0".encode("UTF-8"), "")
- end
- end
- end
-end
diff --git a/lib/gitlab/git/ref.rb b/lib/gitlab/git/ref.rb
index 37ef6836742..ebf7393dc61 100644
--- a/lib/gitlab/git/ref.rb
+++ b/lib/gitlab/git/ref.rb
@@ -1,7 +1,7 @@
module Gitlab
module Git
class Ref
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
# Branch or tag name
# without "refs/tags|heads" prefix
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index d41256d9a84..b9afa05c819 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -1,7 +1,7 @@
module Gitlab
module Git
class Tree
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
attr_accessor :id, :root_id, :name, :path, :type,
:mode, :commit_id, :submodule_url
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index 4c395b4266e..fa182c4deda 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -21,5 +21,13 @@ module Gitlab
nil
end
+
+ def boolean_to_yes_no(bool)
+ if bool
+ 'Yes'
+ else
+ 'No'
+ end
+ end
end
end
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index 0aa21a4bd13..b27f7475115 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -11,4 +11,12 @@ namespace :gettext do
"{#{folders}}/**/*.{#{exts}}"
)
end
+
+ task :compile do
+ # See: https://gitlab.com/gitlab-org/gitlab-ce/issues/33014#note_31218998
+ FileUtils.touch(File.join(Rails.root, 'locale/gitlab.pot'))
+
+ Rake::Task['gettext:pack'].invoke
+ Rake::Task['gettext:po_to_json'].invoke
+ end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index de13f17012b..f6840578145 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -57,6 +57,11 @@ describe Projects::EnvironmentsController do
expect(json_response['available_count']).to eq 3
expect(json_response['stopped_count']).to eq 1
end
+
+ it 'sets the polling interval header' do
+ expect(response).to have_http_status(:ok)
+ expect(response.headers['Poll-Interval']).to eq("3000")
+ end
end
context 'when requesting stopped environments scope' do
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
index c5fba597c1c..f83366136fd 100644
--- a/spec/factories/ci/variables.rb
+++ b/spec/factories/ci/variables.rb
@@ -3,6 +3,10 @@ FactoryGirl.define do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
+ trait(:protected) do
+ protected true
+ end
+
project factory: :empty_project
end
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index b86609e07c5..fa7adbe71ea 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -19,7 +19,7 @@ describe "Container Registry" do
scenario 'user visits container registry main page' do
visit_container_registry
- expect(page).to have_content 'No container image repositories'
+ expect(page).to have_content 'No container images'
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 4d38df05928..44353d880c2 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -157,6 +157,25 @@ describe 'Dropdown assignee', :feature, :js do
end
end
+ describe 'selecting from dropdown without Ajax call' do
+ before do
+ Gitlab::Testing::RequestBlockerMiddleware.block_requests!
+ filtered_search.set('assignee:')
+ end
+
+ after do
+ Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
+ end
+
+ it 'selects current user' do
+ find('#js-dropdown-assignee .filter-dropdown-item', text: user.username).click
+
+ expect(page).to have_css(js_dropdown_assignee, visible: false)
+ expect_tokens([{ name: 'assignee', value: user.username }])
+ expect_filtered_search_input_empty
+ end
+ end
+
describe 'input has existing content' do
it 'opens assignee dropdown with existing search term' do
filtered_search.set('searchTerm assignee:')
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 358b244fb5b..6b707c4be4a 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -135,6 +135,25 @@ describe 'Dropdown author', js: true, feature: true do
end
end
+ describe 'selecting from dropdown without Ajax call' do
+ before do
+ Gitlab::Testing::RequestBlockerMiddleware.block_requests!
+ filtered_search.set('author:')
+ end
+
+ after do
+ Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
+ end
+
+ it 'selects current user' do
+ find('#js-dropdown-author .filter-dropdown-item', text: user.username).click
+
+ expect(page).to have_css(js_dropdown_author, visible: false)
+ expect_tokens([{ name: 'author', value: user.username }])
+ expect_filtered_search_input_empty
+ end
+ end
+
describe 'input has existing content' do
it 'opens author dropdown with existing search term' do
filtered_search.set('searchTerm author:')
diff --git a/spec/features/merge_requests/discussion_spec.rb b/spec/features/merge_requests/discussion_spec.rb
index 1a09cc54c2e..9db235f35ba 100644
--- a/spec/features/merge_requests/discussion_spec.rb
+++ b/spec/features/merge_requests/discussion_spec.rb
@@ -5,7 +5,7 @@ feature 'Merge Request Discussions', feature: true do
login_as :admin
end
- context "Diff discussions" do
+ describe "Diff discussions" do
let(:merge_request) { create(:merge_request, importing: true) }
let(:project) { merge_request.source_project }
let!(:old_merge_request_diff) { merge_request.merge_request_diffs.create(diff_refs: outdated_diff_refs) }
@@ -48,4 +48,43 @@ feature 'Merge Request Discussions', feature: true do
end
end
end
+
+ describe 'Commit comments displayed in MR context', :js do
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+
+ shared_examples 'a functional discussion' do
+ let(:discussion_id) { note.discussion_id(merge_request) }
+
+ it 'is displayed' do
+ expect(page).to have_css(".discussion[data-discussion-id='#{discussion_id}']")
+ end
+
+ it 'can be replied to' do
+ within(".discussion[data-discussion-id='#{discussion_id}']") do
+ click_button 'Reply...'
+ fill_in 'note[note]', with: 'Test!'
+ click_button 'Comment'
+
+ expect(page).to have_css('.note', count: 2)
+ end
+ end
+ end
+
+ before(:each) do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ context 'a regular commit comment' do
+ let(:note) { create(:note_on_commit, project: project) }
+
+ it_behaves_like 'a functional discussion'
+ end
+
+ context 'a commit diff comment' do
+ let(:note) { create(:diff_note_on_commit, project: project) }
+
+ it_behaves_like 'a functional discussion'
+ end
+ end
end
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index b83a230c1f8..d0c982919db 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -19,7 +19,7 @@ describe 'Project variables', js: true do
end
end
- it 'adds new variable' do
+ it 'adds new secret variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
click_button('Add new variable')
@@ -27,6 +27,7 @@ describe 'Project variables', js: true do
expect(page).to have_content('Variables were successfully updated.')
page.within('.variables-table') do
expect(page).to have_content('key')
+ expect(page).to have_content('No')
end
end
@@ -41,6 +42,19 @@ describe 'Project variables', js: true do
end
end
+ it 'adds new protected variable' do
+ fill_in('variable_key', with: 'key')
+ fill_in('variable_value', with: 'value')
+ check('Protected')
+ click_button('Add new variable')
+
+ expect(page).to have_content('Variables were successfully updated.')
+ page.within('.variables-table') do
+ expect(page).to have_content('key')
+ expect(page).to have_content('Yes')
+ end
+ end
+
it 'reveals and hides new variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
@@ -85,7 +99,7 @@ describe 'Project variables', js: true do
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
- expect(project.variables.first.value).to eq('key value')
+ expect(project.variables(true).first.value).to eq('key value')
end
it 'edits variable with empty value' do
@@ -98,6 +112,34 @@ describe 'Project variables', js: true do
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
- expect(project.variables.first.value).to eq('')
+ expect(project.variables(true).first.value).to eq('')
+ end
+
+ it 'edits variable to be protected' do
+ page.within('.variables-table') do
+ find('.btn-variable-edit').click
+ end
+
+ expect(page).to have_content('Update variable')
+ check('Protected')
+ click_button('Save variable')
+
+ expect(page).to have_content('Variable was successfully updated.')
+ expect(project.variables(true).first).to be_protected
+ end
+
+ it 'edits variable to be unprotected' do
+ project.variables.first.update(protected: true)
+
+ page.within('.variables-table') do
+ find('.btn-variable-edit').click
+ end
+
+ expect(page).to have_content('Update variable')
+ uncheck('Protected')
+ click_button('Save variable')
+
+ expect(page).to have_content('Variable was successfully updated.')
+ expect(project.variables(true).first).not_to be_protected
end
end
diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb
index 6157abfe339..049475a5408 100644
--- a/spec/helpers/avatars_helper_spec.rb
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
describe AvatarsHelper do
+ include ApplicationHelper
+
let(:user) { create(:user) }
describe '#user_avatar' do
@@ -18,4 +20,103 @@ describe AvatarsHelper do
is_expected.to include(CGI.escapeHTML(user.avatar_url(size: 16)))
end
end
+
+ describe '#user_avatar_without_link' do
+ let(:options) { { user: user } }
+ subject { helper.user_avatar_without_link(options) }
+
+ it 'displays user avatar' do
+ is_expected.to eq image_tag(
+ avatar_icon(user, 16),
+ class: 'avatar has-tooltip s16 ',
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { container: 'body' }
+ )
+ end
+
+ context 'with css_class parameter' do
+ let(:options) { { user: user, css_class: '.cat-pics' } }
+
+ it 'uses provided css_class' do
+ is_expected.to eq image_tag(
+ avatar_icon(user, 16),
+ class: "avatar has-tooltip s16 #{options[:css_class]}",
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { container: 'body' }
+ )
+ end
+ end
+
+ context 'with lazy parameter' do
+ let(:options) { { user: user, lazy: true } }
+
+ it 'uses data-src instead of src' do
+ is_expected.to eq image_tag(
+ '',
+ class: 'avatar has-tooltip s16 ',
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { container: 'body', src: avatar_icon(user, 16) }
+ )
+ end
+ end
+
+ context 'with size parameter' do
+ let(:options) { { user: user, size: 99 } }
+
+ it 'uses provided size' do
+ is_expected.to eq image_tag(
+ avatar_icon(user, options[:size]),
+ class: "avatar has-tooltip s#{options[:size]} ",
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { container: 'body' }
+ )
+ end
+ end
+
+ context 'with url parameter' do
+ let(:options) { { user: user, url: '/over/the/rainbow.png' } }
+
+ it 'uses provided url' do
+ is_expected.to eq image_tag(
+ options[:url],
+ class: 'avatar has-tooltip s16 ',
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { container: 'body' }
+ )
+ end
+ end
+
+ context 'with user_name parameter' do
+ let(:options) { { user_name: 'Tinky Winky', user_email: 'no@f.un' } }
+
+ context 'with user parameter' do
+ let(:options) { { user: user, user_name: 'Tinky Winky' } }
+
+ it 'prefers user parameter' do
+ is_expected.to eq image_tag(
+ avatar_icon(user, 16),
+ class: 'avatar has-tooltip s16 ',
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { container: 'body' }
+ )
+ end
+ end
+
+ it 'uses user_name and user_email parameter if user is not present' do
+ is_expected.to eq image_tag(
+ avatar_icon(options[:user_email], 16),
+ class: 'avatar has-tooltip s16 ',
+ alt: "#{options[:user_name]}'s avatar",
+ title: options[:user_name],
+ data: { container: 'body' }
+ )
+ end
+ end
+ end
end
diff --git a/spec/javascripts/droplab/plugins/ajax_filter_spec.js b/spec/javascripts/droplab/plugins/ajax_filter_spec.js
new file mode 100644
index 00000000000..8155d98b543
--- /dev/null
+++ b/spec/javascripts/droplab/plugins/ajax_filter_spec.js
@@ -0,0 +1,72 @@
+import AjaxCache from '~/lib/utils/ajax_cache';
+import AjaxFilter from '~/droplab/plugins/ajax_filter';
+
+describe('AjaxFilter', () => {
+ let dummyConfig;
+ const dummyData = 'dummy data';
+ let dummyList;
+
+ beforeEach(() => {
+ dummyConfig = {
+ endpoint: 'dummy endpoint',
+ searchKey: 'dummy search key',
+ };
+ dummyList = {
+ data: [],
+ list: document.createElement('div'),
+ };
+
+ AjaxFilter.hook = {
+ config: {
+ AjaxFilter: dummyConfig,
+ },
+ list: dummyList,
+ };
+ });
+
+ describe('trigger', () => {
+ let ajaxSpy;
+
+ beforeEach(() => {
+ spyOn(AjaxCache, 'retrieve').and.callFake(url => ajaxSpy(url));
+ spyOn(AjaxFilter, '_loadData');
+
+ dummyConfig.onLoadingFinished = jasmine.createSpy('spy');
+
+ const dynamicList = document.createElement('div');
+ dynamicList.dataset.dynamic = true;
+ dummyList.list.appendChild(dynamicList);
+ });
+
+ it('calls onLoadingFinished after loading data', (done) => {
+ ajaxSpy = (url) => {
+ expect(url).toBe('dummy endpoint?dummy search key=');
+ return Promise.resolve(dummyData);
+ };
+
+ AjaxFilter.trigger()
+ .then(() => {
+ expect(dummyConfig.onLoadingFinished.calls.count()).toBe(1);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not call onLoadingFinished if Ajax call fails', (done) => {
+ const dummyError = new Error('My dummy is sick! :-(');
+ ajaxSpy = (url) => {
+ expect(url).toBe('dummy endpoint?dummy search key=');
+ return Promise.reject(dummyError);
+ };
+
+ AjaxFilter.trigger()
+ .then(done.fail)
+ .catch((error) => {
+ expect(error).toBe(dummyError);
+ expect(dummyConfig.onLoadingFinished.calls.count()).toBe(0);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/environments/environments_store_spec.js b/spec/javascripts/environments/environments_store_spec.js
index f617c4bdffe..6e855530b21 100644
--- a/spec/javascripts/environments/environments_store_spec.js
+++ b/spec/javascripts/environments/environments_store_spec.js
@@ -123,4 +123,13 @@ describe('Store', () => {
expect(store.state.paginationInformation).toEqual(expectedResult);
});
});
+
+ describe('getOpenFolders', () => {
+ it('should return open folder', () => {
+ store.storeEnvironments(serverData);
+
+ store.toggleFolder(store.state.environments[1]);
+ expect(store.getOpenFolders()[0]).toEqual(store.state.environments[1]);
+ });
+ });
});
diff --git a/spec/lib/gitlab/git/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index 48fc817d857..1482ef7132d 100644
--- a/spec/lib/gitlab/git/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
-describe Gitlab::Git::EncodingHelper do
- let(:ext_class) { Class.new { extend Gitlab::Git::EncodingHelper } }
+describe Gitlab::EncodingHelper do
+ let(:ext_class) { Class.new { extend Gitlab::EncodingHelper } }
let(:binary_string) { File.read(Rails.root + "spec/fixtures/dk.png") }
describe '#encode!' do
diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb
index 46a238b17f4..0418fc0a1e2 100644
--- a/spec/lib/gitlab/etag_caching/router_spec.rb
+++ b/spec/lib/gitlab/etag_caching/router_spec.rb
@@ -77,6 +77,17 @@ describe Gitlab::EtagCaching::Router do
expect(result).to be_blank
end
+ it 'matches the environments path' do
+ env = build_env(
+ '/my-group/my-project/environments.json'
+ )
+
+ result = described_class.match(env)
+ expect(result).to be_present
+
+ expect(result.name).to eq 'environments'
+ end
+
it 'matches pipeline#show endpoint' do
env = build_env(
'/my-group/my-project/pipelines/2.json'
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 9d0e95d5b19..26215381cc4 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Gitlab::Git::Repository, seed_helper: true do
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 56772409989..00941aec380 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -1,5 +1,7 @@
+require 'spec_helper'
+
describe Gitlab::Utils, lib: true do
- delegate :to_boolean, to: :described_class
+ delegate :to_boolean, :boolean_to_yes_no, to: :described_class
describe '.to_boolean' do
it 'accepts booleans' do
@@ -30,4 +32,11 @@ describe Gitlab::Utils, lib: true do
expect(to_boolean(nil)).to be_nil
end
end
+
+ describe '.boolean_to_yes_no' do
+ it 'converts booleans to Yes or No' do
+ expect(boolean_to_yes_no(true)).to eq('Yes')
+ expect(boolean_to_yes_no(false)).to eq('No')
+ end
+ end
end
diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb
new file mode 100644
index 00000000000..50f4bbda001
--- /dev/null
+++ b/spec/migrations/migrate_old_artifacts_spec.rb
@@ -0,0 +1,117 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170523083112_migrate_old_artifacts.rb')
+
+describe MigrateOldArtifacts do
+ let(:migration) { described_class.new }
+ let!(:directory) { Dir.mktmpdir }
+
+ before do
+ allow(Gitlab.config.artifacts).to receive(:path).and_return(directory)
+ end
+
+ after do
+ FileUtils.remove_entry_secure(directory)
+ end
+
+ context 'with migratable data' do
+ let(:project1) { create(:empty_project, ci_id: 2) }
+ let(:project2) { create(:empty_project, ci_id: 3) }
+ let(:project3) { create(:empty_project) }
+
+ let(:pipeline1) { create(:ci_empty_pipeline, project: project1) }
+ let(:pipeline2) { create(:ci_empty_pipeline, project: project2) }
+ let(:pipeline3) { create(:ci_empty_pipeline, project: project3) }
+
+ let!(:build_with_legacy_artifacts) { create(:ci_build, pipeline: pipeline1) }
+ let!(:build_without_artifacts) { create(:ci_build, pipeline: pipeline1) }
+ let!(:build2) { create(:ci_build, :artifacts, pipeline: pipeline2) }
+ let!(:build3) { create(:ci_build, :artifacts, pipeline: pipeline3) }
+
+ before do
+ store_artifacts_in_legacy_path(build_with_legacy_artifacts)
+ end
+
+ it "legacy artifacts are not accessible" do
+ expect(build_with_legacy_artifacts.artifacts?).to be_falsey
+ end
+
+ it "legacy artifacts are set" do
+ expect(build_with_legacy_artifacts.artifacts_file_identifier).not_to be_nil
+ end
+
+ describe '#min_id' do
+ subject { migration.send(:min_id) }
+
+ it 'returns the newest build for which ci_id is not defined' do
+ is_expected.to eq(build3.id)
+ end
+ end
+
+ describe '#builds_with_artifacts' do
+ subject { migration.send(:builds_with_artifacts).map(&:id) }
+
+ it 'returns a list of builds that has artifacts and could be migrated' do
+ is_expected.to contain_exactly(build_with_legacy_artifacts.id, build2.id)
+ end
+ end
+
+ describe '#up' do
+ context 'when migrating artifacts' do
+ before do
+ migration.up
+ end
+
+ it 'all files do have artifacts' do
+ Ci::Build.with_artifacts do |build|
+ expect(build).to have_artifacts
+ end
+ end
+
+ it 'artifacts are no longer present on legacy path' do
+ expect(File.exist?(legacy_path(build_with_legacy_artifacts))).to eq(false)
+ end
+ end
+
+ context 'when there are aritfacts in old and new directory' do
+ before do
+ store_artifacts_in_legacy_path(build2)
+
+ migration.up
+ end
+
+ it 'does not move old files' do
+ expect(File.exist?(legacy_path(build2))).to eq(true)
+ end
+ end
+ end
+
+ private
+
+ def store_artifacts_in_legacy_path(build)
+ FileUtils.mkdir_p(legacy_path(build))
+
+ FileUtils.copy(
+ Rails.root.join('spec/fixtures/ci_build_artifacts.zip'),
+ File.join(legacy_path(build), "ci_build_artifacts.zip"))
+
+ FileUtils.copy(
+ Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'),
+ File.join(legacy_path(build), "ci_build_artifacts_metadata.gz"))
+
+ build.update_columns(
+ artifacts_file: 'ci_build_artifacts.zip',
+ artifacts_metadata: 'ci_build_artifacts_metadata.gz')
+
+ build.reload
+ end
+
+ def legacy_path(build)
+ File.join(directory,
+ build.created_at.utc.strftime('%Y_%m'),
+ build.project.ci_id.to_s,
+ build.id.to_s)
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index e971b4bc3f9..e2406290c6c 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1215,16 +1215,49 @@ describe Ci::Build, :models do
it { is_expected.to include(tag_variable) }
end
- context 'when secure variable is defined' do
- let(:secure_variable) do
+ context 'when secret variable is defined' do
+ let(:secret_variable) do
{ key: 'SECRET_KEY', value: 'secret_value', public: false }
end
before do
- build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
+ create(:ci_variable,
+ secret_variable.slice(:key, :value).merge(project: project))
end
- it { is_expected.to include(secure_variable) }
+ it { is_expected.to include(secret_variable) }
+ end
+
+ context 'when protected variable is defined' do
+ let(:protected_variable) do
+ { key: 'PROTECTED_KEY', value: 'protected_value', public: false }
+ end
+
+ before do
+ create(:ci_variable,
+ :protected,
+ protected_variable.slice(:key, :value).merge(project: project))
+ end
+
+ context 'when the branch is protected' do
+ before do
+ create(:protected_branch, project: build.project, name: build.ref)
+ end
+
+ it { is_expected.to include(protected_variable) }
+ end
+
+ context 'when the tag is protected' do
+ before do
+ create(:protected_tag, project: build.project, name: build.ref)
+ end
+
+ it { is_expected.to include(protected_variable) }
+ end
+
+ context 'when the ref is not protected' do
+ it { is_expected.not_to include(protected_variable) }
+ end
end
context 'when build is for triggers' do
@@ -1346,15 +1379,30 @@ describe Ci::Build, :models do
end
context 'returns variables in valid order' do
+ let(:build_pre_var) { { key: 'build', value: 'value' } }
+ let(:project_pre_var) { { key: 'project', value: 'value' } }
+ let(:pipeline_pre_var) { { key: 'pipeline', value: 'value' } }
+ let(:build_yaml_var) { { key: 'yaml', value: 'value' } }
+
before do
- allow(build).to receive(:predefined_variables) { ['predefined'] }
- allow(project).to receive(:predefined_variables) { ['project'] }
- allow(pipeline).to receive(:predefined_variables) { ['pipeline'] }
- allow(build).to receive(:yaml_variables) { ['yaml'] }
- allow(project).to receive(:secret_variables) { ['secret'] }
+ allow(build).to receive(:predefined_variables) { [build_pre_var] }
+ allow(project).to receive(:predefined_variables) { [project_pre_var] }
+ allow(pipeline).to receive(:predefined_variables) { [pipeline_pre_var] }
+ allow(build).to receive(:yaml_variables) { [build_yaml_var] }
+
+ allow(project).to receive(:secret_variables_for).with(build.ref) do
+ [create(:ci_variable, key: 'secret', value: 'value')]
+ end
end
- it { is_expected.to eq(%w[predefined project pipeline yaml secret]) }
+ it do
+ is_expected.to eq(
+ [build_pre_var,
+ project_pre_var,
+ pipeline_pre_var,
+ build_yaml_var,
+ { key: 'secret', value: 'value', public: false }])
+ end
end
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index fe8c52d5353..077b10227d7 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -12,11 +12,33 @@ describe Ci::Variable, models: true do
it { is_expected.not_to allow_value('foo bar').for(:key) }
it { is_expected.not_to allow_value('foo/bar').for(:key) }
- before :each do
- subject.value = secret_value
+ describe '.unprotected' do
+ subject { described_class.unprotected }
+
+ context 'when variable is protected' do
+ before do
+ create(:ci_variable, :protected)
+ end
+
+ it 'returns nothing' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when variable is not protected' do
+ let(:variable) { create(:ci_variable, protected: false) }
+
+ it 'returns the variable' do
+ is_expected.to contain_exactly(variable)
+ end
+ end
end
describe '#value' do
+ before do
+ subject.value = secret_value
+ end
+
it 'stores the encrypted value' do
expect(subject.encrypted_value).not_to be_nil
end
@@ -36,4 +58,11 @@ describe Ci::Variable, models: true do
to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt')
end
end
+
+ describe '#to_runner_variable' do
+ it 'returns a hash for the runner' do
+ expect(subject.to_runner_variable)
+ .to eq(key: subject.key, value: subject.value, public: false)
+ end
+ end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 4bda7d4314a..6f0d2db23c7 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -16,6 +16,19 @@ describe Deployment, models: true do
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) }
+ describe 'after_create callbacks' do
+ let(:environment) { create(:environment) }
+ let(:store) { Gitlab::EtagCaching::Store.new }
+
+ it 'invalidates the environment etag cache' do
+ old_value = store.get(environment.etag_cache_key)
+
+ create(:deployment, environment: environment)
+
+ expect(store.get(environment.etag_cache_key)).not_to eq(old_value)
+ end
+ end
+
describe '#includes_commit?' do
let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) }
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 96f075d4f7d..297c2108dc2 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -160,12 +160,6 @@ describe DiffNote, models: true do
context "when noteable is a commit" do
let(:diff_note) { create(:diff_note_on_commit, project: project, position: position) }
- it "doesn't use the DiffPositionUpdateService" do
- expect(Notes::DiffPositionUpdateService).not_to receive(:new)
-
- diff_note
- end
-
it "doesn't update the position" do
diff_note
@@ -178,12 +172,6 @@ describe DiffNote, models: true do
let(:diff_note) { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
context "when the note is active" do
- it "doesn't use the DiffPositionUpdateService" do
- expect(Notes::DiffPositionUpdateService).not_to receive(:new)
-
- diff_note
- end
-
it "doesn't update the position" do
diff_note
@@ -197,18 +185,11 @@ describe DiffNote, models: true do
allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs)
end
- it "uses the DiffPositionUpdateService" do
- service = instance_double("Notes::DiffPositionUpdateService")
- expect(Notes::DiffPositionUpdateService).to receive(:new).with(
- project,
- nil,
- old_diff_refs: position.diff_refs,
- new_diff_refs: commit.diff_refs,
- paths: [path]
- ).and_return(service)
- expect(service).to receive(:execute)
-
+ it "updates the position" do
diff_note
+
+ expect(diff_note.original_position).to eq(position)
+ expect(diff_note.position).not_to eq(position)
end
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 9fbe19b04d5..fe69c8e351d 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Environment, models: true do
- let(:project) { create(:empty_project) }
+ set(:project) { create(:empty_project) }
subject(:environment) { create(:environment, project: project) }
it { is_expected.to belong_to(:project) }
@@ -34,6 +34,26 @@ describe Environment, models: true do
end
end
+ describe 'state machine' do
+ it 'invalidates the cache after a change' do
+ expect(environment).to receive(:expire_etag_cache)
+
+ environment.stop
+ end
+ end
+
+ describe '#expire_etag_cache' do
+ let(:store) { Gitlab::EtagCaching::Store.new }
+
+ it 'changes the cached value' do
+ old_value = store.get(environment.etag_cache_key)
+
+ environment.stop
+
+ expect(store.get(environment.etag_cache_key)).not_to eq(old_value)
+ end
+ end
+
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
env = build(:environment, external_url: "")
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 58f273ade28..060754fab63 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1178,7 +1178,7 @@ describe MergeRequest, models: true do
end
describe "#reload_diff" do
- let(:note) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject) }
+ let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
let(:commit) { subject.project.commit(sample_commit.id) }
@@ -1197,7 +1197,7 @@ describe MergeRequest, models: true do
subject.reload_diff
end
- it "updates diff note positions" do
+ it "updates diff discussion positions" do
old_diff_refs = subject.diff_refs
# Update merge_request_diff so that #diff_refs will return commit.diff_refs
@@ -1211,15 +1211,15 @@ describe MergeRequest, models: true do
subject.merge_request_diff(true)
end
- expect(Notes::DiffPositionUpdateService).to receive(:new).with(
+ expect(Discussions::UpdateDiffPositionService).to receive(:new).with(
subject.project,
subject.author,
old_diff_refs: old_diff_refs,
new_diff_refs: commit.diff_refs,
- paths: note.position.paths
+ paths: discussion.position.paths
).and_call_original
- expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
+ expect_any_instance_of(Discussions::UpdateDiffPositionService).to receive(:execute).with(discussion).and_call_original
expect_any_instance_of(DiffNote).to receive(:save).once
subject.reload_diff(subject.author)
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index da1b29a2bda..86ab2550bfb 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1749,6 +1749,90 @@ describe Project, models: true do
end
end
+ describe '#secret_variables_for' do
+ let(:project) { create(:empty_project) }
+
+ let!(:secret_variable) do
+ create(:ci_variable, value: 'secret', project: project)
+ end
+
+ let!(:protected_variable) do
+ create(:ci_variable, :protected, value: 'protected', project: project)
+ end
+
+ subject { project.secret_variables_for('ref') }
+
+ shared_examples 'ref is protected' do
+ it 'contains all the variables' do
+ is_expected.to contain_exactly(secret_variable, protected_variable)
+ end
+ end
+
+ context 'when the ref is not protected' do
+ before do
+ stub_application_setting(
+ default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+ end
+
+ it 'contains only the secret variables' do
+ is_expected.to contain_exactly(secret_variable)
+ end
+ end
+
+ context 'when the ref is a protected branch' do
+ before do
+ create(:protected_branch, name: 'ref', project: project)
+ end
+
+ it_behaves_like 'ref is protected'
+ end
+
+ context 'when the ref is a protected tag' do
+ before do
+ create(:protected_tag, name: 'ref', project: project)
+ end
+
+ it_behaves_like 'ref is protected'
+ end
+ end
+
+ describe '#protected_for?' do
+ let(:project) { create(:empty_project) }
+
+ subject { project.protected_for?('ref') }
+
+ context 'when the ref is not protected' do
+ before do
+ stub_application_setting(
+ default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+ end
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
+ end
+
+ context 'when the ref is a protected branch' do
+ before do
+ create(:protected_branch, name: 'ref', project: project)
+ end
+
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'when the ref is a protected tag' do
+ before do
+ create(:protected_tag, name: 'ref', project: project)
+ end
+
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+ end
+ end
+
describe '#update_project_statistics' do
let(:project) { create(:empty_project) }
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 63d6d3001ac..83673864fe7 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -42,6 +42,7 @@ describe API::Variables do
expect(response).to have_http_status(200)
expect(json_response['value']).to eq(variable.value)
+ expect(json_response['protected']).to eq(variable.protected?)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
@@ -72,12 +73,13 @@ describe API::Variables do
context 'authorized user with proper permissions' do
it 'creates variable' do
expect do
- post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
+ post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2', protected: true
end.to change{project.variables.count}.by(1)
expect(response).to have_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
+ expect(json_response['protected']).to be_truthy
end
it 'does not allow to duplicate variable key' do
@@ -112,13 +114,14 @@ describe API::Variables do
initial_variable = project.variables.first
value_before = initial_variable.value
- put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP'
+ put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP', protected: true
updated_variable = project.variables.first
expect(response).to have_http_status(200)
expect(value_before).to eq(variable.value)
expect(updated_variable.value).to eq('VALUE_1_UP')
+ expect(updated_variable).to be_protected
end
it 'responds with 404 Not Found if requesting non-existing variable' do
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 8bf02f56282..06fbd7bad90 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -72,10 +72,11 @@ describe Ci::CreatePipelineService, services: true do
end
end
- context 'when merge request head commit sha does not match pipeline sha' do
+ context 'when the pipeline is not the latest for the branch' do
it 'does not update merge request head pipeline' do
merge_request = create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project)
- allow_any_instance_of(MergeRequestDiff).to receive(:head_commit).and_return(double(id: 1234))
+
+ allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(false)
pipeline
diff --git a/spec/services/notes/diff_position_update_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb
index 380c296fd3a..177e32e13bd 100644
--- a/spec/services/notes/diff_position_update_service_spec.rb
+++ b/spec/services/discussions/update_diff_position_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Notes::DiffPositionUpdateService, services: true do
+describe Discussions::UpdateDiffPositionService, services: true do
let(:project) { create(:project, :repository) }
let(:current_user) { project.owner }
let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") }
@@ -138,7 +138,7 @@ describe Notes::DiffPositionUpdateService, services: true do
# .. ..
describe "#execute" do
- let(:note) { create(:diff_note_on_merge_request, project: project, position: old_position) }
+ let(:discussion) { create(:diff_note_on_merge_request, project: project, position: old_position).to_discussion }
let(:old_position) do
Gitlab::Diff::Position.new(
@@ -154,11 +154,11 @@ describe Notes::DiffPositionUpdateService, services: true do
let(:line) { 16 }
it "updates the position" do
- subject.execute(note)
+ subject.execute(discussion)
- expect(note.original_position).to eq(old_position)
- expect(note.position).not_to eq(old_position)
- expect(note.position.new_line).to eq(22)
+ expect(discussion.original_position).to eq(old_position)
+ expect(discussion.position).not_to eq(old_position)
+ expect(discussion.position.new_line).to eq(22)
end
end
@@ -166,27 +166,27 @@ describe Notes::DiffPositionUpdateService, services: true do
let(:line) { 9 }
it "doesn't update the position" do
- subject.execute(note)
+ subject.execute(discussion)
- expect(note.original_position).to eq(old_position)
- expect(note.position).to eq(old_position)
+ expect(discussion.original_position).to eq(old_position)
+ expect(discussion.position).to eq(old_position)
end
it 'sets the change position' do
- subject.execute(note)
+ subject.execute(discussion)
- change_position = note.change_position
+ change_position = discussion.change_position
expect(change_position.start_sha).to eq(old_diff_refs.head_sha)
expect(change_position.head_sha).to eq(new_diff_refs.head_sha)
expect(change_position.old_line).to eq(9)
expect(change_position.new_line).to be_nil
end
- it 'creates a system note' do
+ it 'creates a system discussion' do
expect(SystemNoteService).to receive(:diff_discussion_outdated).with(
- note.to_discussion, project, current_user, instance_of(Gitlab::Diff::Position))
+ discussion, project, current_user, instance_of(Gitlab::Diff::Position))
- subject.execute(note)
+ subject.execute(discussion)
end
end
end
diff --git a/spec/services/gravatar_service_spec.rb b/spec/services/gravatar_service_spec.rb
new file mode 100644
index 00000000000..8c4ad8c7a3e
--- /dev/null
+++ b/spec/services/gravatar_service_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe GravatarService, service: true do
+ describe '#execute' do
+ let(:url) { 'http://example.com/avatar?hash=%{hash}&size=%{size}&email=%{email}&username=%{username}' }
+
+ before do
+ allow(Gitlab.config.gravatar).to receive(:plain_url).and_return(url)
+ end
+
+ it 'replaces the placeholders' do
+ avatar_url = described_class.new.execute('user@example.com', 100, 2, username: 'user')
+
+ expect(avatar_url).to include("hash=#{Digest::MD5.hexdigest('user@example.com')}")
+ expect(avatar_url).to include("size=200")
+ expect(avatar_url).to include("email=user%40example.com")
+ expect(avatar_url).to include("username=user")
+ end
+ end
+end
diff --git a/spec/uploaders/artifact_uploader_spec.rb b/spec/uploaders/artifact_uploader_spec.rb
new file mode 100644
index 00000000000..24e2e3a9f0e
--- /dev/null
+++ b/spec/uploaders/artifact_uploader_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+describe ArtifactUploader do
+ let(:job) { create(:ci_build) }
+ let(:uploader) { described_class.new(job, :artifacts_file) }
+ let(:path) { Gitlab.config.artifacts.path }
+
+ describe '.local_artifacts_store' do
+ subject { described_class.local_artifacts_store }
+
+ it "delegate to artifacts path" do
+ expect(Gitlab.config.artifacts).to receive(:path)
+
+ subject
+ end
+ end
+
+ describe '.artifacts_upload_path' do
+ subject { described_class.artifacts_upload_path }
+
+ it { is_expected.to start_with(path) }
+ it { is_expected.to end_with('tmp/uploads/') }
+ end
+
+ describe '#store_dir' do
+ subject { uploader.store_dir }
+
+ it { is_expected.to start_with(path) }
+ it { is_expected.to end_with("#{job.project_id}/#{job.id}") }
+ end
+
+ describe '#cache_dir' do
+ subject { uploader.cache_dir }
+
+ it { is_expected.to start_with(path) }
+ it { is_expected.to end_with('tmp/cache') }
+ end
+end
diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb
new file mode 100644
index 00000000000..78e9d9cf46c
--- /dev/null
+++ b/spec/uploaders/gitlab_uploader_spec.rb
@@ -0,0 +1,56 @@
+require 'rails_helper'
+require 'carrierwave/storage/fog'
+
+describe GitlabUploader do
+ let(:uploader_class) { Class.new(described_class) }
+
+ subject { uploader_class.new }
+
+ describe '#file_storage?' do
+ context 'when file storage is used' do
+ before do
+ uploader_class.storage(:file)
+ end
+
+ it { is_expected.to be_file_storage }
+ end
+
+ context 'when is remote storage' do
+ before do
+ uploader_class.storage(:fog)
+ end
+
+ it { is_expected.not_to be_file_storage }
+ end
+ end
+
+ describe '#file_cache_storage?' do
+ context 'when file storage is used' do
+ before do
+ uploader_class.cache_storage(:file)
+ end
+
+ it { is_expected.to be_file_cache_storage }
+ end
+
+ context 'when is remote storage' do
+ before do
+ uploader_class.cache_storage(:fog)
+ end
+
+ it { is_expected.not_to be_file_cache_storage }
+ end
+ end
+
+ describe '#move_to_cache' do
+ it 'is true' do
+ expect(subject.move_to_cache).to eq(true)
+ end
+ end
+
+ describe '#move_to_store' do
+ it 'is true' do
+ expect(subject.move_to_store).to eq(true)
+ end
+ end
+end