Commit aba71835 authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge remote-tracking branch 'origin/master' into...

Merge remote-tracking branch 'origin/master' into feature/sm/35954-create-kubernetes-cluster-on-gke-from-k8s-service
parents b229637b 1fe7a42a
...@@ -2,6 +2,23 @@ ...@@ -2,6 +2,23 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.0.3 (2017-10-05)
- [FIXED] find_user Users helper method no longer overrides find_user API helper method. !14418
- [FIXED] Fix CSRF validation issue when closing/opening merge requests from the UI. !14555
- [FIXED] Kubernetes integration: ensure v1.8.0 compatibility. !14635
- [FIXED] Fixes data parameter not being sent in ajax request for jobs log.
- [FIXED] Improves UX of autodevops popover to match gpg one.
- [FIXED] Fixed commenting on side-by-side commit diff.
- [FIXED] Make sure API responds with 401 when invalid authentication info is provided.
- [FIXED] Fix merge request counter updates after merge.
- [FIXED] Fix gitlab-rake gitlab:import:repos task failing.
- [FIXED] Fix pushes to an empty repository not invalidating has_visible_content? cache.
- [FIXED] Ensure all refs are restored on a restore from backup.
- [FIXED] Gitaly RepositoryExists remains opt-in for all method calls.
- [FIXED] Fix 500 error on merged merge requests when GitLab is restored from a backup.
- [FIXED] Adjust MRs being stuck on "process of being merged" for more than 2 hours.
## 10.0.2 (2017-09-27) ## 10.0.2 (2017-09-27)
- [FIXED] Notes will not show an empty bubble when the author isn't a member. !14450 - [FIXED] Notes will not show an empty bubble when the author isn't a member. !14450
...@@ -195,6 +212,10 @@ entry. ...@@ -195,6 +212,10 @@ entry.
- Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi) - Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi)
- [BUGIFX] Improves subgroup creation permissions. !13418 - [BUGIFX] Improves subgroup creation permissions. !13418
## 9.5.8 (2017-10-04)
- [FIXED] Fixed fork button being disabled for users who can fork to a group.
## 9.5.7 (2017-10-03) ## 9.5.7 (2017-10-03)
- Fix gitlab rake:import:repos task. - Fix gitlab rake:import:repos task.
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
showEmptyState: true, showEmptyState: true,
updateAspectRatio: false, updateAspectRatio: false,
updatedAspectRatios: 0, updatedAspectRatios: 0,
hoverData: {},
resizeThrottled: {}, resizeThrottled: {},
}; };
}, },
...@@ -64,6 +65,10 @@ ...@@ -64,6 +65,10 @@
this.updatedAspectRatios = 0; this.updatedAspectRatios = 0;
} }
}, },
hoverChanged(data) {
this.hoverData = data;
},
}, },
created() { created() {
...@@ -72,10 +77,12 @@ ...@@ -72,10 +77,12 @@
deploymentEndpoint: this.deploymentEndpoint, deploymentEndpoint: this.deploymentEndpoint,
}); });
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$on('hoverChanged', this.hoverChanged);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$off('hoverChanged', this.hoverChanged);
window.removeEventListener('resize', this.resizeThrottled, false); window.removeEventListener('resize', this.resizeThrottled, false);
}, },
...@@ -102,6 +109,7 @@ ...@@ -102,6 +109,7 @@
v-for="(graphData, index) in groupData.metrics" v-for="(graphData, index) in groupData.metrics"
:key="index" :key="index"
:graph-data="graphData" :graph-data="graphData"
:hover-data="hoverData"
:update-aspect-ratio="updateAspectRatio" :update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData" :deployment-data="store.deploymentData"
/> />
......
...@@ -73,34 +73,22 @@ ...@@ -73,34 +73,22 @@
<template> <template>
<div class="prometheus-state"> <div class="prometheus-state">
<div class="row"> <div class="state-svg svg-content">
<div class="col-md-4 col-md-offset-4 state-svg svg-content"> <img :src="currentState.svgUrl"/>
<img :src="currentState.svgUrl"/>
</div>
</div> </div>
<div class="row"> <h4 class="state-title">
<div class="col-md-6 col-md-offset-3"> {{currentState.title}}
<h4 class="text-center state-title"> </h4>
{{currentState.title}} <p class="state-description">
</h4> {{currentState.description}}
</div> <a v-if="showButtonDescription" :href="settingsPath">
</div> Prometheus server
<div class="row"> </a>
<div class="col-md-6 col-md-offset-3"> </p>
<div class="description-text text-center state-description"> <div class="state-button">
{{currentState.description}} <a class="btn btn-success" :href="buttonPath">
<a v-if="showButtonDescription" :href="settingsPath"> {{currentState.buttonText}}
Prometheus server </a>
</a>
</div>
</div>
</div>
<div class="row state-button-section">
<div class="col-md-4 col-md-offset-4 text-center state-button">
<a class="btn btn-success" :href="buttonPath">
{{currentState.buttonText}}
</a>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -7,12 +7,10 @@ ...@@ -7,12 +7,10 @@
import MonitoringMixin from '../mixins/monitoring_mixins'; import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import measurements from '../utils/measurements'; import measurements from '../utils/measurements';
import { timeScaleFormat } from '../utils/date_time_formatters'; import { timeScaleFormat, bisectDate } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series'; import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints'; import bp from '../../breakpoints';
const bisectDate = d3.bisector(d => d.time).left;
export default { export default {
props: { props: {
graphData: { graphData: {
...@@ -27,6 +25,11 @@ ...@@ -27,6 +25,11 @@
type: Array, type: Array,
required: true, required: true,
}, },
hoverData: {
type: Object,
required: false,
default: () => ({}),
},
}, },
mixins: [MonitoringMixin], mixins: [MonitoringMixin],
...@@ -52,6 +55,7 @@ ...@@ -52,6 +55,7 @@
currentXCoordinate: 0, currentXCoordinate: 0,
currentFlagPosition: 0, currentFlagPosition: 0,
showFlag: false, showFlag: false,
showFlagContent: false,
showDeployInfo: true, showDeployInfo: true,
timeSeries: [], timeSeries: [],
}; };
...@@ -122,22 +126,14 @@ ...@@ -122,22 +126,14 @@
const d1 = firstTimeSeries.values[overlayIndex]; const d1 = firstTimeSeries.values[overlayIndex];
if (d0 === undefined || d1 === undefined) return; if (d0 === undefined || d1 === undefined) return;
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
this.currentData = evalTime ? d1 : d0; const hoveredDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
this.currentDataIndex = evalTime ? overlayIndex : (overlayIndex - 1); const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
this.currentXCoordinate = Math.floor(firstTimeSeries.timeSeriesScaleX(this.currentData.time));
const currentDeployXPos = this.mouseOverDeployInfo(point.x); const currentDeployXPos = this.mouseOverDeployInfo(point.x);
if (this.currentXCoordinate > (this.graphWidth - 200)) { eventHub.$emit('hoverChanged', {
this.currentFlagPosition = this.currentXCoordinate - 103; hoveredDate,
} else { currentDeployXPos,
this.currentFlagPosition = this.currentXCoordinate; });
}
if (currentDeployXPos) {
this.showFlag = false;
} else {
this.showFlag = true;
}
}, },
renderAxesPaths() { renderAxesPaths() {
...@@ -194,6 +190,10 @@ ...@@ -194,6 +190,10 @@
eventHub.$emit('toggleAspectRatio'); eventHub.$emit('toggleAspectRatio');
} }
}, },
hoverData() {
this.positionFlag();
},
}, },
mounted() { mounted() {
...@@ -203,7 +203,10 @@ ...@@ -203,7 +203,10 @@
</script> </script>
<template> <template>
<div class="prometheus-graph"> <div
class="prometheus-graph"
@mouseover="showFlagContent = true"
@mouseleave="showFlagContent = false">
<h5 class="text-center graph-title"> <h5 class="text-center graph-title">
{{graphData.title}} {{graphData.title}}
</h5> </h5>
...@@ -247,6 +250,7 @@ ...@@ -247,6 +250,7 @@
<graph-deployment <graph-deployment
:show-deploy-info="showDeployInfo" :show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData" :deployment-data="reducedDeploymentData"
:graph-width="graphWidth"
:graph-height="graphHeight" :graph-height="graphHeight"
:graph-height-offset="graphHeightOffset" :graph-height-offset="graphHeightOffset"
/> />
...@@ -257,6 +261,7 @@ ...@@ -257,6 +261,7 @@
:current-flag-position="currentFlagPosition" :current-flag-position="currentFlagPosition"
:graph-height="graphHeight" :graph-height="graphHeight"
:graph-height-offset="graphHeightOffset" :graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent"
/> />
<rect <rect
class="prometheus-graph-overlay" class="prometheus-graph-overlay"
......
...@@ -19,6 +19,10 @@ ...@@ -19,6 +19,10 @@
type: Number, type: Number,
required: true, required: true,
}, },
graphWidth: {
type: Number,
required: true,
},
}, },
computed: { computed: {
...@@ -47,6 +51,14 @@ ...@@ -47,6 +51,14 @@
transformDeploymentGroup(deployment) { transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`; return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
}, },
positionFlag(deployment) {
let xPosition = 3;
if (deployment.xPos > (this.graphWidth - 200)) {
xPosition = -97;
}
return xPosition;
},
}, },
}; };
</script> </script>
...@@ -77,7 +89,7 @@ ...@@ -77,7 +89,7 @@
<svg <svg
v-if="deployment.showDeploymentFlag" v-if="deployment.showDeploymentFlag"
class="js-deploy-info-box" class="js-deploy-info-box"
x="3" :x="positionFlag(deployment)"
y="0" y="0"
width="92" width="92"
height="60"> height="60">
......
...@@ -23,6 +23,10 @@ ...@@ -23,6 +23,10 @@
type: Number, type: Number,
required: true, required: true,
}, },
showFlagContent: {
type: Boolean,
required: true,
},
}, },
data() { data() {
...@@ -57,6 +61,7 @@ ...@@ -57,6 +61,7 @@
transform="translate(-5, 20)"> transform="translate(-5, 20)">
</line> </line>
<svg <svg
v-if="showFlagContent"
class="rect-text-metric" class="rect-text-metric"
:x="currentFlagPosition" :x="currentFlagPosition"
y="0"> y="0">
......
import { bisectDate } from '../utils/date_time_formatters';
const mixins = { const mixins = {
methods: { methods: {
mouseOverDeployInfo(mouseXPos) { mouseOverDeployInfo(mouseXPos) {
...@@ -18,6 +20,7 @@ const mixins = { ...@@ -18,6 +20,7 @@ const mixins = {
return dataFound; return dataFound;
}, },
formatDeployments() { formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => { this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at); const time = new Date(deployment.created_at);
...@@ -40,6 +43,25 @@ const mixins = { ...@@ -40,6 +43,25 @@ const mixins = {
return deploymentDataArray; return deploymentDataArray;
}, []); }, []);
}, },
positionFlag() {
const timeSeries = this.timeSeries[0];
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
this.currentData = timeSeries.values[hoveredDataIndex];
this.currentDataIndex = hoveredDataIndex;
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103;
} else {
this.currentFlagPosition = this.currentXCoordinate;
}
if (this.hoverData.currentDeployXPos) {
this.showFlag = false;
} else {
this.showFlag = true;
}
},
}, },
}; };
......
...@@ -3,8 +3,5 @@ import Dashboard from './components/dashboard.vue'; ...@@ -3,8 +3,5 @@ import Dashboard from './components/dashboard.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#prometheus-graphs', el: '#prometheus-graphs',
components: { render: createElement => createElement(Dashboard),
Dashboard,
},
render: createElement => createElement('dashboard'),
})); }));
...@@ -13,7 +13,7 @@ function normalizeMetrics(metrics) { ...@@ -13,7 +13,7 @@ function normalizeMetrics(metrics) {
...result, ...result,
values: result.values.map(([timestamp, value]) => ({ values: result.values.map(([timestamp, value]) => ({
time: new Date(timestamp * 1000), time: new Date(timestamp * 1000),
value, value: Number(value),
})), })),
})), })),
})), })),
......
...@@ -2,6 +2,7 @@ import d3 from 'd3'; ...@@ -2,6 +2,7 @@ import d3 from 'd3';
export const dateFormat = d3.time.format('%b %-d, %Y'); export const dateFormat = d3.time.format('%b %-d, %Y');
export const timeFormat = d3.time.format('%-I:%M%p'); export const timeFormat = d3.time.format('%-I:%M%p');
export const bisectDate = d3.bisector(d => d.time).left;
export const timeScaleFormat = d3.time.format.multi([ export const timeScaleFormat = d3.time.format.multi([
['.%L', d => d.getMilliseconds()], ['.%L', d => d.getMilliseconds()],
......
...@@ -74,8 +74,8 @@ export default { ...@@ -74,8 +74,8 @@ export default {
<thead v-if="!isMini"> <thead v-if="!isMini">
<tr> <tr>
<th class="name">Name</th> <th class="name">Name</th>
<th class="hidden-sm hidden-xs last-commit">Last Commit</th> <th class="hidden-sm hidden-xs last-commit">Last commit</th>
<th class="hidden-xs last-update text-right">Last Update</th> <th class="hidden-xs last-update text-right">Last update</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
......
...@@ -207,10 +207,13 @@ ...@@ -207,10 +207,13 @@
} }
.prometheus-state { .prometheus-state {
margin-top: 10px; max-width: 430px;
margin: 10px auto;
text-align: center;
.state-button-section { .state-svg {
margin-top: 10px; max-width: 80vw;
margin: 0 auto;
} }
} }
......
...@@ -392,11 +392,11 @@ table.u2f-registrations { ...@@ -392,11 +392,11 @@ table.u2f-registrations {
} }
} }
.gpg-email-badge { .email-badge {
display: inline; display: inline;
margin-right: $gl-padding / 2; margin-right: $gl-padding / 2;
.gpg-email-badge-email { .email-badge-email {
display: inline; display: inline;
margin-right: $gl-padding / 4; margin-right: $gl-padding / 4;
} }
......
...@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController
def remove_email def remove_email
email = user.emails.find(params[:email_id]) email = user.emails.find(params[:email_id])
success = Emails::DestroyService.new(current_user, user: user, email: email.email).execute success = Emails::DestroyService.new(current_user, user: user).execute(email)
respond_to do |format| respond_to do |format|
if success if success
......
...@@ -85,12 +85,21 @@ class ApplicationController < ActionController::Base ...@@ -85,12 +85,21 @@ class ApplicationController < ActionController::Base
super super
payload[:remote_ip] = request.remote_ip payload[:remote_ip] = request.remote_ip
if current_user.present? logged_user = auth_user
payload[:user_id] = current_user.id
payload[:username] = current_user.username if logged_user.present?
payload[:user_id] = logged_user.try(:id)
payload[:username] = logged_user.try(:username)
end end
end end
# Controllers such as GitHttpController may use alternative methods
# (e.g. tokens) to authenticate the user, whereas Devise sets current_user
def auth_user
return current_user if current_user.present?
return try(:authenticated_user)
end
# This filter handles both private tokens and personal access tokens # This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token! def authenticate_user_from_private_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
......
...@@ -10,13 +10,14 @@ class ConfirmationsController < Devise::ConfirmationsController ...@@ -10,13 +10,14 @@ class ConfirmationsController < Devise::ConfirmationsController
users_almost_there_path users_almost_there_path
end end
def after_confirmation_path_for(resource_name, resource) def after_confirmation_path_for(_resource_name, resource)
if signed_in?(resource_name) # incoming resource can either be a :user or an :email
if signed_in?(:user)
after_sign_in(resource) after_sign_in(resource)
else else
Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}") Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}")
flash[:notice] += " Please sign in." flash[:notice] += " Please sign in."
new_session_path(resource_name) new_session_path(:user)
end end
end end
......
class Profiles::EmailsController < Profiles::ApplicationController class Profiles::EmailsController < Profiles::ApplicationController
before_action :find_email, only: [:destroy, :resend_confirmation_instructions]
def index def index
@primary = current_user.email @primary_email = current_user.email
@emails = current_user.emails.order_id_desc @emails = current_user.emails.order_id_desc
end end
def create def create
@email = Emails::CreateService.new(current_user, email_params.merge(user: current_user)).execute @email = Emails::CreateService.new(current_user, email_params.merge(user: current_user)).execute
unless @email.errors.blank?
if @email.errors.blank?
NotificationService.new.new_email(@email)
else
flash[:alert] = @email.errors.full_messages.first flash[:alert] = @email.errors.full_messages.first
end end
...@@ -17,9 +16,7 @@ class Profiles::EmailsController < Profiles::ApplicationController ...@@ -17,9 +16,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
end end
def destroy def destroy
@email = current_user.emails.find(params[:id]) Emails::DestroyService.new(current_user, user: current_user).execute(@email)
Emails::DestroyService.new(current_user, user: current_user, email: @email.email).execute
respond_to do |format| respond_to do |format|
format.html { redirect_to profile_emails_url, status: 302 } format.html { redirect_to profile_emails_url, status: 302 }
...@@ -27,9 +24,23 @@ class Profiles::EmailsController < Profiles::ApplicationController ...@@ -27,9 +24,23 @@ class Profiles::EmailsController < Profiles::ApplicationController
end end
end end
def resend_confirmation_instructions
if Emails::ConfirmService.new(current_user, user: current_user).execute(@email)
flash[:notice] = "Confirmation email sent to #{@email.email}"
else
flash[:alert] = "There was a problem sending the confirmation email"
end
redirect_to profile_emails_url
end
private private
def email_params def email_params
params.require(:email).permit(:email) params.require(:email).permit(:email)
end end
def find_email
@email = current_user.emails.find(params[:id])
end
end end
...@@ -9,6 +9,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -9,6 +9,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
alias_method :user, :actor alias_method :user, :actor
alias_method :authenticated_user, :actor
# Git clients will not know what authenticity token to send along # Git clients will not know what authenticity token to send along
skip_before_action :verify_authenticity_token skip_before_action :verify_authenticity_token
......
...@@ -7,12 +7,6 @@ module Emails ...@@ -7,12 +7,6 @@ module Emails
mail(to: @user.notification_email, subject: subject("Account was created for you")) mail(to: @user.notification_email, subject: subject("Account was created for you"))
end end
def new_email_email(email_id)
@email = Email.find(email_id)
@current_user = @user = @email.user
mail(to: @user.notification_email, subject: subject("Email was added to your account"))
end
def new_ssh_key_email(key_id) def new_ssh_key_email(key_id)
@key = Key.find_by(id: key_id) @key = Key.find_by(id: key_id)
......
...@@ -7,6 +7,13 @@ class Email < ActiveRecord::Base ...@@ -7,6 +7,13 @@ class Email < ActiveRecord::Base
validates :email, presence: true, uniqueness: true, email: true validates :email, presence: true, uniqueness: true, email: true
validate :unique_email, if: ->(email) { email.email_changed? } validate :unique_email, if: ->(email) { email.email_changed? }
scope :confirmed, -> { where.not(confirmed_at: nil) }
after_commit :update_invalid_gpg_signatures, if: -> { previous_changes.key?('confirmed_at') }
devise :confirmable
self.reconfirmable = false # currently email can't be changed, no need to reconfirm
def email=(value) def email=(value)
write_attribute(:email, value.downcase.strip) write_attribute(:email, value.downcase.strip)
end end
...@@ -14,4 +21,9 @@ class Email < ActiveRecord::Base ...@@ -14,4 +21,9 @@ class Email < ActiveRecord::Base
def unique_email def unique_email
self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email) self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email)
end end
# once email is confirmed, update the gpg signatures
def update_invalid_gpg_signatures
user.update_invalid_gpg_signatures if confirmed?
end
end end
...@@ -163,15 +163,16 @@ class User < ActiveRecord::Base ...@@ -163,15 +163,16 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed? before_validation :set_notification_email, if: :email_changed?
before_validation :set_public_email, if: :public_email_changed? before_validation :set_public_email, if: :public_email_changed?
after_update :update_emails_with_primary_email, if: :email_changed?
before_save :ensure_authentication_token, :ensure_incoming_email_token before_save :ensure_authentication_token, :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: :external_changed? before_save :ensure_user_rights_and_limits, if: :external_changed?
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) } before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
after_save :ensure_namespace_correct after_save :ensure_namespace_correct
after_destroy :post_destroy_hook
after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') }
after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') } after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
after_initialize :set_projects_limit after_initialize :set_projects_limit
after_destroy :post_destroy_hook
# User's Layout preference # User's Layout preference
enum layout: [:fixed, :fluid] enum layout: [:fixed, :fluid]
...@@ -525,12 +526,24 @@ class User < ActiveRecord::Base ...@@ -525,12 +526,24 @@ class User < ActiveRecord::Base
errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email) errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end end
# see if the new email is already a verified secondary email
def check_for_verified_email
skip_reconfirmation! if emails.confirmed.where(email: self.email).any?
end
# Note: the use of the Emails services will cause `saves` on the user object, running
# through the callbacks again and can have side effects, such as the `previous_changes`
# hash and `_was` variables getting munged.
# By using an `after_commit` instead of `after_update`, we avoid the recursive callback
# scenario, though it then requires us to use the `previous_changes` hash
def update_emails_with_primary_email def update_emails_with_primary_email
previous_email = previous_changes[:email][0] # grab this before the DestroyService is called
primary_email_record = emails.find_by(email: email) primary_email_record = emails.find_by(email: email)
if primary_email_record Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
Emails::DestroyService.new(self, user: self, email: email).execute
Emails::CreateService.new(self, user: self, email: email_was).execute # the original primary email was confirmed, and we want that to carry over. We don't
end # have access to the original confirmation values at this point, so just set confirmed_at
Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
end end
def update_invalid_gpg_signatures def update_invalid_gpg_signatures
...@@ -816,6 +829,10 @@ class User < ActiveRecord::Base ...@@ -816,6 +829,10 @@ class User < ActiveRecord::Base
avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username) avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
end end
def primary_email_verified?
confirmed? && !temp_oauth_email?
end
def all_emails def all_emails
all_emails = [] all_emails = []
all_emails << email unless temp_oauth_email? all_emails << email unless temp_oauth_email?
...@@ -823,6 +840,18 @@ class User < ActiveRecord::Base ...@@ -823,6 +840,18 @@ class User < ActiveRecord::Base
all_emails all_emails
end end
def verified_emails
verified_emails = []
verified_emails << email if primary_email_verified?
verified_emails.concat(emails.confirmed.pluck(:email))
verified_emails
end
def verified_email?(check_email)
downcased = check_email.downcase
email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
end
def hook_attrs def hook_attrs
{ {
name: name, name: name,
...@@ -1047,10 +1076,6 @@ class User < ActiveRecord::Base ...@@ -1047,10 +1076,6 @@ class User < ActiveRecord::Base
ensure_rss_token! ensure_rss_token!
end end
def verified_email?(email)
self.email == email
end
def sync_attribute?(attribute) def sync_attribute?(attribute)
return true if ldap_user? && attribute == :email return true if ldap_user? && attribute == :email
......
class CommitEntity < API::Entities::RepoCommit class CommitEntity < API::Entities::Commit
include RequestAwareEntity include RequestAwareEntity
expose :author, using: UserEntity expose :author, using: UserEntity
......
module Emails module Emails
class BaseService class BaseService
def initialize(current_user, opts) def initialize(current_user, params = {})
@current_user = current_user @current_user, @params = current_user, params.dup
@user = opts.delete(:user) @user = params.delete(:user)
@email = opts[:email]
end end
end end
end end
module Emails
class ConfirmService < ::Emails::BaseService
def execute(email)
email.resend_confirmation_instructions
end
end
end
module Emails module Emails
class CreateService < ::Emails::BaseService class CreateService < ::Emails::BaseService
def execute def execute(extra_params = {})
@user.emails.create(email: @email) @user.emails.create(@params.merge(extra_params))
end end
end end
end end
module Emails module Emails
class DestroyService < ::Emails::BaseService class DestroyService < ::Emails::BaseService
def execute def execute(email)
update_secondary_emails! if Email.find_by_email!(@email).destroy email.destroy && update_secondary_emails!
end end
private private
......
...@@ -31,13 +31,6 @@ class NotificationService ...@@ -31,13 +31,6 @@ class NotificationService
end end
end end
# Always notify user about email added to profile
def new_email(email)
if email.user&.can?(:receive_notifications)
mailer.new_email_email(email.id).deliver_later
end
end
# When create an issue we should send an email to: # When create an issue we should send an email to:
# #
# * issue assignee if their notification level is not Disabled # * issue assignee if their notification level is not Disabled
......
- confirmation_link = confirmation_url(@resource, confirmation_token: @token)
- if @resource.unconfirmed_email.present?
#content
= email_default_heading(@resource.unconfirmed_email)
%p Click the link below to confirm your email address.
#cta
= link_to 'Confirm your email address', confirmation_link
- else
#content
- if Gitlab.com?
= email_default_heading('Thanks for signing up to GitLab!')
- else
= email_default_heading("Welcome, #{@resource.name}!")
%p To get started, click the link below to confirm your account.
#cta
= link_to 'Confirm your account', confirmation_link
<% if @resource.unconfirmed_email.present? %>
<%= @resource.unconfirmed_email %>,
Use the link below to confirm your email address.
<% else %>
<% if Gitlab.com? %>
Thanks for signing up to GitLab!
<% else %>
Welcome, <%= @resource.name %>!
<% end %>
To get started, use the link below to confirm your account.
<% end %>
<%= confirmation_url(@resource, confirmation_token: @token) %>
#content
= email_default_heading("#{@resource.user.name}, you've added an additional email!")
%p Click the link below to confirm your email address (#{@resource.email})
#cta
= link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token)
%p
If this email was added in error, you can remove it here:
= link_to "Emails", profile_emails_url
Hi <%= @user.name %>! <%= @resource.user.name %>, you've added an additional email!
A new email was added to your account: Use the link below to confirm your email address (<%= @resource.email %>)
email.................. <%= @email.email %> <%= confirmation_url(@resource, confirmation_token: @token) %>
If this email was added in error, you can remove it here: <%= profile_emails_url %> If this email was added in error, you can remove it here: <%= profile_emails_url %>
- confirmation_link = confirmation_url(@resource, confirmation_token: @token) = render partial: "confirmation_instructions_#{@resource.is_a?(User) ? 'account' : 'secondary'}"
- if @resource.unconfirmed_email.present?
#content
= email_default_heading(@resource.unconfirmed_email)
%p Click the link below to confirm your email address.
#cta
= link_to confirmation_link, confirmation_link
- else
#content
- if Gitlab.com?
= email_default_heading('Thanks for signing up to GitLab!')
- else
= email_default_heading("Welcome, #{@resource.name}!")
%p To get started, click the link below to confirm your account.
#cta
= link_to confirmation_link, confirmation_link
Welcome, <%= @resource.name %>! <%= render partial: "confirmation_instructions_#{@resource.is_a?(User) ? 'account' : 'secondary'}" %>
\ No newline at end of file
<% if @resource.unconfirmed_email.present? %>
You can confirm your email (<%= @resource.unconfirmed_email %>) through the link below:
<% else %>
You can confirm your account through the link below:
<% end %>
<%= confirmation_url(@resource, confirmation_token: @token) %>
- header_title group_title(@group, "Milestones", group_milestones_path(@group)) - breadcrumb_title @milestone.title
- add_to_breadcrumbs "Milestones", group_milestones_path(@group)
%p
Hi #{@user.name}!
%p
A new email was added to your account:
%p
email:
%code= @email.email
%p
If this email was added in error, you can remove it here:
= link_to "Emails", profile_emails_url
...@@ -32,19 +32,25 @@ ...@@ -32,19 +32,25 @@
All email addresses will be used to identify your commits. All email addresses will be used to identify your commits.
%ul.well-list %ul.well-list
%li %li
= @primary = render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? }
%span.pull-right %span.pull-right
%span.label.label-success Primary email %span.label.label-success Primary email
- if @primary === current_user.public_email - if @primary_email === current_user.public_email
%span.label.label-info Public email %span.label.label-info Public email
- if @primary === current_user.notification_email - if @primary_email === current_user.notification_email
%span.label.label-info Notification email %span.label.label-info Notification email
- @emails.each do |email| - @emails.each do |email|
%li %li
= email.email = render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
%span.pull-right %span.pull-right
- if email.email === current_user.public_email - if email.email === current_user.public_email
%span.label.label-info Public email %span.label.label-info Public email
- if email.email === current_user.notification_email - if email.email === current_user.notification_email
%span.label.label-info Notification email %span.label.label-info Notification email
= link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-warning prepend-left-10' - unless email.confirmed?
- confirm_title = "#{email.confirmation_sent_at ? 'Resend' : 'Send'} confirmation email"
= link_to confirm_title, resend_confirmation_instructions_profile_email_path(email), method: :put, class: 'btn btn-sm btn-warning prepend-left-10'
= link_to profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-danger prepend-left-10' do
%span.sr-only Remove
= icon('trash')
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
= icon 'key', class: "settings-list-icon hidden-xs" = icon 'key', class: "settings-list-icon hidden-xs"
.key-list-item-info .key-list-item-info
- key.emails_with_verified_status.map do |email, verified| - key.emails_with_verified_status.map do |email, verified|
= render partial: 'email_with_badge', locals: { email: email, verified: verified } = render partial: 'shared/email_with_badge', locals: { email: email, verified: verified }
.description .description
%code= key.fingerprint %code= key.fingerprint
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%th= s_('ProjectFileTree|Name') %th= s_('ProjectFileTree|Name')
%th.hidden-xs %th.hidden-xs
.pull-left= _('Last commit') .pull-left= _('Last commit')
%th.text-right= _('Last Update') %th.text-right= _('Last update')
- if @path.present? - if @path.present?
%tr.tree-item %tr.tree-item
%td.tree-item-file-name %td.tree-item-file-name
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- css_classes << (verified ? 'verified': 'unverified') - css_classes << (verified ? 'verified': 'unverified')
- text = verified ? 'Verified' : 'Unverified' - text = verified ? 'Verified' : 'Unverified'
.gpg-email-badge .email-badge
.gpg-email-badge-email= email .email-badge-email= email
%div{ class: css_classes } %div{ class: css_classes }
= text = text
---
title: find_user Users helper method no longer overrides find_user API helper method.
merge_request: 14418
author:
type: fixed
---
title: Move the deployment flag content to the left when deployment marker is near
the end
merge_request: 14514
author:
type: fixed
--- ---
title: Adjust MRs being stuck on "process of being merged" for more than 2 hours title: Sync up hover and legend data across all graphs for the prometheus dashboard
merge_request: merge_request:
author: author:
type: fixed type: fixed
---
title: Fixes data parameter not being sent in ajax request for jobs log
merge_request:
author:
type: fixed
---
title: Improves UX of autodevops popover to match gpg one
merge_request:
author:
type: fixed
---
title: Fix broken Y-axis scaling in some Prometheus graphs
merge_request: 14693
author:
type: fixed
---
title: Fix CSRF validation issue when closing/opening merge requests from the UI
merge_request: 14555
author:
type: fixed
---
title: Speed up permission checks
merge_request:
author:
type: other
---
title: Make sure API responds with 401 when invalid authentication info is provided
merge_request:
author:
type: fixed
---
title: A confirmation email is now sent when adding a secondary email address
merge_request:
author: digitalmoksha
type: added
---
title: 'Kubernetes integration: ensure v1.8.0 compatibility'
merge_request: 14635
author:
type: fixed
---
title: Fixed fork button being disabled for users who can fork to a group
merge_request:
author:
type: fixed
--- ---
title: Fixed commenting on side-by-side commit diff title: Fixed milestone breadcrumb links
merge_request: merge_request:
author: author:
type: fixed type: fixed
---
title: Remove 'Repo' prefix from API entites
merge_request: 14694
author: Vitaliy @blackst0ne Klachkov
type: other
---
title: Fix gitlab-rake gitlab:import:repos task failing
merge_request:
author:
type: fixed
---
title: Fix pushes to an empty repository not invalidating has_visible_content? cache
merge_request:
author:
type: fixed
--- ---
title: Fix merge request counter updates after merge title: Fix username and ID not logging in production_json.log for Git activity
merge_request: merge_request:
author: author:
type: fixed type: fixed
---
title: Ensure all refs are restored on a restore from backup
merge_request:
author:
type: fixed
---
title: Gitaly RepositoryExists remains opt-in for all method calls
merge_request:
author:
type: fixed
...@@ -175,7 +175,7 @@ Devise.setup do |config| ...@@ -175,7 +175,7 @@ Devise.setup do |config|
# Configure the default scope given to Warden. By default it's the first # Configure the default scope given to Warden. By default it's the first
# devise role declared in your routes (usually :user). # devise role declared in your routes (usually :user).
# config.default_scope = :user config.default_scope = :user # now have an :email scope as well, so set the default
# Configure sign_out behavior. # Configure sign_out behavior.
# Sign_out action can be scoped (i.e. /users/sign_out affects only :user scope). # Sign_out action can be scoped (i.e. /users/sign_out affects only :user scope).
......
# for secondary email confirmations - uses the same confirmation controller as :users
devise_for :emails, path: 'profile/emails', controllers: { confirmations: :confirmations }
resource :profile, only: [:show, :update] do resource :profile, only: [:show, :update] do
member do member do
get :audit_log get :audit_log
...@@ -28,7 +31,11 @@ resource :profile, only: [:show, :update] do ...@@ -28,7 +31,11 @@ resource :profile, only: [:show, :update] do
put :revoke put :revoke
end end
end end
resources :emails, only: [:index, :create, :destroy] resources :emails, only: [:index, :create, :destroy] do
member do
put :resend_confirmation_instructions
end
end
resources :chat_names, only: [:index, :new, :create, :destroy] do resources :chat_names, only: [:index, :new, :create, :destroy] do
collection do collection do
delete :deny delete :deny
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddEmailConfirmation < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index", "remove_concurrent_index" or
# "add_column_with_default" you must disable the use of transactions
# as these methods can not run in an existing transaction.
# When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# that either of them is the _only_ method called in the migration,
# any other changes should go in a separate migration.
# This ensures that upon failure _only_ the index creation or removing fails
# and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
add_column :emails, :confirmation_token, :string
add_column :emails, :confirmed_at, :datetime_with_timezone
add_column :emails, :confirmation_sent_at, :datetime_with_timezone
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddEmailConfirmationIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index", "remove_concurrent_index" or
# "add_column_with_default" you must disable the use of transactions
# as these methods can not run in an existing transaction.
# When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# that either of them is the _only_ method called in the migration,
# any other changes should go in a separate migration.
# This ensures that upon failure _only_ the index creation or removing fails
# and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
disable_ddl_transaction!
# Not necessary to remove duplicates, as :confirmation_token is a new column
def up
add_concurrent_index :emails, :confirmation_token, unique: true
end
def down
remove_concurrent_index :emails, :confirmation_token if index_exists?(:emails, :confirmation_token)
end
end
...@@ -514,8 +514,12 @@ ActiveRecord::Schema.define(version: 20171004121444) do ...@@ -514,8 +514,12 @@ ActiveRecord::Schema.define(version: 20171004121444) do
t.string "email", null: false t.string "email", null: false
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
end end
add_index "emails", ["confirmation_token"], name: "index_emails_on_confirmation_token", unique: true, using: :btree
add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree
add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
......
...@@ -142,9 +142,9 @@ and [projects APIs](../api/projects.md). ...@@ -142,9 +142,9 @@ and [projects APIs](../api/projects.md).
## Implementation details ## Implementation details
When GitLab receives an artifacts archive, an archive metadata file is also When GitLab receives an artifacts archive, an archive metadata file is also
generated. This metadata file describes all the entries that are located in the generated by [GitLab Workhorse]. This metadata file describes all the entries
artifacts archive itself. The metadata file is in a binary format, with that are located in the artifacts archive itself.
additional GZIP compression. The metadata file is in a binary format, with additional GZIP compression.
GitLab does not extract the artifacts archive in order to save space, memory GitLab does not extract the artifacts archive in order to save space, memory
and disk I/O. It instead inspects the metadata file which contains all the and disk I/O. It instead inspects the metadata file which contains all the
......
...@@ -42,6 +42,11 @@ The importer will create any new namespaces (groups) if they don't exist or in ...@@ -42,6 +42,11 @@ The importer will create any new namespaces (groups) if they don't exist or in
the case the namespace is taken, the repository will be imported under the user's the case the namespace is taken, the repository will be imported under the user's
namespace that started the import process. namespace that started the import process.
The importer will also import branches on forks of projects related to open pull
requests. These branches will be imported with a naming scheume similar to
GH-SHA-Username/Pull-Request-number/fork-name/branch. This may lead to a discrepency
in branches compared to the GitHub Repository.
## Importing your GitHub repositories ## Importing your GitHub repositories
The importer page is visible when you create a new project. The importer page is visible when you create a new project.
......
...@@ -26,7 +26,7 @@ to be uploaded to GitLab. For a signature to be verified three conditions need ...@@ -26,7 +26,7 @@ to be uploaded to GitLab. For a signature to be verified three conditions need
to be met: to be met:
1. The public key needs to be added your GitLab account 1. The public key needs to be added your GitLab account
1. One of the emails in the GPG key matches your **primary** email 1. One of the emails in the GPG key matches a **verified** email address you use in GitLab
1. The committer's email matches the verified email from the gpg key 1. The committer's email matches the verified email from the gpg key
## Generating a GPG key ## Generating a GPG key
...@@ -94,7 +94,7 @@ started: ...@@ -94,7 +94,7 @@ started:
``` ```
1. Enter you real name, the email address to be associated with this key (should 1. Enter you real name, the email address to be associated with this key (should
match the primary email address you use in GitLab) and an optional comment match a verified email address you use in GitLab) and an optional comment
(press <kbd>Enter</kbd> to skip): (press <kbd>Enter</kbd> to skip):
``` ```
......
...@@ -13,7 +13,7 @@ module API ...@@ -13,7 +13,7 @@ module API
end end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository branches' do desc 'Get a project repository branches' do
success Entities::RepoBranch success Entities::Branch
end end
params do params do
use :pagination use :pagination
...@@ -23,13 +23,13 @@ module API ...@@ -23,13 +23,13 @@ module API
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
present paginate(branches), with: Entities::RepoBranch, project: user_project present paginate(branches), with: Entities::Branch, project: user_project
end end
end end
resource ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do resource ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
desc 'Get a single branch' do desc 'Get a single branch' do
success Entities::RepoBranch success Entities::Branch
end end
params do params do
requires :branch, type: String, desc: 'The name of the branch' requires :branch, type: String, desc: 'The name of the branch'
...@@ -41,7 +41,7 @@ module API ...@@ -41,7 +41,7 @@ module API
branch = user_project.repository.find_branch(params[:branch]) branch = user_project.repository.find_branch(params[:branch])
not_found!('Branch') unless branch not_found!('Branch') unless branch
present branch, with: Entities::RepoBranch, project: user_project present branch, with: Entities::Branch, project: user_project
end end
end end
...@@ -50,7 +50,7 @@ module API ...@@ -50,7 +50,7 @@ module API
# in `gitlab-org/gitlab-ce!5081`. The API interface has not been changed (to maintain compatibility), # in `gitlab-org/gitlab-ce!5081`. The API interface has not been changed (to maintain compatibility),
# but it works with the changed data model to infer `developers_can_merge` and `developers_can_push`. # but it works with the changed data model to infer `developers_can_merge` and `developers_can_push`.
desc 'Protect a single branch' do desc 'Protect a single branch' do
success Entities::RepoBranch success Entities::Branch
end end
params do params do
requires :branch, type: String, desc: 'The name of the branch' requires :branch, type: String, desc: 'The name of the branch'
...@@ -80,7 +80,7 @@ module API ...@@ -80,7 +80,7 @@ module API
end end
if protected_branch.valid? if protected_branch.valid?
present branch, with: Entities::RepoBranch, project: user_project present branch, with: Entities::Branch, project: user_project
else else
render_api_error!(protected_branch.errors.full_messages, 422) render_api_error!(protected_branch.errors.full_messages, 422)
end end
...@@ -88,7 +88,7 @@ module API ...@@ -88,7 +88,7 @@ module API
# Note: This API will be deprecated in favor of the protected branches API. # Note: This API will be deprecated in favor of the protected branches API.
desc 'Unprotect a single branch' do desc 'Unprotect a single branch' do
success Entities::RepoBranch success Entities::Branch
end end
params do params do
requires :branch, type: String, desc: 'The name of the branch' requires :branch, type: String, desc: 'The name of the branch'
...@@ -101,11 +101,11 @@ module API ...@@ -101,11 +101,11 @@ module API
protected_branch = user_project.protected_branches.find_by(name: branch.name) protected_branch = user_project.protected_branches.find_by(name: branch.name)
protected_branch&.destroy protected_branch&.destroy
present branch, with: Entities::RepoBranch, project: user_project present branch, with: Entities::Branch, project: user_project
end end
desc 'Create branch' do desc 'Create branch' do
success Entities::RepoBranch success Entities::Branch
end end
params do params do
requires :branch, type: String, desc: 'The name of the branch' requires :branch, type: String, desc: 'The name of the branch'
...@@ -119,7 +119,7 @@ module API ...@@ -119,7 +119,7 @@ module API
if result[:status] == :success if result[:status] == :success
present result[:branch], present result[:branch],
with: Entities::RepoBranch, with: Entities::Branch,
project: user_project project: user_project
else else
render_api_error!(result[:message], 400) render_api_error!(result[:message], 400)
......
...@@ -13,7 +13,7 @@ module API ...@@ -13,7 +13,7 @@ module API
end end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository commits' do desc 'Get a project repository commits' do
success Entities::RepoCommit success Entities::Commit
end end
params do params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
...@@ -46,11 +46,11 @@ module API ...@@ -46,11 +46,11 @@ module API
paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count) paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count)
present paginate(paginated_commits), with: Entities::RepoCommit present paginate(paginated_commits), with: Entities::Commit
end end
desc 'Commit multiple file changes as one commit' do desc 'Commit multiple file changes as one commit' do
success Entities::RepoCommitDetail success Entities::CommitDetail
detail 'This feature was introduced in GitLab 8.13' detail 'This feature was introduced in GitLab 8.13'
end end
params do params do
...@@ -72,14 +72,14 @@ module API ...@@ -72,14 +72,14 @@ module API
if result[:status] == :success if result[:status] == :success
commit_detail = user_project.repository.commit(result[:result]) commit_detail = user_project.repository.commit(result[:result])
present commit_detail, with: Entities::RepoCommitDetail present commit_detail, with: Entities::CommitDetail
else else
render_api_error!(result[:message], 400) render_api_error!(result[:message], 400)
end end
end end
desc 'Get a specific commit of a project' do desc 'Get a specific commit of a project' do
success Entities::RepoCommitDetail success Entities::CommitDetail
failure [[404, 'Commit Not Found']] failure [[404, 'Commit Not Found']]
end end
params do params do
...@@ -90,7 +90,7 @@ module API ...@@ -90,7 +90,7 @@ module API
not_found! 'Commit' unless commit not_found! 'Commit' unless commit
present commit, with: Entities::RepoCommitDetail present commit, with: Entities::CommitDetail
end end
desc 'Get the diff for a specific commit of a project' do desc 'Get the diff for a specific commit of a project' do
...@@ -104,7 +104,7 @@ module API ...@@ -104,7 +104,7 @@ module API
not_found! 'Commit' unless commit not_found! 'Commit' unless commit
present commit.raw_diffs.to_a, with: Entities::RepoDiff present commit.raw_diffs.to_a, with: Entities::Diff
end end
desc "Get a commit's comments" do desc "Get a commit's comments" do
...@@ -126,7 +126,7 @@ module API ...@@ -126,7 +126,7 @@ module API
desc 'Cherry pick commit into a branch' do desc 'Cherry pick commit into a branch' do
detail 'This feature was introduced in GitLab 8.15' detail 'This feature was introduced in GitLab 8.15'
success Entities::RepoCommit success Entities::Commit
end end
params do params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag to be cherry picked' requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag to be cherry picked'
...@@ -151,7 +151,7 @@ module API ...@@ -151,7 +151,7 @@ module API
if result[:status] == :success if result[:status] == :success
branch = user_project.repository.find_branch(params[:branch]) branch = user_project.repository.find_branch(params[:branch])
present user_project.repository.commit(branch.dereferenced_target), with: Entities::RepoCommit present user_project.repository.commit(branch.dereferenced_target), with: Entities::Commit
else else
render_api_error!(result[:message], 400) render_api_error!(result[:message], 400)
end end
......
...@@ -220,7 +220,7 @@ module API ...@@ -220,7 +220,7 @@ module API
expose :shared_projects, using: Entities::Project expose :shared_projects, using: Entities::Project
end end
class RepoCommit < Grape::Entity class Commit < Grape::Entity
expose :id, :short_id, :title, :created_at expose :id, :short_id, :title, :created_at
expose :parent_ids expose :parent_ids
expose :safe_message, as: :message expose :safe_message, as: :message
...@@ -228,20 +228,20 @@ module API ...@@ -228,20 +228,20 @@ module API
expose :committer_name, :committer_email, :committed_date expose :committer_name, :committer_email, :committed_date
end end
class RepoCommitStats < Grape::Entity class CommitStats < Grape::Entity
expose :additions, :deletions, :total expose :additions, :deletions, :total
end end
class RepoCommitDetail < RepoCommit class CommitDetail < Commit
expose :stats, using: Entities::RepoCommitStats expose :stats, using: Entities::CommitStats
expose :status expose :status
expose :last_pipeline, using: 'API::Entities::PipelineBasic' expose :last_pipeline, using: 'API::Entities::PipelineBasic'
end end
class RepoBranch < Grape::Entity class Branch < Grape::Entity
expose :name expose :name
expose :commit, using: Entities::RepoCommit do |repo_branch, options| expose :commit, using: Entities::Commit do |repo_branch, options|
options[:project].repository.commit(repo_branch.dereferenced_target) options[:project].repository.commit(repo_branch.dereferenced_target)
end end
...@@ -265,7 +265,7 @@ module API ...@@ -265,7 +265,7 @@ module API
end end
end end
class RepoTreeObject < Grape::Entity class TreeObject < Grape::Entity
expose :id, :name, :type, :path expose :id, :name, :type, :path
expose :mode do |obj, options| expose :mode do |obj, options|
...@@ -305,7 +305,7 @@ module API ...@@ -305,7 +305,7 @@ module API
expose :state, :created_at, :updated_at expose :state, :created_at, :updated_at
end end
class RepoDiff < Grape::Entity class Diff < Grape::Entity
expose :old_path, :new_path, :a_mode, :b_mode expose :old_path, :new_path, :a_mode, :b_mode
expose :new_file?, as: :new_file expose :new_file?, as: :new_file
expose :renamed_file?, as: :renamed_file expose :renamed_file?, as: :renamed_file
...@@ -483,7 +483,7 @@ module API ...@@ -483,7 +483,7 @@ module API
end end
class MergeRequestChanges < MergeRequest class MergeRequestChanges < MergeRequest
expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _| expose :diffs, as: :changes, using: Entities::Diff do |compare, _|
compare.raw_diffs(limits: false).to_a compare.raw_diffs(limits: false).to_a
end end
end end
...@@ -494,9 +494,9 @@ module API ...@@ -494,9 +494,9 @@ module API
end end
class MergeRequestDiffFull < MergeRequestDiff class MergeRequestDiffFull < MergeRequestDiff
expose :commits, using: Entities::RepoCommit expose :commits, using: Entities::Commit
expose :diffs, using: Entities::RepoDiff do |compare, _| expose :diffs, using: Entities::Diff do |compare, _|
compare.raw_diffs(limits: false).to_a compare.raw_diffs(limits: false).to_a
end end
end end
...@@ -592,8 +592,7 @@ module API ...@@ -592,8 +592,7 @@ module API
expose :target_type expose :target_type
expose :target do |todo, options| expose :target do |todo, options|
target = todo.target_type == 'Commit' ? 'RepoCommit' : todo.target_type Entities.const_get(todo.target_type).represent(todo.target, options)
Entities.const_get(target).represent(todo.target, options)
end end
expose :target_url do |todo, options| expose :target_url do |todo, options|
...@@ -729,15 +728,15 @@ module API ...@@ -729,15 +728,15 @@ module API
end end
class Compare < Grape::Entity class Compare < Grape::Entity
expose :commit, using: Entities::RepoCommit do |compare, options| expose :commit, using: Entities::Commit do |compare, options|
Commit.decorate(compare.commits, nil).last ::Commit.decorate(compare.commits, nil).last
end end
expose :commits, using: Entities::RepoCommit do |compare, options| expose :commits, using: Entities::Commit do |compare, options|
Commit.decorate(compare.commits, nil) ::Commit.decorate(compare.commits, nil)
end end
expose :diffs, using: Entities::RepoDiff do |compare, options| expose :diffs, using: Entities::Diff do |compare, options|
compare.diffs(limits: false).to_a compare.diffs(limits: false).to_a
end end
...@@ -773,10 +772,10 @@ module API ...@@ -773,10 +772,10 @@ module API
expose :description expose :description
end end
class RepoTag < Grape::Entity class Tag < Grape::Entity
expose :name, :message expose :name, :message
expose :commit, using: Entities::RepoCommit do |repo_tag, options| expose :commit, using: Entities::Commit do |repo_tag, options|
options[:project].repository.commit(repo_tag.dereferenced_target) options[:project].repository.commit(repo_tag.dereferenced_target)
end end
...@@ -827,7 +826,7 @@ module API ...@@ -827,7 +826,7 @@ module API
expose :created_at, :started_at, :finished_at expose :created_at, :started_at, :finished_at
expose :user, with: User expose :user, with: User
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? } expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :commit, with: RepoCommit expose :commit, with: Commit
expose :runner, with: Runner expose :runner, with: Runner
expose :pipeline, with: PipelineBasic expose :pipeline, with: PipelineBasic
end end
...@@ -880,7 +879,7 @@ module API ...@@ -880,7 +879,7 @@ module API
expose :deployable, using: Entities::Job expose :deployable, using: Entities::Job
end end
class RepoLicense < Grape::Entity class License < Grape::Entity
expose :key, :name, :nickname expose :key, :name, :nickname
expose :featured, as: :popular expose :featured, as: :popular
expose :url, as: :html_url expose :url, as: :html_url
......
...@@ -183,13 +183,13 @@ module API ...@@ -183,13 +183,13 @@ module API
end end
desc 'Get the commits of a merge request' do desc 'Get the commits of a merge request' do
success Entities::RepoCommit success Entities::Commit
end end
get ':id/merge_requests/:merge_request_iid/commits' do get ':id/merge_requests/:merge_request_iid/commits' do
merge_request = find_merge_request_with_access(params[:merge_request_iid]) merge_request = find_merge_request_with_access(params[:merge_request_iid])
commits = ::Kaminari.paginate_array(merge_request.commits) commits = ::Kaminari.paginate_array(merge_request.commits)
present paginate(commits), with: Entities::RepoCommit present paginate(commits), with: Entities::Commit
end end
desc 'Show the merge request changes' do desc 'Show the merge request changes' do
......
...@@ -35,7 +35,7 @@ module API ...@@ -35,7 +35,7 @@ module API
end end
desc 'Get a project repository tree' do desc 'Get a project repository tree' do
success Entities::RepoTreeObject success Entities::TreeObject
end end
params do params do
optional :ref, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' optional :ref, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
...@@ -52,7 +52,7 @@ module API ...@@ -52,7 +52,7 @@ module API
tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive]) tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
entries = ::Kaminari.paginate_array(tree.sorted_entries) entries = ::Kaminari.paginate_array(tree.sorted_entries)
present paginate(entries), with: Entities::RepoTreeObject present paginate(entries), with: Entities::TreeObject
end end
desc 'Get raw blob contents from the repository' desc 'Get raw blob contents from the repository'
......
...@@ -11,18 +11,18 @@ module API ...@@ -11,18 +11,18 @@ module API
end end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository tags' do desc 'Get a project repository tags' do
success Entities::RepoTag success Entities::Tag
end end
params do params do
use :pagination use :pagination
end end
get ':id/repository/tags' do get ':id/repository/tags' do
tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse) tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse)
present paginate(tags), with: Entities::RepoTag, project: user_project present paginate(tags), with: Entities::Tag, project: user_project
end end
desc 'Get a single repository tag' do desc 'Get a single repository tag' do
success Entities::RepoTag success Entities::Tag
end end
params do params do
requires :tag_name, type: String, desc: 'The name of the tag' requires :tag_name, type: String, desc: 'The name of the tag'
...@@ -31,11 +31,11 @@ module API ...@@ -31,11 +31,11 @@ module API
tag = user_project.repository.find_tag(params[:tag_name]) tag = user_project.repository.find_tag(params[:tag_name])
not_found!('Tag') unless tag not_found!('Tag') unless tag
present tag, with: Entities::RepoTag, project: user_project present tag, with: Entities::Tag, project: user_project
end end
desc 'Create a new repository tag' do desc 'Create a new repository tag' do
success Entities::RepoTag success Entities::Tag
end end
params do params do
requires :tag_name, type: String, desc: 'The name of the tag' requires :tag_name, type: String, desc: 'The name of the tag'
...@@ -51,7 +51,7 @@ module API ...@@ -51,7 +51,7 @@ module API
if result[:status] == :success if result[:status] == :success
present result[:tag], present result[:tag],
with: Entities::RepoTag, with: Entities::Tag,
project: user_project project: user_project
else else
render_api_error!(result[:message], 400) render_api_error!(result[:message], 400)
......
...@@ -49,7 +49,7 @@ module API ...@@ -49,7 +49,7 @@ module API
desc 'Get the list of the available license template' do desc 'Get the list of the available license template' do
detail 'This feature was introduced in GitLab 8.7.' detail 'This feature was introduced in GitLab 8.7.'
success ::API::Entities::RepoLicense success ::API::Entities::License
end end
params do params do
optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses' optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
...@@ -60,12 +60,12 @@ module API ...@@ -60,12 +60,12 @@ module API
featured: declared(params)[:popular].present? ? true : nil featured: declared(params)[:popular].present? ? true : nil
} }
licences = ::Kaminari.paginate_array(Licensee::License.all(options)) licences = ::Kaminari.paginate_array(Licensee::License.all(options))
present paginate(licences), with: Entities::RepoLicense present paginate(licences), with: Entities::License
end end
desc 'Get the text for a specific license' do desc 'Get the text for a specific license' do
detail 'This feature was introduced in GitLab 8.7.' detail 'This feature was introduced in GitLab 8.7.'
success ::API::Entities::RepoLicense success ::API::Entities::License
end end
params do params do
requires :name, type: String, desc: 'The name of the template' requires :name, type: String, desc: 'The name of the template'
...@@ -75,7 +75,7 @@ module API ...@@ -75,7 +75,7 @@ module API
template = parsed_license_template template = parsed_license_template
present template, with: ::API::Entities::RepoLicense present template, with: ::API::Entities::License
end end
GLOBAL_TEMPLATE_TYPES.each do |template_type, properties| GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
......
...@@ -331,7 +331,6 @@ module API ...@@ -331,7 +331,6 @@ module API
email = Emails::CreateService.new(current_user, declared_params(include_missing: false).merge(user: user)).execute email = Emails::CreateService.new(current_user, declared_params(include_missing: false).merge(user: user)).execute
if email.errors.blank? if email.errors.blank?
NotificationService.new.new_email(email)
present email, with: Entities::Email present email, with: Entities::Email
else else
render_validation_error!(email) render_validation_error!(email)
...@@ -369,10 +368,8 @@ module API ...@@ -369,10 +368,8 @@ module API
not_found!('Email') unless email not_found!('Email') unless email
destroy_conditionally!(email) do |email| destroy_conditionally!(email) do |email|
Emails::DestroyService.new(current_user, user: user, email: email.email).execute Emails::DestroyService.new(current_user, user: user).execute(email)
end end
user.update_secondary_emails!
end end
desc 'Delete a user. Available only for admins.' do desc 'Delete a user. Available only for admins.' do
...@@ -677,7 +674,6 @@ module API ...@@ -677,7 +674,6 @@ module API
email = Emails::CreateService.new(current_user, declared_params.merge(user: current_user)).execute email = Emails::CreateService.new(current_user, declared_params.merge(user: current_user)).execute
if email.errors.blank? if email.errors.blank?
NotificationService.new.new_email(email)
present email, with: Entities::Email present email, with: Entities::Email
else else
render_validation_error!(email) render_validation_error!(email)
...@@ -693,10 +689,8 @@ module API ...@@ -693,10 +689,8 @@ module API
not_found!('Email') unless email not_found!('Email') unless email
destroy_conditionally!(email) do |email| destroy_conditionally!(email) do |email|
Emails::DestroyService.new(current_user, user: current_user, email: email.email).execute Emails::DestroyService.new(current_user, user: current_user).execute(email)
end end
current_user.update_secondary_emails!
end end
desc 'Get a list of user activities' desc 'Get a list of user activities'
......
...@@ -11,12 +11,12 @@ module API ...@@ -11,12 +11,12 @@ module API
end end
resource :projects, requirements: { id: %r{[^/]+} } do resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository branches' do desc 'Get a project repository branches' do
success ::API::Entities::RepoBranch success ::API::Entities::Branch
end end
get ":id/repository/branches" do get ":id/repository/branches" do
branches = user_project.repository.branches.sort_by(&:name) branches = user_project.repository.branches.sort_by(&:name)
present branches, with: ::API::Entities::RepoBranch, project: user_project present branches, with: ::API::Entities::Branch, project: user_project
end end
desc 'Delete a branch' desc 'Delete a branch'
...@@ -47,7 +47,7 @@ module API ...@@ -47,7 +47,7 @@ module API
end end
desc 'Create branch' do desc 'Create branch' do
success ::API::Entities::RepoBranch success ::API::Entities::Branch
end end
params do params do
requires :branch_name, type: String, desc: 'The name of the branch' requires :branch_name, type: String, desc: 'The name of the branch'
...@@ -60,7 +60,7 @@ module API ...@@ -60,7 +60,7 @@ module API
if result[:status] == :success if result[:status] == :success
present result[:branch], present result[:branch],
with: ::API::Entities::RepoBranch, with: ::API::Entities::Branch,
project: user_project project: user_project
else else
render_api_error!(result[:message], 400) render_api_error!(result[:message], 400)
......
...@@ -13,7 +13,7 @@ module API ...@@ -13,7 +13,7 @@ module API
end end
resource :projects, requirements: { id: %r{[^/]+} } do resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository commits' do desc 'Get a project repository commits' do
success ::API::Entities::RepoCommit success ::API::Entities::Commit
end end
params do params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
...@@ -34,11 +34,11 @@ module API ...@@ -34,11 +34,11 @@ module API
after: params[:since], after: params[:since],
before: params[:until]) before: params[:until])
present commits, with: ::API::Entities::RepoCommit present commits, with: ::API::Entities::Commit
end end
desc 'Commit multiple file changes as one commit' do desc 'Commit multiple file changes as one commit' do
success ::API::Entities::RepoCommitDetail success ::API::Entities::CommitDetail
detail 'This feature was introduced in GitLab 8.13' detail 'This feature was introduced in GitLab 8.13'
end end
params do params do
...@@ -59,14 +59,14 @@ module API ...@@ -59,14 +59,14 @@ module API
if result[:status] == :success if result[:status] == :success
commit_detail = user_project.repository.commits(result[:result], limit: 1).first commit_detail = user_project.repository.commits(result[:result], limit: 1).first
present commit_detail, with: ::API::Entities::RepoCommitDetail present commit_detail, with: ::API::Entities::CommitDetail
else else
render_api_error!(result[:message], 400) render_api_error!(result[:message], 400)
end end
end end
desc 'Get a specific commit of a project' do desc 'Get a specific commit of a project' do
success ::API::Entities::RepoCommitDetail success ::API::Entities::CommitDetail
failure [[404, 'Not Found']] failure [[404, 'Not Found']]
end end
params do params do
...@@ -77,7 +77,7 @@ module API ...@@ -77,7 +77,7 @@ module API
not_found! "Commit" unless commit not_found! "Commit" unless commit
present commit, with: ::API::Entities::RepoCommitDetail present commit, with: ::API::Entities::CommitDetail
end end
desc 'Get the diff for a specific commit of a project' do desc 'Get the diff for a specific commit of a project' do
...@@ -113,7 +113,7 @@ module API ...@@ -113,7 +113,7 @@ module API
desc 'Cherry pick commit into a branch' do desc 'Cherry pick commit into a branch' do
detail 'This feature was introduced in GitLab 8.15' detail 'This feature was introduced in GitLab 8.15'
success ::API::Entities::RepoCommit success ::API::Entities::Commit
end end
params do params do
requires :sha, type: String, desc: 'A commit sha to be cherry picked' requires :sha, type: String, desc: 'A commit sha to be cherry picked'
...@@ -138,7 +138,7 @@ module API ...@@ -138,7 +138,7 @@ module API
if result[:status] == :success if result[:status] == :success
branch = user_project.repository.find_branch(params[:branch]) branch = user_project.repository.find_branch(params[:branch])
present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::RepoCommit present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::Commit
else else
render_api_error!(result[:message], 400) render_api_error!(result[:message], 400)
end end
......
...@@ -220,7 +220,7 @@ module API ...@@ -220,7 +220,7 @@ module API
expose :created_at, :started_at, :finished_at expose :created_at, :started_at, :finished_at
expose :user, with: ::API::Entities::User expose :user, with: ::API::Entities::User
expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? } expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? }
expose :commit, with: ::API::Entities::RepoCommit expose :commit, with: ::API::Entities::Commit
expose :runner, with: ::API::Entities::Runner expose :runner, with: ::API::Entities::Runner
expose :pipeline, with: ::API::Entities::PipelineBasic expose :pipeline, with: ::API::Entities::PipelineBasic
end end
...@@ -237,7 +237,7 @@ module API ...@@ -237,7 +237,7 @@ module API
end end
class MergeRequestChanges < MergeRequest class MergeRequestChanges < MergeRequest
expose :diffs, as: :changes, using: ::API::Entities::RepoDiff do |compare, _| expose :diffs, as: :changes, using: ::API::Entities::Diff do |compare, _|
compare.raw_diffs(limits: false).to_a compare.raw_diffs(limits: false).to_a
end end
end end
......
...@@ -135,12 +135,12 @@ module API ...@@ -135,12 +135,12 @@ module API
end end
desc 'Get the commits of a merge request' do desc 'Get the commits of a merge request' do
success ::API::Entities::RepoCommit success ::API::Entities::Commit
end end
get "#{path}/commits" do get "#{path}/commits" do
merge_request = find_merge_request_with_access(params[:merge_request_id]) merge_request = find_merge_request_with_access(params[:merge_request_id])
present merge_request.commits, with: ::API::Entities::RepoCommit present merge_request.commits, with: ::API::Entities::Commit
end end
desc 'Show the merge request changes' do desc 'Show the merge request changes' do
......
...@@ -19,7 +19,7 @@ module API ...@@ -19,7 +19,7 @@ module API
end end
desc 'Get a project repository tree' do desc 'Get a project repository tree' do
success ::API::Entities::RepoTreeObject success ::API::Entities::TreeObject
end end
params do params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
...@@ -35,7 +35,7 @@ module API ...@@ -35,7 +35,7 @@ module API
tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive]) tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
present tree.sorted_entries, with: ::API::Entities::RepoTreeObject present tree.sorted_entries, with: ::API::Entities::TreeObject
end end
desc 'Get a raw file contents' desc 'Get a raw file contents'
......
...@@ -8,11 +8,11 @@ module API ...@@ -8,11 +8,11 @@ module API
end end
resource :projects, requirements: { id: %r{[^/]+} } do resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository tags' do desc 'Get a project repository tags' do
success ::API::Entities::RepoTag success ::API::Entities::Tag
end end
get ":id/repository/tags" do get ":id/repository/tags" do
tags = user_project.repository.tags.sort_by(&:name).reverse tags = user_project.repository.tags.sort_by(&:name).reverse
present tags, with: ::API::Entities::RepoTag, project: user_project present tags, with: ::API::Entities::Tag, project: user_project
end end
desc 'Delete a repository tag' desc 'Delete a repository tag'
......
...@@ -52,7 +52,7 @@ module API ...@@ -52,7 +52,7 @@ module API
detailed_desc = 'This feature was introduced in GitLab 8.7.' detailed_desc = 'This feature was introduced in GitLab 8.7.'
detailed_desc << DEPRECATION_MESSAGE unless status == :ok detailed_desc << DEPRECATION_MESSAGE unless status == :ok
detail detailed_desc detail detailed_desc
success ::API::Entities::RepoLicense success ::API::Entities::License
end end
params do params do
optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses' optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
...@@ -61,7 +61,7 @@ module API ...@@ -61,7 +61,7 @@ module API
options = { options = {
featured: declared(params)[:popular].present? ? true : nil featured: declared(params)[:popular].present? ? true : nil
} }
present Licensee::License.all(options), with: ::API::Entities::RepoLicense present Licensee::License.all(options), with: ::API::Entities::License
end end
end end
...@@ -70,7 +70,7 @@ module API ...@@ -70,7 +70,7 @@ module API
detailed_desc = 'This feature was introduced in GitLab 8.7.' detailed_desc = 'This feature was introduced in GitLab 8.7.'
detailed_desc << DEPRECATION_MESSAGE unless status == :ok detailed_desc << DEPRECATION_MESSAGE unless status == :ok
detail detailed_desc detail detailed_desc
success ::API::Entities::RepoLicense success ::API::Entities::License
end end
params do params do
requires :name, type: String, desc: 'The name of the template' requires :name, type: String, desc: 'The name of the template'
...@@ -80,7 +80,7 @@ module API ...@@ -80,7 +80,7 @@ module API
template = parsed_license_template template = parsed_license_template
present template, with: ::API::Entities::RepoLicense present template, with: ::API::Entities::License
end end
end end
......
...@@ -206,11 +206,13 @@ module DeclarativePolicy ...@@ -206,11 +206,13 @@ module DeclarativePolicy
end end
def cached_pass?(context) def cached_pass?(context)
passes = @rules.map { |r| r.cached_pass?(context) } @rules.each do |rule|
return false if passes.any? { |p| p == false } pass = rule.cached_pass?(context)
return true if passes.all? { |p| p == true }
nil return pass if pass.nil? || pass == false
end
true
end end
def repr def repr
...@@ -245,11 +247,13 @@ module DeclarativePolicy ...@@ -245,11 +247,13 @@ module DeclarativePolicy
end end
def cached_pass?(context) def cached_pass?(context)
passes = @rules.map { |r| r.cached_pass?(context) } @rules.each do |rule|
return true if passes.any? { |p| p == true } pass = rule.cached_pass?(context)
return false if passes.all? { |p| p == false }
nil return pass if pass.nil? || pass == true
end
false
end end
def score(context) def score(context)
......
...@@ -107,7 +107,7 @@ module DeclarativePolicy ...@@ -107,7 +107,7 @@ module DeclarativePolicy
end end
# This is the core spot where all those `#score` methods matter. # This is the core spot where all those `#score` methods matter.
# It is critcal for performance to run steps in the correct order, # It is critical for performance to run steps in the correct order,
# so that we don't compute expensive conditions (potentially n times # so that we don't compute expensive conditions (potentially n times
# if we're called on, say, a large list of users). # if we're called on, say, a large list of users).
# #
...@@ -139,30 +139,39 @@ module DeclarativePolicy ...@@ -139,30 +139,39 @@ module DeclarativePolicy
return return
end end
steps = Set.new(@steps) remaining_steps = Set.new(@steps)
remaining_enablers = steps.count { |s| s.enable? } remaining_enablers, remaining_preventers = remaining_steps.partition(&:enable?).map { |s| Set.new(s) }
loop do loop do
return if steps.empty? if @state.enabled?
# Once we set this, we never need to unset it, because a single
# prevent will stop this from being enabled
remaining_steps = remaining_preventers
else
# if the permission hasn't yet been enabled and we only have
# prevent steps left, we short-circuit the state here
@state.prevent! if remaining_enablers.empty?
end
# if the permission hasn't yet been enabled and we only have return if remaining_steps.empty?
# prevent steps left, we short-circuit the state here
@state.prevent! if !@state.enabled? && remaining_enablers == 0
lowest_score = Float::INFINITY lowest_score = Float::INFINITY
next_step = nil next_step = nil
steps.each do |step| remaining_steps.each do |step|
score = step.score score = step.score
if score < lowest_score if score < lowest_score
next_step = step next_step = step
lowest_score = score lowest_score = score
end end
end
steps.delete(next_step) break if lowest_score.zero?
end
remaining_enablers -= 1 if next_step.enable? [remaining_steps, remaining_enablers, remaining_preventers].each do |set|
set.delete(next_step)
end
yield next_step, lowest_score yield next_step, lowest_score
end end
......
...@@ -990,7 +990,7 @@ module Gitlab ...@@ -990,7 +990,7 @@ module Gitlab
tmp_ref = fetch_ref( tmp_ref = fetch_ref(
start_repository, start_repository,
source_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", source_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
target_ref: "refs/tmp/#{SecureRandom.hex}/head" target_ref: "refs/tmp/#{SecureRandom.hex}"
) )
yield commit(sha) yield commit(sha)
......
...@@ -64,6 +64,8 @@ module Gitlab ...@@ -64,6 +64,8 @@ module Gitlab
protected protected
def add_or_update_user_identities def add_or_update_user_identities
return unless gl_user
# find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved. # find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider } identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
......
...@@ -852,9 +852,6 @@ msgstr[1] "" ...@@ -852,9 +852,6 @@ msgstr[1] ""
msgid "Last Pipeline" msgid "Last Pipeline"
msgstr "" msgstr ""
msgid "Last Update"
msgstr ""
msgid "Last commit" msgid "Last commit"
msgstr "" msgstr ""
...@@ -864,6 +861,9 @@ msgstr "" ...@@ -864,6 +861,9 @@ msgstr ""
msgid "Last edited by %{name}" msgid "Last edited by %{name}"
msgstr "" msgstr ""
msgid "Last update"
msgstr ""
msgid "Last updated" msgid "Last updated"
msgstr "" msgstr ""
......
...@@ -25,9 +25,9 @@ GEM ...@@ -25,9 +25,9 @@ GEM
mime-types (3.1) mime-types (3.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
mini_portile2 (2.1.0) mini_portile2 (2.3.0)
nokogiri (1.7.0.1) nokogiri (1.8.1)
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.3.0)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
...@@ -75,4 +75,4 @@ DEPENDENCIES ...@@ -75,4 +75,4 @@ DEPENDENCIES
selenium-webdriver (~> 2.53) selenium-webdriver (~> 2.53)
BUNDLED WITH BUNDLED WITH
1.14.6 1.15.4
## Integration tests for GitLab # GitLab QA - Integration tests for GitLab
This directory contains integration tests for GitLab. This directory contains integration tests for GitLab.
It is part of [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa). It is part of the [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa).
## What GitLab QA is? ## What is it?
GitLab QA is an integration tests suite for GitLab. GitLab QA is an integration tests suite for GitLab.
...@@ -20,18 +20,34 @@ against any existing instance. ...@@ -20,18 +20,34 @@ against any existing instance.
## How can I use it? ## How can I use it?
You can use GitLab QA to exercise tests on any live instance! For example, the You can use GitLab QA to exercise tests on any live instance! For example, the
follow call would login to the local GitLab instance and run all specs in following call would login to a local [GDK] instance and run all specs in
`qa/specs/features`: `qa/specs/features`:
``` ```
GITLAB_USERNAME='root' GITLAB_PASSWORD='5iveL!fe' bin/qa Test::Instance http://localhost bin/qa Test::Instance http://localhost:3000
``` ```
You can also supply a specific tests to run as another parameter. For example, to ### Running specific tests
You can also supply specific tests to run as another parameter. For example, to
test the EE license specs, you can run: test the EE license specs, you can run:
``` ```
EE_LICENSE="<YOUR LICENSE KEY>" GITLAB_USERNAME='root' GITLAB_PASSWORD='5iveL!fe' bin/qa Test::Instance http://localhost qa/ee EE_LICENSE="<YOUR LICENSE KEY>" bin/qa Test::Instance http://localhost qa/ee
```
### Overriding the authenticated user
Unless told otherwise, the QA tests will run as the default `root` user seeded
by the GDK.
If you need to authenticate as a different user, you can provide the
`GITLAB_USERNAME` and `GITLAB_PASSWORD` environment variables:
```
GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password bin/qa Test::Instance https://gitlab.example.com
``` ```
All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa#supported-environment-variables). All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa#supported-environment-variables).
[GDK]: https://gitlab.com/gitlab-org/gitlab-development-kit/
...@@ -8,7 +8,7 @@ module QA ...@@ -8,7 +8,7 @@ module QA
end end
def password def password
ENV['GITLAB_PASSWORD'] || 'test1234' ENV['GITLAB_PASSWORD'] || '5iveL!fe'
end end
end end
end end
......
require 'spec_helper'
describe Profiles::EmailsController do
let(:user) { create(:user) }
before do
sign_in(user)
end
describe '#create' do
let(:email_params) { { email: "add_email@example.com" } }
it 'sends an email confirmation' do
expect { post(:create, { email: email_params }) }.to change { ActionMailer::Base.deliveries.size }
expect(ActionMailer::Base.deliveries.last.to).to eq [email_params[:email]]
expect(ActionMailer::Base.deliveries.last.subject).to match "Confirmation instructions"
end
end
describe '#resend_confirmation_instructions' do
let(:email_params) { { email: "add_email@example.com" } }
it 'resends an email confirmation' do
email = user.emails.create(email: 'add_email@example.com')
expect { put(:resend_confirmation_instructions, { id: email }) }.to change { ActionMailer::Base.deliveries.size }
expect(ActionMailer::Base.deliveries.last.to).to eq [email_params[:email]]
expect(ActionMailer::Base.deliveries.last.subject).to match "Confirmation instructions"
end
it 'unable to resend an email confirmation' do
expect { put(:resend_confirmation_instructions, { id: 1 }) }.not_to change { ActionMailer::Base.deliveries.size }
end
end
end
...@@ -15,6 +15,20 @@ describe ProfilesController do ...@@ -15,6 +15,20 @@ describe ProfilesController do
expect(user.unconfirmed_email).to eq('john@gmail.com') expect(user.unconfirmed_email).to eq('john@gmail.com')
end end
it "allows an email update without confirmation if existing verified email" do
user = create(:user)
create(:email, :confirmed, user: user, email: 'john@gmail.com')
sign_in(user)
put :update,
user: { email: "john@gmail.com", name: "John" }
user.reload
expect(response.status).to eq(302)
expect(user.unconfirmed_email).to eq nil
end
it "ignores an email update from a user with an external email address" do it "ignores an email update from a user with an external email address" do
stub_omniauth_setting(sync_profile_from_provider: ['ldap']) stub_omniauth_setting(sync_profile_from_provider: ['ldap'])
stub_omniauth_setting(sync_profile_attributes: true) stub_omniauth_setting(sync_profile_attributes: true)
......
...@@ -2,5 +2,7 @@ FactoryGirl.define do ...@@ -2,5 +2,7 @@ FactoryGirl.define do
factory :email do factory :email do
user user
email { generate(:email_alias) } email { generate(:email_alias) }
trait(:confirmed) { confirmed_at Time.now }
end end
end end
require 'rails_helper'
feature 'Profile > Emails' do
let(:user) { create(:user) }
before do
sign_in(user)
end
describe 'User adds an email' do
before do
visit profile_emails_path
end
scenario 'saves the new email' do
fill_in('Email', with: 'my@email.com')
click_button('Add email address')
expect(page).to have_content('my@email.com Unverified')
expect(page).to have_content("#{user.email} Verified")
expect(page).to have_content('Resend confirmation email')
end
scenario 'does not add a duplicate email' do
fill_in('Email', with: user.email)
click_button('Add email address')
email = user.emails.find_by(email: user.email)
expect(email).to be_nil
expect(page).to have_content('Email has already been taken')
end
end
scenario 'User removes email' do
user.emails.create(email: 'my@email.com')
visit profile_emails_path
expect(page).to have_content("my@email.com")
click_link('Remove')
expect(page).not_to have_content("my@email.com")
end
scenario 'User confirms email' do
email = user.emails.create(email: 'my@email.com')
visit profile_emails_path
expect(page).to have_content("#{email.email} Unverified")
email.confirm
expect(email.confirmed?).to be_truthy
visit profile_emails_path
expect(page).to have_content("#{email.email} Verified")
end
scenario 'User re-sends confirmation email' do
email = user.emails.create(email: 'my@email.com')
visit profile_emails_path
expect { click_link("Resend confirmation email") }.to change { ActionMailer::Base.deliveries.size }
expect(page).to have_content("Confirmation email sent to #{email.email}")
end
scenario 'old unconfirmed emails show Send Confirmation button' do
email = user.emails.create(email: 'my@email.com')
email.update_attribute(:confirmation_sent_at, nil)
visit profile_emails_path
expect(page).not_to have_content('Resend confirmation email')
expect(page).to have_content('Send confirmation email')
end
end
...@@ -4,7 +4,7 @@ feature 'Profile > GPG Keys' do ...@@ -4,7 +4,7 @@ feature 'Profile > GPG Keys' do
let(:user) { create(:user, email: GpgHelpers::User2.emails.first) } let(:user) { create(:user, email: GpgHelpers::User2.emails.first) }
before do before do
login_as(user) sign_in(user)
end end
describe 'User adds a key' do describe 'User adds a key' do
......
...@@ -21,6 +21,7 @@ describe('MonitoringDeployment', () => { ...@@ -21,6 +21,7 @@ describe('MonitoringDeployment', () => {
const component = createComponent({ const component = createComponent({
showDeployInfo: false, showDeployInfo: false,
deploymentData: reducedDeploymentData, deploymentData: reducedDeploymentData,
graphWidth: 440,
graphHeight: 300, graphHeight: 300,
graphHeightOffset: 120, graphHeightOffset: 120,
}); });
...@@ -36,6 +37,7 @@ describe('MonitoringDeployment', () => { ...@@ -36,6 +37,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: false, showDeployInfo: false,
deploymentData: reducedDeploymentData, deploymentData: reducedDeploymentData,
graphHeight: 300, graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120, graphHeightOffset: 120,
}); });
...@@ -49,6 +51,7 @@ describe('MonitoringDeployment', () => { ...@@ -49,6 +51,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: false, showDeployInfo: false,
deploymentData: reducedDeploymentData, deploymentData: reducedDeploymentData,
graphHeight: 300, graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120, graphHeightOffset: 120,
}); });
...@@ -62,6 +65,7 @@ describe('MonitoringDeployment', () => { ...@@ -62,6 +65,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: false, showDeployInfo: false,
deploymentData: reducedDeploymentData, deploymentData: reducedDeploymentData,
graphHeight: 300, graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120, graphHeightOffset: 120,
}); });
...@@ -75,6 +79,7 @@ describe('MonitoringDeployment', () => { ...@@ -75,6 +79,7 @@ describe('MonitoringDeployment', () => {
const component = createComponent({ const component = createComponent({
showDeployInfo: true, showDeployInfo: true,
deploymentData: reducedDeploymentData, deploymentData: reducedDeploymentData,
graphWidth: 440,
graphHeight: 300, graphHeight: 300,
graphHeightOffset: 120, graphHeightOffset: 120,
}); });
...@@ -82,12 +87,29 @@ describe('MonitoringDeployment', () => { ...@@ -82,12 +87,29 @@ describe('MonitoringDeployment', () => {
expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull(); expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull();
}); });
it('positions the flag to the left when the xPos is too far right', () => {
reducedDeploymentData[0].showDeploymentFlag = false;
reducedDeploymentData[0].xPos = 250;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphWidth: 440,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(
component.positionFlag(reducedDeploymentData[0]),
).toBeLessThan(0);
});
it('shows the deployment flag', () => { it('shows the deployment flag', () => {
reducedDeploymentData[0].showDeploymentFlag = true; reducedDeploymentData[0].showDeploymentFlag = true;
const component = createComponent({ const component = createComponent({
showDeployInfo: true, showDeployInfo: true,
deploymentData: reducedDeploymentData, deploymentData: reducedDeploymentData,
graphHeight: 300, graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120, graphHeightOffset: 120,
}); });
...@@ -102,6 +124,7 @@ describe('MonitoringDeployment', () => { ...@@ -102,6 +124,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: true, showDeployInfo: true,
deploymentData: reducedDeploymentData, deploymentData: reducedDeploymentData,
graphHeight: 300, graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120, graphHeightOffset: 120,
}); });
...@@ -115,6 +138,7 @@ describe('MonitoringDeployment', () => { ...@@ -115,6 +138,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: true, showDeployInfo: true,
deploymentData: reducedDeploymentData, deploymentData: reducedDeploymentData,
graphHeight: 300, graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120, graphHeightOffset: 120,
}); });
...@@ -127,6 +151,7 @@ describe('MonitoringDeployment', () => { ...@@ -127,6 +151,7 @@ describe('MonitoringDeployment', () => {
showDeployInfo: true, showDeployInfo: true,
deploymentData: reducedDeploymentData, deploymentData: reducedDeploymentData,
graphHeight: 300, graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120, graphHeightOffset: 120,
}); });
......
...@@ -14,19 +14,22 @@ function getCoordinate(component, selector, coordinate) { ...@@ -14,19 +14,22 @@ function getCoordinate(component, selector, coordinate) {
return parseInt(coordinateVal, 10); return parseInt(coordinateVal, 10);
} }
const defaultValuesComponent = {
currentXCoordinate: 200,
currentYCoordinate: 100,
currentFlagPosition: 100,
currentData: {
time: new Date('2017-06-04T18:17:33.501Z'),
value: '1.49609375',
},
graphHeight: 300,
graphHeightOffset: 120,
showFlagContent: true,
};
describe('GraphFlag', () => { describe('GraphFlag', () => {
it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => { it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => {
const component = createComponent({ const component = createComponent(defaultValuesComponent);
currentXCoordinate: 200,
currentYCoordinate: 100,
currentFlagPosition: 100,
currentData: {
time: new Date('2017-06-04T18:17:33.501Z'),
value: '1.49609375',
},
graphHeight: 300,
graphHeightOffset: 120,
});
expect(getCoordinate(component, '.selected-metric-line', 'x1')) expect(getCoordinate(component, '.selected-metric-line', 'x1'))
.toEqual(component.currentXCoordinate); .toEqual(component.currentXCoordinate);
...@@ -35,17 +38,7 @@ describe('GraphFlag', () => { ...@@ -35,17 +38,7 @@ describe('GraphFlag', () => {
}); });
it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => { it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => {
const component = createComponent({ const component = createComponent(defaultValuesComponent);
currentXCoordinate: 200,
currentYCoordinate: 100,
currentFlagPosition: 100,
currentData: {
time: new Date('2017-06-04T18:17:33.501Z'),
value: '1.49609375',
},
graphHeight: 300,
graphHeightOffset: 120,
});
const svg = component.$el.querySelector('.rect-text-metric'); const svg = component.$el.querySelector('.rect-text-metric');
expect(svg.tagName).toEqual('svg'); expect(svg.tagName).toEqual('svg');
...@@ -54,17 +47,7 @@ describe('GraphFlag', () => { ...@@ -54,17 +47,7 @@ describe('GraphFlag', () => {
describe('Computed props', () => { describe('Computed props', () => {
it('calculatedHeight', () => { it('calculatedHeight', () => {
const component = createComponent({ const component = createComponent(defaultValuesComponent);
currentXCoordinate: 200,
currentYCoordinate: 100,
currentFlagPosition: 100,
currentData: {
time: new Date('2017-06-04T18:17:33.501Z'),
value: '1.49609375',
},
graphHeight: 300,
graphHeightOffset: 120,
});
expect(component.calculatedHeight).toEqual(180); expect(component.calculatedHeight).toEqual(180);
}); });
......
...@@ -86,4 +86,22 @@ describe('Graph', () => { ...@@ -86,4 +86,22 @@ describe('Graph', () => {
expect(component.yAxisLabel).toEqual(component.graphData.y_label); expect(component.yAxisLabel).toEqual(component.graphData.y_label);
expect(component.legendTitle).toEqual(component.graphData.queries[0].label); expect(component.legendTitle).toEqual(component.graphData.queries[0].label);
}); });
it('sets the currentData object based on the hovered data index', () => {
const component = createComponent({
graphData: convertedMetrics[1],
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
graphIdentifier: 0,
hoverData: {
hoveredDate: new Date('Sun Aug 27 2017 06:11:51 GMT-0500 (CDT)'),
currentDeployXPos: null,
},
});
component.positionFlag();
expect(component.currentData).toBe(component.timeSeries[0].values[10]);
expect(component.currentDataIndex).toEqual(10);
});
}); });
...@@ -23,8 +23,8 @@ describe('RepoSidebar', () => { ...@@ -23,8 +23,8 @@ describe('RepoSidebar', () => {
expect(vm.$el.id).toEqual('sidebar'); expect(vm.$el.id).toEqual('sidebar');
expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy(); expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy();
expect(thead.querySelector('.name').textContent).toEqual('Name'); expect(thead.querySelector('.name').textContent).toEqual('Name');
expect(thead.querySelector('.last-commit').textContent).toEqual('Last Commit'); expect(thead.querySelector('.last-commit').textContent).toEqual('Last commit');
expect(thead.querySelector('.last-update').textContent).toEqual('Last Update'); expect(thead.querySelector('.last-update').textContent).toEqual('Last update');
expect(tbody.querySelector('.repo-file-options')).toBeFalsy(); expect(tbody.querySelector('.repo-file-options')).toBeFalsy();
expect(tbody.querySelector('.prev-directory')).toBeFalsy(); expect(tbody.querySelector('.prev-directory')).toBeFalsy();
expect(tbody.querySelector('.loading-file')).toBeFalsy(); expect(tbody.querySelector('.loading-file')).toBeFalsy();
......
...@@ -120,29 +120,4 @@ describe Emails::Profile do ...@@ -120,29 +120,4 @@ describe Emails::Profile do
it { expect { Notify.new_gpg_key_email('foo') }.not_to raise_error } it { expect { Notify.new_gpg_key_email('foo') }.not_to raise_error }
end end
end end
describe 'user added email' do
let(:email) { create(:email) }
subject { Notify.new_email_email(email.id) }
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
it 'is sent to the new user' do
is_expected.to deliver_to email.user.email
end
it 'has the correct subject' do
is_expected.to have_subject /^Email was added to your account$/i
end
it 'contains the new email address' do
is_expected.to have_body_text /#{email.email}/
end
it 'includes a link to emails page' do
is_expected.to have_body_text /#{profile_emails_path}/
end
end
end end
...@@ -11,4 +11,33 @@ describe Email do ...@@ -11,4 +11,33 @@ describe Email do
expect(described_class.new(email: ' inFO@exAMPLe.com ').email) expect(described_class.new(email: ' inFO@exAMPLe.com ').email)
.to eq 'info@example.com' .to eq 'info@example.com'
end end
describe '#update_invalid_gpg_signatures' do
let(:user) do
create(:user, email: 'tula.torphy@abshire.ca').tap do |user|
user.skip_reconfirmation!
end
end
let(:user) { create(:user) }
it 'synchronizes the gpg keys when the email is updated' do
email = user.emails.create(email: 'new@email.com')
expect(user).to receive(:update_invalid_gpg_signatures)
email.confirm
end
end
describe 'scopes' do
let(:user) { create(:user) }
it 'scopes confirmed emails' do
create(:email, :confirmed, user: user)
create(:email, user: user)
expect(user.emails.count).to eq 2
expect(user.emails.confirmed.count).to eq 1
end
end
end end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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