Commit 7788118c authored by Luke "Jared" Bennett's avatar Luke "Jared" Bennett

Merge remote-tracking branch 'origin/master' into add-sentry-js-again-with-vue

parents 637ed8a2 cac04fbd
......@@ -45,6 +45,7 @@ eslint-report.html
/public/uploads.*
/public/uploads/
/shared/artifacts/
/spec/javascripts/fixtures/blob/pdf/
/rails_best_practices_output.html
/tags
/tmp/*
......
......@@ -19,8 +19,8 @@ variables:
before_script:
- bundle --version
- . scripts/utils.sh
- ./scripts/prepare_build.sh
- source scripts/utils.sh
- source scripts/prepare_build.sh
stages:
- prepare
......@@ -253,38 +253,46 @@ spinach mysql 9 10: *spinach-knapsack-mysql
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true"
.exec: &exec
.rake-exec: &rake-exec
<<: *ruby-static-analysis
<<: *dedicated-runner
<<: *except-docs
stage: test
script:
- bundle exec $CI_JOB_NAME
- bundle exec rake $CI_JOB_NAME
rubocop:
static-analysis:
<<: *ruby-static-analysis
<<: *dedicated-runner
<<: *except-docs
stage: test
script:
- bundle exec "rubocop --require rubocop-rspec"
rake haml_lint: *exec
rake scss_lint: *exec
rake config_lint: *exec
rake brakeman: *exec
rake flay: *exec
license_finder: *exec
rake downtime_check:
<<: *exec
- scripts/static-analysis
docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
cache: {}
dependencies: []
before_script: []
script:
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
- bundle exec nanoc
# Check the internal links
- bundle exec nanoc check internal_links
downtime_check:
<<: *rake-exec
except:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
- /^docs\/*/
rake ee_compat_check:
<<: *exec
ee_compat_check:
<<: *rake-exec
only:
- branches@gitlab-org/gitlab-ce
except:
......@@ -306,22 +314,22 @@ rake ee_compat_check:
.db-migrate-reset: &db-migrate-reset
stage: test
<<: *dedicated-runner
<<: *except-docs
script:
- bundle exec rake db:migrate:reset
rake pg db:migrate:reset:
<<: *db-migrate-reset
<<: *use-pg
<<: *except-docs
rake mysql db:migrate:reset:
<<: *db-migrate-reset
<<: *use-mysql
<<: *except-docs
.db-rollback: &db-rollback
stage: test
<<: *dedicated-runner
<<: *except-docs
script:
- bundle exec rake db:rollback STEP=120
- bundle exec rake db:migrate
......@@ -329,16 +337,15 @@ rake mysql db:migrate:reset:
rake pg db:rollback:
<<: *db-rollback
<<: *use-pg
<<: *except-docs
rake mysql db:rollback:
<<: *db-rollback
<<: *use-mysql
<<: *except-docs
.db-seed_fu: &db-seed_fu
stage: test
<<: *dedicated-runner
<<: *except-docs
variables:
SIZE: "1"
SETUP_DB: "false"
......@@ -356,12 +363,10 @@ rake mysql db:rollback:
rake pg db:seed_fu:
<<: *db-seed_fu
<<: *use-pg
<<: *except-docs
rake mysql db:seed_fu:
<<: *db-seed_fu
<<: *use-mysql
<<: *except-docs
rake gitlab:assets:compile:
stage: test
......@@ -402,31 +407,6 @@ rake karma:
paths:
- coverage-javascript/
docs:check:apilint:
image: "phusion/baseimage"
stage: test
<<: *dedicated-runner
cache: {}
dependencies: []
before_script: []
script:
- scripts/lint-doc.sh
docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
cache: {}
dependencies: []
before_script: []
script:
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
- bundle exec nanoc
# Check the internal links
- bundle exec nanoc check internal_links
bundler:audit:
stage: test
<<: *ruby-static-analysis
......@@ -485,14 +465,6 @@ coverage:
- coverage/index.html
- coverage/assets/
lint:javascript:
<<: *dedicated-runner
<<: *except-docs
stage: test
before_script: []
script:
- yarn run eslint
lint:javascript:report:
<<: *dedicated-runner
<<: *except-docs
......@@ -526,22 +498,6 @@ trigger_docs:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
# Notify slack in the end
notify:slack:
stage: post-test
<<: *dedicated-runner
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
script:
- ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_COMMIT_SHA"/pipelines>"
when: on_failure
only:
- master@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- tags@gitlab-org/gitlab-ee
pages:
before_script: []
stage: pages
......
......@@ -961,7 +961,7 @@ RSpec/DescribeSymbol:
# Checks that the second argument to top level describe is the tested method
# name.
RSpec/DescribedClass:
Enabled: false
Enabled: true
# Checks for long example.
RSpec/ExampleLength:
......@@ -983,10 +983,12 @@ RSpec/ExpectActual:
# Checks the file and folder naming of the spec file.
RSpec/FilePath:
Enabled: false
CustomTransform:
RuboCop: rubocop
RSpec: rspec
Enabled: true
IgnoreMethods: true
Exclude:
- 'qa/**/*'
- 'spec/javascripts/fixtures/*'
- 'spec/requests/api/v3/*'
# Checks if there are focused specs.
RSpec/Focus:
......
......@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 9.1.2 (2017-05-01)
- Add index on ci_runners.contacted_at. !10876 (blackst0ne)
- Fix pipeline events description for Slack and Mattermost integration. !10908
- Fixed milestone sidebar showing incorrect number of MRs when collapsed. !10933
- Fix ordering of commits in the network graph. !10936
- Ensure the chat notifications service properly saves the "Notify only default branch" setting. !10959
- Lazily sets UUID in ApplicationSetting for new installations.
- Skip validation when creating internal (ghost, service desk) users.
- Use GitLab Pages v0.4.1.
## 9.1.1 (2017-04-26)
- Add a transaction around move_issues_to_ghost_user. !10465
......
......@@ -85,14 +85,14 @@ gem 'kaminari', '~> 0.17.0'
gem 'hamlit', '~> 2.6.1'
# Files attachments
gem 'carrierwave', '~> 0.11.0'
gem 'carrierwave', '~> 1.0'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
# for backups
gem 'fog-aws', '~> 0.9'
gem 'fog-core', '~> 1.40'
gem 'fog-core', '~> 1.44'
gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
......
......@@ -105,12 +105,10 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
carrierwave (0.11.2)
activemodel (>= 3.2.0)
activesupport (>= 3.2.0)
json (>= 1.7)
carrierwave (1.0.0)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
mimemagic (>= 0.3.0)
cause (0.1)
charlock_holmes (0.7.3)
chronic (0.10.2)
......@@ -184,7 +182,7 @@ GEM
erubis (2.7.0)
escape_utils (1.1.1)
eventmachine (1.0.8)
excon (0.52.0)
excon (0.55.0)
execjs (2.6.0)
expression_parser (0.9.0)
extlib (0.9.16)
......@@ -210,12 +208,12 @@ GEM
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
fog-aws (0.11.0)
fog-aws (0.13.0)
fog-core (~> 1.38)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
fog-core (1.42.0)
fog-core (1.44.1)
builder
excon (~> 0.49)
formatador (~> 0.2)
......@@ -237,9 +235,9 @@ GEM
fog-json (>= 1.0)
fog-xml (>= 0.1)
ipaddress (>= 0.8)
fog-xml (0.1.2)
fog-xml (0.1.3)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
nokogiri (>= 1.5.11, < 2.0.0)
font-awesome-rails (4.7.0.1)
railties (>= 3.2, < 5.1)
foreman (0.78.0)
......@@ -330,7 +328,7 @@ GEM
grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
grpc (1.1.2)
grpc (1.2.5)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
haml (4.0.7)
......@@ -871,7 +869,7 @@ DEPENDENCIES
bundler-audit (~> 0.5.0)
capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.11.0)
carrierwave (~> 1.0)
charlock_holmes (~> 0.7.3)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
......@@ -896,7 +894,7 @@ DEPENDENCIES
ffaker (~> 2.4)
flay (~> 2.8.0)
fog-aws (~> 0.9)
fog-core (~> 1.40)
fog-core (~> 1.44)
fog-google (~> 0.5)
fog-local (~> 0.3)
fog-openstack (~> 0.1)
......
app/assets/images/ci_favicons/favicon_status_canceled.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_canceled.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_canceled.ico
app/assets/images/ci_favicons/favicon_status_canceled.ico
app/assets/images/ci_favicons/favicon_status_canceled.ico
app/assets/images/ci_favicons/favicon_status_canceled.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_created.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_created.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_created.ico
app/assets/images/ci_favicons/favicon_status_created.ico
app/assets/images/ci_favicons/favicon_status_created.ico
app/assets/images/ci_favicons/favicon_status_created.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_failed.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_failed.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_failed.ico
app/assets/images/ci_favicons/favicon_status_failed.ico
app/assets/images/ci_favicons/favicon_status_failed.ico
app/assets/images/ci_favicons/favicon_status_failed.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_manual.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_manual.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_manual.ico
app/assets/images/ci_favicons/favicon_status_manual.ico
app/assets/images/ci_favicons/favicon_status_manual.ico
app/assets/images/ci_favicons/favicon_status_manual.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_not_found.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_not_found.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_not_found.ico
app/assets/images/ci_favicons/favicon_status_not_found.ico
app/assets/images/ci_favicons/favicon_status_not_found.ico
app/assets/images/ci_favicons/favicon_status_not_found.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_pending.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_pending.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_pending.ico
app/assets/images/ci_favicons/favicon_status_pending.ico
app/assets/images/ci_favicons/favicon_status_pending.ico
app/assets/images/ci_favicons/favicon_status_pending.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_running.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_running.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_running.ico
app/assets/images/ci_favicons/favicon_status_running.ico
app/assets/images/ci_favicons/favicon_status_running.ico
app/assets/images/ci_favicons/favicon_status_running.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_skipped.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_skipped.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_skipped.ico
app/assets/images/ci_favicons/favicon_status_skipped.ico
app/assets/images/ci_favicons/favicon_status_skipped.ico
app/assets/images/ci_favicons/favicon_status_skipped.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_success.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_success.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_success.ico
app/assets/images/ci_favicons/favicon_status_success.ico
app/assets/images/ci_favicons/favicon_status_success.ico
app/assets/images/ci_favicons/favicon_status_success.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_warning.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_warning.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_warning.ico
app/assets/images/ci_favicons/favicon_status_warning.ico
app/assets/images/ci_favicons/favicon_status_warning.ico
app/assets/images/ci_favicons/favicon_status_warning.ico
  • 2-up
  • Swipe
  • Onion skin
