Commit 9d42830f authored by Stan Hu's avatar Stan Hu

Merge remote-tracking branch 'origin/security-9-5' into 3435-backport-9-5

parents fc451b7d 693285ef
...@@ -2,6 +2,50 @@ ...@@ -2,6 +2,50 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 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)
- Fix gitlab rake:import:repos task.
## 9.5.6 (2017-09-29)
- [FIXED] Fix MR ready to merge buttons/controls at mobile breakpoint. !14242
- [FIXED] Fix errors thrown in merge request widget with external CI service/integration.
- [FIXED] Update x/x discussions resolved checkmark icon to be green when all discussions resolved.
- [FIXED] Fix 500 error on merged merge requests when GitLab is restored from a backup.
## 9.5.5 (2017-09-18)
- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
- [FIXED] Fix division by zero error in blame age mapping. !13803 (Jeff Stubler)
- [FIXED] Fix problems sanitizing URLs with empty passwords. !14083
- [FIXED] Fix a wrong `X-Gitlab-Event` header when testing webhooks. !14108
- [FIXED] Fixes the 500 errors caused by a race condition in GPG's tmp directory handling. !14194 (Alexis Reigel)
- [FIXED] Fix Pipeline Triggers to show triggered label and predefined variables (e.g. CI_PIPELINE_TRIGGERED). !14244
- [FIXED] Fix project feature being deleted when updating project with invalid visibility level.
- [FIXED] Fix new navigation wrapping and causing height to grow.
- [FIXED] Fix buttons with different height in merge request widget.
- [FIXED] Normalize styles for empty state combo button.
- [FIXED] Fix broken svg in jobs dropdown for success status.
- [FIXED] Improve migrations using triggers.
- [FIXED] Disable GitLab Project Import Button if source disabled.
- [CHANGED] Update the GPG verification semantics: A GPG signature must additionally match the committer in order to be verified. !13771 (Alexis Reigel)
- [OTHER] Fix repository equality check and avoid fetching ref if the commit is already available. This affects merge request creation performance. !13685
- [OTHER] Update documentation for confidential issue. !14117
## 9.5.4 (2017-09-06)
- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
- [SECURITY] Prevent a persistent XSS in the commit author block.
- Fix XSS issue in go-get handling.
- Resolve CSRF token leakage via pathname manipulation on environments page.
- Fixes race condition in project uploads.
- Disallow arbitrary properties in `th` and `td` `style` attributes.
- Disallow the `name` attribute on all user-provided markup.
## 9.5.3 (2017-09-03) ## 9.5.3 (2017-09-03)
- [SECURITY] Filter additional secrets from Rails logs. - [SECURITY] Filter additional secrets from Rails logs.
......
...@@ -321,6 +321,7 @@ group :development, :test do ...@@ -321,6 +321,7 @@ group :development, :test do
gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5' gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3' gem 'rspec-set', '~> 0.1.3'
gem 'rspec-parameterized'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0' gem 'minitest', '~> 5.7.0'
......
...@@ -2,6 +2,7 @@ GEM ...@@ -2,6 +2,7 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
RedCloth (4.3.2) RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2) ace-rails-ap (4.1.2)
actionmailer (4.2.8) actionmailer (4.2.8)
actionpack (= 4.2.8) actionpack (= 4.2.8)
...@@ -41,6 +42,9 @@ GEM ...@@ -41,6 +42,9 @@ GEM
tzinfo (~> 1.1) tzinfo (~> 1.1)
acts-as-taggable-on (4.0.0) acts-as-taggable-on (4.0.0)
activerecord (>= 4.0) activerecord (>= 4.0)
adamantium (0.2.0)
ice_nine (~> 0.11.0)
memoizable (~> 0.4.0)
addressable (2.3.8) addressable (2.3.8)
after_commit_queue (1.3.0) after_commit_queue (1.3.0)
activerecord (>= 3.0) activerecord (>= 3.0)
...@@ -124,6 +128,9 @@ GEM ...@@ -124,6 +128,9 @@ GEM
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
colorize (0.7.7) colorize (0.7.7)
concord (0.1.5)
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
concurrent-ruby (1.0.5) concurrent-ruby (1.0.5)
concurrent-ruby-ext (1.0.5) concurrent-ruby-ext (1.0.5)
concurrent-ruby (= 1.0.5) concurrent-ruby (= 1.0.5)
...@@ -471,9 +478,12 @@ GEM ...@@ -471,9 +478,12 @@ GEM
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mail_room (0.9.1) mail_room (0.9.1)
memoist (0.15.0) memoist (0.15.0)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2) method_source (0.8.2)
mime-types (2.99.3) mime-types (2.99.3)
mimemagic (0.3.0) mimemagic (0.3.0)
mini_mime (0.1.4)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.7.0) minitest (5.7.0)
mmap2 (2.2.7) mmap2 (2.2.7)
...@@ -724,6 +734,10 @@ GEM ...@@ -724,6 +734,10 @@ GEM
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2) rqrcode (>= 0.4.2)
rspec (3.6.0)
rspec-core (~> 3.6.0)
rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.6.0)
rspec-core (3.6.0) rspec-core (3.6.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.6.0)
rspec-expectations (3.6.0) rspec-expectations (3.6.0)
...@@ -732,6 +746,12 @@ GEM ...@@ -732,6 +746,12 @@ GEM
rspec-mocks (3.6.0) rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.6.0)
rspec-parameterized (0.4.0)
binding_of_caller
parser
proc_to_ast
rspec (>= 2.13, < 4)
unparser
rspec-rails (3.6.0) rspec-rails (3.6.0)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
...@@ -896,6 +916,14 @@ GEM ...@@ -896,6 +916,14 @@ GEM
get_process_mem (~> 0) get_process_mem (~> 0)
unicorn (>= 4, < 6) unicorn (>= 4, < 6)
uniform_notifier (1.10.0) uniform_notifier (1.10.0)
unparser (0.2.6)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
diff-lcs (~> 1.3)
equalizer (~> 0.0.9)
parser (>= 2.3.1.2, < 2.5)
procto (~> 0.0.2)
url_safe_base64 (0.2.2) url_safe_base64 (0.2.2)
validates_hostname (1.0.6) validates_hostname (1.0.6)
activerecord (>= 3.0) activerecord (>= 3.0)
...@@ -1098,6 +1126,7 @@ DEPENDENCIES ...@@ -1098,6 +1126,7 @@ DEPENDENCIES
responders (~> 2.0) responders (~> 2.0)
rouge (~> 2.0) rouge (~> 2.0)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0) rspec-rails (~> 3.6.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3) rspec-set (~> 0.1.3)
......
...@@ -14,7 +14,6 @@ ...@@ -14,7 +14,6 @@
/* global NotificationsDropdown */ /* global NotificationsDropdown */
/* global GroupAvatar */ /* global GroupAvatar */
/* global LineHighlighter */ /* global LineHighlighter */
/* global ProjectFork */
/* global BuildArtifacts */ /* global BuildArtifacts */
/* global GroupsSelect */ /* global GroupsSelect */
/* global Search */ /* global Search */
...@@ -468,7 +467,9 @@ import initChangesDropdown from './init_changes_dropdown'; ...@@ -468,7 +467,9 @@ import initChangesDropdown from './init_changes_dropdown';
shortcut_handler = true; shortcut_handler = true;
break; break;
case 'projects:forks:new': case 'projects:forks:new':
new ProjectFork(); import(/* webpackChunkName: 'project_fork' */ './project_fork')
.then(fork => fork.default())
.catch(() => {});
break; break;
case 'projects:artifacts:browse': case 'projects:artifacts:browse':
new ShortcutsNavigation(); new ShortcutsNavigation();
......
...@@ -124,7 +124,6 @@ import './preview_markdown'; ...@@ -124,7 +124,6 @@ import './preview_markdown';
import './project'; import './project';
import './project_avatar'; import './project_avatar';
import './project_find_file'; import './project_find_file';
import './project_fork';
import './project_import'; import './project_import';
import './project_label_subscription'; import './project_label_subscription';
import './project_new'; import './project_new';
...@@ -248,7 +247,10 @@ $(function () { ...@@ -248,7 +247,10 @@ $(function () {
// Initialize popovers // Initialize popovers
$body.popover({ $body.popover({
selector: '[data-toggle="popover"]', selector: '[data-toggle="popover"]',
trigger: 'focus' trigger: 'focus',
// set the viewport to the main content, excluding the navigation bar, so
// the navigation can't overlap the popover
viewport: '.page-with-sidebar'
}); });
$('.trigger-submit').on('change', function () { $('.trigger-submit').on('change', function () {
return $(this).parents('form').submit(); return $(this).parents('form').submit();
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */ export default () => {
(function() { $('.fork-thumbnail a').on('click', function forkThumbnailClicked() {
this.ProjectFork = (function() { if ($(this).hasClass('disabled')) return false;
function ProjectFork() {
$('.fork-thumbnail a').on('click', function() {
$('.fork-namespaces').hide(); $('.fork-namespaces').hide();
return $('.save-project-loader').show(); return $('.save-project-loader').show();
}); });
} };
return ProjectFork;
})();
}).call(window);
...@@ -75,18 +75,20 @@ export default { ...@@ -75,18 +75,20 @@ export default {
class="btn btn-small inline"> class="btn btn-small inline">
Check out branch Check out branch
</a> </a>
<span class="dropdown inline prepend-left-10"> <span class="dropdown prepend-left-10">
<a <a
class="btn btn-xs dropdown-toggle" class="btn btn-small inline dropdown-toggle"
data-toggle="dropdown" data-toggle="dropdown"
aria-label="Download as" aria-label="Download as"
role="button"> role="button">
<i <i
class="fa fa-download" class="fa fa-download"
aria-hidden="true" /> aria-hidden="true">
</i>
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true" /> aria-hidden="true">
</i>
</a> </a>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
<li> <li>
......
...@@ -12,6 +12,9 @@ export default { ...@@ -12,6 +12,9 @@ export default {
ciIcon, ciIcon,
}, },
computed: { computed: {
hasPipeline() {
return this.mr.pipeline && Object.keys(this.mr.pipeline).length > 0;
},
hasCIError() { hasCIError() {
const { hasCI, ciStatus } = this.mr; const { hasCI, ciStatus } = this.mr;
...@@ -28,7 +31,9 @@ export default { ...@@ -28,7 +31,9 @@ export default {
}, },
}, },
template: ` template: `
<div class="mr-widget-heading"> <div
v-if="hasPipeline || hasCIError"
class="mr-widget-heading">
<div class="ci-widget media"> <div class="ci-widget media">
<template v-if="hasCIError"> <template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10"> <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
...@@ -40,7 +45,7 @@ export default { ...@@ -40,7 +45,7 @@ export default {
Could not connect to the CI server. Please check your settings and try again Could not connect to the CI server. Please check your settings and try again
</div> </div>
</template> </template>
<template v-else> <template v-else-if="hasPipeline">
<div class="ci-status-icon append-right-10"> <div class="ci-status-icon append-right-10">
<a <a
class="icon-link" class="icon-link"
......
...@@ -29,6 +29,9 @@ export default { ...@@ -29,6 +29,9 @@ export default {
statusIcon, statusIcon,
}, },
computed: { computed: {
shouldShowMergeWhenPipelineSucceedsText() {
return this.mr.isPipelineActive;
},
commitMessageLinkTitle() { commitMessageLinkTitle() {
const withDesc = 'Include description in commit message'; const withDesc = 'Include description in commit message';
const withoutDesc = "Don't include description in commit message"; const withoutDesc = "Don't include description in commit message";
...@@ -56,7 +59,7 @@ export default { ...@@ -56,7 +59,7 @@ export default {
mergeButtonText() { mergeButtonText() {
if (this.isMergingImmediately) { if (this.isMergingImmediately) {
return 'Merge in progress'; return 'Merge in progress';
} else if (this.mr.isPipelineActive) { } else if (this.shouldShowMergeWhenPipelineSucceedsText) {
return 'Merge when pipeline succeeds'; return 'Merge when pipeline succeeds';
} }
...@@ -68,7 +71,7 @@ export default { ...@@ -68,7 +71,7 @@ export default {
isMergeButtonDisabled() { isMergeButtonDisabled() {
const { commitMessage } = this; const { commitMessage } = this;
return Boolean(!commitMessage.length return Boolean(!commitMessage.length
|| !this.isMergeAllowed() || !this.shouldShowMergeControls()
|| this.isMakingRequest || this.isMakingRequest
|| this.mr.preventMerge); || this.mr.preventMerge);
}, },
...@@ -82,7 +85,12 @@ export default { ...@@ -82,7 +85,12 @@ export default {
}, },
methods: { methods: {
isMergeAllowed() { isMergeAllowed() {
return !(this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.isPipelineFailed); return !this.mr.onlyAllowMergeIfPipelineSucceeds ||
this.mr.isPipelinePassing ||
this.mr.isPipelineSkipped;
},
shouldShowMergeControls() {
return this.isMergeAllowed() || this.shouldShowMergeWhenPipelineSucceedsText;
}, },
updateCommitMessage() { updateCommitMessage() {
const cmwd = this.mr.commitMessageWithDescription; const cmwd = this.mr.commitMessageWithDescription;
...@@ -202,8 +210,8 @@ export default { ...@@ -202,8 +210,8 @@ export default {
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="success" /> <status-icon status="success" />
<div class="media-body"> <div class="media-body">
<div class="media space-children"> <div class="mr-widget-body-controls media space-children">
<span class="btn-group"> <span class="btn-group append-bottom-5">
<button <button
@click="handleMergeButtonClick()" @click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled" :disabled="isMergeButtonDisabled"
...@@ -260,8 +268,8 @@ export default { ...@@ -260,8 +268,8 @@ export default {
</li> </li>
</ul> </ul>
</span> </span>
<div class="media-body space-children"> <div class="media-body-wrap space-children">
<template v-if="isMergeAllowed()"> <template v-if="shouldShowMergeControls()">
<label> <label>
<input <input
id="remove-source-branch-input" id="remove-source-branch-input"
...@@ -286,7 +294,7 @@ export default { ...@@ -286,7 +294,7 @@ export default {
</template> </template>
<template v-else> <template v-else>
<span class="bold"> <span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure The pipeline for this merge request has not succeeded yet
</span> </span>
</template> </template>
</div> </div>
......
...@@ -57,7 +57,7 @@ export default { ...@@ -57,7 +57,7 @@ export default {
return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1; return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1;
}, },
shouldRenderPipelines() { shouldRenderPipelines() {
return Object.keys(this.mr.pipeline).length || this.mr.hasCI; return this.mr.hasCI;
}, },
shouldRenderRelatedLinks() { shouldRenderRelatedLinks() {
return this.mr.relatedLinks; return this.mr.relatedLinks;
......
...@@ -85,7 +85,9 @@ export default class MergeRequestStore { ...@@ -85,7 +85,9 @@ export default class MergeRequestStore {
this.ciEnvironmentsStatusPath = data.ci_environments_status_path; this.ciEnvironmentsStatusPath = data.ci_environments_status_path;
this.hasCI = data.has_ci; this.hasCI = data.has_ci;
this.ciStatus = data.ci_status; this.ciStatus = data.ci_status;
this.isPipelineFailed = this.ciStatus ? (this.ciStatus === 'failed' || this.ciStatus === 'canceled') : false; this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
this.isPipelinePassing = this.ciStatus === 'success' || this.ciStatus === 'success_with_warnings';
this.isPipelineSkipped = this.ciStatus === 'skipped';
this.pipelineDetailedStatus = pipelineStatus; this.pipelineDetailedStatus = pipelineStatus;
this.isPipelineActive = data.pipeline ? data.pipeline.active : false; this.isPipelineActive = data.pipeline ? data.pipeline.active : false;
this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false; this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
......
...@@ -6,3 +6,7 @@ ...@@ -6,3 +6,7 @@
.media-body { .media-body {
flex: 1; flex: 1;
} }
.media-body-wrap {
flex-grow: 1;
}
...@@ -286,11 +286,7 @@ ...@@ -286,11 +286,7 @@
display: flex; display: flex;
max-width: 350px; max-width: 350px;
overflow: hidden; overflow: hidden;
float: right;
@media(max-width: $screen-xs-max) {
width: 100%;
max-width: none;
}
.new-project-item-link { .new-project-item-link {
white-space: nowrap; white-space: nowrap;
...@@ -303,6 +299,23 @@ ...@@ -303,6 +299,23 @@
} }
} }
.empty-state .project-item-select-holder.btn-group {
float: none;
display: inline-block;
.btn {
// overrides styles applied to plain `.empty-state .btn`
margin: 10px 0;
max-width: 300px;
width: auto;
@media(max-width: $screen-xs-max) {
max-width: 250px;
}
}
}
.new-project-item-select-button .fa-caret-down { .new-project-item-select-button .fa-caret-down {
margin-left: 2px; margin-left: 2px;
} }
......
...@@ -70,8 +70,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -70,8 +70,7 @@ $new-sidebar-collapsed-width: 50px;
background-color: $white-light; background-color: $white-light;
} }
.project-title, .sidebar-context-title {
.group-title {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
...@@ -97,21 +96,28 @@ $new-sidebar-collapsed-width: 50px; ...@@ -97,21 +96,28 @@ $new-sidebar-collapsed-width: 50px;
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
left: 0; left: 0;
overflow: auto;
background-color: $gray-normal; background-color: $gray-normal;
box-shadow: inset -2px 0 0 $border-color; box-shadow: inset -2px 0 0 $border-color;
transform: translate3d(0, 0, 0);
&.sidebar-icons-only { &.sidebar-icons-only {
width: $new-sidebar-collapsed-width; width: $new-sidebar-collapsed-width;
.nav-sidebar-inner-scroll {
overflow-x: hidden; overflow-x: hidden;
}
.badge, .badge,
.project-title { .sidebar-context-title {
display: none; display: none;
} }
.nav-item-name { .nav-item-name {
opacity: 0; display: none;
}
.sidebar-top-level-items > li > a {
min-height: 44px;
} }
} }
...@@ -176,6 +182,12 @@ $new-sidebar-collapsed-width: 50px; ...@@ -176,6 +182,12 @@ $new-sidebar-collapsed-width: 50px;
} }
} }
.nav-sidebar-inner-scroll {
height: 100%;
width: 100%;
overflow: auto;
}
.with-performance-bar .nav-sidebar { .with-performance-bar .nav-sidebar {
top: $header-height + $performance-bar-height; top: $header-height + $performance-bar-height;
} }
......
...@@ -367,6 +367,10 @@ ...@@ -367,6 +367,10 @@
} }
} }
.mr-widget-body-controls {
flex-wrap: wrap;
}
.mr_source_commit, .mr_source_commit,
.mr_target_commit { .mr_target_commit {
margin-bottom: 0; margin-bottom: 0;
......
...@@ -766,6 +766,7 @@ ul.notes { ...@@ -766,6 +766,7 @@ ul.notes {
background-color: transparent; background-color: transparent;
border: none; border: none;
outline: 0; outline: 0;
color: $gray-darkest;
transition: color $general-hover-transition-duration $general-hover-transition-curve; transition: color $general-hover-transition-duration $general-hover-transition-curve;
&.is-disabled { &.is-disabled {
...@@ -789,7 +790,7 @@ ul.notes { ...@@ -789,7 +790,7 @@ ul.notes {
} }
svg { svg {
fill: $gray-darkest; fill: currentColor;
height: 16px; height: 16px;
width: 16px; width: 16px;
} }
......
...@@ -417,7 +417,7 @@ a.deploy-project-label { ...@@ -417,7 +417,7 @@ a.deploy-project-label {
text-align: center; text-align: center;
width: 169px; width: 169px;
&:hover, &:hover:not(.disabled),
&.forked { &.forked {
background-color: $row-hover; background-color: $row-hover;
border-color: $row-hover-border; border-color: $row-hover-border;
...@@ -444,6 +444,15 @@ a.deploy-project-label { ...@@ -444,6 +444,15 @@ a.deploy-project-label {
padding-top: $gl-padding; padding-top: $gl-padding;
color: $gl-text-color; color: $gl-text-color;
&.disabled {
opacity: .3;
cursor: not-allowed;
&:hover {
text-decoration: none;
}
}
.caption { .caption {
min-height: 30px; min-height: 30px;
padding: $gl-padding 0; padding: $gl-padding 0;
......
...@@ -212,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -212,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def create_merge_request def create_merge_request
result = MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
if result[:status] == :success if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request]) render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
......
...@@ -11,6 +11,9 @@ module BlameHelper ...@@ -11,6 +11,9 @@ module BlameHelper
end end
def age_map_class(commit_date, duration) def age_map_class(commit_date, duration)
if duration[:started_days_ago] == 0
"blame-commit-age-0"
else
commit_date_days_ago = (duration[:now] - commit_date).to_i / 1.day commit_date_days_ago = (duration[:now] - commit_date).to_i / 1.day
# Numbers 0 to 10 come from this calculation, but only commits on the oldest # Numbers 0 to 10 come from this calculation, but only commits on the oldest
# day get number 10 (all other numbers can be multiple days), so the range # day get number 10 (all other numbers can be multiple days), so the range
...@@ -18,4 +21,5 @@ module BlameHelper ...@@ -18,4 +21,5 @@ module BlameHelper
age_group = [(10 * commit_date_days_ago) / duration[:started_days_ago], 9].min age_group = [(10 * commit_date_days_ago) / duration[:started_days_ago], 9].min
"blame-commit-age-#{age_group}" "blame-commit-age-#{age_group}"
end end
end
end end
...@@ -30,7 +30,7 @@ module BuildsHelper ...@@ -30,7 +30,7 @@ module BuildsHelper
def build_failed_issue_options def build_failed_issue_options
{ {
title: "Build Failed ##{@build.id}", title: "Job Failed ##{@build.id}",
description: project_job_url(@project, @build) description: project_job_url(@project, @build)
} }
end end
......
...@@ -393,6 +393,6 @@ class Commit ...@@ -393,6 +393,6 @@ class Commit
end end
def gpg_commit def gpg_commit
@gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self) @gpg_commit ||= Gitlab::Gpg::Commit.new(self)
end end
end end
...@@ -56,7 +56,7 @@ class GpgKey < ActiveRecord::Base ...@@ -56,7 +56,7 @@ class GpgKey < ActiveRecord::Base
def verified_user_infos def verified_user_infos
user_infos.select do |user_info| user_infos.select do |user_info|
user_info[:email] == user.email user.verified_email?(user_info[:email])
end end
end end
...@@ -64,13 +64,17 @@ class GpgKey < ActiveRecord::Base ...@@ -64,13 +64,17 @@ class GpgKey < ActiveRecord::Base
user_infos.map do |user_info| user_infos.map do |user_info|
[ [
user_info[:email], user_info[:email],
user_info[:email] == user.email user.verified_email?(user_info[:email])
] ]
end.to_h end.to_h
end end
def verified? def verified?
emails_with_verified_status.any? { |_email, verified| verified } emails_with_verified_status.values.any?
end
def verified_and_belongs_to_email?(email)
emails_with_verified_status.fetch(email, false)
end end
def update_invalid_gpg_signatures def update_invalid_gpg_signatures
...@@ -78,9 +82,12 @@ class GpgKey < ActiveRecord::Base ...@@ -78,9 +82,12 @@ class GpgKey < ActiveRecord::Base
end end
def revoke def revoke
GpgSignature.where(gpg_key: self, valid_signature: true).update_all( GpgSignature
.where(gpg_key: self)
.where.not(verification_status: GpgSignature.verification_statuses[:unknown_key])
.update_all(
gpg_key_id: nil, gpg_key_id: nil,
valid_signature: false, verification_status: GpgSignature.verification_statuses[:unknown_key],
updated_at: Time.zone.now updated_at: Time.zone.now
) )
......
class GpgSignature < ActiveRecord::Base class GpgSignature < ActiveRecord::Base
include ShaAttribute include ShaAttribute
include IgnorableColumn
ignore_column :valid_signature
sha_attribute :commit_sha sha_attribute :commit_sha
sha_attribute :gpg_key_primary_keyid sha_attribute :gpg_key_primary_keyid
enum verification_status: {
unverified: 0,
verified: 1,
same_user_different_email: 2,
other_user: 3,
unverified_key: 4,
unknown_key: 5
}
belongs_to :project belongs_to :project
belongs_to :gpg_key belongs_to :gpg_key
...@@ -20,6 +32,6 @@ class GpgSignature < ActiveRecord::Base ...@@ -20,6 +32,6 @@ class GpgSignature < ActiveRecord::Base
end end
def gpg_commit def gpg_commit
Gitlab::Gpg::Commit.new(project, commit_sha) Gitlab::Gpg::Commit.new(commit)
end end
end end
...@@ -269,6 +269,10 @@ class Issue < ActiveRecord::Base ...@@ -269,6 +269,10 @@ class Issue < ActiveRecord::Base
end end
end end
def update_project_counter_caches
Projects::OpenIssuesCountService.new(project).refresh_cache
end
private private
# Returns `true` if the given User can read the current Issue. # Returns `true` if the given User can read the current Issue.
......
...@@ -443,7 +443,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -443,7 +443,8 @@ class MergeRequest < ActiveRecord::Base
end end
def reload_diff_if_branch_changed def reload_diff_if_branch_changed
if source_branch_changed? || target_branch_changed? if (source_branch_changed? || target_branch_changed?) &&
(source_branch_head && target_branch_head)
reload_diff reload_diff
end end
end end
...@@ -794,11 +795,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -794,11 +795,7 @@ class MergeRequest < ActiveRecord::Base
end end
def fetch_ref def fetch_ref
target_project.repository.fetch_ref( write_ref
source_project.repository.path_to_repo,
"refs/heads/#{source_branch}",
ref_path
)
update_column(:ref_fetched, true) update_column(:ref_fetched, true)
end end
...@@ -941,4 +938,19 @@ class MergeRequest < ActiveRecord::Base ...@@ -941,4 +938,19 @@ class MergeRequest < ActiveRecord::Base
true true
end end
def update_project_counter_caches
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
private
def write_ref
target_project.repository.with_repo_branch_commit(
source_project.repository, source_branch) do |commit|
if commit
target_project.repository.write_ref(ref_path, commit.sha)
end
end
end
end end
...@@ -77,6 +77,7 @@ class Project < ActiveRecord::Base ...@@ -77,6 +77,7 @@ class Project < ActiveRecord::Base
attr_accessor :old_path_with_namespace attr_accessor :old_path_with_namespace
attr_accessor :template_name attr_accessor :template_name
attr_writer :pipeline_status attr_writer :pipeline_status
attr_accessor :skip_disk_validation
alias_attribute :title, :name alias_attribute :title, :name
...@@ -165,7 +166,7 @@ class Project < ActiveRecord::Base ...@@ -165,7 +166,7 @@ class Project < ActiveRecord::Base
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics' has_one :statistics, class_name: 'ProjectStatistics'
# Container repositories need to remove data from the container registry, # Container repositories need to remove data from the container registry,
...@@ -192,7 +193,7 @@ class Project < ActiveRecord::Base ...@@ -192,7 +193,7 @@ class Project < ActiveRecord::Base
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data accepts_nested_attributes_for :import_data
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
...@@ -993,6 +994,7 @@ class Project < ActiveRecord::Base ...@@ -993,6 +994,7 @@ class Project < ActiveRecord::Base
# Check if repository already exists on disk # Check if repository already exists on disk
def can_create_repository? def can_create_repository?
return true if skip_disk_validation
return false unless repository_storage_path return false unless repository_storage_path
if gitlab_shell.exists?(repository_storage_path, "#{build_full_path}.git") if gitlab_shell.exists?(repository_storage_path, "#{build_full_path}.git")
...@@ -1061,13 +1063,16 @@ class Project < ActiveRecord::Base ...@@ -1061,13 +1063,16 @@ class Project < ActiveRecord::Base
end end
def change_head(branch) def change_head(branch)
if repository.branch_exists?(branch)
repository.before_change_head repository.before_change_head
repository.rugged.references.create('HEAD', repository.write_ref('HEAD', "refs/heads/#{branch}")
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch) repository.copy_gitattributes(branch)
repository.after_change_head repository.after_change_head
reload_default_branch reload_default_branch
else
errors.add(:base, "Could not change HEAD: branch '#{branch}' does not exist")
false
end
end end
def forked_from?(project) def forked_from?(project)
......
...@@ -41,6 +41,8 @@ class ProjectFeature < ActiveRecord::Base ...@@ -41,6 +41,8 @@ class ProjectFeature < ActiveRecord::Base
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
belongs_to :project, -> { unscope(where: :pending_delete) } belongs_to :project, -> { unscope(where: :pending_delete) }
validates :project, presence: true
validate :repository_children_level validate :repository_children_level
default_value_for :builds_access_level, value: ENABLED, allows_nil: false default_value_for :builds_access_level, value: ENABLED, allows_nil: false
......
...@@ -80,6 +80,6 @@ class PipelinesEmailService < Service ...@@ -80,6 +80,6 @@ class PipelinesEmailService < Service
end end
def retrieve_recipients(data) def retrieve_recipients(data)
recipients.to_s.split(',').reject(&:blank?) recipients.to_s.split(/[,(?:\r?\n) ]+/).reject(&:empty?)
end end
end end
...@@ -58,6 +58,10 @@ class Repository ...@@ -58,6 +58,10 @@ class Repository
@project = project @project = project
end end
def ==(other)
@disk_path == other.disk_path
end
def raw_repository def raw_repository
return nil unless full_path return nil unless full_path
...@@ -73,6 +77,10 @@ class Repository ...@@ -73,6 +77,10 @@ class Repository
) )
end end
def inspect
"#<#{self.class.name}:#{@disk_path}>"
end
# #
# Git repository can contains some hidden refs like: # Git repository can contains some hidden refs like:
# /refs/notes/* # /refs/notes/*
...@@ -224,7 +232,7 @@ class Repository ...@@ -224,7 +232,7 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes) # This will still fail if the file is corrupted (e.g. 0 bytes)
begin begin
rugged.references.create(keep_around_ref_name(sha), sha, force: true) write_ref(keep_around_ref_name(sha), sha)
rescue Rugged::ReferenceError => ex rescue Rugged::ReferenceError => ex
Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
rescue Rugged::OSError => ex rescue Rugged::OSError => ex
...@@ -237,6 +245,10 @@ class Repository ...@@ -237,6 +245,10 @@ class Repository
ref_exists?(keep_around_ref_name(sha)) ref_exists?(keep_around_ref_name(sha))
end end
def write_ref(ref_path, sha)
rugged.references.create(ref_path, sha, force: true)
end
def diverging_commit_counts(branch) def diverging_commit_counts(branch)
root_ref_hash = raw_repository.rev_parse_target(root_ref).oid root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
cache.fetch(:"diverging_commit_counts_#{branch.name}") do cache.fetch(:"diverging_commit_counts_#{branch.name}") do
...@@ -979,27 +991,26 @@ class Repository ...@@ -979,27 +991,26 @@ class Repository
end end
def with_repo_branch_commit(start_repository, start_branch_name) def with_repo_branch_commit(start_repository, start_branch_name)
return yield(nil) if start_repository.empty_repo? return yield nil if start_repository.empty_repo?
branch_name_or_sha =
if start_repository == self if start_repository == self
start_branch_name yield commit(start_branch_name)
else else
tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" start_commit = start_repository.commit(start_branch_name)
fetch_ref(
start_repository.path_to_repo,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
tmp_ref
)
start_repository.commit(start_branch_name).sha return yield nil unless start_commit
end
yield(commit(branch_name_or_sha)) sha = start_commit.sha
ensure if branch_commit = commit(sha)
rugged.references.delete(tmp_ref) if tmp_ref yield branch_commit
else
with_repo_tmp_commit(
start_repository, start_branch_name, sha) do |tmp_commit|
yield tmp_commit
end
end
end
end end
def add_remote(name, url) def add_remote(name, url)
...@@ -1021,7 +1032,12 @@ class Repository ...@@ -1021,7 +1032,12 @@ class Repository
def fetch_ref(source_path, source_ref, target_ref) def fetch_ref(source_path, source_ref, target_ref)
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
run_git(args) message, status = run_git(args)
# Make sure ref was created, and raise Rugged::ReferenceError when not
raise Rugged::ReferenceError, message if status != 0
target_ref
end end
def create_ref(ref, ref_path) def create_ref(ref, ref_path)
...@@ -1203,4 +1219,16 @@ class Repository ...@@ -1203,4 +1219,16 @@ class Repository
.commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset) .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
.map { |c| commit(c) } .map { |c| commit(c) }
end end
def with_repo_tmp_commit(start_repository, start_branch_name, sha)
tmp_ref = fetch_ref(
start_repository.path_to_repo,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
"refs/tmp/#{SecureRandom.hex}/head"
)
yield commit(sha)
ensure
rugged.references.delete(tmp_ref) if tmp_ref
end
end end
...@@ -1045,6 +1045,10 @@ class User < ActiveRecord::Base ...@@ -1045,6 +1045,10 @@ class User < ActiveRecord::Base
ensure_rss_token! ensure_rss_token!
end end
def verified_email?(email)
self.email == email
end
protected protected
# override, from Devise::Validatable # override, from Devise::Validatable
......
...@@ -11,6 +11,8 @@ class GlobalPolicy < BasePolicy ...@@ -11,6 +11,8 @@ class GlobalPolicy < BasePolicy
with_options scope: :user, score: 0 with_options scope: :user, score: 0
condition(:access_locked) { @user.access_locked? } condition(:access_locked) { @user.access_locked? }
condition(:can_create_fork, scope: :user) { @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } }
rule { anonymous }.policy do rule { anonymous }.policy do
prevent :log_in prevent :log_in
prevent :access_api prevent :access_api
...@@ -40,6 +42,10 @@ class GlobalPolicy < BasePolicy ...@@ -40,6 +42,10 @@ class GlobalPolicy < BasePolicy
enable :create_group enable :create_group
end end
rule { can_create_fork }.policy do
enable :create_fork
end
rule { access_locked }.policy do rule { access_locked }.policy do
prevent :log_in prevent :log_in
end end
......
class NamespacePolicy < BasePolicy class NamespacePolicy < BasePolicy
rule { anonymous }.prevent_all rule { anonymous }.prevent_all
condition(:personal_project, scope: :subject) { @subject.kind == 'user' }
condition(:can_create_personal_project, scope: :user) { @user.can_create_project? }
condition(:owner) { @subject.owner == @user } condition(:owner) { @subject.owner == @user }
rule { owner | admin }.policy do rule { owner | admin }.policy do
enable :create_projects enable :create_projects
enable :admin_namespace enable :admin_namespace
end end
rule { personal_project & ~can_create_personal_project }.prevent :create_projects
end end
...@@ -32,8 +32,8 @@ class BuildDetailsEntity < JobEntity ...@@ -32,8 +32,8 @@ class BuildDetailsEntity < JobEntity
private private
def build_failed_issue_options def build_failed_issue_options
{ title: "Build Failed ##{build.id}", { title: "Job Failed ##{build.id}",
description: project_job_path(project, build) } description: "Job [##{build.id}](#{project_job_path(project, build)}) failed for #{build.sha}:\n" }
end end
def current_user def current_user
......
...@@ -14,7 +14,7 @@ module Ci ...@@ -14,7 +14,7 @@ module Ci
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref]) pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref])
.execute(:trigger, ignore_skip_ci: true) do |pipeline| .execute(:trigger, ignore_skip_ci: true) do |pipeline|
trigger.trigger_requests.create!(pipeline: pipeline) pipeline.trigger_requests.create!(trigger: trigger)
create_pipeline_variables!(pipeline) create_pipeline_variables!(pipeline)
end end
......
...@@ -28,7 +28,10 @@ module Projects ...@@ -28,7 +28,10 @@ module Projects
success success
else else
error('Project could not be updated!') model_errors = project.errors.full_messages.to_sentence
error_message = model_errors.presence || 'Project could not be updated!'
error(error_message)
end end
end end
......
...@@ -9,18 +9,17 @@ module TestHooks ...@@ -9,18 +9,17 @@ module TestHooks
end end
def execute def execute
trigger_key = hook.class::TRIGGERS.key(trigger.to_sym)
trigger_data_method = "#{trigger}_data" trigger_data_method = "#{trigger}_data"
if !self.respond_to?(trigger_data_method, true) || if trigger_key.nil? || !self.respond_to?(trigger_data_method, true)
!hook.class::TRIGGERS.value?(trigger.to_sym)
return error('Testing not available for this hook') return error('Testing not available for this hook')
end end
error_message = catch(:validation_error) do error_message = catch(:validation_error) do
sample_data = self.__send__(trigger_data_method) sample_data = self.__send__(trigger_data_method)
return hook.execute(sample_data, trigger) return hook.execute(sample_data, trigger_key)
end end
error(error_message) error(error_message)
......
...@@ -19,7 +19,7 @@ class WebHookService ...@@ -19,7 +19,7 @@ class WebHookService
def initialize(hook, data, hook_name) def initialize(hook, data, hook_name)
@hook = hook @hook = hook
@data = data @data = data
@hook_name = hook_name @hook_name = hook_name.to_s
end end
def execute def execute
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
Groups Groups
= nav_link(path: 'dashboard#activity', html_options: { class: "hidden-xs hidden-sm" }) do = nav_link(path: 'dashboard#activity', html_options: { class: "hidden-xs hidden-sm hidden-md" }) do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
Activity Activity
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
= icon("chevron-down", class: "dropdown-chevron") = icon("chevron-down", class: "dropdown-chevron")
.dropdown-menu .dropdown-menu
%ul %ul
= nav_link(path: 'dashboard#activity', html_options: { class: "visible-xs visible-sm" }) do = nav_link(path: 'dashboard#activity', html_options: { class: "visible-xs visible-sm visible-md" }) do
= link_to activity_dashboard_path, title: 'Activity' do = link_to activity_dashboard_path, title: 'Activity' do
Activity Activity
......
...@@ -4,12 +4,11 @@ ...@@ -4,12 +4,11 @@
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do
= custom_icon('icon_fork') = custom_icon('icon_fork')
%span= s_('GoToYourFork|Fork') %span= s_('GoToYourFork|Fork')
- elsif !current_user.can_create_project?
= link_to new_project_fork_path(@project), title: _('You have reached your project limit'), class: 'btn has-tooltip disabled' do
= custom_icon('icon_fork')
%span= s_('CreateNewFork|Fork')
- else - else
= link_to new_project_fork_path(@project), class: 'btn' do - can_create_fork = current_user.can?(:create_fork)
= link_to new_project_fork_path(@project),
class: "btn btn-default #{'has-tooltip disabled' unless can_create_fork}",
title: (_('You have reached your project limit') unless can_create_fork) do
= custom_icon('icon_fork') = custom_icon('icon_fork')
%span= s_('CreateNewFork|Fork') %span= s_('CreateNewFork|Fork')
.count-with-arrow .count-with-arrow
......
- title = capture do - title = capture do
.gpg-popover-icon.invalid This commit was signed with a different user's verified signature.
= render 'shared/icons/icon_status_notfound_borderless.svg'
%div
This commit was signed with an <strong>unverified</strong> signature.
- locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] } - locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals = render partial: 'projects/commit/signature_badge', locals: locals
- title = capture do
This commit was signed with a verified signature, but the committer email
is <strong>not verified</strong> to belong to the same user.
- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'icon_status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
- if signature - if signature
- if signature.valid_signature? = render partial: "projects/commit/#{signature.verification_status}_signature_badge", locals: { signature: signature }
= render partial: 'projects/commit/valid_signature_badge', locals: { signature: signature }
- else
= render partial: 'projects/commit/invalid_signature_badge', locals: { signature: signature }
- css_classes = commit_signature_badge_classes(css_classes) - signature = local_assigns.fetch(:signature)
- title = local_assigns.fetch(:title)
- label = local_assigns.fetch(:label)
- css_class = local_assigns.fetch(:css_class)
- icon = local_assigns.fetch(:icon)
- show_user = local_assigns.fetch(:show_user, false)
- css_classes = commit_signature_badge_classes(css_class)
- title = capture do - title = capture do
.gpg-popover-status .gpg-popover-status
.gpg-popover-icon{ class: css_class }
= render "shared/icons/#{icon}.svg"
%div
= title = title
- content = capture do - content = capture do
- if show_user
.clearfix .clearfix
= content = render partial: 'projects/commit/signature_badge_user', locals: { signature: signature }
GPG Key ID: GPG Key ID:
%span.monospace= signature.gpg_key_primary_keyid %span.monospace= signature.gpg_key_primary_keyid
= link_to('Learn more about signing commits', help_page_path('user/project/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') = link_to('Learn more about signing commits', help_page_path('user/project/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
%button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } } %button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }
......
- gpg_key = signature.gpg_key
- user = gpg_key&.user
- user_name = signature.gpg_key_user_name
- user_email = signature.gpg_key_user_email
- if user
= link_to user_path(user), class: 'gpg-popover-user-link' do
%div
= user_avatar_without_link(user: user, size: 32)
%div
%strong= user.name
%div= user.to_reference
- else
= mail_to user_email do
%div
= user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
%div
%strong= user_name
%div= user_email
= render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature }
= render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature }
- title = capture do
This commit was signed with an <strong>unverified</strong> signature.
- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless' }
= render partial: 'projects/commit/signature_badge', locals: locals
- title = capture do
.gpg-popover-icon.valid
= render 'shared/icons/icon_status_success_borderless.svg'
%div
This commit was signed with a <strong>verified</strong> signature.
- content = capture do
- gpg_key = signature.gpg_key
- user = gpg_key&.user
- user_name = signature.gpg_key_user_name
- user_email = signature.gpg_key_user_email
- if user
= link_to user_path(user), class: 'gpg-popover-user-link' do
%div
= user_avatar_without_link(user: user, size: 32)
%div
%strong= gpg_key.user.name
%div @#{gpg_key.user.username}
- else
= mail_to user_email do
%div
= user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
%div
%strong= user_name
%div= user_email
- locals = { signature: signature, title: title, content: content, label: 'Verified', css_classes: ['valid'] }
= render partial: 'projects/commit/signature_badge', locals: locals
- title = capture do
This commit was signed with a <strong>verified</strong> signature and the
committer email is verified to belong to the same user.
- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'icon_status_success_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
- if @namespaces.present? - if @namespaces.present?
%label.label-light %label.label-light
%span %span
Click to fork the project to a user or group Click to fork the project
- @namespaces.in_groups_of(6, false) do |group| - @namespaces.in_groups_of(6, false) do |group|
.row .row
- group.each do |namespace| - group.each do |namespace|
...@@ -29,8 +29,12 @@ ...@@ -29,8 +29,12 @@
.caption .caption
= namespace.human_name = namespace.human_name
- else - else
.fork-thumbnail - can_create_project = current_user.can?(:create_projects, namespace)
= link_to project_forks_path(@project, namespace_key: namespace.id), method: "POST" do .fork-thumbnail{ class: ("disabled" unless can_create_project) }
= link_to project_forks_path(@project, namespace_key: namespace.id),
method: "POST",
class: ("disabled has-tooltip" unless can_create_project),
title: (_('You have reached your project limit') unless can_create_project) do
- if /no_((\w*)_)*avatar/.match(avatar) - if /no_((\w*)_)*avatar/.match(avatar)
.no-avatar .no-avatar
= icon 'question' = icon 'question'
......
...@@ -60,7 +60,10 @@ ...@@ -60,7 +60,10 @@
":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
%span.line-resolve-btn.is-disabled{ type: "button", %span.line-resolve-btn.is-disabled{ type: "button",
":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
= render "shared/icons/icon_status_success.svg" %template{ 'v-if' => 'resolvedDiscussionCount === discussionCount' }
= render 'shared/icons/icon_status_success_solid.svg'
%template{ 'v-else' => '' }
= render 'shared/icons/icon_resolve_discussion.svg'
%span.line-resolve-text %span.line-resolve-text
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
= render "discussions/new_issue_for_all_discussions", merge_request: @merge_request = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
......
...@@ -68,6 +68,7 @@ ...@@ -68,6 +68,7 @@
- if git_import_enabled? - if git_import_enabled?
%button.btn.js-toggle-button.import_git{ type: "button" } %button.btn.js-toggle-button.import_git{ type: "button" }
= icon('git', text: 'Repo by URL') = icon('git', text: 'Repo by URL')
- if gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip{ data: { container: 'body' } } .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export') = icon('gitlab', text: 'GitLab export')
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
%template{ 'v-if' => 'isResolved' } %template{ 'v-if' => 'isResolved' }
= render 'shared/icons/icon_status_success_solid.svg' = render 'shared/icons/icon_status_success_solid.svg'
%template{ 'v-else' => '' } %template{ 'v-else' => '' }
= render 'shared/icons/icon_status_success.svg' = render 'shared/icons/icon_resolve_discussion.svg'
- if current_user - if current_user
- if note.emoji_awardable? - if note.emoji_awardable?
......
- if any_projects?(@projects) - if any_projects?(@projects)
.project-item-select-holder.btn-group.pull-right .project-item-select-holder.btn-group
%a.btn.btn-new.new-project-item-link{ href: '', data: { label: local_assigns[:label] } } %a.btn.btn-new.new-project-item-link{ href: '', data: { label: local_assigns[:label] } }
= icon('spinner spin') = icon('spinner spin')
= project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled] = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled]
......
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg>
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg> <svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg>
...@@ -6,7 +6,11 @@ class CreateGpgSignatureWorker ...@@ -6,7 +6,11 @@ class CreateGpgSignatureWorker
project = Project.find_by(id: project_id) project = Project.find_by(id: project_id)
return unless project return unless project
commit = project.commit(commit_sha)
return unless commit
# This calculates and caches the signature in the database # This calculates and caches the signature in the database
Gitlab::Gpg::Commit.new(project, commit_sha).signature Gitlab::Gpg::Commit.new(commit).signature
end end
end end
---
title: Resolve CSRF token leakage via pathname manipulation on environments page
merge_request:
author:
---
title: Allow using newlines in pipeline email service recipients
merge_request: 14250
author:
type: fixed
---
title: Fix XSS issue in go-get handling
merge_request:
author:
---
title: Prevent a persistent XSS in the commit author block
merge_request:
author:
type: security
---
title: Upgrade mail and nokogiri gems due to security issues
merge_request: 13662
author: Markus Koller
type: security
---
title: Fixes race condition in project uploads
merge_request:
author:
---
title: Disallow arbitrary properties in `th` and `td` `style` attributes
merge_request:
author:
---
title: Disallow the `name` attribute on all user-provided markup
merge_request:
author:
...@@ -509,7 +509,7 @@ production: &base ...@@ -509,7 +509,7 @@ production: &base
failure_count_threshold: 10 # number of failures before stopping attempts failure_count_threshold: 10 # number of failures before stopping attempts
failure_wait_time: 30 # Seconds after an access failure before allowing access again failure_wait_time: 30 # Seconds after an access failure before allowing access again
failure_reset_time: 1800 # Time in seconds to expire failures failure_reset_time: 1800 # Time in seconds to expire failures
storage_timeout: 5 # Time in seconds to wait before aborting a storage access attempt storage_timeout: 30 # Time in seconds to wait before aborting a storage access attempt
## Backup settings ## Backup settings
......
...@@ -28,6 +28,8 @@ Gitlab::Seeder.quiet do ...@@ -28,6 +28,8 @@ Gitlab::Seeder.quiet do
project = Project.find_by_full_path('gitlab-org/gitlab-test') project = Project.find_by_full_path('gitlab-org/gitlab-test')
next if project.empty_repo? # We don't have repository on CI
params = { params = {
source_branch: 'feature', source_branch: 'feature',
target_branch: 'master', target_branch: 'master',
......
class AddVerificationStatusToGpgSignatures < ActiveRecord::Migration
DOWNTIME = false
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
# First we remove all signatures because we need to re-verify them all
# again anyway (because of the updated verification logic).
#
# This makes adding the column with default values faster
truncate(:gpg_signatures)
add_column_with_default(:gpg_signatures, :verification_status, :smallint, default: 0)
end
def down
remove_column(:gpg_signatures, :verification_status)
end
end
class DestroyGpgSignatures < ActiveRecord::Migration
DOWNTIME = false
def up
truncate(:gpg_signatures)
end
def down
end
end
class RemoveValidSignatureFromGpgSignatures < ActiveRecord::Migration
DOWNTIME = false
def up
remove_column :gpg_signatures, :valid_signature
end
def down
add_column :gpg_signatures, :valid_signature, :boolean
end
end
class FixProjectsWithoutProjectFeature < ActiveRecord::Migration
DOWNTIME = false
def up
# Deletes corrupted project features
sql = "DELETE FROM project_features WHERE project_id IS NULL"
execute(sql)
# Creates missing project features with private visibility
sql =
%Q{
INSERT INTO project_features(project_id, repository_access_level, issues_access_level, merge_requests_access_level, wiki_access_level,
builds_access_level, snippets_access_level, created_at, updated_at)
SELECT projects.id as project_id,
10 as repository_access_level,
10 as issues_access_level,
10 as merge_requests_access_level,
10 as wiki_access_level,
10 as builds_access_level ,
10 as snippets_access_level,
projects.created_at,
projects.updated_at
FROM projects
LEFT OUTER JOIN project_features ON project_features.project_id = projects.id
WHERE (project_features.id IS NULL)
}
execute(sql)
end
def down
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170824162758) do ActiveRecord::Schema.define(version: 20170913180600) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -596,11 +596,11 @@ ActiveRecord::Schema.define(version: 20170824162758) do ...@@ -596,11 +596,11 @@ ActiveRecord::Schema.define(version: 20170824162758) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "project_id" t.integer "project_id"
t.integer "gpg_key_id" t.integer "gpg_key_id"
t.boolean "valid_signature"
t.binary "commit_sha" t.binary "commit_sha"
t.binary "gpg_key_primary_keyid" t.binary "gpg_key_primary_keyid"
t.text "gpg_key_user_name" t.text "gpg_key_user_name"
t.text "gpg_key_user_email" t.text "gpg_key_user_email"
t.integer "verification_status", limit: 2, default: 0, null: false
end end
add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", unique: true, using: :btree add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", unique: true, using: :btree
......
...@@ -268,6 +268,43 @@ end ...@@ -268,6 +268,43 @@ end
- Avoid scenario titles that add no information, such as "successfully". - Avoid scenario titles that add no information, such as "successfully".
- Avoid scenario titles that repeat the feature title. - Avoid scenario titles that repeat the feature title.
### Table-based / Parameterized tests
This style of testing is used to exercise one piece of code with a comprehensive
range of inputs. By specifying the test case once, alongside a table of inputs
and the expected output for each, your tests can be made easier to read and more
compact.
We use the [rspec-parameterized](https://github.com/tomykaira/rspec-parameterized)
gem. A short example, using the table syntax and checking Ruby equality for a
range of inputs, might look like this:
```ruby
describe "#==" do
using Rspec::Parameterized::TableSyntax
let(:project1) { create(:project) }
let(:project2) { create(:project) }
where(:a, :b, :result) do
1 | 1 | true
1 | 2 | false
true | true | true
true | false | false
project1 | project1 | true
project2 | project2 | true
project 1 | project2 | false
end
with_them do
it { expect(a == b).to eq(result) }
it 'is isomorphic' do
expect(b == a).to eq(result)
end
end
end
```
### Matchers ### Matchers
Custom matchers should be created to clarify the intent and/or hide the Custom matchers should be created to clarify the intent and/or hide the
......
...@@ -22,11 +22,12 @@ GitLab uses its own keyring to verify the GPG signature. It does not access any ...@@ -22,11 +22,12 @@ GitLab uses its own keyring to verify the GPG signature. It does not access any
public key server. public key server.
In order to have a commit verified on GitLab the corresponding public key needs In order to have a commit verified on GitLab the corresponding public key needs
to be uploaded to GitLab. For a signature to be verified two prerequisites need 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 your **primary** email
1. The committer's email matches the verified email from the gpg key
## Generating a GPG key ## Generating a GPG key
......
...@@ -9,7 +9,7 @@ keep security vulnerabilities private or prevent surprises from leaking out. ...@@ -9,7 +9,7 @@ keep security vulnerabilities private or prevent surprises from leaking out.
## Making an issue confidential ## Making an issue confidential
You can make an issue confidential either by creating a new issue or editing You can make an issue confidential during issue creation or by editing
an existing one. an existing one.
When you create a new issue, a checkbox right below the text area is available When you create a new issue, a checkbox right below the text area is available
...@@ -19,11 +19,19 @@ confidential checkbox and hit **Save changes**. ...@@ -19,11 +19,19 @@ confidential checkbox and hit **Save changes**.
![Creating a new confidential issue](img/confidential_issues_create.png) ![Creating a new confidential issue](img/confidential_issues_create.png)
## Making an issue non-confidential ## Modifying issue confidentiality
To make an issue non-confidential, all you have to do is edit it and unmark There are two ways to change an issue's confidentiality.
the confidential checkbox. Once you save the issue, it will gain the default
visibility level you have chosen for your project. The first way is to edit the issue and mark/unmark the confidential checkbox.
Once you save the issue, it will change the confidentiality of the issue.
The second way is to locate the Confidentiality section in the sidebar and click
**Edit**. A popup should appear and give you the option to turn on or turn off confidentiality.
| Turn off confidentiality | Turn on confidentiality |
| :-----------: | :----------: |
| ![Turn off confidentiality](img/turn_off_confidentiality.png) | ![Turn on confidentiality](img/turn_on_confidentiality.png) |
Every change from regular to confidential and vice versa, is indicated by a Every change from regular to confidential and vice versa, is indicated by a
system note in the issue's comments. system note in the issue's comments.
...@@ -49,6 +57,12 @@ issue you are commenting on is confidential. ...@@ -49,6 +57,12 @@ issue you are commenting on is confidential.
![Confidential issue page](img/confidential_issues_issue_page.png) ![Confidential issue page](img/confidential_issues_issue_page.png)
There is also an indicator on the sidebar denoting confidentiality.
| Confidential issue | Not confidential issue |
| :-----------: | :----------: |
| ![Sidebar confidential issue](img/sidebar_confidential_issue.png) | ![Sidebar not confidential issue](img/sidebar_not_confidential_issue.png) |
## Permissions and access to confidential issues ## Permissions and access to confidential issues
There are two kinds of level access for confidential issues. The general rule There are two kinds of level access for confidential issues. The general rule
......
doc/user/project/issues/img/confidential_issues_index_page.png

8.15 KB | W: | H:

doc/user/project/issues/img/confidential_issues_index_page.png

105 KB | W: | H:

doc/user/project/issues/img/confidential_issues_index_page.png
doc/user/project/issues/img/confidential_issues_index_page.png
doc/user/project/issues/img/confidential_issues_index_page.png
doc/user/project/issues/img/confidential_issues_index_page.png
  • 2-up
  • Swipe
  • Onion skin
doc/user/project/issues/img/confidential_issues_issue_page.png

13.9 KB | W: | H:

doc/user/project/issues/img/confidential_issues_issue_page.png

24.8 KB | W: | H:

doc/user/project/issues/img/confidential_issues_issue_page.png
doc/user/project/issues/img/confidential_issues_issue_page.png
doc/user/project/issues/img/confidential_issues_issue_page.png
doc/user/project/issues/img/confidential_issues_issue_page.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -9,6 +9,14 @@ module Gitlab ...@@ -9,6 +9,14 @@ module Gitlab
ActiveRecord::Base.configurations[Rails.env] ActiveRecord::Base.configurations[Rails.env]
end end
def self.username
config['username'] || ENV['USER']
end
def self.database_name
config['database']
end
def self.adapter_name def self.adapter_name
config['adapter'] config['adapter']
end end
......
module Gitlab
module Database
# Model that can be used for querying permissions of a SQL user.
class Grant < ActiveRecord::Base
self.table_name =
if Database.postgresql?
'information_schema.role_table_grants'
else
'mysql.user'
end
def self.scope_to_current_user
if Database.postgresql?
where('grantee = user')
else
where("CONCAT(User, '@', Host) = current_user()")
end
end
# Returns true if the current user can create and execute triggers on the
# given table.
def self.create_and_execute_trigger?(table)
priv =
if Database.postgresql?
where(privilege_type: 'TRIGGER', table_name: table)
else
where(Trigger_priv: 'Y')
end
priv.scope_to_current_user.any?
end
end
end
end
...@@ -358,6 +358,8 @@ module Gitlab ...@@ -358,6 +358,8 @@ module Gitlab
raise 'rename_column_concurrently can not be run inside a transaction' raise 'rename_column_concurrently can not be run inside a transaction'
end end
check_trigger_permissions!(table)
old_col = column_for(table, old) old_col = column_for(table, old)
new_type = type || old_col.type new_type = type || old_col.type
...@@ -430,6 +432,8 @@ module Gitlab ...@@ -430,6 +432,8 @@ module Gitlab
def cleanup_concurrent_column_rename(table, old, new) def cleanup_concurrent_column_rename(table, old, new)
trigger_name = rename_trigger_name(table, old, new) trigger_name = rename_trigger_name(table, old, new)
check_trigger_permissions!(table)
if Database.postgresql? if Database.postgresql?
remove_rename_triggers_for_postgresql(table, trigger_name) remove_rename_triggers_for_postgresql(table, trigger_name)
else else
...@@ -485,14 +489,14 @@ module Gitlab ...@@ -485,14 +489,14 @@ module Gitlab
# Removes the triggers used for renaming a PostgreSQL column concurrently. # Removes the triggers used for renaming a PostgreSQL column concurrently.
def remove_rename_triggers_for_postgresql(table, trigger) def remove_rename_triggers_for_postgresql(table, trigger)
execute("DROP TRIGGER #{trigger} ON #{table}") execute("DROP TRIGGER IF EXISTS #{trigger} ON #{table}")
execute("DROP FUNCTION #{trigger}()") execute("DROP FUNCTION IF EXISTS #{trigger}()")
end end
# Removes the triggers used for renaming a MySQL column concurrently. # Removes the triggers used for renaming a MySQL column concurrently.
def remove_rename_triggers_for_mysql(trigger) def remove_rename_triggers_for_mysql(trigger)
execute("DROP TRIGGER #{trigger}_insert") execute("DROP TRIGGER IF EXISTS #{trigger}_insert")
execute("DROP TRIGGER #{trigger}_update") execute("DROP TRIGGER IF EXISTS #{trigger}_update")
end end
# Returns the (base) name to use for triggers when renaming columns. # Returns the (base) name to use for triggers when renaming columns.
...@@ -611,6 +615,44 @@ module Gitlab ...@@ -611,6 +615,44 @@ module Gitlab
remove_foreign_key(*args) remove_foreign_key(*args)
rescue ArgumentError rescue ArgumentError
end end
def sidekiq_queue_migrate(queue_from, to:)
while sidekiq_queue_length(queue_from) > 0
Sidekiq.redis do |conn|
conn.rpoplpush "queue:#{queue_from}", "queue:#{to}"
end
end
end
def sidekiq_queue_length(queue_name)
Sidekiq.redis do |conn|
conn.llen("queue:#{queue_name}")
end
end
def check_trigger_permissions!(table)
unless Grant.create_and_execute_trigger?(table)
dbname = Database.database_name
user = Database.username
raise <<-EOF
Your database user is not allowed to create, drop, or execute triggers on the
table #{table}.
If you are using PostgreSQL you can solve this by logging in to the GitLab
database (#{dbname}) using a super user and running:
ALTER #{user} WITH SUPERUSER
For MySQL you instead need to run:
GRANT ALL PRIVILEGES ON *.* TO #{user}@'%'
Both queries will grant the user super user permissions, ensuring you don't run
into similar problems in the future (e.g. when new tables are created).
EOF
end
end
end end
end end
end end
...@@ -39,7 +39,7 @@ module Gitlab ...@@ -39,7 +39,7 @@ module Gitlab
fingerprints = CurrentKeyChain.fingerprints_from_key(key) fingerprints = CurrentKeyChain.fingerprints_from_key(key)
GPGME::Key.find(:public, fingerprints).flat_map do |raw_key| GPGME::Key.find(:public, fingerprints).flat_map do |raw_key|
raw_key.uids.map { |uid| { name: uid.name, email: uid.email } } raw_key.uids.map { |uid| { name: uid.name, email: uid.email.downcase } }
end end
end end
end end
...@@ -69,11 +69,17 @@ module Gitlab ...@@ -69,11 +69,17 @@ module Gitlab
def optimistic_using_tmp_keychain def optimistic_using_tmp_keychain
previous_dir = current_home_dir previous_dir = current_home_dir
Dir.mktmpdir do |dir| tmp_dir = Dir.mktmpdir
GPGME::Engine.home_dir = dir GPGME::Engine.home_dir = tmp_dir
yield yield
end
ensure ensure
# Ignore any errors when removing the tmp directory, as we may run into a
# race condition:
# The `gpg-agent` agent process may clean up some files as well while
# `FileUtils.remove_entry` is iterating the directory and removing all
# its contained files and directories recursively, which could raise an
# error.
FileUtils.remove_entry(tmp_dir, true)
GPGME::Engine.home_dir = previous_dir GPGME::Engine.home_dir = previous_dir
end end
end end
......
module Gitlab module Gitlab
module Gpg module Gpg
class Commit class Commit
def self.for_commit(commit) def initialize(commit)
new(commit.project, commit.sha) @commit = commit
end
def initialize(project, sha)
@project = project
@sha = sha
@signature_text, @signed_text = @signature_text, @signed_text =
begin begin
Rugged::Commit.extract_signature(project.repository.rugged, sha) Rugged::Commit.extract_signature(@commit.project.repository.rugged, @commit.sha)
rescue Rugged::OdbError rescue Rugged::OdbError
nil nil
end end
...@@ -26,7 +21,7 @@ module Gitlab ...@@ -26,7 +21,7 @@ module Gitlab
return @signature if @signature return @signature if @signature
cached_signature = GpgSignature.find_by(commit_sha: @sha) cached_signature = GpgSignature.find_by(commit_sha: @commit.sha)
return @signature = cached_signature if cached_signature.present? return @signature = cached_signature if cached_signature.present?
@signature = create_cached_signature! @signature = create_cached_signature!
...@@ -73,20 +68,31 @@ module Gitlab ...@@ -73,20 +68,31 @@ module Gitlab
def attributes(gpg_key) def attributes(gpg_key)
user_infos = user_infos(gpg_key) user_infos = user_infos(gpg_key)
verification_status = verification_status(gpg_key)
{ {
commit_sha: @sha, commit_sha: @commit.sha,
project: @project, project: @commit.project,
gpg_key: gpg_key, gpg_key: gpg_key,
gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint, gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint,
gpg_key_user_name: user_infos[:name], gpg_key_user_name: user_infos[:name],
gpg_key_user_email: user_infos[:email], gpg_key_user_email: user_infos[:email],
valid_signature: gpg_signature_valid_signature_value(gpg_key) verification_status: verification_status
} }
end end
def gpg_signature_valid_signature_value(gpg_key) def verification_status(gpg_key)
!!(gpg_key && gpg_key.verified? && verified_signature.valid?) return :unknown_key unless gpg_key
return :unverified_key unless gpg_key.verified?
return :unverified unless verified_signature.valid?
if gpg_key.verified_and_belongs_to_email?(@commit.committer_email)
:verified
elsif gpg_key.user.all_emails.include?(@commit.committer_email)
:same_user_different_email
else
:other_user
end
end end
def user_infos(gpg_key) def user_infos(gpg_key)
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
def run def run
GpgSignature GpgSignature
.select(:id, :commit_sha, :project_id) .select(:id, :commit_sha, :project_id)
.where('gpg_key_id IS NULL OR valid_signature = ?', false) .where('gpg_key_id IS NULL OR verification_status <> ?', GpgSignature.verification_statuses[:verified])
.where(gpg_key_primary_keyid: @gpg_key.primary_keyid) .where(gpg_key_primary_keyid: @gpg_key.primary_keyid)
.find_each { |sig| sig.gpg_commit.update_signature!(sig) } .find_each { |sig| sig.gpg_commit.update_signature!(sig) }
end end
......
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
end end
def self.valid?(url) def self.valid?(url)
return false unless url return false unless url.present?
Addressable::URI.parse(url.strip) Addressable::URI.parse(url.strip)
...@@ -19,7 +19,12 @@ module Gitlab ...@@ -19,7 +19,12 @@ module Gitlab
end end
def initialize(url, credentials: nil) def initialize(url, credentials: nil)
@url = Addressable::URI.parse(url.strip) @url = Addressable::URI.parse(url.to_s.strip)
%i[user password].each do |symbol|
credentials[symbol] = credentials[symbol].presence if credentials&.key?(symbol)
end
@credentials = credentials @credentials = credentials
end end
...@@ -29,13 +34,13 @@ module Gitlab ...@@ -29,13 +34,13 @@ module Gitlab
def masked_url def masked_url
url = @url.dup url = @url.dup
url.password = "*****" unless url.password.nil? url.password = "*****" if url.password.present?
url.user = "*****" unless url.user.nil? url.user = "*****" if url.user.present?
url.to_s url.to_s
end end
def credentials def credentials
@credentials ||= { user: @url.user, password: @url.password } @credentials ||= { user: @url.user.presence, password: @url.password.presence }
end end
def full_url def full_url
...@@ -47,8 +52,10 @@ module Gitlab ...@@ -47,8 +52,10 @@ module Gitlab
def generate_full_url def generate_full_url
return @url unless valid_credentials? return @url unless valid_credentials?
@full_url = @url.dup @full_url = @url.dup
@full_url.user = credentials[:user]
@full_url.password = credentials[:password] @full_url.password = credentials[:password]
@full_url.user = credentials[:user]
@full_url @full_url
end end
......
...@@ -39,7 +39,8 @@ namespace :gitlab do ...@@ -39,7 +39,8 @@ namespace :gitlab do
project_params = { project_params = {
name: name, name: name,
path: name path: name,
skip_disk_validation: true
} }
# find group namespace # find group namespace
......
require('spec_helper') require('spec_helper')
describe Projects::IssuesController do describe Projects::IssuesController do
let(:project) { create(:project_empty_repo) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
...@@ -841,7 +841,7 @@ describe Projects::IssuesController do ...@@ -841,7 +841,7 @@ describe Projects::IssuesController do
describe 'POST #toggle_award_emoji' do describe 'POST #toggle_award_emoji' do
before do before do
sign_in(user) sign_in(user)
project.team << [user, :developer] project.add_developer(user)
end end
it "toggles the award emoji" do it "toggles the award emoji" do
...@@ -855,6 +855,8 @@ describe Projects::IssuesController do ...@@ -855,6 +855,8 @@ describe Projects::IssuesController do
end end
describe 'POST create_merge_request' do describe 'POST create_merge_request' do
let(:project) { create(:project, :repository) }
before do before do
project.add_developer(user) project.add_developer(user)
sign_in(user) sign_in(user)
......
...@@ -10,6 +10,10 @@ FactoryGirl.define do ...@@ -10,6 +10,10 @@ FactoryGirl.define do
after(:build) do |deployment, evaluator| after(:build) do |deployment, evaluator|
deployment.project ||= deployment.environment.project deployment.project ||= deployment.environment.project
unless deployment.project.repository_exists?
allow(deployment.project.repository).to receive(:fetch_ref)
end
end end
end end
end end
...@@ -6,6 +6,6 @@ FactoryGirl.define do ...@@ -6,6 +6,6 @@ FactoryGirl.define do
project project
gpg_key gpg_key
gpg_key_primary_keyid { gpg_key.primary_keyid } gpg_key_primary_keyid { gpg_key.primary_keyid }
valid_signature true verification_status :verified
end end
end end
...@@ -68,6 +68,17 @@ FactoryGirl.define do ...@@ -68,6 +68,17 @@ FactoryGirl.define do
merge_user author merge_user author
end end
after(:build) do |merge_request|
target_project = merge_request.target_project
source_project = merge_request.source_project
# Fake `write_ref` if we don't have repository
# We have too many existing tests replying on this behaviour
unless [target_project, source_project].all?(&:repository_exists?)
allow(merge_request).to receive(:write_ref)
end
end
factory :merged_merge_request, traits: [:merged] factory :merged_merge_request, traits: [:merged]
factory :closed_merge_request, traits: [:closed] factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:opened] factory :reopened_merge_request, traits: [:opened]
......
...@@ -203,105 +203,4 @@ describe 'Commits' do ...@@ -203,105 +203,4 @@ describe 'Commits' do
end end
end end
end end
describe 'GPG signed commits', :js do
it 'changes from unverified to verified when the user changes his email to match the gpg key' do
user = create :user, email: 'unrelated.user@example.org'
project.team << [user, :master]
Sidekiq::Testing.inline! do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
end
sign_in(user)
visit project_commits_path(project, :'signed-commits')
within '#commits-list' do
expect(page).to have_content 'Unverified'
expect(page).not_to have_content 'Verified'
end
# user changes his email which makes the gpg key verified
Sidekiq::Testing.inline! do
user.skip_reconfirmation!
user.update_attributes!(email: GpgHelpers::User1.emails.first)
end
visit project_commits_path(project, :'signed-commits')
within '#commits-list' do
expect(page).to have_content 'Unverified'
expect(page).to have_content 'Verified'
end
end
it 'changes from unverified to verified when the user adds the missing gpg key' do
user = create :user, email: GpgHelpers::User1.emails.first
project.team << [user, :master]
sign_in(user)
visit project_commits_path(project, :'signed-commits')
within '#commits-list' do
expect(page).to have_content 'Unverified'
expect(page).not_to have_content 'Verified'
end
# user adds the gpg key which makes the signature valid
Sidekiq::Testing.inline! do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
end
visit project_commits_path(project, :'signed-commits')
within '#commits-list' do
expect(page).to have_content 'Unverified'
expect(page).to have_content 'Verified'
end
end
it 'shows popover badges' do
gpg_user = create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard'
Sidekiq::Testing.inline! do
create :gpg_key, key: GpgHelpers::User1.public_key, user: gpg_user
end
user = create :user
project.team << [user, :master]
sign_in(user)
visit project_commits_path(project, :'signed-commits')
# unverified signature
click_on 'Unverified', match: :first
within '.popover' do
expect(page).to have_content 'This commit was signed with an unverified signature.'
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
end
# verified and the gpg user has a gitlab profile
click_on 'Verified', match: :first
within '.popover' do
expect(page).to have_content 'This commit was signed with a verified signature.'
expect(page).to have_content 'Nannie Bernhard'
expect(page).to have_content '@nannie.bernhard'
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
end
# verified and the gpg user's profile doesn't exist anymore
gpg_user.destroy!
visit project_commits_path(project, :'signed-commits')
click_on 'Verified', match: :first
within '.popover' do
expect(page).to have_content 'This commit was signed with a verified signature.'
expect(page).to have_content 'Nannie Bernhard'
expect(page).to have_content 'nannie.bernhard@example.com'
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
end
end
end
end end
require 'rails_helper' require 'rails_helper'
feature 'Merge Request filtering by Labels', js: true do feature 'Merge Request filtering by Labels', :js do
include FilteredSearchHelpers include FilteredSearchHelpers
include MergeRequestHelpers include MergeRequestHelpers
...@@ -12,9 +12,9 @@ feature 'Merge Request filtering by Labels', js: true do ...@@ -12,9 +12,9 @@ feature 'Merge Request filtering by Labels', js: true do
let!(:feature) { create(:label, project: project, title: 'feature') } let!(:feature) { create(:label, project: project, title: 'feature') }
let!(:enhancement) { create(:label, project: project, title: 'enhancement') } let!(:enhancement) { create(:label, project: project, title: 'enhancement') }
let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "bugfix1") } let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "fix") }
let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") } let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "wip") }
let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "feature1") } let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "improve/awesome") }
before do before do
mr1.labels << bug mr1.labels << bug
...@@ -25,7 +25,7 @@ feature 'Merge Request filtering by Labels', js: true do ...@@ -25,7 +25,7 @@ feature 'Merge Request filtering by Labels', js: true do
mr3.title = "Feature1" mr3.title = "Feature1"
mr3.labels << feature mr3.labels << feature
project.team << [user, :master] project.add_master(user)
sign_in(user) sign_in(user)
visit project_merge_requests_path(project) visit project_merge_requests_path(project)
......
...@@ -12,7 +12,7 @@ describe 'Filter merge requests' do ...@@ -12,7 +12,7 @@ describe 'Filter merge requests' do
let!(:wontfix) { create(:label, project: project, title: "Won't fix") } let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
before do before do
project.team << [user, :master] project.add_master(user)
group.add_developer(user) group.add_developer(user)
sign_in(user) sign_in(user)
create(:merge_request, source_project: project, target_project: project) create(:merge_request, source_project: project, target_project: project)
...@@ -170,7 +170,7 @@ describe 'Filter merge requests' do ...@@ -170,7 +170,7 @@ describe 'Filter merge requests' do
describe 'filter merge requests by text' do describe 'filter merge requests by text' do
before do before do
create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "bug") create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "wip")
bug_label = create(:label, project: project, title: 'bug') bug_label = create(:label, project: project, title: 'bug')
milestone = create(:milestone, title: "8", project: project) milestone = create(:milestone, title: "8", project: project)
...@@ -179,7 +179,7 @@ describe 'Filter merge requests' do ...@@ -179,7 +179,7 @@ describe 'Filter merge requests' do
title: "Bug 2", title: "Bug 2",
source_project: project, source_project: project,
target_project: project, target_project: project,
source_branch: "bug2", source_branch: "fix",
milestone: milestone, milestone: milestone,
author: user, author: user,
assignee: user) assignee: user)
...@@ -259,12 +259,12 @@ describe 'Filter merge requests' do ...@@ -259,12 +259,12 @@ describe 'Filter merge requests' do
end end
end end
describe 'filter merge requests and sort', js: true do describe 'filter merge requests and sort', :js do
before do before do
bug_label = create(:label, project: project, title: 'bug') bug_label = create(:label, project: project, title: 'bug')
mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "Frontend") mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "wip")
mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "bug2") mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "fix")
mr1.labels << bug_label mr1.labels << bug_label
mr2.labels << bug_label mr2.labels << bug_label
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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