Commit 88d988a2 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into pipeline-emails

* upstream/master: (58 commits)
  Update endpoint to username validator
  change border color to variable
  Add todo for deprecated user routes and more information about deprecation to changelog
  Provide better error message to the user
  Apply better hierarchy to markdown headers and issue/mr titles
  Swapped button text manipulation outcomes for the toggle query
  Fixed find file keyboard navigation
  Update CHANGELOG for 8.12.7
  Added download-button class and applied button margin
  Enable activerecord_sane_schema_dumper for test
  Updated logo from @luke
  Fix broken specs on MySQL after https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6896
  Fix Test Env (proper error handling when gitlab-shell is not clonned)
  Fix randomly crashing spinach test for merge request
  [Great spinach fix] Replace gsub with delete
  Remove carriage returns from commit description as summary is on a newline and will always include carriage returns
  Convert due_date_select.js filetype to es6.
  Stop directly parsing due_date with Date.parse, prefer parsing implicitly.
  Improve spec for pipeline metrics worker
  Add Pipeline metrics worker
  ...
parents 09a7da72 4e6af0c3
...@@ -22,7 +22,7 @@ before_script: ...@@ -22,7 +22,7 @@ before_script:
- bundle --version - bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"' - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
- retry gem install knapsack - retry gem install knapsack
- '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql'
stages: stages:
- prepare - prepare
......
...@@ -11,6 +11,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -11,6 +11,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Update runner version only when updating contacted_at - Update runner version only when updating contacted_at
- Add link from system note to compare with previous version - Add link from system note to compare with previous version
- Use gitlab-shell v3.6.6 - Use gitlab-shell v3.6.6
- Ability to resolve merge request conflicts with editor !6374
- Add `/projects/visible` API endpoint (Ben Boeckel) - Add `/projects/visible` API endpoint (Ben Boeckel)
- Fix centering of custom header logos (Ashley Dumaine) - Fix centering of custom header logos (Ashley Dumaine)
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
...@@ -19,8 +20,8 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -19,8 +20,8 @@ Please view this file on the master branch, on stable branches it's out of date.
- Clarify documentation for Runners API (Gennady Trafimenkov) - Clarify documentation for Runners API (Gennady Trafimenkov)
- The instrumentation for Banzai::Renderer has been restored - The instrumentation for Banzai::Renderer has been restored
- Change user & group landing page routing from /u/:username to /:username - Change user & group landing page routing from /u/:username to /:username
- Prevent running GfmAutocomplete setup for each diff note !6569
- Added documentation for .gitattributes files - Added documentation for .gitattributes files
- Move Pipeline Metrics to separate worker
- AbstractReferenceFilter caches project_refs on RequestStore when active - AbstractReferenceFilter caches project_refs on RequestStore when active
- Replaced the check sign to arrow in the show build view. !6501 - Replaced the check sign to arrow in the show build view. !6501
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
...@@ -39,7 +40,6 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -39,7 +40,6 @@ Please view this file on the master branch, on stable branches it's out of date.
- Update Gitlab Shell to fix some problems with moving projects between storages - Update Gitlab Shell to fix some problems with moving projects between storages
- Cache rendered markdown in the database, rather than Redis - Cache rendered markdown in the database, rather than Redis
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Do not alter 'force_remove_source_branch' options on MergeRequest unless specified
- Simplify Mentionable concern instance methods - Simplify Mentionable concern instance methods
- API: Ability to retrieve version information (Robert Schilling) - API: Ability to retrieve version information (Robert Schilling)
- Fix permission for setting an issue's due date - Fix permission for setting an issue's due date
...@@ -56,6 +56,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -56,6 +56,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Added soft wrap button to repository file/blob editor - Added soft wrap button to repository file/blob editor
- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms) - Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
- Show the time ago a merge request was deployed to an environment - Show the time ago a merge request was deployed to an environment
- Add RTL support to markdown renderer (Ebrahim Byagowi)
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- Fix todos page mobile viewport layout (ClemMakesApps) - Fix todos page mobile viewport layout (ClemMakesApps)
- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps) - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
...@@ -75,14 +76,12 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -75,14 +76,12 @@ Please view this file on the master branch, on stable branches it's out of date.
- Only update issuable labels if they have been changed - Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496 - Take filters in account in issuable counters. !6496
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
- Prevent flash alert text from being obscured when container is fluid
- Append issue template to existing description !6149 (Joseph Frazier) - Append issue template to existing description !6149 (Joseph Frazier)
- Trending projects now only show public projects and the list of projects is cached for a day - Trending projects now only show public projects and the list of projects is cached for a day
- Memoize Gitlab Shell's secret token (!6599, Justin DiPierro) - Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
- Revoke button in Applications Settings underlines on hover. - Revoke button in Applications Settings underlines on hover.
- Use higher size on Gitlab::Redis connection pool on Sidekiq servers - Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree
- Revert avoid touching file system on Build#artifacts? - Revert avoid touching file system on Build#artifacts?
- Stop using a Redis lease when updating the project activity timestamp whenever a new event is created - Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
- Add disabled delete button to protected branches (ClemMakesApps) - Add disabled delete button to protected branches (ClemMakesApps)
...@@ -93,7 +92,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -93,7 +92,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Replace bootstrap caret with fontawesome caret (ClemMakesApps) - Replace bootstrap caret with fontawesome caret (ClemMakesApps)
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533 - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile - Add organization field to user profile
- Ignore deployment for statistics in Cycle Analytics, except in staging and production stages - Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being.
- Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts) - Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts)
- Fix deploy status responsiveness error !6633 - Fix deploy status responsiveness error !6633
- Make searching for commits case insensitive - Make searching for commits case insensitive
...@@ -123,8 +122,15 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -123,8 +122,15 @@ Please view this file on the master branch, on stable branches it's out of date.
## 8.12.7 ## 8.12.7
- Use gitlab-markup gem instead of github-markup to fix `.rst` file rendering. !6659 - Prevent running `GfmAutocomplete` setup for each diff note. !6569
- Fix GFM autocomplete setup being called several times - Fix long commit messages overflow viewport in file tree. !6573
- Use `gitlab-markup` gem instead of `github-markup` to fix `.rst` file rendering. !6659
- Prevent flash alert text from being obscured when container is fluid. !6694
- Fix due date being displayed as `NaN` in Safari. !6797
- Fix JS bug with select2 because of missing `data-field` attribute in select box. !6812
- Do not alter `force_remove_source_branch` options on MergeRequest unless specified. !6817
- Fix GFM autocomplete setup being called several times. !6840
- Handle case where deployment ref no longer exists. !6855
## 8.12.6 ## 8.12.6
......
...@@ -262,8 +262,6 @@ group :development do ...@@ -262,8 +262,6 @@ group :development do
# thin instead webrick # thin instead webrick
gem 'thin', '~> 1.7.0' gem 'thin', '~> 1.7.0'
gem 'activerecord_sane_schema_dumper', '0.2'
end end
group :development, :test do group :development, :test do
...@@ -310,6 +308,8 @@ group :development, :test do ...@@ -310,6 +308,8 @@ group :development, :test do
gem 'license_finder', '~> 2.1.0', require: false gem 'license_finder', '~> 2.1.0', require: false
gem 'knapsack', '~> 1.11.0' gem 'knapsack', '~> 1.11.0'
gem 'activerecord_sane_schema_dumper', '0.2'
end end
group :test do group :test do
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
case 'projects:milestones:new': case 'projects:milestones:new':
case 'projects:milestones:edit': case 'projects:milestones:edit':
new ZenMode(); new ZenMode();
new DueDateSelect(); new gl.DueDateSelectors();
new GLForm($('.milestone-form')); new GLForm($('.milestone-form'));
break; break;
case 'groups:milestones:new': case 'groups:milestones:new':
...@@ -101,9 +101,6 @@ ...@@ -101,9 +101,6 @@
new ZenMode(); new ZenMode();
new MergedButtons(); new MergedButtons();
break; break;
case "projects:merge_requests:conflicts":
window.mcui = new MergeConflictResolver()
break;
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
Issuable.init(); Issuable.init();
......
(function() {
this.DueDateSelect = (function() {
function DueDateSelect() {
var $datePicker, $dueDate, $loading;
// Milestone edit/new form
$datePicker = $('.datepicker');
if ($datePicker.length) {
$dueDate = $('#milestone_due_date');
$datePicker.datepicker({
dateFormat: 'yy-mm-dd',
onSelect: function(dateText, inst) {
return $dueDate.val(dateText);
}
}).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()));
}
$('.js-clear-due-date').on('click', function(e) {
e.preventDefault();
return $.datepicker._clearDate($datePicker);
});
// Issuable sidebar
$loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
$('.js-due-date-select').each(function(i, dropdown) {
var $block, $dropdown, $dropdownParent, $selectbox, $sidebarValue, $value, $valueContent, abilityName, addDueDate, fieldName, issueUpdateURL;
$dropdown = $(dropdown);
$dropdownParent = $dropdown.closest('.dropdown');
$datePicker = $dropdownParent.find('.js-due-date-calendar');
$block = $dropdown.closest('.block');
$selectbox = $dropdown.closest('.selectbox');
$value = $block.find('.value');
$valueContent = $block.find('.value-content');
$sidebarValue = $('.js-due-date-sidebar-value', $block);
fieldName = $dropdown.data('field-name');
abilityName = $dropdown.data('ability-name');
issueUpdateURL = $dropdown.data('issue-update');
$dropdown.glDropdown({
hidden: function() {
$selectbox.hide();
return $value.css('display', '');
}
});
addDueDate = function(isDropdown) {
var data, date, mediumDate, value;
// Create the post date
value = $("input[name='" + fieldName + "']").val();
if (value !== '') {
date = new Date(value.replace(new RegExp('-', 'g'), ','));
mediumDate = $.datepicker.formatDate('M d, yy', date);
} else {
mediumDate = 'No due date';
}
data = {};
data[abilityName] = {};
data[abilityName].due_date = value;
return $.ajax({
type: 'PUT',
url: issueUpdateURL,
data: data,
dataType: 'json',
beforeSend: function() {
var cssClass;
$loading.fadeIn();
if (isDropdown) {
$dropdown.trigger('loading.gl.dropdown');
$selectbox.hide();
}
$value.css('display', '');
cssClass = Date.parse(mediumDate) ? 'bold' : 'no-value';
$valueContent.html("<span class='" + cssClass + "'>" + mediumDate + "</span>");
$sidebarValue.html(mediumDate);
if (value !== '') {
return $('.js-remove-due-date-holder').removeClass('hidden');
} else {
return $('.js-remove-due-date-holder').addClass('hidden');
}
}
}).done(function(data) {
if (isDropdown) {
$dropdown.trigger('loaded.gl.dropdown');
$dropdown.dropdown('toggle');
}
return $loading.fadeOut();
});
};
$block.on('click', '.js-remove-due-date', function(e) {
e.preventDefault();
$("input[name='" + fieldName + "']").val('');
return addDueDate(false);
});
return $datePicker.datepicker({
dateFormat: 'yy-mm-dd',
defaultDate: $("input[name='" + fieldName + "']").val(),
altField: "input[name='" + fieldName + "']",
onSelect: function() {
return addDueDate(true);
}
});
});
$(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', function(e) {
return e.stopImmediatePropagation();
});
}
return DueDateSelect;
})();
}).call(this);
(function(global) {
class DueDateSelect {
constructor({ $dropdown, $loading } = {}) {
const $dropdownParent = $dropdown.closest('.dropdown');
const $block = $dropdown.closest('.block');
this.$loading = $loading;
this.$dropdown = $dropdown;
this.$dropdownParent = $dropdownParent;
this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
this.$block = $block;
this.$selectbox = $dropdown.closest('.selectbox');
this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content');
this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
this.fieldName = $dropdown.data('field-name'),
this.abilityName = $dropdown.data('ability-name'),
this.issueUpdateURL = $dropdown.data('issue-update')
this.rawSelectedDate = null;
this.displayedDate = null;
this.datePayload = null;
this.initGlDropdown();
this.initRemoveDueDate();
this.initDatePicker();
this.initStopPropagation();
}
initGlDropdown() {
this.$dropdown.glDropdown({
hidden: () => {
this.$selectbox.hide();
this.$value.css('display', '');
}
});
}
initDatePicker() {
this.$datePicker.datepicker({
dateFormat: 'yy-mm-dd',
defaultDate: $("input[name='" + this.fieldName + "']").val(),
altField: "input[name='" + this.fieldName + "']",
onSelect: () => {
return this.saveDueDate(true);
}
});
}
initRemoveDueDate() {
this.$block.on('click', '.js-remove-due-date', (e) => {
e.preventDefault();
$("input[name='" + this.fieldName + "']").val('');
return this.saveDueDate(false);
});
}
initStopPropagation() {
$(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', (e) => {
return e.stopImmediatePropagation();
});
}
saveDueDate(isDropdown) {
this.parseSelectedDate();
this.prepSelectedDate();
this.submitSelectedDate(isDropdown);
}
parseSelectedDate() {
this.rawSelectedDate = $("input[name='" + this.fieldName + "']").val();
if (this.rawSelectedDate.length) {
let dateObj = new Date(this.rawSelectedDate);
this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj);
} else {
this.displayedDate = 'No due date';
}
}
prepSelectedDate() {
const datePayload = {};
datePayload[this.abilityName] = {};
datePayload[this.abilityName].due_date = this.rawSelectedDate;
this.datePayload = datePayload;
}
submitSelectedDate(isDropdown) {
return $.ajax({
type: 'PUT',
url: this.issueUpdateURL,
data: this.datePayload,
dataType: 'json',
beforeSend: () => {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
this.$loading.fadeIn();
if (isDropdown) {
this.$dropdown.trigger('loading.gl.dropdown');
this.$selectbox.hide();
}
this.$value.css('display', '');
this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`);
this.$sidebarValue.html(this.displayedDate);
return selectedDateValue.length ?
$('.js-remove-due-date-holder').removeClass('hidden') :
$('.js-remove-due-date-holder').addClass('hidden');
}
}).done((data) => {
if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle');
}
return this.$loading.fadeOut();
});
}
}
class DueDateSelectors {
constructor() {
this.initMilestoneDueDate();
this.initIssuableSelect();
}
initMilestoneDueDate() {
const $datePicker = $('.datepicker');
if ($datePicker.length) {
const $dueDate = $('#milestone_due_date');
$datePicker.datepicker({
dateFormat: 'yy-mm-dd',
onSelect: (dateText, inst) => {
$dueDate.val(dateText);
}
}).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()));
}
$('.js-clear-due-date').on('click', (e) => {
e.preventDefault();
$.datepicker._clearDate($datePicker);
});
}
initIssuableSelect() {
const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
$('.js-due-date-select').each((i, dropdown) => {
const $dropdown = $(dropdown);
new DueDateSelect({
$dropdown,
$loading
});
});
}
}
global.DueDateSelectors = DueDateSelectors;
})(window.gl || (window.gl = {}));
const HEAD_HEADER_TEXT = 'HEAD//our changes';
const ORIGIN_HEADER_TEXT = 'origin//their changes';
const HEAD_BUTTON_TITLE = 'Use ours';
const ORIGIN_BUTTON_TITLE = 'Use theirs';
class MergeConflictDataProvider {
getInitialData() {
// TODO: remove reliance on jQuery and DOM state introspection
const diffViewType = $.cookie('diff_view');
const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited');
return {
isLoading : true,
hasError : false,
isParallel : diffViewType === 'parallel',
diffViewType : diffViewType,
fixedLayout : fixedLayout,
isSubmitting : false,
conflictsData : {},
resolutionData : {}
}
}
decorateData(vueInstance, data) {
this.vueInstance = vueInstance;
if (data.type === 'error') {
vueInstance.hasError = true;
data.errorMessage = data.message;
}
else {
data.shortCommitSha = data.commit_sha.slice(0, 7);
data.commitMessage = data.commit_message;
this.setParallelLines(data);
this.setInlineLines(data);
this.updateResolutionsData(data);
}
vueInstance.conflictsData = data;
vueInstance.isSubmitting = false;
const conflictsText = this.getConflictsCount() > 1 ? 'conflicts' : 'conflict';
vueInstance.conflictsData.conflictsText = conflictsText;
}
updateResolutionsData(data) {
const vi = this.vueInstance;
data.files.forEach( (file) => {
file.sections.forEach( (section) => {
if (section.conflict) {
vi.$set(`resolutionData['${section.id}']`, false);
}
});
});
}
setParallelLines(data) {
data.files.forEach( (file) => {
file.filePath = this.getFilePath(file);
file.iconClass = `fa-${file.blob_icon}`;
file.blobPath = file.blob_path;
file.parallelLines = [];
const linesObj = { left: [], right: [] };
file.sections.forEach( (section) => {
const { conflict, lines, id } = section;
if (conflict) {
linesObj.left.push(this.getOriginHeaderLine(id));
linesObj.right.push(this.getHeadHeaderLine(id));
}
lines.forEach( (line) => {
const { type } = line;
if (conflict) {
if (type === 'old') {
linesObj.left.push(this.getLineForParallelView(line, id, 'conflict'));
}
else if (type === 'new') {
linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true));
}
}
else {
const lineType = type || 'context';
linesObj.left.push (this.getLineForParallelView(line, id, lineType));
linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
}
});
this.checkLineLengths(linesObj);
});
for (let i = 0, len = linesObj.left.length; i < len; i++) {
file.parallelLines.push([
linesObj.right[i],
linesObj.left[i]
]);
}
});
}
checkLineLengths(linesObj) {
let { left, right } = linesObj;
if (left.length !== right.length) {
if (left.length > right.length) {
const diff = left.length - right.length;
for (let i = 0; i < diff; i++) {
right.push({ lineType: 'emptyLine', richText: '' });
}
}
else {
const diff = right.length - left.length;
for (let i = 0; i < diff; i++) {
left.push({ lineType: 'emptyLine', richText: '' });
}
}
}
}
setInlineLines(data) {
data.files.forEach( (file) => {
file.iconClass = `fa-${file.blob_icon}`;
file.blobPath = file.blob_path;
file.filePath = this.getFilePath(file);
file.inlineLines = []
file.sections.forEach( (section) => {
let currentLineType = 'new';
const { conflict, lines, id } = section;
if (conflict) {
file.inlineLines.push(this.getHeadHeaderLine(id));
}
lines.forEach( (line) => {
const { type } = line;
if ((type === 'new' || type === 'old') && currentLineType !== type) {
currentLineType = type;
file.inlineLines.push({ lineType: 'emptyLine', richText: '' });
}
this.decorateLineForInlineView(line, id, conflict);
file.inlineLines.push(line);
})
if (conflict) {
file.inlineLines.push(this.getOriginHeaderLine(id));
}
});
});
}
handleSelected(sectionId, selection) {
const vi = this.vueInstance;
vi.resolutionData[sectionId] = selection;
vi.conflictsData.files.forEach( (file) => {
file.inlineLines.forEach( (line) => {
if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
this.markLine(line, selection);
}
});
file.parallelLines.forEach( (lines) => {
const left = lines[0];
const right = lines[1];
const hasSameId = right.id === sectionId || left.id === sectionId;
const isLeftMatch = left.hasConflict || left.isHeader;
const isRightMatch = right.hasConflict || right.isHeader;
if (hasSameId && (isLeftMatch || isRightMatch)) {
this.markLine(left, selection);
this.markLine(right, selection);
}
})
});
}
updateViewType(newType) {
const vi = this.vueInstance;
if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) {
return;
}
vi.diffViewType = newType;
vi.isParallel = newType === 'parallel';
$.cookie('diff_view', newType, {
path: (gon && gon.relative_url_root) || '/'
});
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', !vi.isParallel && vi.fixedLayout);
}
markLine(line, selection) {
if (selection === 'head' && line.isHead) {
line.isSelected = true;
line.isUnselected = false;
}
else if (selection === 'origin' && line.isOrigin) {
line.isSelected = true;
line.isUnselected = false;
}
else {
line.isSelected = false;
line.isUnselected = true;
}
}
getConflictsCount() {
return Object.keys(this.vueInstance.resolutionData).length;
}
getResolvedCount() {
let count = 0;
const data = this.vueInstance.resolutionData;
for (const id in data) {
const resolution = data[id];
if (resolution) {
count++;
}
}
return count;
}
isReadyToCommit() {
const { conflictsData, isSubmitting } = this.vueInstance
const allResolved = this.getConflictsCount() === this.getResolvedCount();
const hasCommitMessage = $.trim(conflictsData.commitMessage).length;
return !isSubmitting && hasCommitMessage && allResolved;
}
getCommitButtonText() {
const initial = 'Commit conflict resolution';
const inProgress = 'Committing...';
const vue = this.vueInstance;
return vue ? vue.isSubmitting ? inProgress : initial : initial;
}
decorateLineForInlineView(line, id, conflict) {
const { type } = line;
line.id = id;
line.hasConflict = conflict;
line.isHead = type === 'new';
line.isOrigin = type === 'old';
line.hasMatch = type === 'match';
line.richText = line.rich_text;
line.isSelected = false;
line.isUnselected = false;
}
getLineForParallelView(line, id, lineType, isHead) {
const { old_line, new_line, rich_text } = line;
const hasConflict = lineType === 'conflict';
return {
id,
lineType,
hasConflict,
isHead : hasConflict && isHead,
isOrigin : hasConflict && !isHead,
hasMatch : lineType === 'match',
lineNumber : isHead ? new_line : old_line,
section : isHead ? 'head' : 'origin',
richText : rich_text,
isSelected : false,
isUnselected : false
}
}
getHeadHeaderLine(id) {
return {
id : id,
richText : HEAD_HEADER_TEXT,
buttonTitle : HEAD_BUTTON_TITLE,
type : 'new',
section : 'head',
isHeader : true,
isHead : true,
isSelected : false,
isUnselected: false
}
}
getOriginHeaderLine(id) {
return {
id : id,
richText : ORIGIN_HEADER_TEXT,
buttonTitle : ORIGIN_BUTTON_TITLE,
type : 'old',
section : 'origin',
isHeader : true,
isOrigin : true,
isSelected : false,
isUnselected: false
}
}
handleFailedRequest(vueInstance, data) {
vueInstance.hasError = true;
vueInstance.conflictsData.errorMessage = 'Something went wrong!';
}
getCommitData() {
return {
commit_message: this.vueInstance.conflictsData.commitMessage,
sections: this.vueInstance.resolutionData
}
}
getFilePath(file) {
const { old_path, new_path } = file;
return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
}
}
//= require vue
class MergeConflictResolver {
constructor() {
this.dataProvider = new MergeConflictDataProvider()
this.initVue()
}
initVue() {
const that = this;
this.vue = new Vue({
el : '#conflicts',
name : 'MergeConflictResolver',
data : this.dataProvider.getInitialData(),
created : this.fetchData(),
computed : this.setComputedProperties(),
methods : {
handleSelected(sectionId, selection) {
that.dataProvider.handleSelected(sectionId, selection);
},
handleViewTypeChange(newType) {
that.dataProvider.updateViewType(newType);
},
commit() {
that.commit();
}
}
})
}
setComputedProperties() {
const dp = this.dataProvider;
return {
conflictsCount() { return dp.getConflictsCount() },
resolvedCount() { return dp.getResolvedCount() },
readyToCommit() { return dp.isReadyToCommit() },
commitButtonText() { return dp.getCommitButtonText() }
}
}
fetchData() {
const dp = this.dataProvider;
$.get($('#conflicts').data('conflictsPath'))
.done((data) => {
dp.decorateData(this.vue, data);
})
.error((data) => {
dp.handleFailedRequest(this.vue, data);
})
.always(() => {
this.vue.isLoading = false;
this.vue.$nextTick(() => {
$('#conflicts .js-syntax-highlight').syntaxHighlight();
});
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout);
})
}
commit() {
this.vue.isSubmitting = true;
$.post($('#conflicts').data('resolveConflictsPath'), this.dataProvider.getCommitData())
.done((data) => {
window.location.href = data.redirect_to;
})
.error(() => {
this.vue.isSubmitting = false;
new Flash('Something went wrong!');
});
}
}
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.diffFileEditor = Vue.extend({
props: {
file: Object,
onCancelDiscardConfirmation: Function,
onAcceptDiscardConfirmation: Function
},
data() {
return {
saved: false,
loading: false,
fileLoaded: false,
originalContent: '',
}
},
computed: {
classObject() {
return {
'saved': this.saved,
'is-loading': this.loading
};
}
},
watch: {
['file.showEditor'](val) {
this.resetEditorContent();
if (!val || this.fileLoaded || this.loading) {
return;
}
this.loadEditor();
}
},
ready() {
if (this.file.loadEditor) {
this.loadEditor();
}
},
methods: {
loadEditor() {
this.loading = true;
$.get(this.file.content_path)
.done((file) => {
let content = this.$el.querySelector('pre');
let fileContent = document.createTextNode(file.content);
content.textContent = fileContent.textContent;
this.originalContent = file.content;
this.fileLoaded = true;
this.editor = ace.edit(content);
this.editor.$blockScrolling = Infinity; // Turn off annoying warning
this.editor.getSession().setMode(`ace/mode/${file.blob_ace_mode}`);
this.editor.on('change', () => {
this.saveDiffResolution();
});
this.saveDiffResolution();
})
.fail(() => {
new Flash('Failed to load the file, please try again.');
})
.always(() => {
this.loading = false;
});
},
saveDiffResolution() {
this.saved = true;
// This probably be better placed in the data provider
this.file.content = this.editor.getValue();
this.file.resolveEditChanged = this.file.content !== this.originalContent;
this.file.promptDiscardConfirmation = false;
},
resetEditorContent() {
if (this.fileLoaded) {
this.editor.setValue(this.originalContent, -1);
}
},
cancelDiscardConfirmation(file) {
this.onCancelDiscardConfirmation(file);
},
acceptDiscardConfirmation(file) {
this.onAcceptDiscardConfirmation(file);
}
}
});
})(window.gl || (window.gl = {}));
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.inlineConflictLines = Vue.extend({
props: {
file: Object
},
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
});
})(window.gl || (window.gl = {}));
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLine = Vue.extend({
props: {
file: Object,
line: Object
},
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
template: '#parallel-conflict-line'
});
})(window.gl || (window.gl = {}));
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLines = Vue.extend({
props: {
file: Object
},
mixins: [global.mergeConflicts.utils],
components: {
'parallel-conflict-line': gl.mergeConflicts.parallelConflictLine
}
});
})(window.gl || (window.gl = {}));
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
class mergeConflictsService {
constructor(options) {
this.conflictsPath = options.conflictsPath;
this.resolveConflictsPath = options.resolveConflictsPath;
}
fetchConflictsData() {
return $.ajax({
dataType: 'json',
url: this.conflictsPath
});
}
submitResolveConflicts(data) {
return $.ajax({
url: this.resolveConflictsPath,
data: JSON.stringify(data),
contentType: 'application/json',
dataType: 'json',
method: 'POST'
});
}
};
global.mergeConflicts.mergeConflictsService = mergeConflictsService;
})(window.gl || (window.gl = {}));
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
const diffViewType = $.cookie('diff_view');
const HEAD_HEADER_TEXT = 'HEAD//our changes';
const ORIGIN_HEADER_TEXT = 'origin//their changes';
const HEAD_BUTTON_TITLE = 'Use ours';
const ORIGIN_BUTTON_TITLE = 'Use theirs';
const INTERACTIVE_RESOLVE_MODE = 'interactive';
const EDIT_RESOLVE_MODE = 'edit';
const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE;
const VIEW_TYPES = {
INLINE: 'inline',
PARALLEL: 'parallel'
};
const CONFLICT_TYPES = {
TEXT: 'text',
TEXT_EDITOR: 'text-editor'
};
global.mergeConflicts.mergeConflictsStore = {
state: {
isLoading: true,
hasError: false,
isSubmitting: false,
isParallel: diffViewType === VIEW_TYPES.PARALLEL,
diffViewType: diffViewType,
conflictsData: {}
},
setConflictsData(data) {
this.decorateFiles(data.files);
this.state.conflictsData = {
files: data.files,
commitMessage: data.commit_message,
sourceBranch: data.source_branch,
targetBranch: data.target_branch,
commitMessage: data.commit_message,
shortCommitSha: data.commit_sha.slice(0, 7),
};
},
decorateFiles(files) {
files.forEach((file) => {
file.content = '';
file.resolutionData = {};
file.promptDiscardConfirmation = false;
file.resolveMode = DEFAULT_RESOLVE_MODE;
file.filePath = this.getFilePath(file);
file.iconClass = `fa-${file.blob_icon}`;
file.blobPath = file.blob_path;
if (file.type === CONFLICT_TYPES.TEXT) {
file.showEditor = false;
file.loadEditor = false;
this.setInlineLine(file);
this.setParallelLine(file);
} else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) {
file.showEditor = true;
file.loadEditor = true;
}
});
},
setInlineLine(file) {
file.inlineLines = [];
file.sections.forEach((section) => {
let currentLineType = 'new';
const { conflict, lines, id } = section;
if (conflict) {
file.inlineLines.push(this.getHeadHeaderLine(id));
}
lines.forEach((line) => {
const { type } = line;
if ((type === 'new' || type === 'old') && currentLineType !== type) {
currentLineType = type;
file.inlineLines.push({ lineType: 'emptyLine', richText: '' });
}
this.decorateLineForInlineView(line, id, conflict);
file.inlineLines.push(line);
})
if (conflict) {
file.inlineLines.push(this.getOriginHeaderLine(id));
}
});
},
setParallelLine(file) {
file.parallelLines = [];
const linesObj = { left: [], right: [] };
file.sections.forEach((section) => {
const { conflict, lines, id } = section;
if (conflict) {
linesObj.left.push(this.getOriginHeaderLine(id));
linesObj.right.push(this.getHeadHeaderLine(id));
}
lines.forEach((line) => {
const { type } = line;
if (conflict) {
if (type === 'old') {
linesObj.left.push(this.getLineForParallelView(line, id, 'conflict'));
} else if (type === 'new') {
linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true));
}
} else {
const lineType = type || 'context';
linesObj.left.push (this.getLineForParallelView(line, id, lineType));
linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
}
});
this.checkLineLengths(linesObj);
});
for (let i = 0, len = linesObj.left.length; i < len; i++) {
file.parallelLines.push([
linesObj.right[i],
linesObj.left[i]
]);
}
},
setLoadingState(state) {
this.state.isLoading = state;
},
setErrorState(state) {
this.state.hasError = state;
},
setFailedRequest(message) {
this.state.hasError = true;
this.state.conflictsData.errorMessage = message;
},
getConflictsCount() {
if (!this.state.conflictsData.files.length) {
return 0;
}
const files = this.state.conflictsData.files;
let count = 0;
files.forEach((file) => {
if (file.type === CONFLICT_TYPES.TEXT) {
file.sections.forEach((section) => {
if (section.conflict) {
count++;
}
});
} else {
count++;
}
});
return count;
},
getConflictsCountText() {
const count = this.getConflictsCount();
const text = count ? 'conflicts' : 'conflict';
return `${count} ${text}`;
},
setViewType(viewType) {
this.state.diffView = viewType;
this.state.isParallel = viewType === VIEW_TYPES.PARALLEL;
$.cookie('diff_view', viewType, {
path: gon.relative_url_root || '/'
});
},
getHeadHeaderLine(id) {
return {
id: id,
richText: HEAD_HEADER_TEXT,
buttonTitle: HEAD_BUTTON_TITLE,
type: 'new',
section: 'head',
isHeader: true,
isHead: true,
isSelected: false,
isUnselected: false
};
},
decorateLineForInlineView(line, id, conflict) {
const { type } = line;
line.id = id;
line.hasConflict = conflict;
line.isHead = type === 'new';
line.isOrigin = type === 'old';
line.hasMatch = type === 'match';
line.richText = line.rich_text;
line.isSelected = false;
line.isUnselected = false;
},
getLineForParallelView(line, id, lineType, isHead) {
const { old_line, new_line, rich_text } = line;
const hasConflict = lineType === 'conflict';
return {
id,
lineType,
hasConflict,
isHead: hasConflict && isHead,
isOrigin: hasConflict && !isHead,
hasMatch: lineType === 'match',
lineNumber: isHead ? new_line : old_line,
section: isHead ? 'head' : 'origin',
richText: rich_text,
isSelected: false,
isUnselected: false
};
},
getOriginHeaderLine(id) {
return {
id: id,
richText: ORIGIN_HEADER_TEXT,
buttonTitle: ORIGIN_BUTTON_TITLE,
type: 'old',
section: 'origin',
isHeader: true,
isOrigin: true,
isSelected: false,
isUnselected: false
};
},
getFilePath(file) {
const { old_path, new_path } = file;
return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
},
checkLineLengths(linesObj) {
let { left, right } = linesObj;
if (left.length !== right.length) {
if (left.length > right.length) {
const diff = left.length - right.length;
for (let i = 0; i < diff; i++) {
right.push({ lineType: 'emptyLine', richText: '' });
}
} else {
const diff = right.length - left.length;
for (let i = 0; i < diff; i++) {
left.push({ lineType: 'emptyLine', richText: '' });
}
}
}
},
setPromptConfirmationState(file, state) {
file.promptDiscardConfirmation = state;
},
setFileResolveMode(file, mode) {
if (mode === INTERACTIVE_RESOLVE_MODE) {
file.showEditor = false;
} else if (mode === EDIT_RESOLVE_MODE) {
// Restore Interactive mode when switching to Edit mode
file.showEditor = true;
file.loadEditor = true;
file.resolutionData = {};
this.restoreFileLinesState(file);
}
file.resolveMode = mode;
},
restoreFileLinesState(file) {
file.inlineLines.forEach((line) => {
if (line.hasConflict || line.isHeader) {
line.isSelected = false;
line.isUnselected = false;
}
});
file.parallelLines.forEach((lines) => {
const left = lines[0];
const right = lines[1];
const isLeftMatch = left.hasConflict || left.isHeader;
const isRightMatch = right.hasConflict || right.isHeader;
if (isLeftMatch || isRightMatch) {
left.isSelected = false;
left.isUnselected = false;
right.isSelected = false;
right.isUnselected = false;
}
});
},
isReadyToCommit() {
const files = this.state.conflictsData.files;
const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length;
let unresolved = 0;
for (let i = 0, l = files.length; i < l; i++) {
let file = files[i];
if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
let numberConflicts = 0;
let resolvedConflicts = Object.keys(file.resolutionData).length
// We only check for conflicts type 'text'
// since conflicts `text_editor` can´t be resolved in interactive mode
if (file.type === CONFLICT_TYPES.TEXT) {
for (let j = 0, k = file.sections.length; j < k; j++) {
if (file.sections[j].conflict) {
numberConflicts++;
}
}
if (resolvedConflicts !== numberConflicts) {
unresolved++;
}
}
} else if (file.resolveMode === EDIT_RESOLVE_MODE) {
// Unlikely to happen since switching to Edit mode saves content automatically.
// Checking anyway in case the save strategy changes in the future
if (!file.content) {
unresolved++;
continue;
}
}
}
return !this.state.isSubmitting && hasCommitMessage && !unresolved;
},
getCommitButtonText() {
const initial = 'Commit conflict resolution';
const inProgress = 'Committing...';
return this.state ? this.state.isSubmitting ? inProgress : initial : initial;
},
getCommitData() {
let commitData = {};
commitData = {
commit_message: this.state.conflictsData.commitMessage,
files: []
};
this.state.conflictsData.files.forEach((file) => {
let addFile;
addFile = {
old_path: file.old_path,
new_path: file.new_path
};
if (file.type === CONFLICT_TYPES.TEXT) {
// Submit only one data for type of editing
if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
addFile.sections = file.resolutionData;
} else if (file.resolveMode === EDIT_RESOLVE_MODE) {
addFile.content = file.content;
}
} else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) {
addFile.content = file.content;
}
commitData.files.push(addFile);
});
return commitData;
},
handleSelected(file, sectionId, selection) {
Vue.set(file.resolutionData, sectionId, selection);
file.inlineLines.forEach((line) => {
if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
this.markLine(line, selection);
}
});
file.parallelLines.forEach((lines) => {
const left = lines[0];
const right = lines[1];
const hasSameId = right.id === sectionId || left.id === sectionId;
const isLeftMatch = left.hasConflict || left.isHeader;
const isRightMatch = right.hasConflict || right.isHeader;
if (hasSameId && (isLeftMatch || isRightMatch)) {
this.markLine(left, selection);
this.markLine(right, selection);
}
});
},
markLine(line, selection) {
if (selection === 'head' && line.isHead) {
line.isSelected = true;
line.isUnselected = false;
} else if (selection === 'origin' && line.isOrigin) {
line.isSelected = true;
line.isUnselected = false;
} else {
line.isSelected = false;
line.isUnselected = true;
}
},
setSubmitState(state) {
this.state.isSubmitting = state;
},
fileTextTypePresent() {
return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT);
}
};
})(window.gl || (window.gl = {}));
//= require vue
//= require ./merge_conflict_store
//= require ./merge_conflict_service
//= require ./mixins/line_conflict_utils
//= require ./mixins/line_conflict_actions
//= require ./components/diff_file_editor
//= require ./components/inline_conflict_lines
//= require ./components/parallel_conflict_line
//= require ./components/parallel_conflict_lines
$(() => {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
const conflictsEl = document.querySelector('#conflicts');
const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({
conflictsPath: conflictsEl.dataset.conflictsPath,
resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath
});
gl.MergeConflictsResolverApp = new Vue({
el: '#conflicts',
data: mergeConflictsStore.state,
components: {
'diff-file-editor': gl.mergeConflicts.diffFileEditor,
'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines,
'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines
},
computed: {
conflictsCountText() { return mergeConflictsStore.getConflictsCountText() },
readyToCommit() { return mergeConflictsStore.isReadyToCommit() },
commitButtonText() { return mergeConflictsStore.getCommitButtonText() },
showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent() }
},
created() {
mergeConflictsService
.fetchConflictsData()
.done((data) => {
if (data.type === 'error') {
mergeConflictsStore.setFailedRequest(data.message);
} else {
mergeConflictsStore.setConflictsData(data);
}
})
.error(() => {
mergeConflictsStore.setFailedRequest();
})
.always(() => {
mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => {
$(conflictsEl.querySelectorAll('.js-syntax-highlight')).syntaxHighlight();
});
});
},
methods: {
handleViewTypeChange(viewType) {
mergeConflictsStore.setViewType(viewType);
},
onClickResolveModeButton(file, mode) {
if (mode === INTERACTIVE_RESOLVE_MODE && file.resolveEditChanged) {
mergeConflictsStore.setPromptConfirmationState(file, true);
return;
}
mergeConflictsStore.setFileResolveMode(file, mode);
},
acceptDiscardConfirmation(file) {
mergeConflictsStore.setPromptConfirmationState(file, false);
mergeConflictsStore.setFileResolveMode(file, INTERACTIVE_RESOLVE_MODE);
},
cancelDiscardConfirmation(file) {
mergeConflictsStore.setPromptConfirmationState(file, false);
},
commit() {
mergeConflictsStore.setSubmitState(true);
mergeConflictsService
.submitResolveConflicts(mergeConflictsStore.getCommitData())
.done((data) => {
window.location.href = data.redirect_to;
})
.error(() => {
mergeConflictsStore.setSubmitState(false);
new Flash('Failed to save merge conflicts resolutions. Please try again!');
});
}
}
})
});
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.actions = {
methods: {
handleSelected(file, sectionId, selection) {
gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection);
}
}
};
})(window.gl || (window.gl = {}));
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.utils = {
methods: {
lineCssClass(line) {
return {
'head': line.isHead,
'origin': line.isOrigin,
'match': line.hasMatch,
'selected': line.isSelected,
'unselected': line.isUnselected
};
}
}
};
})(window.gl || (window.gl = {}));
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide') graphCollapsed ? $btnText.text('Hide') : $btnText.text('Expand')
} }
addMarginToBuildColumns() { addMarginToBuildColumns() {
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
function ProjectFindFile(element1, options) { function ProjectFindFile(element1, options) {
this.element = element1; this.element = element1;
this.options = options; this.options = options;
this.goToBlob = bind(this.goToBlob, this);
this.goToTree = bind(this.goToTree, this); this.goToTree = bind(this.goToTree, this);
this.selectRowDown = bind(this.selectRowDown, this); this.selectRowDown = bind(this.selectRowDown, this);
this.selectRowUp = bind(this.selectRowUp, this); this.selectRowUp = bind(this.selectRowUp, this);
...@@ -154,6 +155,14 @@ ...@@ -154,6 +155,14 @@
return location.href = this.options.treeUrl; return location.href = this.options.treeUrl;
}; };
ProjectFindFile.prototype.goToBlob = function() {
var $link = this.element.find(".tree-item.selected .tree-item-file-name a");
if ($link.length) {
$link.get(0).click();
}
};
return ProjectFindFile; return ProjectFindFile;
})(); })();
......
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
this.renderState(); this.renderState();
return $.ajax({ return $.ajax({
type: 'GET', type: 'GET',
url: `/u/${username}/exists`, url: `/users/${username}/exists`,
dataType: 'json', dataType: 'json',
success: (res) => this.setAvailabilityState(res.exists) success: (res) => this.setAvailabilityState(res.exists)
}); });
......
...@@ -45,40 +45,38 @@ ...@@ -45,40 +45,38 @@
} }
h1 { h1 {
font-size: 2em; font-size: 1.75em;
font-weight: 600; font-weight: 600;
margin: 1em 0 10px; margin: 16px 0 10px;
padding: 0 0 0.3em; padding: 0 0 0.3em;
border-bottom: 1px solid $btn-default-border; border-bottom: 1px solid $white-dark;
color: $gl-gray-dark; color: $gl-gray-dark;
} }
h2 { h2 {
font-size: 1.6em; font-size: 1.5em;
font-weight: 600; font-weight: 600;
margin: 1em 0 10px; margin: 16px 0 10px;
padding-bottom: 0.3em;
border-bottom: 1px solid $btn-default-border;
color: $gl-gray-dark; color: $gl-gray-dark;
} }
h3 { h3 {
margin: 1em 0 10px; margin: 16px 0 10px;
font-size: 1.4em; font-size: 1.3em;
} }
h4 { h4 {
margin: 1em 0 10px; margin: 16px 0 10px;
font-size: 1.25em; font-size: 1.2em;
} }
h5 { h5 {
margin: 1em 0 10px; margin: 16px 0 10px;
font-size: 1em; font-size: 1em;
} }
h6 { h6 {
margin: 1em 0 10px; margin: 16px 0 10px;
font-size: 0.95em; font-size: 0.95em;
} }
...@@ -87,7 +85,12 @@ ...@@ -87,7 +85,12 @@
font-size: inherit; font-size: inherit;
padding: 8px 21px; padding: 8px 21px;
margin: 12px 0; margin: 12px 0;
border-left: 3px solid #e7e9ed; border-left: 3px solid $white-dark;
}
blockquote:dir(rtl) {
border-left: 0;
border-right: 3px solid $white-dark;
} }
blockquote p { blockquote p {
...@@ -112,6 +115,10 @@ ...@@ -112,6 +115,10 @@
} }
} }
table:dir(rtl) th {
text-align: right;
}
pre { pre {
margin: 12px 0; margin: 12px 0;
font-size: 13px; font-size: 13px;
...@@ -129,6 +136,10 @@ ...@@ -129,6 +136,10 @@
margin: 3px 0 3px 28px !important; margin: 3px 0 3px 28px !important;
} }
ul:dir(rtl), ol:dir(rtl) {
margin: 3px 28px 3px 0 !important;
}
li { li {
line-height: 1.6em; line-height: 1.6em;
} }
......
...@@ -56,6 +56,7 @@ $border-gray-light: #dcdcdc; ...@@ -56,6 +56,7 @@ $border-gray-light: #dcdcdc;
$border-gray-normal: #d7d7d7; $border-gray-normal: #d7d7d7;
$border-gray-dark: #c6cacf; $border-gray-dark: #c6cacf;
$border-green-extra-light: #9adb84;
$border-green-light: #2faa60; $border-green-light: #2faa60;
$border-green-normal: #2ca05b; $border-green-normal: #2ca05b;
$border-green-dark: #279654; $border-green-dark: #279654;
......
...@@ -20,9 +20,11 @@ ...@@ -20,9 +20,11 @@
.detail-page-description { .detail-page-description {
.title { .title {
margin: 0; margin: 0 0 16px;
font-size: 23px; font-size: 2em;
color: $gl-gray-dark; color: $gl-gray-dark;
padding: 0 0 0.3em;
border-bottom: 1px solid $white-dark;
} }
.description { .description {
......
...@@ -237,4 +237,51 @@ $colors: ( ...@@ -237,4 +237,51 @@ $colors: (
.btn-success .fa-spinner { .btn-success .fa-spinner {
color: #fff; color: #fff;
} }
.editor-wrap {
&.is-loading {
.editor {
display: none;
}
.loading {
display: block;
}
}
&.saved {
.editor {
border-top: solid 2px $border-green-extra-light;
}
}
.editor {
pre {
height: 350px;
border: none;
border-radius: 0;
margin-bottom: 0;
}
}
.loading {
display: none;
}
}
.discard-changes-alert {
background-color: $background-color;
text-align: right;
padding: $gl-padding-top $gl-padding;
color: $gl-text-color;
.discard-actions {
display: inline-block;
margin-left: 10px;
}
}
.resolve-conflicts-form {
padding-top: $gl-padding;
}
} }
...@@ -429,13 +429,6 @@ ...@@ -429,13 +429,6 @@
} }
} }
.merge-request-details {
.title {
margin-bottom: 20px;
}
}
.merge-request-tabs { .merge-request-tabs {
background-color: #fff; background-color: #fff;
......
...@@ -169,4 +169,8 @@ ...@@ -169,4 +169,8 @@
margin-top: 11px; margin-top: 11px;
position: relative; position: relative;
z-index: 2; z-index: 2;
.download-button {
margin-left: $btn-side-margin;
}
} }
...@@ -118,8 +118,13 @@ class ApplicationController < ActionController::Base ...@@ -118,8 +118,13 @@ class ApplicationController < ActionController::Base
end end
def render_404 def render_404
respond_to do |format|
format.html do
render file: Rails.root.join("public", "404"), layout: false, status: "404" render file: Rails.root.join("public", "404"), layout: false, status: "404"
end end
format.any { head :not_found }
end
end
def no_cache_headers def no_cache_headers
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
......
...@@ -9,15 +9,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -9,15 +9,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check, :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines, :merge, :merge_check,
:ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
] ]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs] before_action :define_commit_vars, only: [:diffs]
before_action :define_diff_comment_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :apply_diff_view_cookie!, only: [:new_diffs] before_action :apply_diff_view_cookie!, only: [:new_diffs]
before_action :build_merge_request, only: [:new, :new_diffs] before_action :build_merge_request, only: [:new, :new_diffs]
...@@ -33,7 +33,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -33,7 +33,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authenticate_user!, only: [:assign_related_issues] before_action :authenticate_user!, only: [:assign_related_issues]
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts] before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts]
def index def index
@merge_requests = merge_requests_collection @merge_requests = merge_requests_collection
...@@ -170,6 +170,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -170,6 +170,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
end end
def conflict_for_path
return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
file = @merge_request.conflicts.file_for_path(params[:old_path], params[:new_path])
return render_404 unless file
render json: file, full_content: true
end
def resolve_conflicts def resolve_conflicts
return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui? return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
...@@ -184,7 +194,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -184,7 +194,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.' flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'
render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) } render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) }
rescue Gitlab::Conflict::File::MissingResolution => e rescue Gitlab::Conflict::ResolutionError => e
render status: :bad_request, json: { message: e.message } render status: :bad_request, json: { message: e.message }
end end
end end
......
...@@ -49,6 +49,10 @@ module Ci ...@@ -49,6 +49,10 @@ module Ci
transition any => :canceled transition any => :canceled
end end
# IMPORTANT
# Do not add any operations to this state_machine
# Create a separate worker for each new operation
before_transition [:created, :pending] => :running do |pipeline| before_transition [:created, :pending] => :running do |pipeline|
pipeline.started_at = Time.now pipeline.started_at = Time.now
end end
...@@ -62,13 +66,11 @@ module Ci ...@@ -62,13 +66,11 @@ module Ci
end end
after_transition [:created, :pending] => :running do |pipeline| after_transition [:created, :pending] => :running do |pipeline|
MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)). pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
end end
after_transition any => [:success] do |pipeline| after_transition any => [:success] do |pipeline|
MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)). pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
update_all(latest_build_finished_at: pipeline.finished_at)
end end
after_transition [:created, :pending, :running] => :success do |pipeline| after_transition [:created, :pending, :running] => :success do |pipeline|
......
...@@ -871,7 +871,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -871,7 +871,7 @@ class MergeRequest < ActiveRecord::Base
# files. # files.
conflicts.files.each(&:lines) conflicts.files.each(&:lines)
@conflicts_can_be_resolved_in_ui = conflicts.files.length > 0 @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
rescue Rugged::OdbError, Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing
@conflicts_can_be_resolved_in_ui = false @conflicts_can_be_resolved_in_ui = false
end end
end end
......
module MergeRequests module MergeRequests
class ResolveService < MergeRequests::BaseService class ResolveService < MergeRequests::BaseService
class MissingFiles < Gitlab::Conflict::ResolutionError
end
attr_accessor :conflicts, :rugged, :merge_index, :merge_request attr_accessor :conflicts, :rugged, :merge_index, :merge_request
def execute(merge_request) def execute(merge_request)
...@@ -10,8 +13,16 @@ module MergeRequests ...@@ -10,8 +13,16 @@ module MergeRequests
fetch_their_commit! fetch_their_commit!
conflicts.files.each do |file| params[:files].each do |file_params|
write_resolved_file_to_index(file, params[:sections]) conflict_file = merge_request.conflicts.file_for_path(file_params[:old_path], file_params[:new_path])
write_resolved_file_to_index(conflict_file, file_params)
end
unless merge_index.conflicts.empty?
missing_files = merge_index.conflicts.map { |file| file[:ours][:path] }
raise MissingFiles, "Missing resolutions for the following files: #{missing_files.join(', ')}"
end end
commit_params = { commit_params = {
...@@ -23,8 +34,13 @@ module MergeRequests ...@@ -23,8 +34,13 @@ module MergeRequests
project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params) project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params)
end end
def write_resolved_file_to_index(file, resolutions) def write_resolved_file_to_index(file, params)
new_file = file.resolve_lines(resolutions).map(&:text).join("\n") new_file = if params[:sections]
file.resolve_lines(params[:sections]).map(&:text).join("\n")
elsif params[:content]
file.resolve_content(params[:content])
end
our_path = file.our_path our_path = file.our_path
merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode) merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
......
- if !project.empty_repo? && can?(current_user, :download_code, project) - if !project.empty_repo? && can?(current_user, :download_code, project)
%span{class: 'hidden-xs hidden-sm'} %span{class: 'hidden-xs hidden-sm download-button'}
.dropdown.inline .dropdown.inline
%button.btn{ 'data-toggle' => 'dropdown' } %button.btn{ 'data-toggle' => 'dropdown' }
= icon('download') = icon('download')
......
- class_bindings = "{ |
'head': line.isHead, |
'origin': line.isOrigin, |
'match': line.hasMatch, |
'selected': line.isSelected, |
'unselected': line.isUnselected }"
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js')
= page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/show/mr_title" = render "projects/merge_requests/show/mr_title"
.merge-request-details.issuable-details .merge-request-details.issuable-details
...@@ -24,6 +20,21 @@ ...@@ -24,6 +20,21 @@
= render partial: "projects/merge_requests/conflicts/commit_stats" = render partial: "projects/merge_requests/conflicts/commit_stats"
.files-wrapper{"v-if" => "!isLoading && !hasError"} .files-wrapper{"v-if" => "!isLoading && !hasError"}
= render partial: "projects/merge_requests/conflicts/parallel_view", locals: { class_bindings: class_bindings } .files
= render partial: "projects/merge_requests/conflicts/inline_view", locals: { class_bindings: class_bindings } .diff-file.file-holder.conflict{"v-for" => "file in conflictsData.files"}
.file-title
%i.fa.fa-fw{":class" => "file.iconClass"}
%strong {{file.filePath}}
= render partial: 'projects/merge_requests/conflicts/file_actions'
.diff-content.diff-wrap-lines
.diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
= render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines"
.diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
= render partial: "projects/merge_requests/conflicts/components/parallel_conflict_lines"
%div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"}
= render partial: "projects/merge_requests/conflicts/components/diff_file_editor"
= render partial: "projects/merge_requests/conflicts/submit_form" = render partial: "projects/merge_requests/conflicts/submit_form"
-# Components
= render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line'
.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"} .content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"}
.inline-parallel-buttons .inline-parallel-buttons{"v-if" => "showDiffViewTypeSwitcher"}
.btn-group .btn-group
%a.btn{ | %button.btn{":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')"}
":class" => "{'active': !isParallel}", |
"@click" => "handleViewTypeChange('inline')"}
Inline Inline
%a.btn{ | %button.btn{":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')"}
":class" => "{'active': isParallel}", |
"@click" => "handleViewTypeChange('parallel')"}
Side-by-side Side-by-side
.js-toggle-container .js-toggle-container
.commit-stat-summary .commit-stat-summary
Showing Showing
%strong.cred {{conflictsCount}} {{conflictsData.conflictsText}} %strong.cred {{conflictsCountText}}
between between
%strong {{conflictsData.source_branch}} %strong {{conflictsData.sourceBranch}}
and and
%strong {{conflictsData.target_branch}} %strong {{conflictsData.targetBranch}}
.file-actions
.btn-group{"v-if" => "file.type === 'text'"}
%button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }",
'@click' => "onClickResolveModeButton(file, 'interactive')",
type: 'button' }
Interactive mode
%button.btn{ ':class' => "{ 'active': file.resolveMode == 'edit' }",
'@click' => "onClickResolveModeButton(file, 'edit')",
type: 'button' }
Edit inline
%a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
View file @{{conflictsData.shortCommitSha}}
.files{"v-show" => "!isParallel"}
.diff-file.file-holder.conflict.inline-view{"v-for" => "file in conflictsData.files"}
.file-title
%i.fa.fa-fw{":class" => "file.iconClass"}
%strong {{file.filePath}}
.file-actions
%a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
View file @{{conflictsData.shortCommitSha}}
.diff-content.diff-wrap-lines
.diff-wrap-lines.code.file-content.js-syntax-highlight
%table
%tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
%template{"v-if" => "!line.isHeader"}
%td.diff-line-num.new_line{":class" => class_bindings}
%a {{line.new_line}}
%td.diff-line-num.old_line{":class" => class_bindings}
%a {{line.old_line}}
%td.line_content{":class" => class_bindings}
{{{line.richText}}}
%template{"v-if" => "line.isHeader"}
%td.diff-line-num.header{":class" => class_bindings}
%td.diff-line-num.header{":class" => class_bindings}
%td.line_content.header{":class" => class_bindings}
%strong {{{line.richText}}}
%button.btn{"@click" => "handleSelected(line.id, line.section)"}
{{line.buttonTitle}}
.files{"v-show" => "isParallel"}
.diff-file.file-holder.conflict.parallel-view{"v-for" => "file in conflictsData.files"}
.file-title
%i.fa.fa-fw{":class" => "file.iconClass"}
%strong {{file.filePath}}
.file-actions
%a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
View file @{{conflictsData.shortCommitSha}}
.diff-content.diff-wrap-lines
.diff-wrap-lines.code.file-content.js-syntax-highlight
%table
%tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
%template{"v-for" => "line in section"}
%template{"v-if" => "line.isHeader"}
%td.diff-line-num.header{":class" => class_bindings}
%td.line_content.header{":class" => class_bindings}
%strong {{line.richText}}
%button.btn{"@click" => "handleSelected(line.id, line.section)"}
{{line.buttonTitle}}
%template{"v-if" => "!line.isHeader"}
%td.diff-line-num.old_line{":class" => class_bindings}
{{line.lineNumber}}
%td.line_content.parallel{":class" => class_bindings}
{{{line.richText}}}
.content-block.oneline-block.files-changed .form-horizontal.resolve-conflicts-form
%strong.resolved-count {{resolvedCount}} .form-group
of %label.col-sm-2.control-label{ "for" => "commit-message" }
%strong.total-count {{conflictsCount}} Commit message
conflicts have been resolved .col-sm-10
.commit-message-container
.commit-message-container.form-group
.max-width-marker .max-width-marker
%textarea.form-control.js-commit-message{"v-model" => "conflictsData.commitMessage"} %textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" }
{{{conflictsData.commitMessage}}} .form-group
.col-sm-offset-2.col-sm-10
%button{type: "button", class: "btn btn-success js-submit-button", ":disabled" => "!readyToCommit", "@click" => "commit()"} .row
.col-xs-6
%button{ type: "button", class: "btn btn-success js-submit-button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
%span {{commitButtonText}} %span {{commitButtonText}}
.col-xs-6.text-right
= link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel" = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel"
%diff-file-editor{"inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation"}
.diff-editor-wrap{ "v-show" => "file.showEditor" }
.discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" }
.discard-changes-alert
Are you sure you want to discard your changes?
.discard-actions
%button.btn.btn-sm.btn-close{ "@click" => "acceptDiscardConfirmation(file)" } Discard changes
%button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel
.editor-wrap{ ":class" => "classObject" }
.loading
%i.fa.fa-spinner.fa-spin
.editor
%pre{ "style" => "height: 350px" }
%inline-conflict-lines{ "inline-template" => "true", ":file" => "file"}
%table
%tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
%td.diff-line-num.new_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
%a {{line.new_line}}
%td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
%a {{line.old_line}}
%td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
{{{line.richText}}}
%td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%strong {{{line.richText}}}
%button.btn{ "@click" => "handleSelected(file, line.id, line.section)" }
{{line.buttonTitle}}
%script{"id" => 'parallel-conflict-line', "type" => "text/x-template"}
%td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%strong {{line.richText}}
%button.btn{"@click" => "handleSelected(file, line.id, line.section)"}
{{line.buttonTitle}}
%td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
{{line.lineNumber}}
%td.line_content.parallel{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
{{{line.richText}}}
%parallel-conflict-lines{"inline-template" => "true", ":file" => "file"}
%table
%tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
%td{"is"=>"parallel-conflict-line", "v-for" => "line in section", ":line" => "line", ":file" => "file"}
...@@ -171,5 +171,5 @@ ...@@ -171,5 +171,5 @@
new LabelsSelect(); new LabelsSelect();
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
new Subscription('.subscription') new Subscription('.subscription')
new DueDateSelect(); new gl.DueDateSelectors();
sidebar = new Sidebar(); sidebar = new Sidebar();
class PipelineMetricsWorker
include Sidekiq::Worker
sidekiq_options queue: :default
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
update_metrics_for_active_pipeline(pipeline) if pipeline.active?
update_metrics_for_succeeded_pipeline(pipeline) if pipeline.success?
end
end
private
def update_metrics_for_active_pipeline(pipeline)
metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
end
def update_metrics_for_succeeded_pipeline(pipeline)
metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at)
end
def metrics(pipeline)
MergeRequest::Metrics.where(merge_request_id: merge_requests(pipeline))
end
def merge_requests(pipeline)
pipeline.merge_requests.map(&:id)
end
end
...@@ -89,6 +89,7 @@ module Gitlab ...@@ -89,6 +89,7 @@ module Gitlab
config.assets.precompile << "profile/profile_bundle.js" config.assets.precompile << "profile/profile_bundle.js"
config.assets.precompile << "diff_notes/diff_notes_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js"
config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "boards/boards_bundle.js"
config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
config.assets.precompile << "boards/test_utils/simulate_drag.js" config.assets.precompile << "boards/test_utils/simulate_drag.js"
config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js"
config.assets.precompile << "snippet/snippet_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js"
......
...@@ -67,6 +67,7 @@ if Gitlab::Metrics.enabled? ...@@ -67,6 +67,7 @@ if Gitlab::Metrics.enabled?
['app', 'finders'] => ['app', 'finders'], ['app', 'finders'] => ['app', 'finders'],
['app', 'mailers', 'emails'] => ['app', 'mailers'], ['app', 'mailers', 'emails'] => ['app', 'mailers'],
['app', 'services', '**'] => ['app', 'services'], ['app', 'services', '**'] => ['app', 'services'],
['lib', 'gitlab', 'conflicts'] => ['lib'],
['lib', 'gitlab', 'diff'] => ['lib'], ['lib', 'gitlab', 'diff'] => ['lib'],
['lib', 'gitlab', 'email', 'message'] => ['lib'], ['lib', 'gitlab', 'email', 'message'] => ['lib'],
['lib', 'gitlab', 'checks'] => ['lib'] ['lib', 'gitlab', 'checks'] => ['lib']
......
...@@ -267,6 +267,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: ...@@ -267,6 +267,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
get :commits get :commits
get :diffs get :diffs
get :conflicts get :conflicts
get :conflict_for_path
get :builds get :builds
get :pipelines get :pipelines
get :merge_check get :merge_check
......
require 'constraints/user_url_constrainer' require 'constraints/user_url_constrainer'
get '/u/:username', to: redirect('/%{username}'),
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
registrations: :registrations, registrations: :registrations,
passwords: :passwords, passwords: :passwords,
...@@ -23,7 +20,7 @@ constraints(UserUrlConstrainer.new) do ...@@ -23,7 +20,7 @@ constraints(UserUrlConstrainer.new) do
end end
end end
scope(path: 'u/:username', scope(path: 'users/:username',
as: :user, as: :user,
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
controller: :users) do controller: :users) do
...@@ -36,3 +33,12 @@ scope(path: 'u/:username', ...@@ -36,3 +33,12 @@ scope(path: 'u/:username',
get :exists get :exists
get '/', to: redirect('/%{username}') get '/', to: redirect('/%{username}')
end end
# Compatibility with old routing
# TODO (dzaporozhets): remove in 10.0
get '/u/:username', to: redirect('/%{username}'), constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
# TODO (dzaporozhets): remove in 9.0
get '/u/:username/groups', to: redirect('/users/%{username}/groups'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
get '/u/:username/projects', to: redirect('/users/%{username}/projects'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
get '/u/:username/snippets', to: redirect('/users/%{username}/snippets'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
get '/u/:username/contributed', to: redirect('/users/%{username}/contributed'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
...@@ -43,7 +43,7 @@ Example Response: ...@@ -43,7 +43,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/root" "web_url": "http://gitlab.example.com/root"
}, },
"created_at": "2016-06-15T10:09:34.206Z", "created_at": "2016-06-15T10:09:34.206Z",
"updated_at": "2016-06-15T10:09:34.206Z", "updated_at": "2016-06-15T10:09:34.206Z",
...@@ -59,7 +59,7 @@ Example Response: ...@@ -59,7 +59,7 @@ Example Response:
"id": 26, "id": 26,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/user4" "web_url": "http://gitlab.example.com/user4"
}, },
"created_at": "2016-06-15T10:09:34.177Z", "created_at": "2016-06-15T10:09:34.177Z",
"updated_at": "2016-06-15T10:09:34.177Z", "updated_at": "2016-06-15T10:09:34.177Z",
...@@ -103,7 +103,7 @@ Example Response: ...@@ -103,7 +103,7 @@ Example Response:
"id": 26, "id": 26,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/user4" "web_url": "http://gitlab.example.com/user4"
}, },
"created_at": "2016-06-15T10:09:34.177Z", "created_at": "2016-06-15T10:09:34.177Z",
"updated_at": "2016-06-15T10:09:34.177Z", "updated_at": "2016-06-15T10:09:34.177Z",
...@@ -146,7 +146,7 @@ Example Response: ...@@ -146,7 +146,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/root" "web_url": "http://gitlab.example.com/root"
}, },
"created_at": "2016-06-17T17:47:29.266Z", "created_at": "2016-06-17T17:47:29.266Z",
"updated_at": "2016-06-17T17:47:29.266Z", "updated_at": "2016-06-17T17:47:29.266Z",
...@@ -190,7 +190,7 @@ Example Response: ...@@ -190,7 +190,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/root" "web_url": "http://gitlab.example.com/root"
}, },
"created_at": "2016-06-17T17:47:29.266Z", "created_at": "2016-06-17T17:47:29.266Z",
"updated_at": "2016-06-17T17:47:29.266Z", "updated_at": "2016-06-17T17:47:29.266Z",
...@@ -238,7 +238,7 @@ Example Response: ...@@ -238,7 +238,7 @@ Example Response:
"id": 26, "id": 26,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/user4" "web_url": "http://gitlab.example.com/user4"
}, },
"created_at": "2016-06-15T10:09:34.197Z", "created_at": "2016-06-15T10:09:34.197Z",
"updated_at": "2016-06-15T10:09:34.197Z", "updated_at": "2016-06-15T10:09:34.197Z",
...@@ -279,7 +279,7 @@ Example Response: ...@@ -279,7 +279,7 @@ Example Response:
"id": 26, "id": 26,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/user4" "web_url": "http://gitlab.example.com/user4"
}, },
"created_at": "2016-06-15T10:09:34.197Z", "created_at": "2016-06-15T10:09:34.197Z",
"updated_at": "2016-06-15T10:09:34.197Z", "updated_at": "2016-06-15T10:09:34.197Z",
...@@ -319,7 +319,7 @@ Example Response: ...@@ -319,7 +319,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/root" "web_url": "http://gitlab.example.com/root"
}, },
"created_at": "2016-06-17T19:59:55.888Z", "created_at": "2016-06-17T19:59:55.888Z",
"updated_at": "2016-06-17T19:59:55.888Z", "updated_at": "2016-06-17T19:59:55.888Z",
...@@ -362,7 +362,7 @@ Example Response: ...@@ -362,7 +362,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/root" "web_url": "http://gitlab.example.com/root"
}, },
"created_at": "2016-06-17T19:59:55.888Z", "created_at": "2016-06-17T19:59:55.888Z",
"updated_at": "2016-06-17T19:59:55.888Z", "updated_at": "2016-06-17T19:59:55.888Z",
......
...@@ -64,7 +64,7 @@ Example of response ...@@ -64,7 +64,7 @@ Example of response
"state": "active", "state": "active",
"twitter": "", "twitter": "",
"username": "root", "username": "root",
"web_url": "http://gitlab.dev/u/root", "web_url": "http://gitlab.dev/root",
"website_url": "" "website_url": ""
} }
}, },
...@@ -108,7 +108,7 @@ Example of response ...@@ -108,7 +108,7 @@ Example of response
"state": "active", "state": "active",
"twitter": "", "twitter": "",
"username": "root", "username": "root",
"web_url": "http://gitlab.dev/u/root", "web_url": "http://gitlab.dev/root",
"website_url": "" "website_url": ""
} }
} }
...@@ -212,7 +212,7 @@ Example of response ...@@ -212,7 +212,7 @@ Example of response
"state": "active", "state": "active",
"twitter": "", "twitter": "",
"username": "root", "username": "root",
"web_url": "http://gitlab.dev/u/root", "web_url": "http://gitlab.dev/root",
"website_url": "" "website_url": ""
} }
} }
...@@ -279,7 +279,7 @@ Example of response ...@@ -279,7 +279,7 @@ Example of response
"state": "active", "state": "active",
"twitter": "", "twitter": "",
"username": "root", "username": "root",
"web_url": "http://gitlab.dev/u/root", "web_url": "http://gitlab.dev/root",
"website_url": "" "website_url": ""
} }
} }
......
...@@ -288,7 +288,7 @@ Example response: ...@@ -288,7 +288,7 @@ Example response:
```json ```json
{ {
"author" : { "author" : {
"web_url" : "https://gitlab.example.com/u/thedude", "web_url" : "https://gitlab.example.com/thedude",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
"username" : "thedude", "username" : "thedude",
"state" : "active", "state" : "active",
...@@ -343,7 +343,7 @@ Example response: ...@@ -343,7 +343,7 @@ Example response:
"author" : { "author" : {
"username" : "thedude", "username" : "thedude",
"state" : "active", "state" : "active",
"web_url" : "https://gitlab.example.com/u/thedude", "web_url" : "https://gitlab.example.com/thedude",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
"id" : 28, "id" : 28,
"name" : "Jeff Lebowski" "name" : "Jeff Lebowski"
...@@ -370,7 +370,7 @@ Example response: ...@@ -370,7 +370,7 @@ Example response:
"id" : 28, "id" : 28,
"name" : "Jeff Lebowski", "name" : "Jeff Lebowski",
"username" : "thedude", "username" : "thedude",
"web_url" : "https://gitlab.example.com/u/thedude", "web_url" : "https://gitlab.example.com/thedude",
"state" : "active", "state" : "active",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png" "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png"
}, },
...@@ -408,7 +408,7 @@ Example response: ...@@ -408,7 +408,7 @@ Example response:
```json ```json
{ {
"author" : { "author" : {
"web_url" : "https://gitlab.example.com/u/thedude", "web_url" : "https://gitlab.example.com/thedude",
"name" : "Jeff Lebowski", "name" : "Jeff Lebowski",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
"username" : "thedude", "username" : "thedude",
......
...@@ -56,7 +56,7 @@ Example of response ...@@ -56,7 +56,7 @@ Example of response
"state": "active", "state": "active",
"twitter": "", "twitter": "",
"username": "root", "username": "root",
"web_url": "http://localhost:3000/u/root", "web_url": "http://localhost:3000/root",
"website_url": "" "website_url": ""
} }
}, },
...@@ -75,7 +75,7 @@ Example of response ...@@ -75,7 +75,7 @@ Example of response
"name": "Administrator", "name": "Administrator",
"state": "active", "state": "active",
"username": "root", "username": "root",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
} }
}, },
{ {
...@@ -114,7 +114,7 @@ Example of response ...@@ -114,7 +114,7 @@ Example of response
"state": "active", "state": "active",
"twitter": "", "twitter": "",
"username": "root", "username": "root",
"web_url": "http://localhost:3000/u/root", "web_url": "http://localhost:3000/root",
"website_url": "" "website_url": ""
} }
}, },
...@@ -133,7 +133,7 @@ Example of response ...@@ -133,7 +133,7 @@ Example of response
"name": "Administrator", "name": "Administrator",
"state": "active", "state": "active",
"username": "root", "username": "root",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
} }
} }
] ]
...@@ -169,7 +169,7 @@ Example of response ...@@ -169,7 +169,7 @@ Example of response
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"environment": { "environment": {
"id": 9, "id": 9,
...@@ -193,7 +193,7 @@ Example of response ...@@ -193,7 +193,7 @@ Example of response
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root", "web_url": "http://localhost:3000/root",
"created_at": "2016-08-11T07:09:20.351Z", "created_at": "2016-08-11T07:09:20.351Z",
"is_admin": true, "is_admin": true,
"bio": null, "bio": null,
......
...@@ -46,7 +46,7 @@ Example response: ...@@ -46,7 +46,7 @@ Example response:
"author" : { "author" : {
"state" : "active", "state" : "active",
"id" : 18, "id" : 18,
"web_url" : "https://gitlab.example.com/u/eileen.lowe", "web_url" : "https://gitlab.example.com/eileen.lowe",
"name" : "Alexandra Bashirian", "name" : "Alexandra Bashirian",
"avatar_url" : null, "avatar_url" : null,
"username" : "eileen.lowe" "username" : "eileen.lowe"
...@@ -67,7 +67,7 @@ Example response: ...@@ -67,7 +67,7 @@ Example response:
"state" : "active", "state" : "active",
"id" : 1, "id" : 1,
"name" : "Administrator", "name" : "Administrator",
"web_url" : "https://gitlab.example.com/u/root", "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null, "avatar_url" : null,
"username" : "root" "username" : "root"
}, },
...@@ -134,7 +134,7 @@ Example response: ...@@ -134,7 +134,7 @@ Example response:
}, },
"author" : { "author" : {
"state" : "active", "state" : "active",
"web_url" : "https://gitlab.example.com/u/root", "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null, "avatar_url" : null,
"username" : "root", "username" : "root",
"id" : 1, "id" : 1,
...@@ -145,7 +145,7 @@ Example response: ...@@ -145,7 +145,7 @@ Example response:
"iid" : 1, "iid" : 1,
"assignee" : { "assignee" : {
"avatar_url" : null, "avatar_url" : null,
"web_url" : "https://gitlab.example.com/u/lennie", "web_url" : "https://gitlab.example.com/lennie",
"state" : "active", "state" : "active",
"username" : "lennie", "username" : "lennie",
"id" : 9, "id" : 9,
...@@ -215,7 +215,7 @@ Example response: ...@@ -215,7 +215,7 @@ Example response:
}, },
"author" : { "author" : {
"state" : "active", "state" : "active",
"web_url" : "https://gitlab.example.com/u/root", "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null, "avatar_url" : null,
"username" : "root", "username" : "root",
"id" : 1, "id" : 1,
...@@ -226,7 +226,7 @@ Example response: ...@@ -226,7 +226,7 @@ Example response:
"iid" : 1, "iid" : 1,
"assignee" : { "assignee" : {
"avatar_url" : null, "avatar_url" : null,
"web_url" : "https://gitlab.example.com/u/lennie", "web_url" : "https://gitlab.example.com/lennie",
"state" : "active", "state" : "active",
"username" : "lennie", "username" : "lennie",
"id" : 9, "id" : 9,
...@@ -281,7 +281,7 @@ Example response: ...@@ -281,7 +281,7 @@ Example response:
}, },
"author" : { "author" : {
"state" : "active", "state" : "active",
"web_url" : "https://gitlab.example.com/u/root", "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null, "avatar_url" : null,
"username" : "root", "username" : "root",
"id" : 1, "id" : 1,
...@@ -292,7 +292,7 @@ Example response: ...@@ -292,7 +292,7 @@ Example response:
"iid" : 1, "iid" : 1,
"assignee" : { "assignee" : {
"avatar_url" : null, "avatar_url" : null,
"web_url" : "https://gitlab.example.com/u/lennie", "web_url" : "https://gitlab.example.com/lennie",
"state" : "active", "state" : "active",
"username" : "lennie", "username" : "lennie",
"id" : 9, "id" : 9,
...@@ -357,7 +357,7 @@ Example response: ...@@ -357,7 +357,7 @@ Example response:
"name" : "Alexandra Bashirian", "name" : "Alexandra Bashirian",
"avatar_url" : null, "avatar_url" : null,
"state" : "active", "state" : "active",
"web_url" : "https://gitlab.example.com/u/eileen.lowe", "web_url" : "https://gitlab.example.com/eileen.lowe",
"id" : 18, "id" : 18,
"username" : "eileen.lowe" "username" : "eileen.lowe"
}, },
...@@ -414,7 +414,7 @@ Example response: ...@@ -414,7 +414,7 @@ Example response:
"username" : "eileen.lowe", "username" : "eileen.lowe",
"id" : 18, "id" : 18,
"state" : "active", "state" : "active",
"web_url" : "https://gitlab.example.com/u/eileen.lowe" "web_url" : "https://gitlab.example.com/eileen.lowe"
}, },
"state" : "closed", "state" : "closed",
"title" : "Issues with auth", "title" : "Issues with auth",
...@@ -500,7 +500,7 @@ Example response: ...@@ -500,7 +500,7 @@ Example response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/axel.block" "web_url": "https://gitlab.example.com/axel.block"
}, },
"author": { "author": {
"name": "Kris Steuber", "name": "Kris Steuber",
...@@ -508,7 +508,7 @@ Example response: ...@@ -508,7 +508,7 @@ Example response:
"id": 10, "id": 10,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/solon.cremin" "web_url": "https://gitlab.example.com/solon.cremin"
}, },
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/11", "web_url": "http://example.com/example/example/issues/11",
...@@ -557,7 +557,7 @@ Example response: ...@@ -557,7 +557,7 @@ Example response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/axel.block" "web_url": "https://gitlab.example.com/axel.block"
}, },
"author": { "author": {
"name": "Kris Steuber", "name": "Kris Steuber",
...@@ -565,7 +565,7 @@ Example response: ...@@ -565,7 +565,7 @@ Example response:
"id": 10, "id": 10,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/solon.cremin" "web_url": "https://gitlab.example.com/solon.cremin"
}, },
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/11", "web_url": "http://example.com/example/example/issues/11",
...@@ -614,7 +614,7 @@ Example response: ...@@ -614,7 +614,7 @@ Example response:
"id": 21, "id": 21,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/keyon" "web_url": "https://gitlab.example.com/keyon"
}, },
"author": { "author": {
"name": "Vivian Hermann", "name": "Vivian Hermann",
...@@ -622,7 +622,7 @@ Example response: ...@@ -622,7 +622,7 @@ Example response:
"id": 11, "id": 11,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/orville" "web_url": "https://gitlab.example.com/orville"
}, },
"subscribed": false, "subscribed": false,
"due_date": null, "due_date": null,
...@@ -669,7 +669,7 @@ Example response: ...@@ -669,7 +669,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"action_name": "marked", "action_name": "marked",
"target_type": "Issue", "target_type": "Issue",
...@@ -700,7 +700,7 @@ Example response: ...@@ -700,7 +700,7 @@ Example response:
"id": 14, "id": 14,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/francisca" "web_url": "https://gitlab.example.com/francisca"
}, },
"author": { "author": {
"name": "Maxie Medhurst", "name": "Maxie Medhurst",
...@@ -708,7 +708,7 @@ Example response: ...@@ -708,7 +708,7 @@ Example response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/craig_rutherford" "web_url": "https://gitlab.example.com/craig_rutherford"
}, },
"subscribed": true, "subscribed": true,
"user_notes_count": 7, "user_notes_count": 7,
......
...@@ -24,7 +24,7 @@ Parameters: ...@@ -24,7 +24,7 @@ Parameters:
"id": 25, "id": 25,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/cfa35b8cd2ec278026357769582fa563?s=40\u0026d=identicon", "avatar_url": "http://www.gravatar.com/avatar/cfa35b8cd2ec278026357769582fa563?s=40\u0026d=identicon",
"web_url": "http://localhost:3000/u/john_smith", "web_url": "http://localhost:3000/john_smith",
"created_at": "2015-09-03T07:24:01.670Z", "created_at": "2015-09-03T07:24:01.670Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
......
...@@ -621,7 +621,7 @@ Example response when the GitLab issue tracker is used: ...@@ -621,7 +621,7 @@ Example response when the GitLab issue tracker is used:
"author" : { "author" : {
"state" : "active", "state" : "active",
"id" : 18, "id" : 18,
"web_url" : "https://gitlab.example.com/u/eileen.lowe", "web_url" : "https://gitlab.example.com/eileen.lowe",
"name" : "Alexandra Bashirian", "name" : "Alexandra Bashirian",
"avatar_url" : null, "avatar_url" : null,
"username" : "eileen.lowe" "username" : "eileen.lowe"
...@@ -642,7 +642,7 @@ Example response when the GitLab issue tracker is used: ...@@ -642,7 +642,7 @@ Example response when the GitLab issue tracker is used:
"state" : "active", "state" : "active",
"id" : 1, "id" : 1,
"name" : "Administrator", "name" : "Administrator",
"web_url" : "https://gitlab.example.com/u/root", "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null, "avatar_url" : null,
"username" : "root" "username" : "root"
}, },
...@@ -711,7 +711,7 @@ Example response: ...@@ -711,7 +711,7 @@ Example response:
"id": 19, "id": 19,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/leila" "web_url": "https://gitlab.example.com/leila"
}, },
"assignee": { "assignee": {
"name": "Celine Wehner", "name": "Celine Wehner",
...@@ -719,7 +719,7 @@ Example response: ...@@ -719,7 +719,7 @@ Example response:
"id": 16, "id": 16,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/carli" "web_url": "https://gitlab.example.com/carli"
}, },
"source_project_id": 5, "source_project_id": 5,
"target_project_id": 5, "target_project_id": 5,
...@@ -787,7 +787,7 @@ Example response: ...@@ -787,7 +787,7 @@ Example response:
"id": 19, "id": 19,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/leila" "web_url": "https://gitlab.example.com/leila"
}, },
"assignee": { "assignee": {
"name": "Celine Wehner", "name": "Celine Wehner",
...@@ -795,7 +795,7 @@ Example response: ...@@ -795,7 +795,7 @@ Example response:
"id": 16, "id": 16,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/carli" "web_url": "https://gitlab.example.com/carli"
}, },
"source_project_id": 5, "source_project_id": 5,
"target_project_id": 5, "target_project_id": 5,
...@@ -858,7 +858,7 @@ Example response: ...@@ -858,7 +858,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"action_name": "marked", "action_name": "marked",
"target_type": "MergeRequest", "target_type": "MergeRequest",
...@@ -881,7 +881,7 @@ Example response: ...@@ -881,7 +881,7 @@ Example response:
"id": 14, "id": 14,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/francisca" "web_url": "https://gitlab.example.com/francisca"
}, },
"assignee": { "assignee": {
"name": "Dr. Gabrielle Strosin", "name": "Dr. Gabrielle Strosin",
...@@ -889,7 +889,7 @@ Example response: ...@@ -889,7 +889,7 @@ Example response:
"id": 4, "id": 4,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/733005fcd7e6df12d2d8580171ccb966?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/733005fcd7e6df12d2d8580171ccb966?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/barrett.krajcik" "web_url": "https://gitlab.example.com/barrett.krajcik"
}, },
"source_project_id": 3, "source_project_id": 3,
"target_project_id": 3, "target_project_id": 3,
......
...@@ -143,7 +143,7 @@ Example Response: ...@@ -143,7 +143,7 @@ Example Response:
"state": "active", "state": "active",
"created_at": "2013-09-30T13:46:01Z", "created_at": "2013-09-30T13:46:01Z",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/pipin" "web_url": "https://gitlab.example.com/pipin"
}, },
"created_at": "2016-04-05T22:10:44.164Z", "created_at": "2016-04-05T22:10:44.164Z",
"system": false, "system": false,
...@@ -268,7 +268,7 @@ Example Response: ...@@ -268,7 +268,7 @@ Example Response:
"state": "active", "state": "active",
"created_at": "2013-09-30T13:46:01Z", "created_at": "2013-09-30T13:46:01Z",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/pipin" "web_url": "https://gitlab.example.com/pipin"
}, },
"created_at": "2016-04-06T16:51:53.239Z", "created_at": "2016-04-06T16:51:53.239Z",
"system": false, "system": false,
...@@ -398,7 +398,7 @@ Example Response: ...@@ -398,7 +398,7 @@ Example Response:
"state": "active", "state": "active",
"created_at": "2013-09-30T13:46:01Z", "created_at": "2013-09-30T13:46:01Z",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/pipin" "web_url": "https://gitlab.example.com/pipin"
}, },
"created_at": "2016-04-05T22:11:59.923Z", "created_at": "2016-04-05T22:11:59.923Z",
"system": false, "system": false,
......
...@@ -34,7 +34,7 @@ Example of response ...@@ -34,7 +34,7 @@ Example of response
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2016-08-16T10:23:19.007Z", "created_at": "2016-08-16T10:23:19.007Z",
"updated_at": "2016-08-16T10:23:19.216Z", "updated_at": "2016-08-16T10:23:19.216Z",
...@@ -57,7 +57,7 @@ Example of response ...@@ -57,7 +57,7 @@ Example of response
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2016-08-16T10:23:21.184Z", "created_at": "2016-08-16T10:23:21.184Z",
"updated_at": "2016-08-16T10:23:21.314Z", "updated_at": "2016-08-16T10:23:21.314Z",
...@@ -103,7 +103,7 @@ Example of response ...@@ -103,7 +103,7 @@ Example of response
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2016-08-11T11:28:34.085Z", "created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z", "updated_at": "2016-08-11T11:32:35.169Z",
...@@ -148,7 +148,7 @@ Response: ...@@ -148,7 +148,7 @@ Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2016-08-11T11:28:34.085Z", "created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z", "updated_at": "2016-08-11T11:32:35.169Z",
...@@ -193,7 +193,7 @@ Response: ...@@ -193,7 +193,7 @@ Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2016-08-11T11:28:34.085Z", "created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z", "updated_at": "2016-08-11T11:32:35.169Z",
......
...@@ -465,7 +465,7 @@ Parameters: ...@@ -465,7 +465,7 @@ Parameters:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "root" "author_username": "root"
}, },
...@@ -482,7 +482,7 @@ Parameters: ...@@ -482,7 +482,7 @@ Parameters:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "john", "author_username": "john",
"data": { "data": {
...@@ -528,7 +528,7 @@ Parameters: ...@@ -528,7 +528,7 @@ Parameters:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "root" "author_username": "root"
}, },
...@@ -552,7 +552,7 @@ Parameters: ...@@ -552,7 +552,7 @@ Parameters:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2015-12-04T10:33:56.698Z", "created_at": "2015-12-04T10:33:56.698Z",
"system": false, "system": false,
...@@ -567,7 +567,7 @@ Parameters: ...@@ -567,7 +567,7 @@ Parameters:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "root" "author_username": "root"
} }
......
...@@ -44,7 +44,7 @@ Example Response: ...@@ -44,7 +44,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"action_name": "marked", "action_name": "marked",
"target_type": "MergeRequest", "target_type": "MergeRequest",
...@@ -67,7 +67,7 @@ Example Response: ...@@ -67,7 +67,7 @@ Example Response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/craig_rutherford" "web_url": "https://gitlab.example.com/craig_rutherford"
}, },
"assignee": { "assignee": {
"name": "Administrator", "name": "Administrator",
...@@ -75,7 +75,7 @@ Example Response: ...@@ -75,7 +75,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"source_project_id": 2, "source_project_id": 2,
"target_project_id": 2, "target_project_id": 2,
...@@ -117,7 +117,7 @@ Example Response: ...@@ -117,7 +117,7 @@ Example Response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/craig_rutherford" "web_url": "https://gitlab.example.com/craig_rutherford"
}, },
"action_name": "assigned", "action_name": "assigned",
"target_type": "MergeRequest", "target_type": "MergeRequest",
...@@ -140,7 +140,7 @@ Example Response: ...@@ -140,7 +140,7 @@ Example Response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/craig_rutherford" "web_url": "https://gitlab.example.com/craig_rutherford"
}, },
"assignee": { "assignee": {
"name": "Administrator", "name": "Administrator",
...@@ -148,7 +148,7 @@ Example Response: ...@@ -148,7 +148,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"source_project_id": 2, "source_project_id": 2,
"target_project_id": 2, "target_project_id": 2,
...@@ -215,7 +215,7 @@ Example Response: ...@@ -215,7 +215,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"action_name": "marked", "action_name": "marked",
"target_type": "MergeRequest", "target_type": "MergeRequest",
...@@ -238,7 +238,7 @@ Example Response: ...@@ -238,7 +238,7 @@ Example Response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/craig_rutherford" "web_url": "https://gitlab.example.com/craig_rutherford"
}, },
"assignee": { "assignee": {
"name": "Administrator", "name": "Administrator",
...@@ -246,7 +246,7 @@ Example Response: ...@@ -246,7 +246,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"source_project_id": 2, "source_project_id": 2,
"target_project_id": 2, "target_project_id": 2,
......
...@@ -20,7 +20,7 @@ GET /users ...@@ -20,7 +20,7 @@ GET /users
"name": "John Smith", "name": "John Smith",
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
"web_url": "http://localhost:3000/u/john_smith" "web_url": "http://localhost:3000/john_smith"
}, },
{ {
"id": 2, "id": 2,
...@@ -28,7 +28,7 @@ GET /users ...@@ -28,7 +28,7 @@ GET /users
"name": "Jack Smith", "name": "Jack Smith",
"state": "blocked", "state": "blocked",
"avatar_url": "http://gravatar.com/../e32131cd8.jpeg", "avatar_url": "http://gravatar.com/../e32131cd8.jpeg",
"web_url": "http://localhost:3000/u/jack_smith" "web_url": "http://localhost:3000/jack_smith"
} }
] ]
``` ```
...@@ -48,7 +48,7 @@ GET /users ...@@ -48,7 +48,7 @@ GET /users
"name": "John Smith", "name": "John Smith",
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
"web_url": "http://localhost:3000/u/john_smith", "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
...@@ -81,7 +81,7 @@ GET /users ...@@ -81,7 +81,7 @@ GET /users
"name": "Jack Smith", "name": "Jack Smith",
"state": "blocked", "state": "blocked",
"avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg", "avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg",
"web_url": "http://localhost:3000/u/jack_smith", "web_url": "http://localhost:3000/jack_smith",
"created_at": "2012-05-23T08:01:01Z", "created_at": "2012-05-23T08:01:01Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
...@@ -141,7 +141,7 @@ Parameters: ...@@ -141,7 +141,7 @@ Parameters:
"name": "John Smith", "name": "John Smith",
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
"web_url": "http://localhost:3000/u/john_smith", "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
...@@ -172,7 +172,7 @@ Parameters: ...@@ -172,7 +172,7 @@ Parameters:
"name": "John Smith", "name": "John Smith",
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
"web_url": "http://localhost:3000/u/john_smith", "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
...@@ -293,7 +293,7 @@ GET /user ...@@ -293,7 +293,7 @@ GET /user
"name": "John Smith", "name": "John Smith",
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
"web_url": "http://localhost:3000/u/john_smith", "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
...@@ -665,7 +665,7 @@ Example response: ...@@ -665,7 +665,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "root" "author_username": "root"
}, },
...@@ -682,7 +682,7 @@ Example response: ...@@ -682,7 +682,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "john", "author_username": "john",
"data": { "data": {
...@@ -728,7 +728,7 @@ Example response: ...@@ -728,7 +728,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "root" "author_username": "root"
}, },
...@@ -752,7 +752,7 @@ Example response: ...@@ -752,7 +752,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2015-12-04T10:33:56.698Z", "created_at": "2015-12-04T10:33:56.698Z",
"system": false, "system": false,
...@@ -767,7 +767,7 @@ Example response: ...@@ -767,7 +767,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "root" "author_username": "root"
} }
......
...@@ -254,5 +254,5 @@ impact on runtime performance, and as such, using a constant instead of ...@@ -254,5 +254,5 @@ impact on runtime performance, and as such, using a constant instead of
referencing an object directly may even slow code down. referencing an object directly may even slow code down.
[#15607]: https://gitlab.com/gitlab-org/gitlab-ce/issues/15607 [#15607]: https://gitlab.com/gitlab-org/gitlab-ce/issues/15607
[yorickpeterse]: https://gitlab.com/u/yorickpeterse [yorickpeterse]: https://gitlab.com/yorickpeterse
[anti-pattern]: https://en.wikipedia.org/wiki/Anti-pattern [anti-pattern]: https://en.wikipedia.org/wiki/Anti-pattern
doc/raketasks/backup_hrz.png

8.7 KB | W: | H:

doc/raketasks/backup_hrz.png

31 KB | W: | H:

doc/raketasks/backup_hrz.png
doc/raketasks/backup_hrz.png
doc/raketasks/backup_hrz.png
doc/raketasks/backup_hrz.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -128,6 +128,7 @@ Feature: Explore Projects ...@@ -128,6 +128,7 @@ Feature: Explore Projects
And project "Archive" has comments And project "Archive" has comments
And I sign in as a user And I sign in as a user
And project "Community" has comments And project "Community" has comments
And trending projects are refreshed
When I visit the explore trending projects When I visit the explore trending projects
Then I should see project "Community" Then I should see project "Community"
And I should not see project "Internal" And I should not see project "Internal"
......
...@@ -21,7 +21,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps ...@@ -21,7 +21,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(response_headers['Content-Type']).to have_content("application/atom+xml") expect(response_headers['Content-Type']).to have_content("application/atom+xml")
expect(body).to have_selector("title", text: "#{@project.name}:master commits") expect(body).to have_selector("title", text: "#{@project.name}:master commits")
expect(body).to have_selector("author email", text: commit.author_email) expect(body).to have_selector("author email", text: commit.author_email)
expect(body).to have_selector("entry summary", text: commit.description[0..10]) expect(body).to have_selector("entry summary", text: commit.description[0..10].delete("\r"))
end end
step 'I click on tag link' do step 'I click on tag link' do
......
...@@ -512,6 +512,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -512,6 +512,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see new target branch changes' do step 'I should see new target branch changes' do
expect(page).to have_content 'Request to merge fix into feature' expect(page).to have_content 'Request to merge fix into feature'
expect(page).to have_content 'Target branch changed from merge-test to feature' expect(page).to have_content 'Target branch changed from merge-test to feature'
wait_for_ajax
end end
step 'I click on "Email Patches"' do step 'I click on "Email Patches"' do
......
...@@ -218,6 +218,10 @@ module SharedProject ...@@ -218,6 +218,10 @@ module SharedProject
2.times { create(:note_on_issue, project: project) } 2.times { create(:note_on_issue, project: project) }
end end
step 'trending projects are refreshed' do
TrendingProject.refresh!
end
step 'project "Shop" has labels: "bug", "feature", "enhancement"' do step 'project "Shop" has labels: "bug", "feature", "enhancement"' do
project = Project.find_by(name: "Shop") project = Project.find_by(name: "Shop")
create(:label, project: project, title: 'bug') create(:label, project: project, title: 'bug')
......
module Banzai
module Filter
# HTML filter that sets dir="auto" for RTL languages support
class SetDirectionFilter < HTML::Pipeline::Filter
def call
# select these elements just on top level of the document
doc.xpath('p|h1|h2|h3|h4|h5|h6|ol|ul[not(@class="section-nav")]|blockquote|table').each do |el|
el['dir'] = 'auto'
end
doc
end
end
end
end
...@@ -25,7 +25,9 @@ module Banzai ...@@ -25,7 +25,9 @@ module Banzai
Filter::MilestoneReferenceFilter, Filter::MilestoneReferenceFilter,
Filter::TaskListFilter, Filter::TaskListFilter,
Filter::InlineDiffFilter Filter::InlineDiffFilter,
Filter::SetDirectionFilter
] ]
end end
......
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
include IconsHelper include IconsHelper
class MissingResolution < StandardError class MissingResolution < ResolutionError
end end
CONTEXT_LINES = 3 CONTEXT_LINES = 3
...@@ -21,12 +21,34 @@ module Gitlab ...@@ -21,12 +21,34 @@ module Gitlab
@match_line_headers = {} @match_line_headers = {}
end end
def content
merge_file_result[:data]
end
def our_blob
@our_blob ||= repository.blob_at(merge_request.diff_refs.head_sha, our_path)
end
def type
lines unless @type
@type.inquiry
end
# Array of Gitlab::Diff::Line objects # Array of Gitlab::Diff::Line objects
def lines def lines
@lines ||= Gitlab::Conflict::Parser.new.parse(merge_file_result[:data], return @lines if defined?(@lines)
begin
@type = 'text'
@lines = Gitlab::Conflict::Parser.new.parse(content,
our_path: our_path, our_path: our_path,
their_path: their_path, their_path: their_path,
parent_file: self) parent_file: self)
rescue Gitlab::Conflict::Parser::ParserError
@type = 'text-editor'
@lines = nil
end
end end
def resolve_lines(resolution) def resolve_lines(resolution)
...@@ -53,6 +75,14 @@ module Gitlab ...@@ -53,6 +75,14 @@ module Gitlab
end.compact end.compact
end end
def resolve_content(resolution)
if resolution == content
raise MissingResolution, "Resolved content has no changes for file #{our_path}"
end
resolution
end
def highlight_lines! def highlight_lines!
their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n") their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n") our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
...@@ -170,21 +200,39 @@ module Gitlab ...@@ -170,21 +200,39 @@ module Gitlab
match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}" match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}"
end end
def as_json(opts = nil) def as_json(opts = {})
{ json_hash = {
old_path: their_path, old_path: their_path,
new_path: our_path, new_path: our_path,
blob_icon: file_type_icon_class('file', our_mode, our_path), blob_icon: file_type_icon_class('file', our_mode, our_path),
blob_path: namespace_project_blob_path(merge_request.project.namespace, blob_path: namespace_project_blob_path(merge_request.project.namespace,
merge_request.project, merge_request.project,
::File.join(merge_request.diff_refs.head_sha, our_path)), ::File.join(merge_request.diff_refs.head_sha, our_path))
sections: sections
} }
json_hash.tap do |json_hash|
if opts[:full_content]
json_hash[:content] = content
json_hash[:blob_ace_mode] = our_blob && our_blob.language.try(:ace_mode)
else
json_hash[:sections] = sections if type.text?
json_hash[:type] = type
json_hash[:content_path] = content_path
end
end
end
def content_path
conflict_for_path_namespace_project_merge_request_path(merge_request.project.namespace,
merge_request.project,
merge_request,
old_path: their_path,
new_path: our_path)
end end
# Don't try to print merge_request or repository. # Don't try to print merge_request or repository.
def inspect def inspect
instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode].map do |instance_variable| instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode, :type].map do |instance_variable|
value = instance_variable_get("@#{instance_variable}") value = instance_variable_get("@#{instance_variable}")
"#{instance_variable}=\"#{value}\"" "#{instance_variable}=\"#{value}\""
......
...@@ -30,6 +30,10 @@ module Gitlab ...@@ -30,6 +30,10 @@ module Gitlab
end end
end end
def file_for_path(old_path, new_path)
files.find { |file| file.their_path == old_path && file.our_path == new_path }
end
def as_json(opts = nil) def as_json(opts = nil)
{ {
target_branch: merge_request.target_branch, target_branch: merge_request.target_branch,
......
module Gitlab module Gitlab
module Conflict module Conflict
class Parser class Parser
class ParserError < StandardError class UnresolvableError < StandardError
end end
class UnexpectedDelimiter < ParserError class UnmergeableFile < UnresolvableError
end end
class MissingEndDelimiter < ParserError class UnsupportedEncoding < UnresolvableError
end
# Recoverable errors - the conflict can be resolved in an editor, but not with
# sections.
class ParserError < StandardError
end end
class UnmergeableFile < ParserError class UnexpectedDelimiter < ParserError
end end
class UnsupportedEncoding < ParserError class MissingEndDelimiter < ParserError
end end
def parse(text, our_path:, their_path:, parent_file: nil) def parse(text, our_path:, their_path:, parent_file: nil)
......
module Gitlab
module Conflict
class ResolutionError < StandardError
end
end
end
...@@ -570,7 +570,7 @@ describe Projects::MergeRequestsController do ...@@ -570,7 +570,7 @@ describe Projects::MergeRequestsController do
context 'when the conflicts cannot be resolved in the UI' do context 'when the conflicts cannot be resolved in the UI' do
before do before do
allow_any_instance_of(Gitlab::Conflict::Parser). allow_any_instance_of(Gitlab::Conflict::Parser).
to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnexpectedDelimiter) to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
get :conflicts, get :conflicts,
namespace_id: merge_request_with_conflicts.project.namespace.to_param, namespace_id: merge_request_with_conflicts.project.namespace.to_param,
...@@ -597,6 +597,10 @@ describe Projects::MergeRequestsController do ...@@ -597,6 +597,10 @@ describe Projects::MergeRequestsController do
format: 'json' format: 'json'
end end
it 'matches the schema' do
expect(response).to match_response_schema('conflicts')
end
it 'includes meta info about the MR' do it 'includes meta info about the MR' do
expect(json_response['commit_message']).to include('Merge branch') expect(json_response['commit_message']).to include('Merge branch')
expect(json_response['commit_sha']).to match(/\h{40}/) expect(json_response['commit_sha']).to match(/\h{40}/)
...@@ -658,26 +662,97 @@ describe Projects::MergeRequestsController do ...@@ -658,26 +662,97 @@ describe Projects::MergeRequestsController do
end end
end end
describe 'GET conflict_for_path' do
let(:json_response) { JSON.parse(response.body) }
def conflict_for_path(path)
get :conflict_for_path,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
project_id: merge_request_with_conflicts.project.to_param,
id: merge_request_with_conflicts.iid,
old_path: path,
new_path: path,
format: 'json'
end
context 'when the conflicts cannot be resolved in the UI' do
before do
allow_any_instance_of(Gitlab::Conflict::Parser).
to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
conflict_for_path('files/ruby/regex.rb')
end
it 'returns a 404 status code' do
expect(response).to have_http_status(:not_found)
end
end
context 'when the file does not exist cannot be resolved in the UI' do
before { conflict_for_path('files/ruby/regexp.rb') }
it 'returns a 404 status code' do
expect(response).to have_http_status(:not_found)
end
end
context 'with an existing file' do
let(:path) { 'files/ruby/regex.rb' }
before { conflict_for_path(path) }
it 'returns a 200 status code' do
expect(response).to have_http_status(:ok)
end
it 'returns the file in JSON format' do
content = merge_request_with_conflicts.conflicts.file_for_path(path, path).content
expect(json_response).to include('old_path' => path,
'new_path' => path,
'blob_icon' => 'file-text-o',
'blob_path' => a_string_ending_with(path),
'blob_ace_mode' => 'ruby',
'content' => content)
end
end
end
context 'POST resolve_conflicts' do context 'POST resolve_conflicts' do
let(:json_response) { JSON.parse(response.body) } let(:json_response) { JSON.parse(response.body) }
let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha } let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
def resolve_conflicts(sections) def resolve_conflicts(files)
post :resolve_conflicts, post :resolve_conflicts,
namespace_id: merge_request_with_conflicts.project.namespace.to_param, namespace_id: merge_request_with_conflicts.project.namespace.to_param,
project_id: merge_request_with_conflicts.project.to_param, project_id: merge_request_with_conflicts.project.to_param,
id: merge_request_with_conflicts.iid, id: merge_request_with_conflicts.iid,
format: 'json', format: 'json',
sections: sections, files: files,
commit_message: 'Commit message' commit_message: 'Commit message'
end end
context 'with valid params' do context 'with valid params' do
before do before do
resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head', resolved_files = [
{
'new_path' => 'files/ruby/popen.rb',
'old_path' => 'files/ruby/popen.rb',
'sections' => {
'2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head'
}
}, {
'new_path' => 'files/ruby/regex.rb',
'old_path' => 'files/ruby/regex.rb',
'sections' => {
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin') '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
}
}
]
resolve_conflicts(resolved_files)
end end
it 'creates a new commit on the branch' do it 'creates a new commit on the branch' do
...@@ -692,7 +767,23 @@ describe Projects::MergeRequestsController do ...@@ -692,7 +767,23 @@ describe Projects::MergeRequestsController do
context 'when sections are missing' do context 'when sections are missing' do
before do before do
resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head') resolved_files = [
{
'new_path' => 'files/ruby/popen.rb',
'old_path' => 'files/ruby/popen.rb',
'sections' => {
'2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head'
}
}, {
'new_path' => 'files/ruby/regex.rb',
'old_path' => 'files/ruby/regex.rb',
'sections' => {
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head'
}
}
]
resolve_conflicts(resolved_files)
end end
it 'returns a 400 error' do it 'returns a 400 error' do
...@@ -700,7 +791,71 @@ describe Projects::MergeRequestsController do ...@@ -700,7 +791,71 @@ describe Projects::MergeRequestsController do
end end
it 'has a message with the name of the first missing section' do it 'has a message with the name of the first missing section' do
expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9') expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21')
end
it 'does not create a new commit' do
expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
end
end
context 'when files are missing' do
before do
resolved_files = [
{
'new_path' => 'files/ruby/regex.rb',
'old_path' => 'files/ruby/regex.rb',
'sections' => {
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
}
}
]
resolve_conflicts(resolved_files)
end
it 'returns a 400 error' do
expect(response).to have_http_status(:bad_request)
end
it 'has a message with the name of the missing file' do
expect(json_response['message']).to include('files/ruby/popen.rb')
end
it 'does not create a new commit' do
expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
end
end
context 'when a file has identical content to the conflict' do
before do
resolved_files = [
{
'new_path' => 'files/ruby/popen.rb',
'old_path' => 'files/ruby/popen.rb',
'content' => merge_request_with_conflicts.conflicts.file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb').content
}, {
'new_path' => 'files/ruby/regex.rb',
'old_path' => 'files/ruby/regex.rb',
'sections' => {
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
}
}
]
resolve_conflicts(resolved_files)
end
it 'returns a 400 error' do
expect(response).to have_http_status(:bad_request)
end
it 'has a message with the path of the problem file' do
expect(json_response['message']).to include('files/ruby/popen.rb')
end end
it 'does not create a new commit' do it 'does not create a new commit' do
......
...@@ -53,7 +53,7 @@ describe "User Feed", feature: true do ...@@ -53,7 +53,7 @@ describe "User Feed", feature: true do
end end
it 'has XHTML summaries in issue descriptions' do it 'has XHTML summaries in issue descriptions' do
expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p>I guess/ expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p dir="auto">I guess/
end end
it 'has XHTML summaries in notes' do it 'has XHTML summaries in notes' do
......
...@@ -12,29 +12,139 @@ feature 'Merge request conflict resolution', js: true, feature: true do ...@@ -12,29 +12,139 @@ feature 'Merge request conflict resolution', js: true, feature: true do
end end
end end
context 'when a merge request can be resolved in the UI' do shared_examples "conflicts are resolved in Interactive mode" do
let(:merge_request) { create_merge_request('conflict-resolvable') } it 'conflicts are resolved in Interactive mode' do
within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do
click_button 'Use ours'
end
within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
all('button', text: 'Use ours').each do |button|
button.click
end
end
click_button 'Commit conflict resolution'
wait_for_ajax
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
click_on 'Changes'
wait_for_ajax
within find('.diff-file', text: 'files/ruby/popen.rb') do
expect(page).to have_selector('.line_content.new', text: "vars = { 'PWD' => path }")
expect(page).to have_selector('.line_content.new', text: "options = { chdir: path }")
end
within find('.diff-file', text: 'files/ruby/regex.rb') do
expect(page).to have_selector('.line_content.new', text: "def username_regexp")
expect(page).to have_selector('.line_content.new', text: "def project_name_regexp")
expect(page).to have_selector('.line_content.new', text: "def path_regexp")
expect(page).to have_selector('.line_content.new', text: "def archive_formats_regexp")
expect(page).to have_selector('.line_content.new', text: "def git_reference_regexp")
expect(page).to have_selector('.line_content.new', text: "def default_regexp")
end
end
end
shared_examples "conflicts are resolved in Edit inline mode" do
it 'conflicts are resolved in Edit inline mode' do
expect(find('#conflicts')).to have_content('popen.rb')
within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do
click_button 'Edit inline'
wait_for_ajax
execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("One morning");')
end
within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
click_button 'Edit inline'
wait_for_ajax
execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");')
end
click_button 'Commit conflict resolution'
wait_for_ajax
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
click_on 'Changes'
wait_for_ajax
expect(page).to have_content('One morning')
expect(page).to have_content('Gregor Samsa woke from troubled dreams')
end
end
context 'can be resolved in the UI' do
before do before do
project.team << [user, :developer] project.team << [user, :developer]
login_as(user) login_as(user)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end end
context 'the conflicts are resolvable' do
let(:merge_request) { create_merge_request('conflict-resolvable') }
before { visit namespace_project_merge_request_path(project.namespace, project, merge_request) }
it 'shows a link to the conflict resolution page' do it 'shows a link to the conflict resolution page' do
expect(page).to have_link('conflicts', href: /\/conflicts\Z/) expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
end end
context 'visiting the conflicts resolution page' do context 'in Inline view mode' do
before { click_link('conflicts', href: /\/conflicts\Z/) } before { click_link('conflicts', href: /\/conflicts\Z/) }
it 'shows the conflicts' do include_examples "conflicts are resolved in Interactive mode"
begin include_examples "conflicts are resolved in Edit inline mode"
expect(find('#conflicts')).to have_content('popen.rb') end
rescue Capybara::Poltergeist::JavascriptError
retry context 'in Parallel view mode' do
before do
click_link('conflicts', href: /\/conflicts\Z/)
click_button 'Side-by-side'
end
include_examples "conflicts are resolved in Interactive mode"
include_examples "conflicts are resolved in Edit inline mode"
end
end end
context 'the conflict contain markers' do
let(:merge_request) { create_merge_request('conflict-contains-conflict-markers') }
before do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
click_link('conflicts', href: /\/conflicts\Z/)
end
it 'conflicts can not be resolved in Interactive mode' do
within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do
expect(page).not_to have_content 'Interactive mode'
expect(page).not_to have_content 'Edit inline'
end
end
it 'conflicts are resolved in Edit inline mode' do
within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do
wait_for_ajax
execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("Gregor Samsa woke from troubled dreams");')
end
click_button 'Commit conflict resolution'
wait_for_ajax
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
click_on 'Changes'
wait_for_ajax
find('.click-to-expand').click
wait_for_ajax
expect(page).to have_content('Gregor Samsa woke from troubled dreams')
end end
end end
end end
...@@ -42,7 +152,6 @@ feature 'Merge request conflict resolution', js: true, feature: true do ...@@ -42,7 +152,6 @@ feature 'Merge request conflict resolution', js: true, feature: true do
UNRESOLVABLE_CONFLICTS = { UNRESOLVABLE_CONFLICTS = {
'conflict-too-large' => 'when the conflicts contain a large file', 'conflict-too-large' => 'when the conflicts contain a large file',
'conflict-binary-file' => 'when the conflicts contain a binary file', 'conflict-binary-file' => 'when the conflicts contain a binary file',
'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers',
'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another', 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another',
'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file', 'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file',
} }
......
require 'spec_helper'
feature 'Find file keyboard shortcuts', feature: true, js: true do
include WaitForAjax
let(:user) { create(:user) }
let(:project) { create(:project) }
before do
project.team << [user, :master]
login_as user
visit namespace_project_find_file_path(project.namespace, project, project.repository.root_ref)
wait_for_ajax
end
it 'opens file when pressing enter key' do
fill_in 'file_find', with: 'CHANGELOG'
find('#file_find').native.send_keys(:enter)
expect(page).to have_selector('.blob-content-holder')
page.within('.file-title') do
expect(page).to have_content('CHANGELOG')
end
end
it 'navigates files with arrow keys' do
fill_in 'file_find', with: 'application.'
find('#file_find').native.send_keys(:down)
find('#file_find').native.send_keys(:enter)
expect(page).to have_selector('.blob-content-holder')
page.within('.file-title') do
expect(page).to have_content('application.js')
end
end
end
...@@ -51,6 +51,18 @@ feature 'Users', feature: true, js: true do ...@@ -51,6 +51,18 @@ feature 'Users', feature: true, js: true do
expect(current_path).to eq user_path(user) expect(current_path).to eq user_path(user)
expect(page).to have_text(user.name) expect(page).to have_text(user.name)
end end
scenario '/u/user1/groups redirects to user groups page' do
visit '/u/user1/groups'
expect(current_path).to eq user_groups_path(user)
end
scenario '/u/user1/projects redirects to user projects page' do
visit '/u/user1/projects'
expect(current_path).to eq user_projects_path(user)
end
end end
feature 'username validation' do feature 'username validation' do
......
{
"type": "object",
"required": [
"commit_message",
"commit_sha",
"source_branch",
"target_branch",
"files"
],
"properties": {
"commit_message": {"type": "string"},
"commit_sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
"source_branch": {"type": "string"},
"target_branch": {"type": "string"},
"files": {
"type": "array",
"items": {
"oneOf": [
{ "$ref": "#/definitions/conflict-text-with-sections" },
{ "$ref": "#/definitions/conflict-text-for-editor" }
]
}
}
},
"definitions": {
"conflict-base": {
"type": "object",
"required": [
"old_path",
"new_path",
"blob_icon",
"blob_path"
],
"properties": {
"old_path": {"type": "string"},
"new_path": {"type": "string"},
"blob_icon": {"type": "string"},
"blob_path": {"type": "string"}
}
},
"conflict-text-for-editor": {
"allOf": [
{"$ref": "#/definitions/conflict-base"},
{
"type": "object",
"required": [
"type",
"content_path"
],
"properties": {
"type": {"type": {"enum": ["text-editor"]}},
"content_path": {"type": "string"}
}
}
]
},
"conflict-text-with-sections": {
"allOf": [
{"$ref": "#/definitions/conflict-base"},
{
"type": "object",
"required": [
"type",
"content_path",
"sections"
],
"properties": {
"type": {"type": {"enum": ["text"]}},
"content_path": {"type": "string"},
"sections": {
"type": "array",
"items": {
"oneOf": [
{ "$ref": "#/definitions/section-context" },
{ "$ref": "#/definitions/section-conflict" }
]
}
}
}
}
]
},
"section-base": {
"type": "object",
"required": [
"conflict",
"lines"
],
"properties": {
"conflict": {"type": "boolean"},
"lines": {
"type": "array",
"items": {
"type": "object",
"required": [
"old_line",
"new_line",
"text",
"rich_text"
],
"properties": {
"type": {"type": "string"},
"old_line": {"type": "string"},
"new_line": {"type": "string"},
"text": {"type": "string"},
"rich_text": {"type": "string"}
}
}
}
}
},
"section-context": {
"allOf": [
{"$ref": "#/definitions/section-base"},
{
"type": "object",
"properties": {
"conflict": {"enum": [false]}
}
}
]
},
"section-conflict": {
"allOf": [
{"$ref": "#/definitions/section-base"},
{
"type": "object",
"required": ["id"],
"properties": {
"conflict": {"enum": [true]},
"id": {"type": "string"}
}
}
]
}
}
}
...@@ -24,7 +24,7 @@ describe Banzai::ObjectRenderer do ...@@ -24,7 +24,7 @@ describe Banzai::ObjectRenderer do
with(an_instance_of(Array)). with(an_instance_of(Array)).
and_call_original and_call_original
expect(object).to receive(:redacted_note_html=).with('<p>hello</p>') expect(object).to receive(:redacted_note_html=).with('<p dir="auto">hello</p>')
expect(object).to receive(:user_visible_reference_count=).with(0) expect(object).to receive(:user_visible_reference_count=).with(0)
renderer.render([object], :note) renderer.render([object], :note)
...@@ -92,10 +92,10 @@ describe Banzai::ObjectRenderer do ...@@ -92,10 +92,10 @@ describe Banzai::ObjectRenderer do
docs = renderer.render_attributes(objects, :note) docs = renderer.render_attributes(objects, :note)
expect(docs[0]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment) expect(docs[0]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
expect(docs[0].to_html).to eq('<p>hello</p>') expect(docs[0].to_html).to eq('<p dir="auto">hello</p>')
expect(docs[1]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment) expect(docs[1]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
expect(docs[1].to_html).to eq('<p>bye</p>') expect(docs[1].to_html).to eq('<p dir="auto">bye</p>')
end end
it 'returns when no objects to render' do it 'returns when no objects to render' do
......
...@@ -4,11 +4,11 @@ describe Banzai::Pipeline::DescriptionPipeline do ...@@ -4,11 +4,11 @@ describe Banzai::Pipeline::DescriptionPipeline do
def parse(html) def parse(html)
# When we pass HTML to Redcarpet, it gets wrapped in `p` tags... # When we pass HTML to Redcarpet, it gets wrapped in `p` tags...
# ...except when we pass it pre-wrapped text. Rabble rabble. # ...except when we pass it pre-wrapped text. Rabble rabble.
unwrap = !html.start_with?('<p>') unwrap = !html.start_with?('<p ')
output = described_class.to_html(html, project: spy) output = described_class.to_html(html, project: spy)
output.gsub!(%r{\A<p>(.*)</p>(.*)\z}, '\1\2') if unwrap output.gsub!(%r{\A<p dir="auto">(.*)</p>(.*)\z}, '\1\2') if unwrap
output output
end end
...@@ -27,11 +27,17 @@ describe Banzai::Pipeline::DescriptionPipeline do ...@@ -27,11 +27,17 @@ describe Banzai::Pipeline::DescriptionPipeline do
end end
end end
%w(b i strong em a ins del sup sub p).each do |elem| %w(b i strong em a ins del sup sub).each do |elem|
it "still allows '#{elem}' elements" do it "still allows '#{elem}' elements" do
exp = act = "<#{elem}>Description</#{elem}>" exp = act = "<#{elem}>Description</#{elem}>"
expect(parse(act).strip).to eq exp expect(parse(act).strip).to eq exp
end end
end end
it "still allows 'p' elements" do
exp = act = "<p dir=\"auto\">Description</p>"
expect(parse(act).strip).to eq exp
end
end end
...@@ -257,5 +257,16 @@ FILE ...@@ -257,5 +257,16 @@ FILE
it 'includes the blob icon for the file' do it 'includes the blob icon for the file' do
expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o') expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o')
end end
context 'with the full_content option passed' do
it 'includes the full content of the conflict' do
expect(conflict_file.as_json(full_content: true)).to have_key(:content)
end
it 'includes the detected language of the conflict file' do
expect(conflict_file.as_json(full_content: true)[:blob_ace_mode]).
to eq('ruby')
end
end
end end
end end
...@@ -187,33 +187,24 @@ describe Ci::Pipeline, models: true do ...@@ -187,33 +187,24 @@ describe Ci::Pipeline, models: true do
end end
end end
describe "merge request metrics" do describe 'merge request metrics' do
let(:project) { FactoryGirl.create :project } let(:project) { FactoryGirl.create :project }
let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) } let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) } let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
context 'when transitioning to running' do before do
it 'records the build start time' do expect(PipelineMetricsWorker).to receive(:perform_async).with(pipeline.id)
time = Time.now
Timecop.freeze(time) { build.run }
expect(merge_request.reload.metrics.latest_build_started_at).to be_within(1.second).of(time)
end end
it 'clears the build end time' do context 'when transitioning to running' do
build.run it 'schedules metrics workers' do
pipeline.run
expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil
end end
end end
context 'when transitioning to success' do context 'when transitioning to success' do
it 'records the build end time' do it 'schedules metrics workers' do
build.run pipeline.succeed
time = Time.now
Timecop.freeze(time) { build.success }
expect(merge_request.reload.metrics.latest_build_finished_at).to be_within(1.second).of(time)
end end
end end
end end
......
...@@ -64,7 +64,7 @@ describe CacheMarkdownField do ...@@ -64,7 +64,7 @@ describe CacheMarkdownField do
let(:html) { "<p><code>Foo</code></p>" } let(:html) { "<p><code>Foo</code></p>" }
let(:updated_markdown) { "`Bar`" } let(:updated_markdown) { "`Bar`" }
let(:updated_html) { "<p><code>Bar</code></p>" } let(:updated_html) { "<p dir=\"auto\"><code>Bar</code></p>" }
subject { ThingWithMarkdownFields.new(foo: markdown, foo_html: html) } subject { ThingWithMarkdownFields.new(foo: markdown, foo_html: html) }
......
...@@ -1155,12 +1155,6 @@ describe MergeRequest, models: true do ...@@ -1155,12 +1155,6 @@ describe MergeRequest, models: true do
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
end end
it 'returns a falsey value when the conflicts contain a file with ambiguous conflict markers' do
merge_request = create_merge_request('conflict-contains-conflict-markers')
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
end
it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do
merge_request = create_merge_request('conflict-missing-side') merge_request = create_merge_request('conflict-missing-side')
...@@ -1172,6 +1166,12 @@ describe MergeRequest, models: true do ...@@ -1172,6 +1166,12 @@ describe MergeRequest, models: true do
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
end end
it 'returns a truthy value when the conflicts have to be resolved in an editor' do
merge_request = create_merge_request('conflict-contains-conflict-markers')
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
end
end end
describe "#forked_source_project_missing?" do describe "#forked_source_project_missing?" do
......
...@@ -15,27 +15,27 @@ describe UsersController, "routing" do ...@@ -15,27 +15,27 @@ describe UsersController, "routing" do
end end
it "to #groups" do it "to #groups" do
expect(get("/u/User/groups")).to route_to('users#groups', username: 'User') expect(get("/users/User/groups")).to route_to('users#groups', username: 'User')
end end
it "to #projects" do it "to #projects" do
expect(get("/u/User/projects")).to route_to('users#projects', username: 'User') expect(get("/users/User/projects")).to route_to('users#projects', username: 'User')
end end
it "to #contributed" do it "to #contributed" do
expect(get("/u/User/contributed")).to route_to('users#contributed', username: 'User') expect(get("/users/User/contributed")).to route_to('users#contributed', username: 'User')
end end
it "to #snippets" do it "to #snippets" do
expect(get("/u/User/snippets")).to route_to('users#snippets', username: 'User') expect(get("/users/User/snippets")).to route_to('users#snippets', username: 'User')
end end
it "to #calendar" do it "to #calendar" do
expect(get("/u/User/calendar")).to route_to('users#calendar', username: 'User') expect(get("/users/User/calendar")).to route_to('users#calendar', username: 'User')
end end
it "to #calendar_activities" do it "to #calendar_activities" do
expect(get("/u/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User') expect(get("/users/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User')
end end
end end
......
...@@ -24,15 +24,26 @@ describe MergeRequests::ResolveService do ...@@ -24,15 +24,26 @@ describe MergeRequests::ResolveService do
end end
describe '#execute' do describe '#execute' do
context 'with valid params' do context 'with section params' do
let(:params) do let(:params) do
{ {
files: [
{
old_path: 'files/ruby/popen.rb',
new_path: 'files/ruby/popen.rb',
sections: {
'2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head'
}
}, {
old_path: 'files/ruby/regex.rb',
new_path: 'files/ruby/regex.rb',
sections: { sections: {
'2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
}, }
}
],
commit_message: 'This is a commit message!' commit_message: 'This is a commit message!'
} }
end end
...@@ -49,7 +60,7 @@ describe MergeRequests::ResolveService do ...@@ -49,7 +60,7 @@ describe MergeRequests::ResolveService do
it 'creates a commit with the correct parents' do it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id)). expect(merge_request.source_branch_head.parents.map(&:id)).
to eq(['1450cd639e0bc6721eb02800169e464f212cde06', to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
'75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b']) '824be604a34828eb682305f0d963056cfac87b2d'])
end end
end end
...@@ -74,8 +85,70 @@ describe MergeRequests::ResolveService do ...@@ -74,8 +85,70 @@ describe MergeRequests::ResolveService do
end end
end end
context 'when a resolution is missing' do context 'with content and sections params' do
let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } } let(:popen_content) { "class Popen\nend" }
let(:params) do
{
files: [
{
old_path: 'files/ruby/popen.rb',
new_path: 'files/ruby/popen.rb',
content: popen_content
}, {
old_path: 'files/ruby/regex.rb',
new_path: 'files/ruby/regex.rb',
sections: {
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
}
}
],
commit_message: 'This is a commit message!'
}
end
before do
MergeRequests::ResolveService.new(project, user, params).execute(merge_request)
end
it 'creates a commit with the message' do
expect(merge_request.source_branch_head.message).to eq(params[:commit_message])
end
it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id)).
to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
'824be604a34828eb682305f0d963056cfac87b2d'])
end
it 'sets the content to the content given' do
blob = merge_request.source_project.repository.blob_at(merge_request.source_branch_head.sha,
'files/ruby/popen.rb')
expect(blob.data).to eq(popen_content)
end
end
context 'when a resolution section is missing' do
let(:invalid_params) do
{
files: [
{
old_path: 'files/ruby/popen.rb',
new_path: 'files/ruby/popen.rb',
content: ''
}, {
old_path: 'files/ruby/regex.rb',
new_path: 'files/ruby/regex.rb',
sections: { '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head' }
}
],
commit_message: 'This is a commit message!'
}
end
let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
it 'raises a MissingResolution error' do it 'raises a MissingResolution error' do
...@@ -83,5 +156,53 @@ describe MergeRequests::ResolveService do ...@@ -83,5 +156,53 @@ describe MergeRequests::ResolveService do
to raise_error(Gitlab::Conflict::File::MissingResolution) to raise_error(Gitlab::Conflict::File::MissingResolution)
end end
end end
context 'when the content of a file is unchanged' do
let(:invalid_params) do
{
files: [
{
old_path: 'files/ruby/popen.rb',
new_path: 'files/ruby/popen.rb',
content: ''
}, {
old_path: 'files/ruby/regex.rb',
new_path: 'files/ruby/regex.rb',
content: merge_request.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content
}
],
commit_message: 'This is a commit message!'
}
end
let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
it 'raises a MissingResolution error' do
expect { service.execute(merge_request) }.
to raise_error(Gitlab::Conflict::File::MissingResolution)
end
end
context 'when a file is missing' do
let(:invalid_params) do
{
files: [
{
old_path: 'files/ruby/popen.rb',
new_path: 'files/ruby/popen.rb',
content: ''
}
],
commit_message: 'This is a commit message!'
}
end
let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
it 'raises a MissingFiles error' do
expect { service.execute(merge_request) }.
to raise_error(MergeRequests::ResolveService::MissingFiles)
end
end
end end
end end
...@@ -27,10 +27,10 @@ module TestEnv ...@@ -27,10 +27,10 @@ module TestEnv
'expand-collapse-lines' => '238e82d', 'expand-collapse-lines' => '238e82d',
'video' => '8879059', 'video' => '8879059',
'crlf-diff' => '5938907', 'crlf-diff' => '5938907',
'conflict-start' => '75284c7', 'conflict-start' => '824be60',
'conflict-resolvable' => '1450cd6', 'conflict-resolvable' => '1450cd6',
'conflict-binary-file' => '259a6fb', 'conflict-binary-file' => '259a6fb',
'conflict-contains-conflict-markers' => '5e0964c', 'conflict-contains-conflict-markers' => '78a3086',
'conflict-missing-side' => 'eb227b3', 'conflict-missing-side' => 'eb227b3',
'conflict-non-utf8' => 'd0a293c', 'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f', 'conflict-too-large' => '39fa04f',
...@@ -98,7 +98,9 @@ module TestEnv ...@@ -98,7 +98,9 @@ module TestEnv
def setup_gitlab_shell def setup_gitlab_shell
unless File.directory?(Gitlab.config.gitlab_shell.path) unless File.directory?(Gitlab.config.gitlab_shell.path)
`rake gitlab:shell:install` unless system('rake', 'gitlab:shell:install')
raise 'Can`t clone gitlab-shell'
end
end end
end end
......
require 'spec_helper'
describe PipelineMetricsWorker do
let(:project) { create(:project) }
let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
let(:pipeline) do
create(:ci_empty_pipeline,
status: status,
project: project,
ref: 'master',
sha: project.repository.commit('master').id,
started_at: 1.hour.ago,
finished_at: Time.now)
end
describe '#perform' do
subject { described_class.new.perform(pipeline.id) }
context 'when pipeline is running' do
let(:status) { 'running' }
it 'records the build start time' do
subject
expect(merge_request.reload.metrics.latest_build_started_at).to be_within(1.second).of(pipeline.started_at)
end
it 'clears the build end time' do
subject
expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil
end
end
context 'when pipeline succeeded' do
let(:status) { 'success' }
it 'records the build end time' do
subject
expect(merge_request.reload.metrics.latest_build_finished_at).to be_within(1.second).of(pipeline.finished_at)
end
end
end
end
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