......@@ -62,6 +62,7 @@ function glEmojiTag(inputName, options) {
data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}"
title="${emojiInfo.description}"
>
${contents}
</gl-emoji>
......
/* eslint-disable no-new */
import Vue from 'vue';
import PDFLab from 'vendor/pdflab';
import workerSrc from 'vendor/pdf.worker';
Vue.use(PDFLab, {
workerSrc,
});
import pdfLab from '../../pdf/index.vue';
export default () => {
const el = document.getElementById('js-pdf-viewer');
......@@ -20,6 +15,9 @@ export default () => {
pdf: el.dataset.endpoint,
};
},
components: {
pdfLab,
},
methods: {
onLoad() {
this.loading = false;
......
......@@ -6,7 +6,7 @@ export default class BlobViewer {
this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn');
this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]');
this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]');
this.$blobContentHolder = $('#blob-content-holder');
this.$fileHolder = $('.file-holder');
let initialViewerName = document.querySelector('.blob-viewer:not(.hidden)').getAttribute('data-type');
......@@ -82,7 +82,7 @@ export default class BlobViewer {
viewer.setAttribute('data-loaded', 'true');
this.$blobContentHolder.trigger('highlight:line');
this.$fileHolder.trigger('highlight:line');
this.toggleCopyButtonState();
});
......
......@@ -27,6 +27,9 @@ gl.issueBoards.BoardSidebar = Vue.extend({
computed: {
showSidebar () {
return Object.keys(this.issue).length;
},
assigneeId() {
return this.issue.assignee ? this.issue.assignee.id : 0;
}
},
watch: {
......
......@@ -44,6 +44,7 @@ import GroupsList from './groups_list';
import ProjectsList from './projects_list';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import Landing from './landing';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
......@@ -148,8 +149,19 @@ const ShortcutsBlob = require('./shortcuts_blob');
new ProjectsList();
break;
case 'dashboard:groups:index':
new GroupsList();
break;
case 'explore:groups:index':
new GroupsList();
const landingElement = document.querySelector('.js-explore-groups-landing');
if (!landingElement) break;
const exploreGroupsLanding = new Landing(
landingElement,
landingElement.querySelector('.dismiss-button'),
'explore_groups_landing_dismissed',
);
exploreGroupsLanding.toggle();
break;
case 'projects:milestones:new':
case 'projects:milestones:edit':
......@@ -356,6 +368,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'users:show':
new UserCallout();
break;
case 'snippets:show':
new LineHighlighter();
new BlobViewer();
break;
}
switch (path.first()) {
case 'sessions':
......@@ -434,6 +450,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
shortcut_handler = new ShortcutsNavigation();
if (path[2] === 'show') {
new ZenMode();
new LineHighlighter();
new BlobViewer();
}
break;
case 'labels':
......
import Cookies from 'js-cookie';
class Landing {
constructor(landingElement, dismissButton, cookieName) {
this.landingElement = landingElement;
this.cookieName = cookieName;
this.dismissButton = dismissButton;
this.eventWrapper = {};
}
toggle() {
const isDismissed = this.isDismissed();
this.landingElement.classList.toggle('hidden', isDismissed);
if (!isDismissed) this.addEvents();
}
addEvents() {
this.eventWrapper.dismissLanding = this.dismissLanding.bind(this);
this.dismissButton.addEventListener('click', this.eventWrapper.dismissLanding);
}
removeEvents() {
this.dismissButton.removeEventListener('click', this.eventWrapper.dismissLanding);
}
dismissLanding() {
this.landingElement.classList.add('hidden');
Cookies.set(this.cookieName, 'true', { expires: 365 });
}
isDismissed() {
return Cookies.get(this.cookieName) === 'true';
}
}
export default Landing;
......@@ -57,9 +57,9 @@ require('vendor/jquery.scrollTo');
}
LineHighlighter.prototype.bindEvents = function() {
const $blobContentHolder = $('#blob-content-holder');
$blobContentHolder.on('click', 'a[data-line-number]', this.clickHandler);
$blobContentHolder.on('highlight:line', this.highlightHash);
const $fileHolder = $('.file-holder');
$fileHolder.on('click', 'a[data-line-number]', this.clickHandler);
$fileHolder.on('highlight:line', this.highlightHash);
};
LineHighlighter.prototype.highlightHash = function() {
......
import d3 from 'd3';
export const dateFormat = d3.time.format('%b %d, %Y');
export const timeFormat = d3.time.format('%H:%M%p');
/* global Flash */
import d3 from 'd3';
import {
dateFormat,
timeFormat,
} from './constants';
export default class Deployments {
constructor(width, height) {
this.width = width;
this.height = height;
this.endpoint = document.getElementById('js-metrics').dataset.deploymentEndpoint;
this.createGradientDef();
}
init(chartData) {
this.chartData = chartData;
this.x = d3.time.scale().range([0, this.width]);
this.x.domain(d3.extent(this.chartData, d => d.time));
this.charts = d3.selectAll('.prometheus-graph');
this.getData();
}
getData() {
$.ajax({
url: this.endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error getting deployment information.'))
.done((data) => {
this.data = data.deployments.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
const xPos = Math.floor(this.x(time));
time.setSeconds(this.chartData[0].time.getSeconds());
if (xPos >= 0) {
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
tag: deployment.tag,
ref: deployment.ref.name,
xPos,
});
}
return deploymentDataArray;
}, []);
this.plotData();
});
}
plotData() {
this.charts.each((d, i) => {
const svg = d3.select(this.charts[0][i]);
const chart = svg.select('.graph-container');
const key = svg.node().getAttribute('graph-type');
this.createLine(chart, key);
this.createDeployInfoBox(chart, key);
});
}
createGradientDef() {
const defs = d3.select('body')
.append('svg')
.attr({
height: 0,
width: 0,
})
.append('defs');
defs.append('linearGradient')
.attr({
id: 'shadow-gradient',
})
.append('stop')
.attr({
offset: '0%',
'stop-color': '#000',
'stop-opacity': 0.4,
})
.select(this.selectParentNode)
.append('stop')
.attr({
offset: '100%',
'stop-color': '#000',
'stop-opacity': 0,
});
}
createLine(chart, key) {
chart.append('g')
.attr({
class: 'deploy-info',
})
.selectAll('.deploy-info')
.data(this.data)
.enter()
.append('g')
.attr({
class: d => `deploy-info-${d.id}-${key}`,
transform: d => `translate(${Math.floor(d.xPos) + 1}, 0)`,
})
.append('rect')
.attr({
x: 1,
y: 0,
height: this.height + 1,
width: 3,
fill: 'url(#shadow-gradient)',
})
.select(this.selectParentNode)
.append('line')
.attr({
class: 'deployment-line',
x1: 0,
x2: 0,
y1: 0,
y2: this.height + 1,
});
}
createDeployInfoBox(chart, key) {
chart.selectAll('.deploy-info')
.selectAll('.js-deploy-info-box')
.data(this.data)
.enter()
.select(d => document.querySelector(`.deploy-info-${d.id}-${key}`))
.append('svg')
.attr({
class: 'js-deploy-info-box hidden',
x: 3,
y: 0,
width: 92,
height: 60,
})
.append('rect')
.attr({
class: 'rect-text-metric deploy-info-rect rect-metric',
x: 1,
y: 1,
rx: 2,
width: 90,
height: 58,
})
.select(this.selectParentNode)
.append('g')
.attr({
transform: 'translate(5, 2)',
})
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
})
.text(Deployments.refText)
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text',
y: 18,
})
.text(d => dateFormat(d.time))
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
y: 38,
})
.text(d => timeFormat(d.time));
}
static toggleDeployTextbox(deploy, key, showInfoBox) {
d3.selectAll(`.deploy-info-${deploy.id}-${key} .js-deploy-info-box`)
.classed('hidden', !showInfoBox);
}
mouseOverDeployInfo(mouseXPos, key) {
if (!this.data) return false;
let dataFound = false;
this.data.forEach((d) => {
if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
dataFound = d.xPos + 1;
Deployments.toggleDeployTextbox(d, key, true);
} else {
Deployments.toggleDeployTextbox(d, key, false);
}
});
return dataFound;
}
/* `this` is bound to the D3 node */
selectParentNode() {
return this.parentNode;
}
static refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 6);
}
}
......@@ -3,16 +3,20 @@
import d3 from 'd3';
import statusCodes from '~/lib/utils/http_status';
import { formatRelevantDigits } from '~/lib/utils/number_utils';
import Deployments from './deployments';
import '../lib/utils/common_utils';
import { formatRelevantDigits } from '../lib/utils/number_utils';
import '../flash';
import {
dateFormat,
timeFormat,
} from './constants';
const prometheusContainer = '.prometheus-container';
const prometheusParentGraphContainer = '.prometheus-graphs';
const prometheusGraphsContainer = '.prometheus-graph';
const prometheusStatesContainer = '.prometheus-state';
const metricsEndpoint = 'metrics.json';
const timeFormat = d3.time.format('%H:%M');
const dayFormat = d3.time.format('%b %e, %a');
const bisectDate = d3.bisector(d => d.time).left;
const extraAddedWidthParent = 100;
......@@ -36,6 +40,7 @@ class PrometheusGraph {
this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0;
this.deployments = new Deployments(this.width, this.height);
this.configureGraph();
this.init();
} else {
......@@ -74,6 +79,12 @@ class PrometheusGraph {
$(prometheusParentGraphContainer).show();
this.transformData(metricsResponse);
this.createGraph();
const firstMetricData = this.graphSpecificProperties[
Object.keys(this.graphSpecificProperties)[0]
].data;
this.deployments.init(firstMetricData);
}
});
}
......@@ -96,6 +107,7 @@ class PrometheusGraph {
.attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.bottom + this.margin.top)
.append('g')
.attr('class', 'graph-container')
.attr('transform', `translate(${this.margin.left},${this.margin.top})`);
const axisLabelContainer = d3.select(prometheusGraphContainer)
......@@ -116,6 +128,7 @@ class PrometheusGraph {
.scale(y)
.ticks(this.commonGraphProperties.axis_no_ticks)
.tickSize(-this.width)
.outerTickSize(0)
.orient('left');
this.createAxisLabelContainers(axisLabelContainer, key);
......@@ -248,7 +261,8 @@ class PrometheusGraph {
const d1 = currentGraphProps.data[overlayIndex];
const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay;
const currentData = evalTime ? d1 : d0;
const currentTimeCoordinate = currentGraphProps.xScale(currentData.time);
const currentTimeCoordinate = Math.floor(currentGraphProps.xScale(currentData.time));
const currentDeployXPos = this.deployments.mouseOverDeployInfo(currentXCoordinate, key);
const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value));
const maxMetricValue = currentGraphProps.yScale(maxValueFromData);
......@@ -256,13 +270,12 @@ class PrometheusGraph {
// Clear up all the pieces of the flag
d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .text-metric`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric:not(.deploy-info-rect)`).remove();
const currentChart = d3.select(currentPrometheusGraphContainer).select('g');
currentChart.append('line')
.attr('class', 'selected-metric-line')
.attr({
class: `${currentDeployXPos ? 'hidden' : ''} selected-metric-line`,
x1: currentTimeCoordinate,
y1: currentGraphProps.yScale(0),
x2: currentTimeCoordinate,
......@@ -272,33 +285,45 @@ class PrometheusGraph {
currentChart.append('circle')
.attr('class', 'circle-metric')
.attr('fill', currentGraphProps.line_color)
.attr('cx', currentTimeCoordinate)
.attr('cx', currentDeployXPos || currentTimeCoordinate)
.attr('cy', currentGraphProps.yScale(currentData.value))
.attr('r', this.commonGraphProperties.circle_radius_metric);
if (currentDeployXPos) return;
// The little box with text
const rectTextMetric = currentChart.append('g')
.attr('class', 'rect-text-metric')
.attr('translate', `(${currentTimeCoordinate}, ${currentGraphProps.yScale(currentData.value)})`);
const rectTextMetric = currentChart.append('svg')
.attr({
class: 'rect-text-metric',
x: currentTimeCoordinate,
y: 0,
});
rectTextMetric.append('rect')
.attr('class', 'rect-metric')
.attr('x', currentTimeCoordinate + 10)
.attr('y', maxMetricValue)
.attr('width', this.commonGraphProperties.rect_text_width)
.attr('height', this.commonGraphProperties.rect_text_height);
.attr({
class: 'rect-metric',
x: 4,
y: 1,
rx: 2,
width: this.commonGraphProperties.rect_text_width,
height: this.commonGraphProperties.rect_text_height,
});
rectTextMetric.append('text')
.attr('class', 'text-metric')
.attr('x', currentTimeCoordinate + 35)
.attr('y', maxMetricValue + 35)
.attr({
class: 'text-metric text-metric-bold',
x: 8,
y: 35,
})
.text(timeFormat(currentData.time));
rectTextMetric.append('text')
.attr('class', 'text-metric-date')
.attr('x', currentTimeCoordinate + 15)
.attr('y', maxMetricValue + 15)
.text(dayFormat(currentData.time));
.attr({
class: 'text-metric-date',
x: 8,
y: 15,
})
.text(dateFormat(currentData.time));
let currentMetricValue = formatRelevantDigits(currentData.value);
if (key === 'cpu_values') {
......
<template>
<div class="pdf-viewer" v-if="hasPDF">
<page v-for="(page, index) in pages"
:key="index"
:v-if="!loading"
:page="page"
:number="index + 1" />
</div>
</template>
<script>
import pdfjsLib from 'pdfjs-dist';
import workerSrc from 'vendor/pdf.worker';
import page from './page/index.vue';
export default {
props: {
pdf: {
type: [String, Uint8Array],
required: true,
},
},
data() {
return {
loading: false,
pages: [],
};
},
components: { page },
watch: { pdf: 'load' },
computed: {
document() {
return typeof this.pdf === 'string' ? this.pdf : { data: this.pdf };
},
hasPDF() {
return this.pdf && this.pdf.length > 0;
},
},
methods: {
load() {
this.pages = [];
return pdfjsLib.getDocument(this.document)
.then(this.renderPages)
.then(() => this.$emit('pdflabload'))
.catch(error => this.$emit('pdflaberror', error))
.then(() => { this.loading = false; });
},
renderPages(pdf) {
const pagePromises = [];
this.loading = true;
for (let num = 1; num <= pdf.numPages; num += 1) {
pagePromises.push(
pdf.getPage(num).then(p => this.pages.push(p)),
);
}
return Promise.all(pagePromises);
},
},
mounted() {
pdfjsLib.PDFJS.workerSrc = workerSrc;
if (this.hasPDF) this.load();
},
};
</script>
<style>
.pdf-viewer {
background: url('./assets/img/bg.gif');
display: flex;
flex-flow: column nowrap;
}
</style>
<template>
<canvas
class="pdf-page"
ref="canvas"
:data-page="number" />
</template>
<script>
export default {
props: {
page: {
type: Object,
required: true,
},
number: {
type: Number,
required: true,
},
},
data() {
return {
scale: 4,
rendering: false,
};
},
computed: {
viewport() {
return this.page.getViewport(this.scale);
},
context() {
return this.$refs.canvas.getContext('2d');
},
renderContext() {
return {
canvasContext: this.context,
viewport: this.viewport,
};
},
},
mounted() {
this.$refs.canvas.height = this.viewport.height;
this.$refs.canvas.width = this.viewport.width;
this.rendering = true;
this.page.render(this.renderContext)
.then(() => { this.rendering = false; })
.catch(error => this.$emit('pdflaberror', error));
},
};
</script>
<style>
.pdf-page {
margin: 8px auto 0 auto;
border-top: 1px #ddd solid;
border-bottom: 1px #ddd solid;
width: 100%;
}
.pdf-page:first-child {
margin-top: 0px;
border-top: 0px;
}
.pdf-page:last-child {
margin-bottom: 0px;
border-bottom: 0px;
}
</style>
......@@ -30,7 +30,7 @@
$els.each((function(_this) {
return function(i, dropdown) {
var options = {};
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, defaultNullUser, firstUser, issueURL, selectedId, selectedIdDefault, showAnyUser, showNullUser, showMenuAbove;
$dropdown = $(dropdown);
options.projectId = $dropdown.data('project-id');
options.groupId = $dropdown.data('group-id');
......@@ -38,11 +38,11 @@
options.todoFilter = $dropdown.data('todo-filter');
options.todoStateFilter = $dropdown.data('todo-state-filter');
showNullUser = $dropdown.data('null-user');
defaultNullUser = $dropdown.data('null-user-default');
showMenuAbove = $dropdown.data('showMenuAbove');
showAnyUser = $dropdown.data('any-user');
firstUser = $dropdown.data('first-user');
options.authorId = $dropdown.data('author-id');
selectedId = $dropdown.data('selected');
defaultLabel = $dropdown.data('default-label');
issueURL = $dropdown.data('issueUpdate');
$selectbox = $dropdown.closest('.selectbox');
......@@ -51,6 +51,8 @@
$value = $block.find('.value');
$collapsedSidebar = $block.find('.sidebar-collapsed-user');
$loading = $block.find('.block-loading').fadeOut();
selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null;
selectedId = $dropdown.data('selected') || selectedIdDefault;
var updateIssueBoardsIssue = function () {
$loading.removeClass('hidden').fadeIn();
......@@ -186,12 +188,14 @@
fieldName: $dropdown.data('field-name'),
toggleLabel: function(selected, el) {
if (selected && 'id' in selected && $(el).hasClass('is-active')) {
$dropdown.find('.dropdown-toggle-text').removeClass('is-default');
if (selected.text) {
return selected.text;
} else {
return selected.name;
}
} else {
$dropdown.find('.dropdown-toggle-text').addClass('is-default');
return defaultLabel;
}
},
......@@ -204,13 +208,14 @@
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(user, $el, e) {
var isIssueIndex, isMRIndex, page, selected;
var isIssueIndex, isMRIndex, page, selected, isSelecting;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
isSelecting = (user.id !== selectedId);
selectedId = isSelecting ? user.id : selectedIdDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
selectedId = user.id;
if (selectedId === gon.current_user_id) {
$('.assign-to-me-link').hide();
} else {
......@@ -221,12 +226,11 @@
if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedId = user.id;
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (user.id) {
if (user.id && isSelecting) {
gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({
id: user.id,
username: user.username,
......@@ -248,6 +252,9 @@
},
opened: function(e) {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar')) {
selectedId = parseInt($dropdown[0].dataset.selected, 10) || selectedIdDefault;
}
$el.find('.is-active').removeClass('is-active');
$el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active');
},
......
......@@ -227,8 +227,8 @@
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
left: 7px;
bottom: 9px;
left: 11px;
bottom: 7px;
opacity: 0;
@include transition(opacity, transform);
}
......
......@@ -254,6 +254,63 @@
padding: 10px 0;
}
.landing {
margin-bottom: $gl-padding;
overflow: hidden;
display: flex;
position: relative;
border: 1px solid $blue-300;
border-radius: $border-radius-default;
background-color: $blue-25;
justify-content: center;
.dismiss-button {
position: absolute;
right: 6px;
top: 6px;
cursor: pointer;
color: $blue-300;
z-index: 1;
border: none;
background-color: transparent;
&:hover,
&:focus {
border: none;
color: $blue-400;
}
}
.svg-container {
align-self: center;
}
.inner-content {
text-align: left;
white-space: nowrap;
h4 {
color: $gl-text-color;
font-size: 17px;
}
p {
color: $gl-text-color;
margin-bottom: $gl-padding;
}
}
@media (max-width: $screen-sm-min) {
flex-direction: column;
.inner-content {
white-space: normal;
padding: 0 28px;
text-align: center;
}
}
}
.empty-state {
margin: 100px 0 0;
......
......@@ -424,6 +424,11 @@ table {
}
}
.bordered-box {
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
.str-truncated {
&-60 {
@include str-truncated(60%);
......
......@@ -390,7 +390,8 @@
&::before {
position: absolute;
left: 6px;
top: 6px;
top: 50%;
transform: translateY(-50%);
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
......
......@@ -61,11 +61,13 @@
.file-content {
background: $white-light;
&.image_file {
&.image_file,
&.video {
background: $file-image-bg;
text-align: center;
img {
img,
video {
padding: 20px;
max-width: 80%;
}
......
......@@ -93,11 +93,6 @@
top: $gl-padding-top;
}
.bordered-box {
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
.content-list {
li {
padding: 18px $gl-padding $gl-padding;
......@@ -139,42 +134,9 @@
}
}
.landing {
margin-bottom: $gl-padding;
overflow: hidden;
.dismiss-icon {
position: absolute;
right: $cycle-analytics-box-padding;
cursor: pointer;
color: $cycle-analytics-dismiss-icon-color;
}
.svg-container {
text-align: center;
svg {
width: 136px;
height: 136px;
}
}
.inner-content {
@media (max-width: $screen-xs-max) {
padding: 0 28px;
text-align: center;
}
h4 {
color: $gl-text-color;
font-size: 17px;
}
p {
color: $cycle-analytics-box-text-color;
margin-bottom: $gl-padding;
}
}
.landing svg {
width: 136px;
height: 136px;
}
.fa-spinner {
......
......@@ -157,7 +157,8 @@
.prometheus-graph {
text {
fill: $stat-graph-axis-fill;
fill: $gl-text-color;
stroke-width: 0;
}
.label-axis-text,
......@@ -210,27 +211,33 @@
.rect-text-metric {
fill: $white-light;
stroke-width: 1;
stroke: $black;
stroke: $gray-darkest;
}
.rect-axis-text {
fill: $white-light;
}
.text-metric,
.text-median-metric,
.text-metric-usage,
.text-metric-date {
fill: $black;
.text-metric {
font-weight: 600;
}
.text-metric-date {
font-weight: 200;
.selected-metric-line {
stroke: $gl-gray-dark;
stroke-width: 1;
}
.selected-metric-line {
.deployment-line {
stroke: $black;
stroke-width: 1;
stroke-width: 2;
}
.deploy-info-text {
dominant-baseline: text-before-edge;
}
.text-metric-bold {
font-weight: 600;
}
.prometheus-state {
......
......@@ -88,3 +88,26 @@
color: $gl-text-color-secondary;
margin-top: 10px;
}
.explore-groups.landing {
margin-top: 10px;
.inner-content {
padding: 0;
p {
margin: 7px 0 0;
max-width: 480px;
padding: 0 $gl-padding;
@media (max-width: $screen-sm-min) {
margin: 0 auto;
}
}
}
svg {
width: 62px;
height: 50px;
}
}
class Admin::HooksController < Admin::ApplicationController
before_action :hook, only: :edit
def index
@hooks = SystemHook.all
@hook = SystemHook.new
......@@ -15,15 +17,25 @@ class Admin::HooksController < Admin::ApplicationController
end
end
def edit
end
def update
if hook.update_attributes(hook_params)
flash[:notice] = 'System hook was successfully updated.'
redirect_to admin_hooks_path
else
render 'edit'
end
end
def destroy
@hook = SystemHook.find(params[:id])
@hook.destroy
hook.destroy
redirect_to admin_hooks_path
end
def test
@hook = SystemHook.find(params[:hook_id])
data = {
event_name: "project_create",
name: "Ruby",
......@@ -32,11 +44,17 @@ class Admin::HooksController < Admin::ApplicationController
owner_name: "Someone",
owner_email: "example@gitlabhq.com"
}
@hook.execute(data, 'system_hooks')
hook.execute(data, 'system_hooks')
redirect_back_or_default
end
private
def hook
@hook ||= SystemHook.find(params[:id])
end
def hook_params
params.require(:hook).permit(
:enable_ssl_verification,
......
module NotesActions
include RendersNotes
extend ActiveSupport::Concern
included do
before_action :authorize_admin_note!, only: [:update, :destroy]
end
def index
current_fetched_at = Time.now.to_i
notes_json = { notes: [], last_fetched_at: current_fetched_at }
@notes = notes_finder.execute.inc_relations_for_view
@notes = prepare_notes_for_rendering(@notes)
@notes.each do |note|
next if note.cross_reference_not_visible_for?(current_user)
notes_json[:notes] << note_json(note)
end
render json: notes_json
end
def create
create_params = note_params.merge(
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
)
@note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def destroy
if note.editable?
Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
format.js { head :ok }
end
end
private
def note_json(note)
attrs = {
commands_changes: note.commands_changes
}
if note.persisted?
attrs.merge!(
valid: true,
id: note.id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
note: note.note
)
discussion = note.to_discussion(noteable)
unless discussion.individual_note?
attrs.merge!(
discussion_resolvable: discussion.resolvable?,
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
end
else
attrs.merge!(
valid: false,
errors: note.errors
)
end
attrs
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
end
def note_params
params.require(:note).permit(
:project_id,
:noteable_type,
:noteable_id,
:commit_id,
:noteable,
:type,
:note,
:attachment,
# LegacyDiffNote
:line_code,
# DiffNote
:position
)
end
def noteable
@noteable ||= notes_finder.target
end
def last_fetched_at
request.headers['X-Last-Fetched-At']
end
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, finder_params)
end
end
......@@ -14,4 +14,8 @@ module RendersBlob
html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_asynchronously: false)
}
end
def override_max_blob_size(blob)
blob.override_max_size! if params[:override_max_size] == 'true'
end
end
......@@ -10,6 +10,8 @@ module RendersNotes
private
def preload_max_access_for_authors(notes, project)
return nil unless project
user_ids = notes.map(&:author_id)
project.team.max_member_access_for_user_ids(user_ids)
end
......
......@@ -5,10 +5,12 @@ module SnippetsActions
end
def raw
disposition = params[:inline] == 'false' ? 'attachment' : 'inline'
send_data(
convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8',
disposition: 'inline',
disposition: disposition,
filename: @snippet.sanitized_file_name
)
end
......
......@@ -22,7 +22,8 @@ module ToggleAwardEmoji
def to_todoable(awardable)
case awardable
when Note
awardable.noteable
# we don't create todos for personal snippet comments for now
awardable.for_personal_snippet? ? nil : awardable.noteable
when MergeRequest, Issue
awardable
when Snippet
......
......@@ -35,7 +35,7 @@ class Projects::BlobController < Projects::ApplicationController
end
def show
@blob.override_max_size! if params[:override_max_size] == 'true'
override_max_blob_size(@blob)
respond_to do |format|
format.html do
......
class Projects::DeploymentsController < Projects::ApplicationController
before_action :authorize_read_environment!
before_action :authorize_read_deployment!
def index
deployments = environment.deployments.reorder(created_at: :desc)
deployments = deployments.where('created_at > ?', params[:after].to_time) if params[:after]&.to_time
render json: { deployments: DeploymentSerializer.new(user: @current_user, project: project)
.represent_concise(deployments) }
end
private
def environment
@environment ||= project.environments.find(params[:environment_id])
end
end
class Projects::HooksController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
before_action :hook, only: :edit
respond_to :html
......@@ -17,6 +18,18 @@ class Projects::HooksController < Projects::ApplicationController
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end
def edit
end
def update
if hook.update_attributes(hook_params)
flash[:notice] = 'Hook was successfully updated.'
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
else
render 'edit'
end
end
def test
if !@project.empty_repo?
status, message = TestHookService.new.execute(hook, current_user)
......
class Projects::NotesController < Projects::ApplicationController
include RendersNotes
include NotesActions
include ToggleAwardEmoji
# Authorize
before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
def index
current_fetched_at = Time.now.to_i
notes_json = { notes: [], last_fetched_at: current_fetched_at }
@notes = notes_finder.execute.inc_relations_for_view
@notes = prepare_notes_for_rendering(@notes)
@notes.each do |note|
next if note.cross_reference_not_visible_for?(current_user)
notes_json[:notes] << note_json(note)
end
render json: notes_json
end
#
# This is a fix to make spinach feature tests passing:
# Controller actions are returned from AbstractController::Base and methods of parent classes are
# excluded in order to return only specific controller related methods.
# That is ok for the app (no :create method in ancestors)
# but fails for tests because there is a :create method on FactoryGirl (one of the ancestors)
#
# see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78
#
def create
create_params = note_params.merge(
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
)
@note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def destroy
if note.editable?
Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
format.js { head :ok }
end
super
end
def delete_attachment
......@@ -110,7 +64,7 @@ class Projects::NotesController < Projects::ApplicationController
def note_html(note)
render_to_string(
"projects/notes/_note",
"shared/notes/_note",
layout: false,
formats: [:html],
locals: { note: note }
......@@ -152,76 +106,11 @@ class Projects::NotesController < Projects::ApplicationController
)
end
def note_json(note)
attrs = {
commands_changes: note.commands_changes
}
if note.persisted?
attrs.merge!(
valid: true,
id: note.id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
note: note.note
)
discussion = note.to_discussion(noteable)
unless discussion.individual_note?
attrs.merge!(
discussion_resolvable: discussion.resolvable?,
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
end
else
attrs.merge!(
valid: false,
errors: note.errors
)
end
attrs
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
def finder_params
params.merge(last_fetched_at: last_fetched_at)
end
def authorize_resolve_note!
return access_denied! unless can?(current_user, :resolve_note, note)
end
def note_params
params.require(:note).permit(
:project_id,
:noteable_type,
:noteable_id,
:commit_id,
:noteable,
:type,
:note,
:attachment,
# LegacyDiffNote
:line_code,
# DiffNote
:position
)
end
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
end
def noteable
@noteable ||= notes_finder.target
end
def last_fetched_at
request.headers['X-Last-Fetched-At']
end
end
......@@ -3,6 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController
include ToggleAwardEmoji
include SpammableActions
include SnippetsActions
include RendersBlob
before_action :module_enabled
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
......@@ -55,11 +56,23 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def show
@note = @project.notes.new(noteable: @snippet)
@noteable = @snippet
@discussions = @snippet.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
blob = @snippet.blob
override_max_blob_size(blob)
respond_to do |format|
format.html do
@note = @project.notes.new(noteable: @snippet)
@noteable = @snippet
@discussions = @snippet.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
render 'show'
end
format.json do
render_blob_json(blob)
end
end
end
def destroy
......
class Snippets::NotesController < ApplicationController
include NotesActions
include ToggleAwardEmoji
skip_before_action :authenticate_user!, only: [:index]
before_action :snippet
before_action :authorize_read_snippet!, only: [:show, :index, :create]
private
def note
@note ||= snippet.notes.find(params[:id])
end
alias_method :awardable, :note
def note_html(note)
render_to_string(
"shared/notes/_note",
layout: false,
formats: [:html],
locals: { note: note }
)
end
def project
nil
end
def snippet
PersonalSnippet.find_by(id: params[:snippet_id])
end
def note_params
super.merge(noteable_id: params[:snippet_id])
end
def finder_params
params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet')
end
def authorize_read_snippet!
return render_404 unless can?(current_user, :read_personal_snippet, snippet)
end
end
class SnippetsController < ApplicationController
include RendersNotes
include ToggleAwardEmoji
include SpammableActions
include SnippetsActions
include MarkdownPreview
include RendersBlob
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
before_action :authorize_read_snippet!, only: [:show, :raw, :download]
before_action :authorize_read_snippet!, only: [:show, :raw]
# Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update]
......@@ -15,7 +17,7 @@ class SnippetsController < ApplicationController
# Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy]
skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download]
skip_before_action :authenticate_user!, only: [:index, :show, :raw]
layout 'snippets'
respond_to :html
......@@ -60,6 +62,23 @@ class SnippetsController < ApplicationController
end
def show
blob = @snippet.blob
override_max_blob_size(blob)
@noteable = @snippet
@discussions = @snippet.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
respond_to do |format|
format.html do
render 'show'
end
format.json do
render_blob_json(blob)
end
end
end
def destroy
......@@ -70,14 +89,6 @@ class SnippetsController < ApplicationController
redirect_to snippets_path
end
def download
send_data(
convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8',
filename: @snippet.sanitized_file_name
)
end
def preview_markdown
render_markdown_preview(params[:text], skip_project_check: true)
end
......
......@@ -68,6 +68,8 @@ class NotesFinder
MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
when "snippet", "project_snippet"
SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
when "personal_snippet"
PersonalSnippet.all
else
raise 'invalid target_type'
end
......
module AwardEmojiHelper
def toggle_award_url(awardable)
return url_for([:toggle_award_emoji, awardable]) unless @project
return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note)
if awardable.is_a?(Note)
# We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x)
toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id)
if awardable.for_personal_snippet?
toggle_award_emoji_snippet_note_path(awardable.noteable, awardable)
else
toggle_award_emoji_namespace_project_note_path(@project.namespace, @project, awardable.id)
end
else
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
end
......
......@@ -119,7 +119,15 @@ module BlobHelper
end
def blob_raw_url
namespace_project_raw_path(@project.namespace, @project, @id)
if @snippet
if @snippet.project_id
raw_namespace_project_snippet_path(@project.namespace, @project, @snippet)
else
raw_snippet_path(@snippet)
end
elsif @blob
namespace_project_raw_path(@project.namespace, @project, @id)
end
end
# SVGs can contain malicious JavaScript; only include whitelisted
......@@ -209,11 +217,21 @@ module BlobHelper
end
def copy_blob_source_button(blob)
return unless blob.rendered_as_text?(ignore_errors: false)
clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm js-copy-blob-source-btn", title: "Copy source to clipboard")
end
def open_raw_file_button(path)
link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' }
def open_raw_blob_button(blob)
if blob.raw_binary?
icon = icon('download')
title = 'Download'
else
icon = icon('file-code-o')
title = 'Open raw'
end
link_to icon, blob_raw_url, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
end
def blob_render_error_reason(viewer)
......
......@@ -74,7 +74,7 @@ module MarkupHelper
context[:project] ||= @project
html = markdown_unsafe(text, context)
banzai_postprocess(html, context)
prepare_for_rendering(html, context)
end
def markdown_field(object, field)
......@@ -82,13 +82,13 @@ module MarkupHelper
return '' unless object.present?
html = Banzai.render_field(object, field)
banzai_postprocess(html, object.banzai_render_context(field))
prepare_for_rendering(html, object.banzai_render_context(field))
end
def markup(file_name, text, context = {})
context[:project] ||= @project
html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
banzai_postprocess(html, context)
prepare_for_rendering(html, context)
end
def render_wiki_content(wiki_page)
......@@ -107,14 +107,14 @@ module MarkupHelper
wiki_page.formatted_content.html_safe
end
banzai_postprocess(html, context)
prepare_for_rendering(html, context)
end
def markup_unsafe(file_name, text, context = {})
return '' unless text.present?
if gitlab_markdown?(file_name)
Hamlit::RailsHelpers.preserve(markdown_unsafe(text, context))
markdown_unsafe(text, context)
elsif asciidoc?(file_name)
asciidoc_unsafe(text)
elsif plain?(file_name)
......@@ -225,8 +225,7 @@ module MarkupHelper
Gitlab::OtherMarkup.render(file_name, text)
end
# Calls Banzai.post_process with some common context options
def banzai_postprocess(html, context = {})
def prepare_for_rendering(html, context = {})
return '' unless html.present?
context.merge!(
......@@ -239,7 +238,9 @@ module MarkupHelper
requested_path: @path
)
Banzai.post_process(html, context)
html = Banzai.post_process(html, context)
Hamlit::RailsHelpers.preserve(html)
end
extend self
......
module MergeRequestsHelper
def new_mr_path_from_push_event(event)
target_project = event.project.forked_from_project || event.project
target_project = event.project.default_merge_request_target
new_namespace_project_merge_request_path(
event.project.namespace,
event.project,
......@@ -127,6 +127,10 @@ module MergeRequestsHelper
end
end
def target_projects(project)
[project, project.default_merge_request_target].uniq
end
def merge_request_button_visibility(merge_request, closed)
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end
......
......@@ -8,6 +8,14 @@ module SnippetsHelper
end
end
def download_snippet_path(snippet)
if snippet.project_id
raw_namespace_project_snippet_path(@project.namespace, @project, snippet, inline: false)
else
raw_snippet_path(snippet, inline: false)
end
end
# Return the path of a snippets index for a user or for a project
#
# @returns String, path to snippet index
......
......@@ -27,6 +27,8 @@ class Blob < SimpleDelegator
BlobViewer::Image,
BlobViewer::Sketch,
BlobViewer::Video,
BlobViewer::PDF,
BlobViewer::BinarySTL,
......
module BlobViewer
class Video < Base
include Rich
include ClientSide
self.partial_name = 'video'
self.extensions = UploaderHelper::VIDEO_EXT
self.binary = true
self.switcher_icon = 'film'
self.switcher_title = 'video'
end
end
......@@ -78,6 +78,9 @@ module CacheMarkdownField
def cached_html_up_to_date?(markdown_field)
html_field = cached_markdown_fields.html_field(markdown_field)
cached = !cached_html_for(markdown_field).nil? && !__send__(markdown_field).nil?
return false unless cached
markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false
......
......@@ -100,6 +100,7 @@ class MergeRequest < ActiveRecord::Base
validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork?
validate :validate_target_project, on: :create
scope :by_source_or_target_branch, ->(branch_name) do
where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
......@@ -330,6 +331,12 @@ class MergeRequest < ActiveRecord::Base
end
end
def validate_target_project
return true if target_project.merge_requests_enabled?
errors.add :base, 'Target project has disabled merge requests'
end
def validate_fork
return true unless target_project && source_project
return true if target_project == source_project
......
......@@ -33,7 +33,7 @@ class Namespace < ActiveRecord::Base
validates :path,
presence: true,
length: { maximum: 255 },
namespace: true
dynamic_path: true
validate :nesting_level_allowed
......@@ -220,6 +220,10 @@ class Namespace < ActiveRecord::Base
Project.inside_path(full_path)
end
def has_parent?
parent.present?
end
private
def repository_storage_paths
......
......@@ -196,13 +196,14 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
project_path: true,
dynamic_path: true,
length: { maximum: 255 },
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message }
message: Gitlab::Regex.project_path_regex_message },
uniqueness: { scope: :namespace_id }
validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id }
validates :path, uniqueness: { scope: :namespace_id }
validates :import_url, addressable_url: true, if: :external_import?
validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
......@@ -1270,6 +1271,9 @@ class Project < ActiveRecord::Base
else
update_attribute(name, value)
end
rescue ActiveRecord::RecordNotSaved => e
handle_update_attribute_error(e, value)
end
def pushes_since_gc
......@@ -1314,6 +1318,14 @@ class Project < ActiveRecord::Base
namespace_id_changed?
end
def default_merge_request_target
if forked_from_project&.merge_requests_enabled?
forked_from_project
else
self
end
end
alias_method :name_with_namespace, :full_name
alias_method :human_name, :full_name
alias_method :path_with_namespace, :full_path
......@@ -1383,4 +1395,16 @@ class Project < ActiveRecord::Base
ContainerRepository.build_root_repository(self).has_tags?
end
def handle_update_attribute_error(ex, value)
if ex.message.start_with?('Failed to replace')
if value.respond_to?(:each)
invalid = value.detect(&:invalid?)
raise ex, ([ex.message] + invalid.errors.full_messages).join(' ') if invalid
end
end
raise ex
end
end
......@@ -505,14 +505,8 @@ class Repository
delegate :tag_names, to: :raw_repository
cache_method :tag_names, fallback: []
def branch_count
branches.size
end
delegate :branch_count, :tag_count, to: :raw_repository
cache_method :branch_count, fallback: 0
def tag_count
raw_repository.rugged.tags.count
end
cache_method :tag_count, fallback: 0
def avatar
......@@ -961,15 +955,13 @@ class Repository
end
def is_ancestor?(ancestor_id, descendant_id)
# NOTE: This feature is intentionally disabled until
# https://gitlab.com/gitlab-org/gitlab-ce/issues/30586 is resolved
# Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
# if is_enabled
# raw_repository.is_ancestor?(ancestor_id, descendant_id)
# else
merge_base_commit(ancestor_id, descendant_id) == ancestor_id
# end
# end
Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
if is_enabled
raw_repository.is_ancestor?(ancestor_id, descendant_id)
else
merge_base_commit(ancestor_id, descendant_id) == ancestor_id
end
end
end
def empty_repo?
......
class Snippet < ActiveRecord::Base
include Gitlab::VisibilityLevel
include Linguist::BlobHelper
include CacheMarkdownField
include Noteable
include Participable
......@@ -87,47 +86,26 @@ class Snippet < ActiveRecord::Base
]
end
def data
content
def blob
@blob ||= Blob.decorate(SnippetBlob.new(self), nil)
end
def hook_attrs
attributes
end
def size
0
end
def file_name
super.to_s
end
# alias for compatibility with blobs and highlighting
def path
file_name
end
def name
file_name
end
def sanitized_file_name
file_name.gsub(/[^a-zA-Z0-9_\-\.]+/, '')
end
def mode
nil
end
def visibility_level_field
:visibility_level
end
def no_highlighting?
content.lines.count > 1000
end
def notes_with_associations
notes.includes(:author)
end
......
class SnippetBlob
include Linguist::BlobHelper
attr_reader :snippet
def initialize(snippet)
@snippet = snippet
end
delegate :id, to: :snippet
def name
snippet.file_name
end
alias_method :path, :name
def size
data.bytesize
end
def data
snippet.content
end
def rendered_markup
return unless Gitlab::MarkupHelper.gitlab_markdown?(name)
Banzai.render_field(snippet, :content)
end
def mode
nil
end
def binary?
false
end
def load_all_data!(repository)
# No-op
end
def lfs_pointer?
false
end
def lfs_oid
nil
end
def lfs_size
nil
end
def truncated?
false
end
end
......@@ -118,7 +118,7 @@ class User < ActiveRecord::Base
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username,
namespace: true,
dynamic_path: true,
presence: true,
uniqueness: { case_sensitive: false }
......
......@@ -18,8 +18,10 @@ class DeploymentEntity < Grape::Entity
end
end
expose :created_at
expose :tag
expose :last?
expose :user, using: UserEntity
expose :commit, using: CommitEntity
expose :deployable, using: BuildEntity
......
class DeploymentSerializer < BaseSerializer
entity DeploymentEntity
def represent_concise(resource, opts = {})
opts[:only] = [:iid, :id, :sha, :created_at, :tag, :last?, :id, ref: [:name]]
represent(resource, opts)
end
end
......@@ -7,6 +7,9 @@ class StatusEntity < Grape::Entity
expose :details_path
expose :favicon do |status|
ActionController::Base.helpers.image_path(File.join('ci_favicons', "#{status.favicon}.ico"))
dir = 'ci_favicons'
dir = File.join(dir, 'dev') if Rails.env.development?
ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
end
end
......@@ -28,7 +28,7 @@ module MergeRequests
def find_target_project
return target_project if target_project.present? && can?(current_user, :read_project, target_project)
project.forked_from_project || project
project.default_merge_request_target
end
def find_target_branch
......
......@@ -281,7 +281,7 @@ class TodoService
def attributes_for_target(target)
attributes = {
project_id: target.project.id,
project_id: target&.project&.id,
target_id: target.id,
target_type: target.class.name,
commit_id: nil
......
# DynamicPathValidator
#
# Custom validator for GitLab path values.
# These paths are assigned to `Namespace` (& `Group` as a subclass) & `Project`
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class DynamicPathValidator < ActiveModel::EachValidator
# All routes that appear on the top level must be listed here.
# This will make sure that groups cannot be created with these names
# as these routes would be masked by the paths already in place.
#
# Example:
# /api/api-project
#
# the path `api` shouldn't be allowed because it would be masked by `api/*`
#
TOP_LEVEL_ROUTES = %w[
-
.well-known
abuse_reports
admin
all
api
assets
autocomplete
ci
dashboard
explore
files
groups
health_check
help
hooks
import
invites
issues
jwt
koding
member
merge_requests
new
notes
notification_settings
oauth
profile
projects
public
repository
robots.txt
s
search
sent_notifications
services
snippets
teams
u
unicorn_test
unsubscribes
uploads
users
].freeze
# This list should contain all words following `/*namespace_id/:project_id` in
# routes that contain a second wildcard.
#
# Example:
# /*namespace_id/:project_id/badges/*ref/build
#
# If `badges` was allowed as a project/group name, we would not be able to access the
# `badges` route for those projects:
#
# Consider a namespace with path `foo/bar` and a project called `badges`.
# The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg`
#
# When accessing this path the route would be matched to the `badges` path
# with the following params:
# - namespace_id: `foo`
# - project_id: `bar`
# - ref: `badges/master`
#
# Failing to find the project, this would result in a 404.
#
# By rejecting `badges` the router can _count_ on the fact that `badges` will
# be preceded by the `namespace/project`.
WILDCARD_ROUTES = %w[
badges
blame
blob
builds
commits
create
create_dir
edit
environments/folders
files
find_file
gitlab-lfs/objects
info/lfs/objects
new
preview
raw
refs
tree
update
wikis
].freeze
# These are all the paths that follow `/groups/*id/ or `/groups/*group_id`
# We need to reject these because we have a `/groups/*id` page that is the same
# as the `/*id`.
#
# If we would allow a subgroup to be created with the name `activity` then
# this group would not be accessible through `/groups/parent/activity` since
# this would map to the activity-page of it's parent.
GROUP_ROUTES = %w[
activity
avatar
edit
group_members
issues
labels
merge_requests
milestones
projects
subgroups
].freeze
CHILD_ROUTES = (WILDCARD_ROUTES | GROUP_ROUTES).freeze
def self.without_reserved_wildcard_paths_regex
@without_reserved_wildcard_paths_regex ||= regex_excluding_child_paths(WILDCARD_ROUTES)
end
def self.without_reserved_child_paths_regex
@without_reserved_child_paths_regex ||= regex_excluding_child_paths(CHILD_ROUTES)
end
# This is used to validate a full path.
# It doesn't match paths
# - Starting with one of the top level words
# - Containing one of the child level words in the middle of a path
def self.regex_excluding_child_paths(child_routes)
reserved_top_level_words = Regexp.union(TOP_LEVEL_ROUTES)
not_starting_in_reserved_word = %r{\A/?(?!(#{reserved_top_level_words})(/|\z))}
reserved_child_level_words = Regexp.union(child_routes)
not_containing_reserved_child = %r{(?!\S+/(#{reserved_child_level_words})(/|\z))}
%r{#{not_starting_in_reserved_word}
#{not_containing_reserved_child}
#{Gitlab::Regex.full_namespace_regex}}x
end
def self.valid?(path)
path =~ Gitlab::Regex.full_namespace_regex && !full_path_reserved?(path)
end
def self.full_path_reserved?(path)
path = path.to_s.downcase
_project_part, namespace_parts = path.reverse.split('/', 2).map(&:reverse)
wildcard_reserved?(path) || child_reserved?(namespace_parts)
end
def self.child_reserved?(path)
return false unless path
path !~ without_reserved_child_paths_regex
end
def self.wildcard_reserved?(path)
return false unless path
path !~ without_reserved_wildcard_paths_regex
end
delegate :full_path_reserved?,
:child_reserved?,
to: :class
def path_reserved_for_record?(record, value)
full_path = record.respond_to?(:full_path) ? record.full_path : value
# For group paths the entire path cannot contain a reserved child word
# The path doesn't contain the last `_project_part` so we need to validate
# if the entire path.
# Example:
# A *group* with full path `parent/activity` is reserved.
# A *project* with full path `parent/activity` is allowed.
if record.is_a? Group
child_reserved?(full_path)
else
full_path_reserved?(full_path)
end
end
def validate_each(record, attribute, value)
unless value =~ Gitlab::Regex.namespace_regex
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
return
end
if path_reserved_for_record?(record, value)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
# NamespaceValidator
#
# Custom validator for GitLab namespace values.
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class NamespaceValidator < ActiveModel::EachValidator
RESERVED = %w[
.well-known
admin
all
assets
ci
dashboard
files
groups
help
hooks
issues
merge_requests
new
notes
profile
projects
public
repository
robots.txt
s
search
services
snippets
teams
u
unsubscribes
users
].freeze
WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file
artifacts graphs refs badges].freeze
STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze
def self.valid?(value)
!reserved?(value) && follow_format?(value)
end
def self.reserved?(value, strict: false)
if strict
STRICT_RESERVED.include?(value)
else
RESERVED.include?(value)
end
end
def self.follow_format?(value)
value =~ Gitlab::Regex.namespace_regex
end
delegate :reserved?, :follow_format?, to: :class
def validate_each(record, attribute, value)
unless follow_format?(value)
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end
strict = record.is_a?(Group) && record.parent_id
if reserved?(value, strict: strict)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
# ProjectPathValidator
#
# Custom validator for GitLab project path values.
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class ProjectPathValidator < ActiveModel::EachValidator
# All project routes with wildcard argument must be listed here.
# Otherwise it can lead to routing issues when route considered as project name.
#
# Example:
# /group/project/tree/deploy_keys
#
# without tree as reserved name routing can match 'group/project' as group name,
# 'tree' as project name and 'deploy_keys' as route.
#
RESERVED = (NamespaceValidator::STRICT_RESERVED -
%w[dashboard help ci admin search notes services assets profile public]).freeze
def self.valid?(value)
!reserved?(value)
end
def self.reserved?(value)
RESERVED.include?(value)
end
delegate :reserved?, to: :class
def validate_each(record, attribute, value)
if reserved?(value)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
......@@ -73,6 +73,12 @@
= container_reg
%span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled
- gitlab_pages = 'GitLab Pages'
- gitlab_pages_enabled = Gitlab.config.pages.enabled
%p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") }
= gitlab_pages
%span.light.pull-right
= boolean_to_icon gitlab_pages_enabled
.col-md-4
%h4
......
= form_errors(hook)
.form-group
= form.label :url, 'URL', class: 'control-label'
.col-sm-10
= form.text_field :url, class: 'form-control'
.form-group
= form.label :token, 'Secret Token', class: 'control-label'
.col-sm-10
= form.text_field :token, class: 'form-control'
%p.help-block
Use this token to validate received payloads
.form-group
= form.label :url, 'Trigger', class: 'control-label'
.col-sm-10.prepend-top-10
%div
System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events.
.prepend-top-default
= form.check_box :push_events, class: 'pull-left'
.prepend-left-20
= form.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This url will be triggered by a push to the repository
%div
= form.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= form.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This url will be triggered when a new tag is pushed to the repository
.form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox'
.col-sm-10
.checkbox
= form.label :enable_ssl_verification do
= form.check_box :enable_ssl_verification
%strong Enable SSL verification
- page_title 'Edit System Hook'
%h3.page-title
Edit System Hook
%p.light
#{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
used for binding events when GitLab creates a User or Project.
%hr
= form_for @hook, as: :hook, url: admin_hook_path, html: { class: 'form-horizontal' } do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
= f.submit 'Save changes', class: 'btn btn-create'
- page_title "System Hooks"
- page_title 'System Hooks'
%h3.page-title
System hooks
%p.light
#{link_to "System hooks ", help_page_path("system_hooks/system_hooks"), class: "vlink"} can be
#{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
used for binding events when GitLab creates a User or Project.
%hr
= form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f|
= form_errors(@hook)
.form-group
= f.label :url, 'URL', class: 'control-label'
.col-sm-10
= f.text_field :url, class: 'form-control'
.form-group
= f.label :token, 'Secret Token', class: 'control-label'
.col-sm-10
= f.text_field :token, class: 'form-control'
%p.help-block
Use this token to validate received payloads
.form-group
= f.label :url, "Trigger", class: 'control-label'
.col-sm-10.prepend-top-10
%div
System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events.
.prepend-top-default
= f.check_box :push_events, class: 'pull-left'
.prepend-left-20
= f.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This url will be triggered by a push to the repository
%div
= f.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= f.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This url will be triggered when a new tag is pushed to the repository
.form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox'
.col-sm-10
.checkbox
= f.label :enable_ssl_verification do
= f.check_box :enable_ssl_verification
%strong Enable SSL verification
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
= f.submit "Add system hook", class: "btn btn-create"
= f.submit 'Add system hook', class: 'btn btn-create'
%hr
- if @hooks.any?
......@@ -62,11 +22,12 @@
- @hooks.each do |hook|
%li
.controls
= link_to 'Test hook', admin_hook_test_path(hook), class: "btn btn-sm"
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm"
= link_to 'Test hook', test_admin_hook_path(hook), class: 'btn btn-sm'
= link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm'
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
.monospace= hook.url
%div
- %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray= trigger.titleize
%span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
%span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
......@@ -2,10 +2,10 @@
%ul.nav-links
= nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: 'Your groups' do
Your Groups
Your groups
= nav_link(page: explore_groups_path) do
= link_to explore_groups_path, title: 'Explore groups' do
Explore Groups
= link_to explore_groups_path, title: 'Explore public groups' do
Explore public groups
.nav-controls
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'
......
.discussion-notes
%ul.notes{ data: { discussion_id: discussion.id } }
= render partial: "projects/notes/note", collection: discussion.notes, as: :note
= render partial: "shared/notes/note", collection: discussion.notes, as: :note
- if current_user
.discussion-reply-holder
......
......@@ -7,6 +7,15 @@
= render 'explore/head'
= render 'nav'
- if cookies[:explore_groups_landing_dismissed] != 'true'
.explore-groups.landing.content-block.js-explore-groups-landing.hidden
%button.dismiss-button{ type: 'button', 'aria-label' => 'Dismiss' }= icon('times')
.svg-container
= custom_icon('icon_explore_groups_splash')
.inner-content
%p Below you will find all the groups that are public.
%p You can easily contribute to them by requesting to join these groups.
- if @groups.present?
= render 'groups'
- else
......
......@@ -27,8 +27,7 @@
.row
.col-md-8
.documentation-index
= preserve do
= markdown(@help_index)
= markdown(@help_index)
.col-md-4
.panel.panel-default
.panel-heading
......
......@@ -2,8 +2,7 @@
%div{ class: container_class }
.wiki-holder.prepend-top-default.append-bottom-default
.wiki
= preserve do
= render_wiki_content(@wiki_home)
= render_wiki_content(@wiki_home)
- else
- can_create_wiki = can?(current_user, :create_wiki, @project)
.project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] }
......
......@@ -15,8 +15,8 @@
= render 'projects/blob/viewer_switcher', blob: blob unless blame
.btn-group{ role: "group" }<
= copy_blob_source_button(blob) if !blame && blob.rendered_as_text?(ignore_errors: false)
= open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id))
= copy_blob_source_button(blob) unless blame
= open_raw_blob_button(blob)
= view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group{ role: "group" }<
......
- blob = viewer.blob
- rendered_markup = blob.rendered_markup if blob.respond_to?(:rendered_markup)
.file-content.wiki
= markup(blob.name, blob.data)
= markup(blob.name, blob.data, rendered: rendered_markup)
.file-content.video
%video{ src: blob_raw_url, controls: true, data: { setup: '{}' } }
......@@ -28,8 +28,9 @@
":value" => "issue.assignee.id",
"v-if" => "issue.assignee" }
.dropdown
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true" },
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", null_user_default: "true" },
":data-issuable-id" => "issue.id",
":data-selected" => "assigneeId",
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
Select assignee
= icon("chevron-down")
......
......@@ -60,7 +60,7 @@
git init
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git add .
git commit
git commit -m "Initial commit"
git push -u origin master
%fieldset
......
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.
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.
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.
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.
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.
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.
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.
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.
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