Commit 226db913 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-04-06

parents 3d2fd170 3d28a3e5
...@@ -4,7 +4,8 @@ import $ from 'jquery'; ...@@ -4,7 +4,8 @@ import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { __ } from './locale'; import { __ } from './locale';
import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils'; import { updateTooltipTitle } from './lib/utils/common_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
...@@ -243,7 +244,7 @@ class AwardsHandler { ...@@ -243,7 +244,7 @@ class AwardsHandler {
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length; const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length;
if (this.isInVueNoteablePage() && !isMainAwardsBlock) { if (isInVueNoteablePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', ''); const id = votesBlock.attr('id').replace('note_', '');
this.hideMenuElement($('.emoji-menu')); this.hideMenuElement($('.emoji-menu'));
...@@ -295,16 +296,8 @@ class AwardsHandler { ...@@ -295,16 +296,8 @@ class AwardsHandler {
} }
} }
isVueMRDiscussions() {
return isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
}
isInVueNoteablePage() {
return isInIssuePage() || isInEpicPage() || this.isVueMRDiscussions();
}
getVotesBlock() { getVotesBlock() {
if (this.isInVueNoteablePage()) { if (isInVueNoteablePage()) {
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry'); const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
if ($el.length) { if ($el.length) {
......
/* eslint-disable import/prefer-default-export */ import $ from 'jquery';
import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie } from './common_utils';
const isVueMRDiscussions = () => isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
export const addClassIfElementExists = (element, className) => { export const addClassIfElementExists = (element, className) => {
if (element) { if (element) {
element.classList.add(className); element.classList.add(className);
} }
}; };
export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isVueMRDiscussions();
...@@ -13,8 +13,11 @@ export default function initMrNotes() { ...@@ -13,8 +13,11 @@ export default function initMrNotes() {
data() { data() {
const notesDataset = document.getElementById('js-vue-mr-discussions') const notesDataset = document.getElementById('js-vue-mr-discussions')
.dataset; .dataset;
const noteableData = JSON.parse(notesDataset.noteableData);
noteableData.noteableType = notesDataset.noteableType;
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData,
currentUserData: JSON.parse(notesDataset.currentUserData), currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: JSON.parse(notesDataset.notesData), notesData: JSON.parse(notesDataset.notesData),
}; };
......
...@@ -49,16 +49,7 @@ export default { ...@@ -49,16 +49,7 @@ export default {
computed: { computed: {
...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']), ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
noteableType() { noteableType() {
// FIXME -- @fatihacet Get this from JSON data. return this.noteableData.noteableType;
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
if (this.noteableData.noteableType === EPIC_NOTEABLE_TYPE) {
return EPIC_NOTEABLE_TYPE;
}
return this.noteableData.merge_params
? MERGE_REQUEST_NOTEABLE_TYPE
: ISSUE_NOTEABLE_TYPE;
}, },
allNotes() { allNotes() {
if (this.isLoading) { if (this.isLoading) {
......
...@@ -14,3 +14,9 @@ export const EPIC_NOTEABLE_TYPE = 'epic'; ...@@ -14,3 +14,9 @@ export const EPIC_NOTEABLE_TYPE = 'epic';
export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request'; export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post'; export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
MergeRequest: MERGE_REQUEST_NOTEABLE_TYPE,
Epic: EPIC_NOTEABLE_TYPE,
};
...@@ -9,16 +9,7 @@ export default { ...@@ -9,16 +9,7 @@ export default {
}, },
computed: { computed: {
noteableType() { noteableType() {
switch (this.note.noteable_type) { return constants.NOTEABLE_TYPE_MAPPING[this.note.noteable_type];
case 'MergeRequest':
return constants.MERGE_REQUEST_NOTEABLE_TYPE;
case 'Issue':
return constants.ISSUE_NOTEABLE_TYPE;
case 'Epic':
return constants.EPIC_NOTEABLE_TYPE;
default:
return '';
}
}, },
}, },
}; };
<script>
import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import Flash from '~/flash';
export default {
components: {
GlModal,
},
props: {
actionUrl: {
type: String,
required: true,
},
rootUrl: {
type: String,
required: true,
},
initialUsername: {
type: String,
required: true,
},
},
data() {
return {
isRequestPending: false,
username: this.initialUsername,
newUsername: this.initialUsername,
};
},
computed: {
path() {
return sprintf(s__('Profiles|Current path: %{path}'), {
path: `${this.rootUrl}${this.username}`,
});
},
modalText() {
return sprintf(
s__(`Profiles|
You are going to change the username %{currentUsernameBold} to %{newUsernameBold}.
Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group.
Please update your Git repository remotes as soon as possible.`),
{
currentUsernameBold: `<strong>${_.escape(this.username)}</strong>`,
newUsernameBold: `<strong>${_.escape(this.newUsername)}</strong>`,
currentUsername: _.escape(this.username),
newUsername: _.escape(this.newUsername),
},
false,
);
},
},
methods: {
onConfirm() {
this.isRequestPending = true;
const username = this.newUsername;
const putData = {
user: {
username,
},
};
return axios
.put(this.actionUrl, putData)
.then(result => {
Flash(result.data.message, 'notice');
this.username = username;
this.isRequestPending = false;
})
.catch(error => {
Flash(error.response.data.message);
this.isRequestPending = false;
throw error;
});
},
},
modalId: 'username-change-confirmation-modal',
inputId: 'username-change-input',
buttonText: s__('Profiles|Update username'),
};
</script>
<template>
<div>
<div class="form-group">
<label :for="$options.inputId">{{ s__('Profiles|Path') }}</label>
<div class="input-group">
<div class="input-group-addon">{{ rootUrl }}</div>
<input
:id="$options.inputId"
class="form-control"
required="required"
v-model="newUsername"
:disabled="isRequestPending"
/>
</div>
<p class="help-block">
{{ path }}
</p>
</div>
<button
:data-target="`#${$options.modalId}`"
class="btn btn-warning"
type="button"
data-toggle="modal"
:disabled="isRequestPending || newUsername === username"
>
{{ $options.buttonText }}
</button>
<gl-modal
:id="$options.modalId"
:header-title-text="s__('Profiles|Change username') + '?'"
footer-primary-button-variant="warning"
:footer-primary-button-text="$options.buttonText"
@submit="onConfirm"
>
<span v-html="modalText"></span>
</gl-modal>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import UpdateUsername from './components/update_username.vue';
import deleteAccountModal from './components/delete_account_modal.vue'; import deleteAccountModal from './components/delete_account_modal.vue';
export default () => { export default () => {
Vue.use(Translate); Vue.use(Translate);
const updateUsernameElement = document.getElementById('update-username');
// eslint-disable-next-line no-new
new Vue({
el: updateUsernameElement,
components: {
UpdateUsername,
},
render(createElement) {
return createElement('update-username', {
props: { ...updateUsernameElement.dataset },
});
},
});
const deleteAccountButton = document.getElementById('delete-account-button'); const deleteAccountButton = document.getElementById('delete-account-button');
const deleteAccountModalEl = document.getElementById('delete-account-modal'); const deleteAccountModalEl = document.getElementById('delete-account-modal');
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
......
<script> <script>
const buttonVariants = [ const buttonVariants = ['danger', 'primary', 'success', 'warning'];
'danger',
'primary',
'success',
'warning',
];
export default { export default {
name: 'GlModal', name: 'GlModal',
props: { props: {
id: { id: {
type: String, type: String,
required: false, required: false,
default: null, default: null,
},
headerTitleText: {
type: String,
required: false,
default: '',
},
footerPrimaryButtonVariant: {
type: String,
required: false,
default: 'primary',
validator: value => buttonVariants.indexOf(value) !== -1,
},
footerPrimaryButtonText: {
type: String,
required: false,
default: '',
},
}, },
headerTitleText: {
type: String,
required: false,
default: '',
},
footerPrimaryButtonVariant: {
type: String,
required: false,
default: 'primary',
validator: value => buttonVariants.includes(value),
},
footerPrimaryButtonText: {
type: String,
required: false,
default: '',
},
},
methods: { methods: {
emitCancel(event) { emitCancel(event) {
this.$emit('cancel', event); this.$emit('cancel', event);
}, },
emitSubmit(event) { emitSubmit(event) {
this.$emit('submit', event); this.$emit('submit', event);
},
}, },
}; },
};
</script> </script>
<template> <template>
...@@ -60,7 +55,7 @@ ...@@ -60,7 +55,7 @@
<slot name="header"> <slot name="header">
<button <button
type="button" type="button"
class="close" class="close js-modal-close-action"
data-dismiss="modal" data-dismiss="modal"
:aria-label="s__('Modal|Close')" :aria-label="s__('Modal|Close')"
@click="emitCancel($event)" @click="emitCancel($event)"
...@@ -83,7 +78,7 @@ ...@@ -83,7 +78,7 @@
<slot name="footer"> <slot name="footer">
<button <button
type="button" type="button"
class="btn" class="btn js-modal-cancel-action"
data-dismiss="modal" data-dismiss="modal"
@click="emitCancel($event)" @click="emitCancel($event)"
> >
...@@ -91,7 +86,7 @@ ...@@ -91,7 +86,7 @@
</button> </button>
<button <button
type="button" type="button"
class="btn" class="btn js-modal-primary-action"
:class="`btn-${footerPrimaryButtonVariant}`" :class="`btn-${footerPrimaryButtonVariant}`"
data-dismiss="modal" data-dismiss="modal"
@click="emitSubmit($event)" @click="emitSubmit($event)"
......
...@@ -53,13 +53,19 @@ class ProfilesController < Profiles::ApplicationController ...@@ -53,13 +53,19 @@ class ProfilesController < Profiles::ApplicationController
def update_username def update_username
result = Users::UpdateService.new(current_user, user: @user, username: username_param).execute result = Users::UpdateService.new(current_user, user: @user, username: username_param).execute
options = if result[:status] == :success respond_to do |format|
{ notice: "Username successfully changed" } if result[:status] == :success
else message = s_("Profiles|Username successfully changed")
{ alert: "Username change failed - #{result[:message]}" }
end
redirect_back_or_default(default: { action: 'show' }, options: options) format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
format.json { render json: { message: message }, status: :ok }
else
message = s_("Profiles|Username change failed - %{message}") % { message: result[:message] }
format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: message }) }
format.json { render json: { message: message }, status: :unprocessable_entity }
end
end
end end
private private
......
...@@ -17,7 +17,7 @@ class MergeRequestDiffCommit < ActiveRecord::Base ...@@ -17,7 +17,7 @@ class MergeRequestDiffCommit < ActiveRecord::Base
commit_hash.merge( commit_hash.merge(
merge_request_diff_id: merge_request_diff_id, merge_request_diff_id: merge_request_diff_id,
relative_order: index, relative_order: index,
sha: sha_attribute.type_cast_for_database(sha), sha: sha_attribute.serialize(sha), # rubocop:disable Cop/ActiveRecordSerialize
authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]), authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]),
committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date]) committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date])
) )
......
...@@ -718,10 +718,6 @@ class User < ActiveRecord::Base ...@@ -718,10 +718,6 @@ class User < ActiveRecord::Base
projects_limit - personal_projects_count projects_limit - personal_projects_count
end end
def personal_projects_count
@personal_projects_count ||= personal_projects.count
end
def recent_push(project = nil) def recent_push(project = nil)
service = Users::LastPushEventService.new(self) service = Users::LastPushEventService.new(self)
...@@ -1068,6 +1064,12 @@ class User < ActiveRecord::Base ...@@ -1068,6 +1064,12 @@ class User < ActiveRecord::Base
end end
end end
def personal_projects_count(force: false)
Rails.cache.fetch(['users', id, 'personal_projects_count'], force: force, expires_in: 24.hours, raw: true) do
personal_projects.count
end.to_i
end
def update_todos_count_cache def update_todos_count_cache
todos_done_count(force: true) todos_done_count(force: true)
todos_pending_count(force: true) todos_pending_count(force: true)
...@@ -1078,6 +1080,7 @@ class User < ActiveRecord::Base ...@@ -1078,6 +1080,7 @@ class User < ActiveRecord::Base
invalidate_merge_request_cache_counts invalidate_merge_request_cache_counts
invalidate_todos_done_count invalidate_todos_done_count
invalidate_todos_pending_count invalidate_todos_pending_count
invalidate_personal_projects_count
end end
def invalidate_issue_cache_counts def invalidate_issue_cache_counts
...@@ -1096,6 +1099,10 @@ class User < ActiveRecord::Base ...@@ -1096,6 +1099,10 @@ class User < ActiveRecord::Base
Rails.cache.delete(['users', id, 'todos_pending_count']) Rails.cache.delete(['users', id, 'todos_pending_count'])
end end
def invalidate_personal_projects_count
Rails.cache.delete(['users', id, 'personal_projects_count'])
end
# This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth # This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
# flow means we don't call that automatically (and can't conveniently do so). # flow means we don't call that automatically (and can't conveniently do so).
# #
......
...@@ -98,6 +98,8 @@ module Projects ...@@ -98,6 +98,8 @@ module Projects
system_hook_service.execute_hooks_for(@project, :create) system_hook_service.execute_hooks_for(@project, :create)
setup_authorizations setup_authorizations
current_user.invalidate_personal_projects_count
end end
# Refresh the current user's authorizations inline (so they can access the # Refresh the current user's authorizations inline (so they can access the
......
...@@ -35,6 +35,8 @@ module Projects ...@@ -35,6 +35,8 @@ module Projects
system_hook_service.execute_hooks_for(project, :destroy) system_hook_service.execute_hooks_for(project, :destroy)
log_info("Project \"#{project.full_path}\" was removed") log_info("Project \"#{project.full_path}\" was removed")
current_user.invalidate_personal_projects_count
true true
rescue => error rescue => error
attempt_rollback(project, error.message) attempt_rollback(project, error.message)
......
...@@ -26,6 +26,8 @@ module Projects ...@@ -26,6 +26,8 @@ module Projects
transfer(project) transfer(project)
current_user.invalidate_personal_projects_count
true true
rescue Projects::TransferService::TransferError => ex rescue Projects::TransferService::TransferError => ex
project.reload project.reload
......
...@@ -57,20 +57,8 @@ ...@@ -57,20 +57,8 @@
= succeed '.' do = succeed '.' do
= link_to 'Learn more', help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank' = link_to 'Learn more', help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank'
.col-lg-8 .col-lg-8
= form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f| - data = { initial_username: current_user.username, root_url: root_url, action_url: update_username_profile_path(format: :json) }
.form-group #update-username{ data: data }
= f.label :username, "Path", class: "label-light"
.input-group
.input-group-addon
= root_url
= f.text_field :username, required: true, class: 'form-control'
.help-block
Current path:
#{root_url}#{current_user.username}
.prepend-top-default
= f.button class: "btn btn-warning", type: "submit" do
= icon "spinner spin", class: "hidden loading-username"
Update username
%hr %hr
.row.prepend-top-default .row.prepend-top-default
......
...@@ -8,4 +8,5 @@ ...@@ -8,4 +8,5 @@
%section.js-vue-notes-event %section.js-vue-notes-event
#js-vue-notes{ data: { notes_data: notes_data(@issue), #js-vue-notes{ data: { notes_data: notes_data(@issue),
noteable_data: serialize_issuable(@issue), noteable_data: serialize_issuable(@issue),
noteable_type: 'issue',
current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } } current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
...@@ -92,6 +92,7 @@ ...@@ -92,6 +92,7 @@
- if has_vue_discussions_cookie? - if has_vue_discussions_cookie?
#js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request), #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request),
noteable_data: serialize_issuable(@merge_request), noteable_data: serialize_issuable(@merge_request),
noteable_type: 'merge_request',
current_user_data: UserSerializer.new.represent(current_user).to_json} } current_user_data: UserSerializer.new.represent(current_user).to_json} }
#commits.commits.tab-pane #commits.commits.tab-pane
......
---
title: Added confirmation modal for changing username
merge_request: 17405
author:
type: added
---
title: Cache personal projects count.
merge_request: 18197
author:
type: performance
module ActiveRecord # Remove this initializer when upgraded to Rails 5.0
class PredicateBuilder unless Gitlab.rails5?
class ArrayHandler module ActiveRecord
module TypeCasting class PredicateBuilder
def call(attribute, value) class ArrayHandler
# This is necessary because by default ActiveRecord does not respect module TypeCasting
# custom type definitions (like our `ShaAttribute`) when providing an def call(attribute, value)
# array in `where`, like in `where(commit_sha: [sha1, sha2, sha3])`. # This is necessary because by default ActiveRecord does not respect
model = attribute.relation&.engine # custom type definitions (like our `ShaAttribute`) when providing an
type = model.user_provided_columns[attribute.name] if model # array in `where`, like in `where(commit_sha: [sha1, sha2, sha3])`.
value = value.map { |value| type.type_cast_for_database(value) } if type model = attribute.relation&.engine
type = model.user_provided_columns[attribute.name] if model
value = value.map { |value| type.type_cast_for_database(value) } if type
super(attribute, value) super(attribute, value)
end
end end
end
prepend TypeCasting prepend TypeCasting
end
end end
end end
end end
...@@ -44,6 +44,7 @@ the `author` field. GitLab team members **should not**. ...@@ -44,6 +44,7 @@ the `author` field. GitLab team members **should not**.
- _Any_ contribution from a community member, no matter how small, **may** have - _Any_ contribution from a community member, no matter how small, **may** have
a changelog entry regardless of these guidelines if the contributor wants one. a changelog entry regardless of these guidelines if the contributor wants one.
Example: "Fixed a typo on the search results page. (Jane Smith)" Example: "Fixed a typo on the search results page. (Jane Smith)"
- Performance improvements **should** have a changelog entry.
## Writing good changelog entries ## Writing good changelog entries
......
...@@ -72,15 +72,23 @@ The maximum size your Git repository is allowed to be including LFS. ...@@ -72,15 +72,23 @@ The maximum size your Git repository is allowed to be including LFS.
## Shared Runners ## Shared Runners
Shared Runners on GitLab.com run in [autoscale mode] and powered by Shared Runners on GitLab.com run in [autoscale mode] and powered by
DigitalOcean. Autoscaling means reduced waiting times to spin up builds, Google Cloud Platform and DigitalOcean. Autoscaling means reduced
and isolated VMs for each project, thus maximizing security. waiting times to spin up CI/CD jobs, and isolated VMs for each project,
thus maximizing security.
They're free to use for public open source projects and limited to 2000 CI They're free to use for public open source projects and limited to 2000 CI
minutes per month per group for private projects. Read about all minutes per month per group for private projects. Read about all
[GitLab.com plans](https://about.gitlab.com/pricing/). [GitLab.com plans](https://about.gitlab.com/pricing/).
All your builds run on 2GB (RAM) ephemeral instances, with CoreOS and the latest In case of DigitalOcean based Runners, all your CI/CD jobs run on ephemeral
Docker Engine installed. The default region of the VMs is NYC. instances with 2GB of RAM, CoreOS and the latest Docker Engine installed.
Instances provide 2 vCPUs and 60GB of SSD disk space. The default region of the
VMs is NYC1.
In case of Google Cloud Platform based Runners, all your CI/CD jobs run on
ephemeral instances with 3.75GB of RAM, CoreOS and the latest Docker Engine
installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default
region of the VMs is US East1.
Below are the shared Runners settings. Below are the shared Runners settings.
...@@ -88,52 +96,116 @@ Below are the shared Runners settings. ...@@ -88,52 +96,116 @@ Below are the shared Runners settings.
| ----------- | ----------------- | ---------- | | ----------- | ----------------- | ---------- |
| [GitLab Runner] | [Runner versions dashboard][ci_version_dashboard] | - | | [GitLab Runner] | [Runner versions dashboard][ci_version_dashboard] | - |
| Executor | `docker+machine` | - | | Executor | `docker+machine` | - |
| Default Docker image | `ruby:2.1` | - | | Default Docker image | `ruby:2.5` | - |
| `privileged` (run [Docker in Docker]) | `true` | `false` | | `privileged` (run [Docker in Docker]) | `true` | `false` |
[ci_version_dashboard]: https://monitor.gitlab.net/dashboard/db/ci?refresh=5m&orgId=1&panelId=12&fullscreen&from=now-1h&to=now&var-runner_type=All&var-cache_server=All&var-gl_monitor_fqdn=postgres-01.db.prd.gitlab.com&var-has_minutes=yes&var-hanging_droplets_cleaner=All&var-droplet_zero_machines_cleaner=All&var-runner_job_failure_reason=All&theme=light [ci_version_dashboard]: https://monitor.gitlab.net/dashboard/db/ci?from=now-1h&to=now&refresh=5m&orgId=1&panelId=12&fullscreen&theme=light
### `config.toml` ### `config.toml`
The full contents of our `config.toml` are: The full contents of our `config.toml` are:
**DigitalOcean**
```toml ```toml
concurrent = X
check_interval = 1
metrics_server = "X"
sentry_dsn = "X"
[[runners]] [[runners]]
name = "docker-auto-scale" name = "docker-auto-scale"
limit = X
request_concurrency = X request_concurrency = X
url = "https://gitlab.com/ci" url = "https://gitlab.com/"
token = "SHARED_RUNNER_TOKEN" token = "SHARED_RUNNER_TOKEN"
executor = "docker+machine" executor = "docker+machine"
environment = [ environment = [
"DOCKER_DRIVER=overlay2" "DOCKER_DRIVER=overlay2"
] ]
limit = X
[runners.docker] [runners.docker]
image = "ruby:2.1" image = "ruby:2.5"
privileged = true privileged = true
[runners.machine] [runners.machine]
IdleCount = 40 IdleCount = 20
IdleTime = 1800 IdleTime = 1800
OffPeakPeriods = ["* * * * * sat,sun *"]
OffPeakTimezone = "UTC"
OffPeakIdleCount = 5
OffPeakIdleTime = 1800
MaxBuilds = 1 MaxBuilds = 1
MachineName = "srm-%s"
MachineDriver = "digitalocean" MachineDriver = "digitalocean"
MachineName = "machine-%s-digital-ocean-2gb"
MachineOptions = [ MachineOptions = [
"digitalocean-image=coreos-stable", "digitalocean-image=X",
"digitalocean-ssh-user=core", "digitalocean-ssh-user=core",
"digitalocean-access-token=DIGITAL_OCEAN_ACCESS_TOKEN",
"digitalocean-region=nyc1", "digitalocean-region=nyc1",
"digitalocean-size=2gb", "digitalocean-size=s-2vcpu-2gb",
"digitalocean-private-networking", "digitalocean-private-networking",
"digitalocean-userdata=/etc/gitlab-runner/cloudinit.sh", "digitalocean-tags=shared_runners,gitlab_com",
"engine-registry-mirror=http://IP_TO_OUR_REGISTRY_MIRROR" "engine-registry-mirror=http://INTERNAL_IP_OF_OUR_REGISTRY_MIRROR",
"digitalocean-access-token=DIGITAL_OCEAN_ACCESS_TOKEN",
] ]
[runners.cache] [runners.cache]
Type = "s3" Type = "s3"
ServerAddress = "IP_TO_OUR_CACHE_SERVER" BucketName = "runner"
Insecure = true
Shared = true
ServerAddress = "INTERNAL_IP_OF_OUR_CACHE_SERVER"
AccessKey = "ACCESS_KEY" AccessKey = "ACCESS_KEY"
SecretKey = "ACCESS_SECRET_KEY" SecretKey = "ACCESS_SECRET_KEY"
```
**Google Cloud Platform**
```toml
concurrent = X
check_interval = 1
metrics_server = "X"
sentry_dsn = "X"
[[runners]]
name = "docker-auto-scale"
request_concurrency = X
url = "https://gitlab.com/"
token = "SHARED_RUNNER_TOKEN"
executor = "docker+machine"
environment = [
"DOCKER_DRIVER=overlay2"
]
limit = X
[runners.docker]
image = "ruby:2.5"
privileged = true
[runners.machine]
IdleCount = 20
IdleTime = 1800
OffPeakPeriods = ["* * * * * sat,sun *"]
OffPeakTimezone = "UTC"
OffPeakIdleCount = 5
OffPeakIdleTime = 1800
MaxBuilds = 1
MachineName = "srm-%s"
MachineDriver = "google"
MachineOptions = [
"google-project=PROJECT",
"google-disk-size=25",
"google-machine-type=n1-standard-1",
"google-username=core",
"google-tags=gitlab-com,srm",
"google-use-internal-ip",
"google-zone=us-east1-d",
"google-machine-image=PROJECT/global/images/IMAGE",
"engine-registry-mirror=http://INTERNAL_IP_OF_OUR_REGISTRY_MIRROR"
]
[runners.cache]
Type = "s3"
BucketName = "runner" BucketName = "runner"
Insecure = true
Shared = true Shared = true
ServerAddress = "INTERNAL_IP_OF_OUR_CACHE_SERVER"
AccessKey = "ACCESS_KEY"
SecretKey = "ACCESS_SECRET_KEY"
``` ```
## Sidekiq ## Sidekiq
......
...@@ -96,7 +96,7 @@ module Gitlab ...@@ -96,7 +96,7 @@ module Gitlab
commit_hash.merge( commit_hash.merge(
merge_request_diff_id: merge_request_diff.id, merge_request_diff_id: merge_request_diff.id,
relative_order: index, relative_order: index,
sha: sha_attribute.type_cast_for_database(sha) sha: sha_attribute.serialize(sha)
) )
end end
......
module Gitlab module Gitlab
module Database module Database
BINARY_TYPE = if Gitlab::Database.postgresql? BINARY_TYPE =
# PostgreSQL defines its own class with slightly different if Gitlab::Database.postgresql?
# behaviour from the default Binary type. # PostgreSQL defines its own class with slightly different
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Bytea # behaviour from the default Binary type.
else ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Bytea
ActiveRecord::Type::Binary else
end # In Rails 5.0 `Type` has been moved from `ActiveRecord` to `ActiveModel`
# https://github.com/rails/rails/commit/9cc8c6f3730df3d94c81a55be9ee1b7b4ffd29f6#diff-f8ba7983a51d687976e115adcd95822b
# Remove this method and leave just `ActiveModel::Type::Binary` when removing Gitlab.rails5? code.
if Gitlab.rails5?
ActiveModel::Type::Binary
else
ActiveRecord::Type::Binary
end
end
# Class for casting binary data to hexadecimal SHA1 hashes (and vice-versa). # Class for casting binary data to hexadecimal SHA1 hashes (and vice-versa).
# #
...@@ -16,18 +24,39 @@ module Gitlab ...@@ -16,18 +24,39 @@ module Gitlab
class ShaAttribute < BINARY_TYPE class ShaAttribute < BINARY_TYPE
PACK_FORMAT = 'H*'.freeze PACK_FORMAT = 'H*'.freeze
# Casts binary data to a SHA1 in hexadecimal. # It is called from activerecord-4.2.10/lib/active_record internal methods.
# Remove this method when removing Gitlab.rails5? code.
def type_cast_from_database(value) def type_cast_from_database(value)
value = super unpack_sha(super)
end
# It is called from activerecord-4.2.10/lib/active_record internal methods.
# Remove this method when removing Gitlab.rails5? code.
def type_cast_for_database(value)
serialize(value)
end
# It is called from activerecord-5.0.6/lib/active_record/attribute.rb
# Remove this method when removing Gitlab.rails5? code..
def deserialize(value)
value = Gitlab.rails5? ? super : method(:type_cast_from_database).super_method.call(value)
unpack_sha(value)
end
# Rename this method to `deserialize(value)` removing Gitlab.rails5? code.
# Casts binary data to a SHA1 in hexadecimal.
def unpack_sha(value)
# Uncomment this line when removing Gitlab.rails5? code.
# value = super
value ? value.unpack(PACK_FORMAT)[0] : nil value ? value.unpack(PACK_FORMAT)[0] : nil
end end
# Casts a SHA1 in hexadecimal to the proper binary format. # Casts a SHA1 in hexadecimal to the proper binary format.
def type_cast_for_database(value) def serialize(value)
arg = value ? [value].pack(PACK_FORMAT) : nil arg = value ? [value].pack(PACK_FORMAT) : nil
super(arg) Gitlab.rails5? ? super(arg) : method(:type_cast_for_database).super_method.call(arg)
end end
end end
end end
......
...@@ -84,6 +84,28 @@ describe ProfilesController, :request_store do ...@@ -84,6 +84,28 @@ describe ProfilesController, :request_store do
expect(user.username).to eq(new_username) expect(user.username).to eq(new_username)
end end
it 'updates a username using JSON request' do
sign_in(user)
put :update_username,
user: { username: new_username },
format: :json
expect(response.status).to eq(200)
expect(json_response['message']).to eq('Username successfully changed')
end
it 'renders an error message when the username was not updated' do
sign_in(user)
put :update_username,
user: { username: 'invalid username.git' },
format: :json
expect(response.status).to eq(422)
expect(json_response['message']).to match(/Username change failed/)
end
it 'raises a correct error when the username is missing' do it 'raises a correct error when the username is missing' do
sign_in(user) sign_in(user)
......
...@@ -97,9 +97,13 @@ describe 'Profile account page', :js do ...@@ -97,9 +97,13 @@ describe 'Profile account page', :js do
end end
it 'changes my username' do it 'changes my username' do
fill_in 'user_username', with: 'new-username' fill_in 'username-change-input', with: 'new-username'
click_button('Update username') page.find('[data-target="#username-change-confirmation-modal"]').click
page.within('.modal') do
find('.js-modal-primary-action').click
end
expect(page).to have_content('new-username') expect(page).to have_content('new-username')
end end
......
require 'rails_helper' require 'rails_helper'
feature 'Profile > Account' do feature 'Profile > Account', :js do
given(:user) { create(:user, username: 'foo') } given(:user) { create(:user, username: 'foo') }
before do before do
...@@ -59,6 +59,12 @@ end ...@@ -59,6 +59,12 @@ end
def update_username(new_username) def update_username(new_username)
allow(user.namespace).to receive(:move_dir) allow(user.namespace).to receive(:move_dir)
visit profile_account_path visit profile_account_path
fill_in 'user_username', with: new_username
click_button 'Update username' fill_in 'username-change-input', with: new_username
page.find('[data-target="#username-change-confirmation-modal"]').click
page.within('.modal') do
find('.js-modal-primary-action').click
end
end end
...@@ -52,6 +52,7 @@ export const noteableDataMock = { ...@@ -52,6 +52,7 @@ export const noteableDataMock = {
updated_at: '2017-08-04T09:53:01.226Z', updated_at: '2017-08-04T09:53:01.226Z',
updated_by_id: 1, updated_by_id: 1,
web_url: '/gitlab-org/gitlab-ce/issues/26', web_url: '/gitlab-org/gitlab-ce/issues/26',
noteableType: 'issue',
}; };
export const lastFetchedAt = '1501862675'; export const lastFetchedAt = '1501862675';
......
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import updateUsername from '~/profile/account/components/update_username.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('UpdateUsername component', () => {
const rootUrl = gl.TEST_HOST;
const actionUrl = `${gl.TEST_HOST}/update/username`;
const username = 'hasnoname';
const newUsername = 'new_username';
let Component;
let vm;
let axiosMock;
beforeEach(() => {
axiosMock = new MockAdapter(axios);
Component = Vue.extend(updateUsername);
vm = mountComponent(Component, {
actionUrl,
rootUrl,
initialUsername: username,
});
});
afterEach(() => {
vm.$destroy();
axiosMock.restore();
});
const findElements = () => {
const modalSelector = `#${vm.$options.modalId}`;
return {
input: vm.$el.querySelector(`#${vm.$options.inputId}`),
openModalBtn: vm.$el.querySelector(`[data-target="${modalSelector}"]`),
modal: vm.$el.querySelector(modalSelector),
modalBody: vm.$el.querySelector(`${modalSelector} .modal-body`),
modalHeader: vm.$el.querySelector(`${modalSelector} .modal-title`),
confirmModalBtn: vm.$el.querySelector(`${modalSelector} .btn-warning`),
};
};
it('has a disabled button if the username was not changed', done => {
const { input, openModalBtn } = findElements();
input.dispatchEvent(new Event('input'));
Vue.nextTick()
.then(() => {
expect(vm.username).toBe(username);
expect(vm.newUsername).toBe(username);
expect(openModalBtn).toBeDisabled();
})
.then(done)
.catch(done.fail);
});
it('has an enabled button which if the username was changed', done => {
const { input, openModalBtn } = findElements();
input.value = newUsername;
input.dispatchEvent(new Event('input'));
Vue.nextTick()
.then(() => {
expect(vm.username).toBe(username);
expect(vm.newUsername).toBe(newUsername);
expect(openModalBtn).not.toBeDisabled();
})
.then(done)
.catch(done.fail);
});
it('confirmation modal contains proper header and body', done => {
const { modalBody, modalHeader } = findElements();
vm.newUsername = newUsername;
Vue.nextTick()
.then(() => {
expect(modalHeader.textContent).toContain('Change username?');
expect(modalBody.textContent).toContain(
`You are going to change the username ${username} to ${newUsername}`,
);
})
.then(done)
.catch(done.fail);
});
it('confirmation modal should escape usernames properly', done => {
const { modalBody } = findElements();
vm.username = vm.newUsername = '<i>Italic</i>';
Vue.nextTick()
.then(() => {
expect(modalBody.innerHTML).toContain('&lt;i&gt;Italic&lt;/i&gt;');
expect(modalBody.innerHTML).not.toContain(vm.username);
})
.then(done)
.catch(done.fail);
});
it('executes API call on confirmation button click', done => {
const { confirmModalBtn } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => [200, { message: 'Username changed' }]);
spyOn(axios, 'put').and.callThrough();
vm.newUsername = newUsername;
Vue.nextTick()
.then(() => {
confirmModalBtn.click();
expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } });
})
.then(done)
.catch(done.fail);
});
it('sets the username after a successful update', done => {
const { input, openModalBtn } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => {
expect(input).toBeDisabled();
expect(openModalBtn).toBeDisabled();
return [200, { message: 'Username changed' }];
});
vm.newUsername = newUsername;
vm
.onConfirm()
.then(() => {
expect(vm.username).toBe(newUsername);
expect(vm.newUsername).toBe(newUsername);
expect(input).not.toBeDisabled();
expect(input.value).toBe(newUsername);
expect(openModalBtn).toBeDisabled();
})
.then(done)
.catch(done.fail);
});
it('does not set the username after a erroneous update', done => {
const { input, openModalBtn } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => {
expect(input).toBeDisabled();
expect(openModalBtn).toBeDisabled();
return [400, { message: 'Invalid username' }];
});
const invalidUsername = 'anything.git';
vm.newUsername = invalidUsername;
vm
.onConfirm()
.then(() => done.fail('Expected onConfirm to throw!'))
.catch(() => {
expect(vm.username).toBe(username);
expect(vm.newUsername).toBe(invalidUsername);
expect(input).not.toBeDisabled();
expect(input.value).toBe(invalidUsername);
expect(openModalBtn).not.toBeDisabled();
})
.then(done)
.catch(done.fail);
});
});
...@@ -19,15 +19,15 @@ describe Gitlab::Database::ShaAttribute do ...@@ -19,15 +19,15 @@ describe Gitlab::Database::ShaAttribute do
let(:attribute) { described_class.new } let(:attribute) { described_class.new }
describe '#type_cast_from_database' do describe '#deserialize' do
it 'converts the binary SHA to a String' do it 'converts the binary SHA to a String' do
expect(attribute.type_cast_from_database(binary_from_db)).to eq(sha) expect(attribute.deserialize(binary_from_db)).to eq(sha)
end end
end end
describe '#type_cast_for_database' do describe '#serialize' do
it 'converts a SHA String to binary data' do it 'converts a SHA String to binary data' do
expect(attribute.type_cast_for_database(sha).to_s).to eq(binary_sha) expect(attribute.serialize(sha).to_s).to eq(binary_sha)
end end
end end
end end
...@@ -36,7 +36,7 @@ describe MergeRequestDiffCommit do ...@@ -36,7 +36,7 @@ describe MergeRequestDiffCommit do
"committer_email": "dmitriy.zaporozhets@gmail.com", "committer_email": "dmitriy.zaporozhets@gmail.com",
"merge_request_diff_id": merge_request_diff_id, "merge_request_diff_id": merge_request_diff_id,
"relative_order": 0, "relative_order": 0,
"sha": sha_attribute.type_cast_for_database('5937ac0a7beb003549fc5fd26fc247adbce4a52e') "sha": sha_attribute.serialize("5937ac0a7beb003549fc5fd26fc247adbce4a52e")
}, },
{ {
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
...@@ -48,7 +48,7 @@ describe MergeRequestDiffCommit do ...@@ -48,7 +48,7 @@ describe MergeRequestDiffCommit do
"committer_email": "dmitriy.zaporozhets@gmail.com", "committer_email": "dmitriy.zaporozhets@gmail.com",
"merge_request_diff_id": merge_request_diff_id, "merge_request_diff_id": merge_request_diff_id,
"relative_order": 1, "relative_order": 1,
"sha": sha_attribute.type_cast_for_database('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') "sha": sha_attribute.serialize("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
} }
] ]
end end
...@@ -79,7 +79,7 @@ describe MergeRequestDiffCommit do ...@@ -79,7 +79,7 @@ describe MergeRequestDiffCommit do
"committer_email": "alejorro70@gmail.com", "committer_email": "alejorro70@gmail.com",
"merge_request_diff_id": merge_request_diff_id, "merge_request_diff_id": merge_request_diff_id,
"relative_order": 0, "relative_order": 0,
"sha": sha_attribute.type_cast_for_database('ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69') "sha": sha_attribute.serialize("ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69")
}] }]
end end
......
...@@ -2337,6 +2337,20 @@ describe User do ...@@ -2337,6 +2337,20 @@ describe User do
end end
end end
context '#invalidate_personal_projects_count' do
let(:user) { build_stubbed(:user) }
it 'invalidates cache for personal projects counter' do
cache_mock = double
expect(cache_mock).to receive(:delete).with(['users', user.id, 'personal_projects_count'])
allow(Rails).to receive(:cache).and_return(cache_mock)
user.invalidate_personal_projects_count
end
end
describe '#allow_password_authentication_for_web?' do describe '#allow_password_authentication_for_web?' do
context 'regular user' do context 'regular user' do
let(:user) { build(:user) } let(:user) { build(:user) }
...@@ -2386,11 +2400,9 @@ describe User do ...@@ -2386,11 +2400,9 @@ describe User do
user = build(:user) user = build(:user)
projects = double(:projects, count: 1) projects = double(:projects, count: 1)
expect(user).to receive(:personal_projects).once.and_return(projects) expect(user).to receive(:personal_projects).and_return(projects)
2.times do expect(user.personal_projects_count).to eq(1)
expect(user.personal_projects_count).to eq(1)
end
end end
end end
......
...@@ -28,6 +28,14 @@ describe Projects::CreateService, '#execute' do ...@@ -28,6 +28,14 @@ describe Projects::CreateService, '#execute' do
end end
end end
describe 'after create actions' do
it 'invalidate personal_projects_count caches' do
expect(user).to receive(:invalidate_personal_projects_count)
create_project(user, opts)
end
end
context "admin creates project with other user's namespace_id" do context "admin creates project with other user's namespace_id" do
it 'sets the correct permissions' do it 'sets the correct permissions' do
admin = create(:admin) admin = create(:admin)
......
...@@ -79,6 +79,12 @@ describe Projects::DestroyService do ...@@ -79,6 +79,12 @@ describe Projects::DestroyService do
end end
it_behaves_like 'deleting the project' it_behaves_like 'deleting the project'
it 'invalidates personal_project_count cache' do
expect(user).to receive(:invalidate_personal_projects_count)
destroy_project(project, user)
end
end end
context 'Sidekiq fake' do context 'Sidekiq fake' do
......
...@@ -37,6 +37,12 @@ describe Projects::TransferService do ...@@ -37,6 +37,12 @@ describe Projects::TransferService do
transfer_project(project, user, group) transfer_project(project, user, group)
end end
it 'invalidates the user\'s personal_project_count cache' do
expect(user).to receive(:invalidate_personal_projects_count)
transfer_project(project, user, group)
end
it 'executes system hooks' do it 'executes system hooks' do
transfer_project(project, user, group) do |service| transfer_project(project, user, group) do |service|
expect(service).to receive(:execute_system_hooks) expect(service).to receive(:execute_system_hooks)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment