Commit fb390c57 authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'origin/master' into 8-13-stable

parents 1b78dbd8 146e0cbc
......@@ -22,7 +22,7 @@ before_script:
- bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
- 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:
- prepare
......
......@@ -2,6 +2,8 @@ Please view this file on the master branch, on stable branches it's out of date.
## 8.13.0 (2016-10-22)
- Fix save button on project pipeline settings page. (!6955)
- Avoid race condition when asynchronously removing expired artifacts. (!6881)
- Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
- Respond with 404 Not Found for non-existent tags (Linus Thiel)
- Truncate long labels with ellipsis in labels page
......@@ -11,15 +13,16 @@ Please view this file on the master branch, on stable branches it's out of date.
- Update runner version only when updating contacted_at
- Add link from system note to compare with previous version
- Use gitlab-shell v3.6.6
- Ability to resolve merge request conflicts with editor !6374
- Add `/projects/visible` API endpoint (Ben Boeckel)
- Fix centering of custom header logos (Ashley Dumaine)
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
- Cancelled pipelines could be retried. !6927
- Updating verbiage on git basics to be more intuitive
- Clarify documentation for Runners API (Gennady Trafimenkov)
- The instrumentation for Banzai::Renderer has been restored
- 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
- Move Pipeline Metrics to separate worker
- AbstractReferenceFilter caches project_refs on RequestStore when active
......@@ -40,7 +43,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
- Cache rendered markdown in the database, rather than Redis
- 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
- API: Ability to retrieve version information (Robert Schilling)
- Fix permission for setting an issue's due date
......@@ -54,6 +56,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API
- Add new issue button to each list on Issues Board
- Execute specific named route method from toggle_award_url helper method
- Added soft wrap button to repository file/blob editor
- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
- Show the time ago a merge request was deployed to an environment
......@@ -63,6 +66,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
- Remove redundant mixins (ClemMakesApps)
- Added 'Download' button to the Snippets page (Justin DiPierro)
- Add visibility level to project repository
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- Fix that manual jobs would no longer block jobs in the next stage. !6604
......@@ -77,14 +81,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
- Take filters in account in issuable counters. !6496
- 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)
- 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)
- Revoke button in Applications Settings underlines on hover.
- Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- 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?
- 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)
......@@ -95,7 +97,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Replace bootstrap caret with fontawesome caret (ClemMakesApps)
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- 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 deploy status responsiveness error !6633
- Make searching for commits case insensitive
......@@ -107,6 +109,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Reduce queries needed to find users using their SSH keys when pushing commits
- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
- Fix broken repository 500 errors in project list
- Fix the diff in the merge request view when converting a symlink to a regular file
- Fix Pipeline list commit column width should be adjusted
- Close todos when accepting merge requests via the API !6486 (tonygambone)
- Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo)
......@@ -118,7 +121,6 @@ Please view this file on the master branch, on stable branches it's out of date.
- Grouped pipeline dropdown is a scrollable container
- Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
- Fixes padding in all clipboard icons that have .btn class
- Fix due date being displayed as NaN in Safari
- Fix a typo in doc/api/labels.md
- API: all unknown routing will be handled with 404 Not Found
- Add docs for request profiling
......@@ -126,8 +128,15 @@ Please view this file on the master branch, on stable branches it's out of date.
## 8.12.7
- Use gitlab-markup gem instead of github-markup to fix `.rst` file rendering. !6659
- Fix GFM autocomplete setup being called several times
- Prevent running `GfmAutocomplete` setup for each diff note. !6569
- 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
......
......@@ -262,8 +262,6 @@ group :development do
# thin instead webrick
gem 'thin', '~> 1.7.0'
gem 'activerecord_sane_schema_dumper', '0.2'
end
group :development, :test do
......@@ -310,6 +308,8 @@ group :development, :test do
gem 'license_finder', '~> 2.1.0', require: false
gem 'knapsack', '~> 1.11.0'
gem 'activerecord_sane_schema_dumper', '0.2'
end
group :test do
......
......@@ -101,9 +101,6 @@
new ZenMode();
new MergedButtons();
break;
case "projects:merge_requests:conflicts":
window.mcui = new MergeConflictResolver()
break;
case 'projects:merge_requests:index':
shortcut_handler = new ShortcutsNavigation();
Issuable.init();
......
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 @@
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
graphCollapsed ? $btnText.text('Hide') : $btnText.text('Expand')
}
addMarginToBuildColumns() {
......
......@@ -7,6 +7,7 @@
function ProjectFindFile(element1, options) {
this.element = element1;
this.options = options;
this.goToBlob = bind(this.goToBlob, this);
this.goToTree = bind(this.goToTree, this);
this.selectRowDown = bind(this.selectRowDown, this);
this.selectRowUp = bind(this.selectRowUp, this);
......@@ -154,6 +155,14 @@
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;
})();
......
......@@ -4,9 +4,8 @@
this.ProjectNew = (function() {
function ProjectNew() {
this.toggleSettings = bind(this.toggleSettings, this);
this.$selects = $('.features select').filter(function () {
return $(this).data('field');
});
this.$selects = $('.features select');
this.$repoSelects = this.$selects.filter('.js-repo-select');
$('.project-edit-container').on('ajax:before', (function(_this) {
return function() {
......@@ -16,6 +15,7 @@
})(this));
this.toggleSettings();
this.toggleSettingsOnclick();
this.toggleRepoVisibility();
}
ProjectNew.prototype.toggleSettings = function() {
......@@ -43,6 +43,38 @@
}
};
ProjectNew.prototype.toggleRepoVisibility = function () {
var $repoAccessLevel = $('.js-repo-access-level select');
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
.nextAll()
.hide();
$repoAccessLevel.off('change')
.on('change', function () {
var selectedVal = parseInt($repoAccessLevel.val());
this.$repoSelects.each(function () {
var $this = $(this),
repoSelectVal = parseInt($this.val());
$this.find('option').show();
if (selectedVal < repoSelectVal) {
$this.val(selectedVal);
}
$this.find("option[value='" + selectedVal + "']").nextAll().hide();
});
if (selectedVal) {
this.$repoSelects.removeClass('disabled');
} else {
this.$repoSelects.addClass('disabled');
}
}.bind(this));
};
return ProjectNew;
})();
......
......@@ -76,7 +76,7 @@
this.renderState();
return $.ajax({
type: 'GET',
url: `/u/${username}/exists`,
url: `/users/${username}/exists`,
dataType: 'json',
success: (res) => this.setAvailabilityState(res.exists)
});
......
......@@ -45,40 +45,38 @@
}
h1 {
font-size: 2em;
font-size: 1.75em;
font-weight: 600;
margin: 1em 0 10px;
margin: 16px 0 10px;
padding: 0 0 0.3em;
border-bottom: 1px solid $btn-default-border;
border-bottom: 1px solid $white-dark;
color: $gl-gray-dark;
}
h2 {
font-size: 1.6em;
font-size: 1.5em;
font-weight: 600;
margin: 1em 0 10px;
padding-bottom: 0.3em;
border-bottom: 1px solid $btn-default-border;
margin: 16px 0 10px;
color: $gl-gray-dark;
}
h3 {
margin: 1em 0 10px;
font-size: 1.4em;
margin: 16px 0 10px;
font-size: 1.3em;
}
h4 {
margin: 1em 0 10px;
font-size: 1.25em;
margin: 16px 0 10px;
font-size: 1.2em;
}
h5 {
margin: 1em 0 10px;
margin: 16px 0 10px;
font-size: 1em;
}
h6 {
margin: 1em 0 10px;
margin: 16px 0 10px;
font-size: 0.95em;
}
......@@ -87,12 +85,12 @@
font-size: inherit;
padding: 8px 21px;
margin: 12px 0;
border-left: 3px solid #e7e9ed;
border-left: 3px solid $white-dark;
}
blockquote:dir(rtl) {
border-left: 0;
border-right: 3px solid #e7e9ed;
border-right: 3px solid $white-dark;
}
blockquote p {
......
......@@ -56,6 +56,7 @@ $border-gray-light: #dcdcdc;
$border-gray-normal: #d7d7d7;
$border-gray-dark: #c6cacf;
$border-green-extra-light: #9adb84;
$border-green-light: #2faa60;
$border-green-normal: #2ca05b;
$border-green-dark: #279654;
......
......@@ -20,9 +20,11 @@
.detail-page-description {
.title {
margin: 0;
font-size: 23px;
margin: 0 0 16px;
font-size: 2em;
color: $gl-gray-dark;
padding: 0 0 0.3em;
border-bottom: 1px solid $white-dark;
}
.description {
......
......@@ -237,4 +237,51 @@ $colors: (
.btn-success .fa-spinner {
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 @@
}
}
.merge-request-details {
.title {
margin-bottom: 20px;
}
}
.merge-request-tabs {
background-color: #fff;
......
......@@ -761,62 +761,6 @@ pre.light-well {
.dropdown-menu {
width: 300px;
}
&.from .compare-dropdown-toggle {
width: 237px;
}
&.to .compare-dropdown-toggle {
width: 254px;
}
.dropdown-toggle-text {
display: block;
height: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
.compare-ellipsis {
display: inline;
}
@media (max-width: $screen-xs-max) {
.compare-form-group {
.input-group {
width: 100%;
& > .compare-dropdown-toggle {
width: 100%;
}
}
.dropdown-menu {
width: 100%;
}
}
.compare-switch-container {
text-align: center;
padding: 0 0 $gl-padding;
.commits-compare-switch {
float: none;
}
}
.compare-ellipsis {
display: block;
text-align: center;
padding: 0 0 $gl-padding;
}
.commits-compare-btn {
width: 100%;
}
}
.clearable-input {
......@@ -855,3 +799,30 @@ pre.light-well {
border-bottom-right-radius: 0;
}
}
.project-home-empty {
border-top: 0;
.container-fluid {
background: none;
}
p {
margin-left: auto;
margin-right: auto;
max-width: 650px;
}
}
.project-feature-nested {
@media (min-width: $screen-sm-min) {
padding-left: 45px;
}
}
.project-repo-select {
&.disabled {
opacity: 0.5;
pointer-events: none;
}
}
......@@ -169,4 +169,8 @@
margin-top: 11px;
position: relative;
z-index: 2;
.download-button {
margin-left: $btn-side-margin;
}
}
......@@ -118,7 +118,12 @@ class ApplicationController < ActionController::Base
end
def render_404
render file: Rails.root.join("public", "404"), layout: false, status: "404"
respond_to do |format|
format.html do
render file: Rails.root.join("public", "404"), layout: false, status: "404"
end
format.any { head :not_found }
end
end
def no_cache_headers
......
......@@ -9,15 +9,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
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
]
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_commit_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 :apply_diff_view_cookie!, only: [:new_diffs]
before_action :build_merge_request, only: [:new, :new_diffs]
......@@ -33,7 +33,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
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
@merge_requests = merge_requests_collection
......@@ -170,6 +170,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
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
return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
......@@ -184,7 +194,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
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) }
rescue Gitlab::Conflict::File::MissingResolution => e
rescue Gitlab::Conflict::ResolutionError => e
render status: :bad_request, json: { message: e.message }
end
end
......
class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
before_action :authenticate_user!, except: [:show, :activity, :refs]
......@@ -103,16 +104,7 @@ class ProjectsController < Projects::ApplicationController
respond_to do |format|
format.html do
@notification_setting = current_user.notification_settings_for(@project) if current_user
if @project.repository_exists?
if @project.empty_repo?
render 'projects/empty'
else
render :show
end
else
render 'projects/no_repo'
end
render_landing_page
end
format.atom do
......@@ -285,6 +277,26 @@ class ProjectsController < Projects::ApplicationController
private
# Render project landing depending of which features are available
# So if page is not availble in the list it renders the next page
#
# pages list order: repository readme, wiki home, issues list, customize workflow
def render_landing_page
if @project.feature_available?(:repository, current_user)
return render 'projects/no_repo' unless @project.repository_exists?
render 'projects/empty' if @project.empty_repo?
else
if @project.wiki_enabled?
@wiki_home = @project.wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user)
@issues = issues_collection
@issues = @issues.page(params[:page])
end
render :show
end
end
def determine_layout
if [:new, :create].include?(action_name.to_sym)
'application'
......@@ -308,7 +320,8 @@ class ProjectsController < Projects::ApplicationController
project_feature_attributes:
[
:issues_access_level, :builds_access_level,
:wiki_access_level, :merge_requests_access_level, :snippets_access_level
:wiki_access_level, :merge_requests_access_level,
:snippets_access_level, :repository_access_level
]
}
......
module AwardEmojiHelper
def toggle_award_url(awardable)
if @project
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
return url_for([:toggle_award_emoji, awardable]) unless @project
if awardable.is_a?(Note)
# We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (6.5x)
toggle_award_emoji_namespace_project_note_url(namespace_id: @project.namespace_id, project_id: @project.id, id: awardable.id)
else
url_for([:toggle_award_emoji, awardable])
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
end
end
end
......@@ -50,6 +50,20 @@ module PreferencesHelper
end
def default_project_view
current_user ? current_user.project_view : 'readme'
return 'readme' unless current_user
user_view = current_user.project_view
if @project.feature_available?(:repository, current_user)
user_view
elsif user_view == "activity"
"activity"
elsif @project.wiki_enabled?
"wiki"
elsif @project.feature_available?(:issues, current_user)
"projects/issues/issues"
else
"customize_workflow"
end
end
end
......@@ -134,16 +134,35 @@ module ProjectsHelper
options = project_feature_options
if @project.private?
level = @project.project_feature.send(field)
options.delete('Everyone with access')
highest_available_option = options.values.max if @project.project_feature.send(field) == ProjectFeature::ENABLED
highest_available_option = options.values.max if level == ProjectFeature::ENABLED
end
options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
content_tag(:select, options, name: "project[project_feature_attributes][#{field}]", id: "project_project_feature_attributes_#{field}", class: "pull-right form-control", data: { field: field }).html_safe
content_tag(
:select,
options,
name: "project[project_feature_attributes][#{field}]",
id: "project_project_feature_attributes_#{field}",
class: "pull-right form-control #{repo_children_classes(field)}",
data: { field: field }
).html_safe
end
private
def repo_children_classes(field)
needs_repo_check = [:merge_requests_access_level, :builds_access_level]
return unless needs_repo_check.include?(field)
classes = "project-repo-select js-repo-select"
classes << " disabled" unless @project.feature_available?(:repository, current_user)
classes
end
def get_project_nav_tabs(project, current_user)
nav_tabs = [:home]
......@@ -155,12 +174,8 @@ module ProjectsHelper
nav_tabs << :merge_requests
end
if can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
end
if can?(current_user, :read_build, project)
nav_tabs << :builds
nav_tabs << :pipelines
end
if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
......@@ -435,4 +450,8 @@ module ProjectsHelper
'Everyone with access' => ProjectFeature::ENABLED
}
end
def project_child_container_class(view_path)
view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
end
end
......@@ -154,7 +154,7 @@ module Ci
def retryable?
builds.latest.any? do |build|
build.failed? && build.retryable?
(build.failed? || build.canceled?) && build.retryable?
end
end
......
......@@ -871,7 +871,7 @@ class MergeRequest < ActiveRecord::Base
# files.
conflicts.files.each(&:lines)
@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
end
end
......
......@@ -13,23 +13,26 @@ class ProjectFeature < ActiveRecord::Base
# Enabled: enabled for everyone able to access the project
#
# Permision levels
# Permission levels
DISABLED = 0
PRIVATE = 10
ENABLED = 20
FEATURES = %i(issues merge_requests wiki snippets builds)
FEATURES = %i(issues merge_requests wiki snippets builds repository)
# Default scopes force us to unscope here since a service may need to check
# permissions for a project in pending_delete
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
belongs_to :project, -> { unscope(where: :pending_delete) }
validate :repository_children_level
default_value_for :builds_access_level, value: ENABLED, allows_nil: false
default_value_for :issues_access_level, value: ENABLED, allows_nil: false
default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false
default_value_for :snippets_access_level, value: ENABLED, allows_nil: false
default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
default_value_for :repository_access_level, value: ENABLED, allows_nil: false
def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
......@@ -57,6 +60,18 @@ class ProjectFeature < ActiveRecord::Base
private
# Validates builds and merge requests access level
# which cannot be higher than repository access level
def repository_children_level
validator = lambda do |field|
level = public_send(field) || ProjectFeature::ENABLED
not_allowed = level > repository_access_level
self.errors.add(field, "cannot have higher visibility level than repository access level") if not_allowed
end
%i(merge_requests_access_level builds_access_level).each(&validator)
end
def get_permission(user, level)
case level
when DISABLED
......
......@@ -162,11 +162,13 @@ class ProjectPolicy < BasePolicy
end
def disabled_features!
repository_enabled = project.feature_available?(:repository, user)
unless project.feature_available?(:issues, user)
cannot!(*named_abilities(:issue))
end
unless project.feature_available?(:merge_requests, user)
unless project.feature_available?(:merge_requests, user) && repository_enabled
cannot!(*named_abilities(:merge_request))
end
......@@ -183,13 +185,21 @@ class ProjectPolicy < BasePolicy
cannot!(*named_abilities(:wiki))
end
unless project.feature_available?(:builds, user)
unless project.feature_available?(:builds, user) && repository_enabled
cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline))
cannot!(*named_abilities(:environment))
cannot!(*named_abilities(:deployment))
end
unless repository_enabled
cannot! :push_code
cannot! :push_code_to_protected_branches
cannot! :download_code
cannot! :fork_project
cannot! :read_commit_status
end
unless project.container_registry_enabled
cannot!(*named_abilities(:container_image))
end
......
module MergeRequests
class ResolveService < MergeRequests::BaseService
class MissingFiles < Gitlab::Conflict::ResolutionError
end
attr_accessor :conflicts, :rugged, :merge_index, :merge_request
def execute(merge_request)
......@@ -10,8 +13,16 @@ module MergeRequests
fetch_their_commit!
conflicts.files.each do |file|
write_resolved_file_to_index(file, params[:sections])
params[:files].each do |file_params|
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
commit_params = {
......@@ -23,8 +34,13 @@ module MergeRequests
project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params)
end
def write_resolved_file_to_index(file, resolutions)
new_file = file.resolve_lines(resolutions).map(&:text).join("\n")
def write_resolved_file_to_index(file, params)
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
merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
......
.row-content-block.project-home-empty
%div.text-center{ class: container_class }
%h4
Customize your workflow!
%p
Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and builds, GitLab can help manage your workflow from idea to production!
- if can?(current_user, :admin_project, @project)
= link_to "Get started", edit_project_path(@project), class: "btn btn-success"
......@@ -22,5 +22,6 @@
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
.project-clone-holder
= render "shared/clone_panel"
- if @project.feature_available?(:repository, current_user)
.project-clone-holder
= render "shared/clone_panel"
- if @wiki_home.present?
%div{ class: container_class }
.wiki-holder.prepend-top-default.append-bottom-default
.wiki
= preserve do
= render_wiki_content(@wiki_home)
- else
- can_create_wiki = can?(current_user, :create_wiki, @project)
.project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] }
%div.text-center{ class: container_class }
%h4
This project does not have a wiki homepage yet
- if can_create_wiki
%p
Add a homepage to your wiki that contains information about your project
%p
We recommend you
= link_to "add a homepage", namespace_project_wiki_path(@project.namespace, @project, :home)
to your project's wiki and GitLab will show it here instead of this message.
- 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
%button.btn{ 'data-toggle' => 'dropdown' }
= icon('download')
......
......@@ -50,24 +50,39 @@
.form_group.prepend-top-20
.row
.col-md-9
= feature_fields.label :issues_access_level, "Issues", class: 'label-light'
%span.help-block Lightweight issue tracking system for this project
.col-md-3
= project_feature_access_select(:issues_access_level)
= feature_fields.label :repository_access_level, "Repository", class: 'label-light'
%span.help-block Push files to be stored in this project
.col-md-3.js-repo-access-level
= project_feature_access_select(:repository_access_level)
.col-sm-12
.row
.col-md-9.project-feature-nested
= feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
%span.help-block Submit changes to be merged upstream
.col-md-3
= project_feature_access_select(:merge_requests_access_level)
.row
.col-md-9.project-feature-nested
= feature_fields.label :builds_access_level, "Builds", class: 'label-light'
%span.help-block Submit, test and deploy your changes before merge
.col-md-3
= project_feature_access_select(:builds_access_level)
.row
.col-md-9
= feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
%span.help-block Submit changes to be merged upstream
= feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
%span.help-block Share code pastes with others out of Git repository
.col-md-3
= project_feature_access_select(:merge_requests_access_level)
= project_feature_access_select(:snippets_access_level)
.row
.col-md-9
= feature_fields.label :builds_access_level, "Builds", class: 'label-light'
%span.help-block Submit Test and deploy your changes before merge
= feature_fields.label :issues_access_level, "Issues", class: 'label-light'
%span.help-block Lightweight issue tracking system for this project
.col-md-3
= project_feature_access_select(:builds_access_level)
= project_feature_access_select(:issues_access_level)
.row
.col-md-9
......@@ -76,24 +91,17 @@
.col-md-3
= project_feature_access_select(:wiki_access_level)
.row
.col-md-9
= feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
%span.help-block Share code pastes with others out of Git repository
.col-md-3
= project_feature_access_select(:snippets_access_level)
- if Gitlab.config.lfs.enabled && current_user.admin?
.row
.col-md-9
= f.label :lfs_enabled, 'LFS', class: 'label-light'
%span.help-block
.checkbox
= f.label :lfs_enabled do
= f.check_box :lfs_enabled
%strong LFS
%br
%span.descr
Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
.col-md-3
= f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' }
- if Gitlab.config.registry.enabled
- if Gitlab.config.lfs.enabled && current_user.admin?
.form-group
.checkbox
= f.label :container_registry_enabled do
......
%ul.content-list.issues-list.issuable-list
= render @issues
= render partial: "projects/issues/issue", collection: @issues
- if @issues.blank?
%li
.nothing-here-block No issues to show
......
- 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"
- 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"
.merge-request-details.issuable-details
......@@ -24,6 +20,21 @@
= render partial: "projects/merge_requests/conflicts/commit_stats"
.files-wrapper{"v-if" => "!isLoading && !hasError"}
= render partial: "projects/merge_requests/conflicts/parallel_view", locals: { class_bindings: class_bindings }
= render partial: "projects/merge_requests/conflicts/inline_view", locals: { class_bindings: class_bindings }
.files
.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"
-# Components
= render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line'
.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"}
.inline-parallel-buttons
.inline-parallel-buttons{"v-if" => "showDiffViewTypeSwitcher"}
.btn-group
%a.btn{ |
":class" => "{'active': !isParallel}", |
"@click" => "handleViewTypeChange('inline')"}
%button.btn{":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')"}
Inline
%a.btn{ |
":class" => "{'active': isParallel}", |
"@click" => "handleViewTypeChange('parallel')"}
%button.btn{":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')"}
Side-by-side
.js-toggle-container
.commit-stat-summary
Showing
%strong.cred {{conflictsCount}} {{conflictsData.conflictsText}}
%strong.cred {{conflictsCountText}}
between
%strong {{conflictsData.source_branch}}
%strong {{conflictsData.sourceBranch}}
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
%strong.resolved-count {{resolvedCount}}
of
%strong.total-count {{conflictsCount}}
conflicts have been resolved
.commit-message-container.form-group
.max-width-marker
%textarea.form-control.js-commit-message{"v-model" => "conflictsData.commitMessage"}
{{{conflictsData.commitMessage}}}
%button{type: "button", class: "btn btn-success js-submit-button", ":disabled" => "!readyToCommit", "@click" => "commit()"}
%span {{commitButtonText}}
= link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel"
.form-horizontal.resolve-conflicts-form
.form-group
%label.col-sm-2.control-label{ "for" => "commit-message" }
Commit message
.col-sm-10
.commit-message-container
.max-width-marker
%textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" }
.form-group
.col-sm-offset-2.col-sm-10
.row
.col-xs-6
%button{ type: "button", class: "btn btn-success js-submit-button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
%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"
%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"}
......@@ -7,7 +7,7 @@
.col-lg-9
%h5.prepend-top-0
Pipelines
= form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project), remote: true, authenticity_token: true do |f|
= form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f|
%fieldset.builds-feature
- unless @repository.gitlab_ci_yml
.form-group
......
......@@ -12,72 +12,74 @@
= render 'projects/last_push'
= render "home_panel"
%nav.project-stats{ class: (container_class) }
%ul.nav
%li
= link_to project_files_path(@project) do
Files (#{repository_size})
%li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
#{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)})
%li
= link_to namespace_project_branches_path(@project.namespace, @project) do
#{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
%li
= link_to namespace_project_tags_path(@project.namespace, @project) do
#{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
- if default_project_view != 'readme' && @repository.readme
- if @project.feature_available?(:repository, current_user)
%nav.project-stats{ class: container_class }
%ul.nav
%li
= link_to 'Readme', readme_path(@project)
- if @repository.changelog
= link_to project_files_path(@project) do
Files (#{repository_size})
%li
= link_to 'Changelog', changelog_path(@project)
- if @repository.license_blob
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
#{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)})
%li
= link_to license_short_name(@project), license_path(@project)
- if @repository.contribution_guide
= link_to namespace_project_branches_path(@project.namespace, @project) do
#{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
%li
= link_to 'Contribution guide', contribution_guide_path(@project)
= link_to namespace_project_tags_path(@project.namespace, @project) do
#{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
- if @repository.gitlab_ci_yml
%li
= link_to 'CI configuration', ci_configuration_path(@project)
- if current_user && can_push_branch?(@project, @project.default_branch)
- unless @repository.changelog
%li.missing
= link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
Add Changelog
- unless @repository.license_blob
%li.missing
= link_to add_special_file_path(@project, file_name: 'LICENSE') do
Add License
- unless @repository.contribution_guide
%li.missing
= link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
Add Contribution guide
- unless @repository.gitlab_ci_yml
%li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
Set Up CI
%li.project-repo-buttons-right
.project-repo-buttons.project-right-buttons
- if current_user
= render 'shared/members/access_request_buttons', source: @project
= render "projects/buttons/koding"
= render 'projects/buttons/download', project: @project, ref: @ref
= render 'projects/buttons/dropdown'
= render 'shared/notifications/button', notification_setting: @notification_setting
- if @repository.commit
.project-last-commit{ class: container_class }
= render 'projects/last_commit', commit: @repository.commit, project: @project
- if default_project_view != 'readme' && @repository.readme
%li
= link_to 'Readme', readme_path(@project)
- if @repository.changelog
%li
= link_to 'Changelog', changelog_path(@project)
- if @repository.license_blob
%li
= link_to license_short_name(@project), license_path(@project)
- if @repository.contribution_guide
%li
= link_to 'Contribution guide', contribution_guide_path(@project)
- if @repository.gitlab_ci_yml
%li
= link_to 'CI configuration', ci_configuration_path(@project)
- if current_user && can_push_branch?(@project, @project.default_branch)
- unless @repository.changelog
%li.missing
= link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
Add Changelog
- unless @repository.license_blob
%li.missing
= link_to add_special_file_path(@project, file_name: 'LICENSE') do
Add License
- unless @repository.contribution_guide
%li.missing
= link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
Add Contribution guide
- unless @repository.gitlab_ci_yml
%li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
Set Up CI
%li.project-repo-buttons-right
.project-repo-buttons.project-right-buttons
- if current_user
= render 'shared/members/access_request_buttons', source: @project
= render "projects/buttons/koding"
.btn-group.project-repo-btn-group
= render 'projects/buttons/download', project: @project, ref: @ref
= render 'projects/buttons/dropdown'
= render 'shared/notifications/button', notification_setting: @notification_setting
- if @repository.commit
.project-last-commit{ class: container_class }
= render 'projects/last_commit', commit: @repository.commit, project: @project
%div{ class: container_class }
- if @project.archived?
......@@ -86,5 +88,7 @@
= icon("exclamation-triangle fw")
Archived project! Repository is read-only
%div{class: "project-show-#{default_project_view}"}
= render default_project_view
- view_path = default_project_view
%div{ class: project_child_container_class(view_path) }
= render view_path
......@@ -2,10 +2,14 @@ class ExpireBuildInstanceArtifactsWorker
include Sidekiq::Worker
def perform(build_id)
build = Ci::Build.with_expired_artifacts.reorder(nil).find_by(id: build_id)
return unless build
build = Ci::Build
.with_expired_artifacts
.reorder(nil)
.find_by(id: build_id)
Rails.logger.info "Removing artifacts build #{build.id}..."
return unless build.try(:project)
Rails.logger.info "Removing artifacts for build #{build.id}..."
build.erase_artifacts!
end
end
......@@ -89,6 +89,7 @@ module Gitlab
config.assets.precompile << "profile/profile_bundle.js"
config.assets.precompile << "diff_notes/diff_notes_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 << "blob_edit/blob_edit_bundle.js"
config.assets.precompile << "snippet/snippet_bundle.js"
......
......@@ -67,6 +67,7 @@ if Gitlab::Metrics.enabled?
['app', 'finders'] => ['app', 'finders'],
['app', 'mailers', 'emails'] => ['app', 'mailers'],
['app', 'services', '**'] => ['app', 'services'],
['lib', 'gitlab', 'conflicts'] => ['lib'],
['lib', 'gitlab', 'diff'] => ['lib'],
['lib', 'gitlab', 'email', 'message'] => ['lib'],
['lib', 'gitlab', 'checks'] => ['lib']
......
......@@ -267,6 +267,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
get :commits
get :diffs
get :conflicts
get :conflict_for_path
get :builds
get :pipelines
get :merge_check
......
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,
registrations: :registrations,
passwords: :passwords,
......@@ -23,7 +20,7 @@ constraints(UserUrlConstrainer.new) do
end
end
scope(path: 'u/:username',
scope(path: 'users/:username',
as: :user,
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
controller: :users) do
......@@ -36,3 +33,12 @@ scope(path: 'u/:username',
get :exists
get '/', to: redirect('/%{username}')
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_\-]+/ }
class AddRepositoryAccessLevelToProjectFeature < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DOWNTIME = false
def up
add_column_with_default(:project_features, :repository_access_level, :integer, default: ProjectFeature::ENABLED)
end
def down
remove_column :project_features, :repository_access_level
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161007133303) do
ActiveRecord::Schema.define(version: 20161012180455) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -830,6 +830,7 @@ ActiveRecord::Schema.define(version: 20161007133303) do
t.integer "builds_access_level"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "repository_access_level", default: 20, null: false
end
add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree
......
......@@ -43,7 +43,7 @@ Example Response:
"id": 1,
"state": "active",
"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",
"updated_at": "2016-06-15T10:09:34.206Z",
......@@ -59,7 +59,7 @@ Example Response:
"id": 26,
"state": "active",
"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",
"updated_at": "2016-06-15T10:09:34.177Z",
......@@ -103,7 +103,7 @@ Example Response:
"id": 26,
"state": "active",
"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",
"updated_at": "2016-06-15T10:09:34.177Z",
......@@ -146,7 +146,7 @@ Example Response:
"id": 1,
"state": "active",
"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",
"updated_at": "2016-06-17T17:47:29.266Z",
......@@ -190,7 +190,7 @@ Example Response:
"id": 1,
"state": "active",
"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",
"updated_at": "2016-06-17T17:47:29.266Z",
......@@ -238,7 +238,7 @@ Example Response:
"id": 26,
"state": "active",
"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",
"updated_at": "2016-06-15T10:09:34.197Z",
......@@ -279,7 +279,7 @@ Example Response:
"id": 26,
"state": "active",
"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",
"updated_at": "2016-06-15T10:09:34.197Z",
......@@ -319,7 +319,7 @@ Example Response:
"id": 1,
"state": "active",
"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",
"updated_at": "2016-06-17T19:59:55.888Z",
......@@ -362,7 +362,7 @@ Example Response:
"id": 1,
"state": "active",
"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",
"updated_at": "2016-06-17T19:59:55.888Z",
......
......@@ -64,7 +64,7 @@ Example of response
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://gitlab.dev/u/root",
"web_url": "http://gitlab.dev/root",
"website_url": ""
}
},
......@@ -108,7 +108,7 @@ Example of response
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://gitlab.dev/u/root",
"web_url": "http://gitlab.dev/root",
"website_url": ""
}
}
......@@ -212,7 +212,7 @@ Example of response
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://gitlab.dev/u/root",
"web_url": "http://gitlab.dev/root",
"website_url": ""
}
}
......@@ -279,7 +279,7 @@ Example of response
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://gitlab.dev/u/root",
"web_url": "http://gitlab.dev/root",
"website_url": ""
}
}
......
......@@ -288,7 +288,7 @@ Example response:
```json
{
"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",
"username" : "thedude",
"state" : "active",
......@@ -343,7 +343,7 @@ Example response:
"author" : {
"username" : "thedude",
"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",
"id" : 28,
"name" : "Jeff Lebowski"
......@@ -370,7 +370,7 @@ Example response:
"id" : 28,
"name" : "Jeff Lebowski",
"username" : "thedude",
"web_url" : "https://gitlab.example.com/u/thedude",
"web_url" : "https://gitlab.example.com/thedude",
"state" : "active",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png"
},
......@@ -408,7 +408,7 @@ Example response:
```json
{
"author" : {
"web_url" : "https://gitlab.example.com/u/thedude",
"web_url" : "https://gitlab.example.com/thedude",
"name" : "Jeff Lebowski",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
"username" : "thedude",
......
......@@ -56,7 +56,7 @@ Example of response
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://localhost:3000/u/root",
"web_url": "http://localhost:3000/root",
"website_url": ""
}
},
......@@ -75,7 +75,7 @@ Example of response
"name": "Administrator",
"state": "active",
"username": "root",
"web_url": "http://localhost:3000/u/root"
"web_url": "http://localhost:3000/root"
}
},
{
......@@ -114,7 +114,7 @@ Example of response
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://localhost:3000/u/root",
"web_url": "http://localhost:3000/root",
"website_url": ""
}
},
......@@ -133,7 +133,7 @@ Example of response
"name": "Administrator",
"state": "active",
"username": "root",
"web_url": "http://localhost:3000/u/root"
"web_url": "http://localhost:3000/root"
}
}
]
......@@ -169,7 +169,7 @@ Example of response
"id": 1,
"state": "active",
"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": {
"id": 9,
......@@ -193,7 +193,7 @@ Example of response
"id": 1,
"state": "active",
"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",
"is_admin": true,
"bio": null,
......
......@@ -46,7 +46,7 @@ Example response:
"author" : {
"state" : "active",
"id" : 18,
"web_url" : "https://gitlab.example.com/u/eileen.lowe",
"web_url" : "https://gitlab.example.com/eileen.lowe",
"name" : "Alexandra Bashirian",
"avatar_url" : null,
"username" : "eileen.lowe"
......@@ -67,7 +67,7 @@ Example response:
"state" : "active",
"id" : 1,
"name" : "Administrator",
"web_url" : "https://gitlab.example.com/u/root",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root"
},
......@@ -134,7 +134,7 @@ Example response:
},
"author" : {
"state" : "active",
"web_url" : "https://gitlab.example.com/u/root",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
......@@ -145,7 +145,7 @@ Example response:
"iid" : 1,
"assignee" : {
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/u/lennie",
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
......@@ -215,7 +215,7 @@ Example response:
},
"author" : {
"state" : "active",
"web_url" : "https://gitlab.example.com/u/root",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
......@@ -226,7 +226,7 @@ Example response:
"iid" : 1,
"assignee" : {
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/u/lennie",
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
......@@ -281,7 +281,7 @@ Example response:
},
"author" : {
"state" : "active",
"web_url" : "https://gitlab.example.com/u/root",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
......@@ -292,7 +292,7 @@ Example response:
"iid" : 1,
"assignee" : {
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/u/lennie",
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
......@@ -357,7 +357,7 @@ Example response:
"name" : "Alexandra Bashirian",
"avatar_url" : null,
"state" : "active",
"web_url" : "https://gitlab.example.com/u/eileen.lowe",
"web_url" : "https://gitlab.example.com/eileen.lowe",
"id" : 18,
"username" : "eileen.lowe"
},
......@@ -414,7 +414,7 @@ Example response:
"username" : "eileen.lowe",
"id" : 18,
"state" : "active",
"web_url" : "https://gitlab.example.com/u/eileen.lowe"
"web_url" : "https://gitlab.example.com/eileen.lowe"
},
"state" : "closed",
"title" : "Issues with auth",
......@@ -500,7 +500,7 @@ Example response:
"id": 12,
"state": "active",
"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": {
"name": "Kris Steuber",
......@@ -508,7 +508,7 @@ Example response:
"id": 10,
"state": "active",
"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,
"web_url": "http://example.com/example/example/issues/11",
......@@ -557,7 +557,7 @@ Example response:
"id": 12,
"state": "active",
"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": {
"name": "Kris Steuber",
......@@ -565,7 +565,7 @@ Example response:
"id": 10,
"state": "active",
"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,
"web_url": "http://example.com/example/example/issues/11",
......@@ -614,7 +614,7 @@ Example response:
"id": 21,
"state": "active",
"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": {
"name": "Vivian Hermann",
......@@ -622,7 +622,7 @@ Example response:
"id": 11,
"state": "active",
"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,
"due_date": null,
......@@ -669,7 +669,7 @@ Example response:
"id": 1,
"state": "active",
"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",
"target_type": "Issue",
......@@ -700,7 +700,7 @@ Example response:
"id": 14,
"state": "active",
"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": {
"name": "Maxie Medhurst",
......@@ -708,7 +708,7 @@ Example response:
"id": 12,
"state": "active",
"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,
"user_notes_count": 7,
......
......@@ -24,7 +24,7 @@ Parameters:
"id": 25,
"state": "active",
"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",
"is_admin": false,
"bio": null,
......
......@@ -621,7 +621,7 @@ Example response when the GitLab issue tracker is used:
"author" : {
"state" : "active",
"id" : 18,
"web_url" : "https://gitlab.example.com/u/eileen.lowe",
"web_url" : "https://gitlab.example.com/eileen.lowe",
"name" : "Alexandra Bashirian",
"avatar_url" : null,
"username" : "eileen.lowe"
......@@ -642,7 +642,7 @@ Example response when the GitLab issue tracker is used:
"state" : "active",
"id" : 1,
"name" : "Administrator",
"web_url" : "https://gitlab.example.com/u/root",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root"
},
......@@ -711,7 +711,7 @@ Example response:
"id": 19,
"state": "active",
"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": {
"name": "Celine Wehner",
......@@ -719,7 +719,7 @@ Example response:
"id": 16,
"state": "active",
"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,
"target_project_id": 5,
......@@ -787,7 +787,7 @@ Example response:
"id": 19,
"state": "active",
"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": {
"name": "Celine Wehner",
......@@ -795,7 +795,7 @@ Example response:
"id": 16,
"state": "active",
"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,
"target_project_id": 5,
......@@ -858,7 +858,7 @@ Example response:
"id": 1,
"state": "active",
"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",
"target_type": "MergeRequest",
......@@ -881,7 +881,7 @@ Example response:
"id": 14,
"state": "active",
"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": {
"name": "Dr. Gabrielle Strosin",
......@@ -889,7 +889,7 @@ Example response:
"id": 4,
"state": "active",
"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,
"target_project_id": 3,
......
......@@ -143,7 +143,7 @@ Example Response:
"state": "active",
"created_at": "2013-09-30T13:46:01Z",
"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",
"system": false,
......@@ -268,7 +268,7 @@ Example Response:
"state": "active",
"created_at": "2013-09-30T13:46:01Z",
"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",
"system": false,
......@@ -398,7 +398,7 @@ Example Response:
"state": "active",
"created_at": "2013-09-30T13:46:01Z",
"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",
"system": false,
......
......@@ -34,7 +34,7 @@ Example of response
"id": 1,
"state": "active",
"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",
"updated_at": "2016-08-16T10:23:19.216Z",
......@@ -57,7 +57,7 @@ Example of response
"id": 1,
"state": "active",
"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",
"updated_at": "2016-08-16T10:23:21.314Z",
......@@ -103,7 +103,7 @@ Example of response
"id": 1,
"state": "active",
"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",
"updated_at": "2016-08-11T11:32:35.169Z",
......@@ -148,7 +148,7 @@ Response:
"id": 1,
"state": "active",
"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",
"updated_at": "2016-08-11T11:32:35.169Z",
......@@ -193,7 +193,7 @@ Response:
"id": 1,
"state": "active",
"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",
"updated_at": "2016-08-11T11:32:35.169Z",
......
......@@ -465,7 +465,7 @@ Parameters:
"id": 1,
"state": "active",
"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"
},
......@@ -482,7 +482,7 @@ Parameters:
"id": 1,
"state": "active",
"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",
"data": {
......@@ -528,7 +528,7 @@ Parameters:
"id": 1,
"state": "active",
"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"
},
......@@ -552,7 +552,7 @@ Parameters:
"id": 1,
"state": "active",
"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",
"system": false,
......@@ -567,7 +567,7 @@ Parameters:
"id": 1,
"state": "active",
"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"
}
......
......@@ -44,7 +44,7 @@ Example Response:
"id": 1,
"state": "active",
"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",
"target_type": "MergeRequest",
......@@ -67,7 +67,7 @@ Example Response:
"id": 12,
"state": "active",
"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": {
"name": "Administrator",
......@@ -75,7 +75,7 @@ Example Response:
"id": 1,
"state": "active",
"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,
"target_project_id": 2,
......@@ -117,7 +117,7 @@ Example Response:
"id": 12,
"state": "active",
"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",
"target_type": "MergeRequest",
......@@ -140,7 +140,7 @@ Example Response:
"id": 12,
"state": "active",
"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": {
"name": "Administrator",
......@@ -148,7 +148,7 @@ Example Response:
"id": 1,
"state": "active",
"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,
"target_project_id": 2,
......@@ -215,7 +215,7 @@ Example Response:
"id": 1,
"state": "active",
"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",
"target_type": "MergeRequest",
......@@ -238,7 +238,7 @@ Example Response:
"id": 12,
"state": "active",
"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": {
"name": "Administrator",
......@@ -246,7 +246,7 @@ Example Response:
"id": 1,
"state": "active",
"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,
"target_project_id": 2,
......
......@@ -20,7 +20,7 @@ GET /users
"name": "John Smith",
"state": "active",
"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,
......@@ -28,7 +28,7 @@ GET /users
"name": "Jack Smith",
"state": "blocked",
"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
"name": "John Smith",
"state": "active",
"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",
"is_admin": false,
"bio": null,
......@@ -81,7 +81,7 @@ GET /users
"name": "Jack Smith",
"state": "blocked",
"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",
"is_admin": false,
"bio": null,
......@@ -141,7 +141,7 @@ Parameters:
"name": "John Smith",
"state": "active",
"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",
"is_admin": false,
"bio": null,
......@@ -172,7 +172,7 @@ Parameters:
"name": "John Smith",
"state": "active",
"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",
"is_admin": false,
"bio": null,
......@@ -293,7 +293,7 @@ GET /user
"name": "John Smith",
"state": "active",
"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",
"is_admin": false,
"bio": null,
......@@ -665,7 +665,7 @@ Example response:
"id": 1,
"state": "active",
"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"
},
......@@ -682,7 +682,7 @@ Example response:
"id": 1,
"state": "active",
"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",
"data": {
......@@ -728,7 +728,7 @@ Example response:
"id": 1,
"state": "active",
"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"
},
......@@ -752,7 +752,7 @@ Example response:
"id": 1,
"state": "active",
"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",
"system": false,
......@@ -767,7 +767,7 @@ Example response:
"id": 1,
"state": "active",
"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"
}
......
......@@ -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.
[#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
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
......@@ -4,7 +4,7 @@ module Gitlab
include Gitlab::Routing.url_helpers
include IconsHelper
class MissingResolution < StandardError
class MissingResolution < ResolutionError
end
CONTEXT_LINES = 3
......@@ -21,12 +21,34 @@ module Gitlab
@match_line_headers = {}
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
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,
their_path: their_path,
parent_file: self)
rescue Gitlab::Conflict::Parser::ParserError
@type = 'text-editor'
@lines = nil
end
end
def resolve_lines(resolution)
......@@ -53,6 +75,14 @@ module Gitlab
end.compact
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!
their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
......@@ -170,21 +200,39 @@ module Gitlab
match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}"
end
def as_json(opts = nil)
{
def as_json(opts = {})
json_hash = {
old_path: their_path,
new_path: our_path,
blob_icon: file_type_icon_class('file', our_mode, our_path),
blob_path: namespace_project_blob_path(merge_request.project.namespace,
merge_request.project,
::File.join(merge_request.diff_refs.head_sha, our_path)),
sections: sections
::File.join(merge_request.diff_refs.head_sha, our_path))
}
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
# Don't try to print merge_request or repository.
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}")
"#{instance_variable}=\"#{value}\""
......
......@@ -30,6 +30,10 @@ module Gitlab
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)
{
target_branch: merge_request.target_branch,
......
module Gitlab
module Conflict
class Parser
class ParserError < StandardError
class UnresolvableError < StandardError
end
class UnexpectedDelimiter < ParserError
class UnmergeableFile < UnresolvableError
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
class UnmergeableFile < ParserError
class UnexpectedDelimiter < ParserError
end
class UnsupportedEncoding < ParserError
class MissingEndDelimiter < ParserError
end
def parse(text, our_path:, their_path:, parent_file: nil)
......
module Gitlab
module Conflict
class ResolutionError < StandardError
end
end
end
......@@ -125,6 +125,10 @@ module Gitlab
repository.blob_at(commit.id, file_path)
end
def cache_key
"#{file_path}-#{new_file}-#{deleted_file}-#{renamed_file}"
end
end
end
end
......@@ -35,16 +35,16 @@ module Gitlab
# for the highlighted ones, so we just skip their execution.
# If the highlighted diff files lines are not cached we calculate and cache them.
#
# The content of the cache is a Hash where the key correspond to the file_path and the values are Arrays of
# The content of the cache is a Hash where the key identifies the file and the values are Arrays of
# hashes that represent serialized diff lines.
#
def cache_highlight!(diff_file)
file_path = diff_file.file_path
item_key = diff_file.cache_key
if highlight_cache[file_path]
highlight_diff_file_from_cache!(diff_file, highlight_cache[file_path])
if highlight_cache[item_key]
highlight_diff_file_from_cache!(diff_file, highlight_cache[item_key])
else
highlight_cache[file_path] = diff_file.highlighted_diff_lines.map(&:to_hash)
highlight_cache[item_key] = diff_file.highlighted_diff_lines.map(&:to_hash)
end
end
......
......@@ -570,7 +570,7 @@ describe Projects::MergeRequestsController do
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::UnexpectedDelimiter)
to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
get :conflicts,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
......@@ -597,6 +597,10 @@ describe Projects::MergeRequestsController do
format: 'json'
end
it 'matches the schema' do
expect(response).to match_response_schema('conflicts')
end
it 'includes meta info about the MR' do
expect(json_response['commit_message']).to include('Merge branch')
expect(json_response['commit_sha']).to match(/\h{40}/)
......@@ -658,26 +662,97 @@ describe Projects::MergeRequestsController do
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
let(:json_response) { JSON.parse(response.body) }
let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
def resolve_conflicts(sections)
def resolve_conflicts(files)
post :resolve_conflicts,
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,
format: 'json',
sections: sections,
files: files,
commit_message: 'Commit message'
end
context 'with valid params' do
before do
resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin')
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_21_21' => 'origin',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
}
}
]
resolve_conflicts(resolved_files)
end
it 'creates a new commit on the branch' do
......@@ -692,7 +767,23 @@ describe Projects::MergeRequestsController do
context 'when sections are missing' 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
it 'returns a 400 error' do
......@@ -700,7 +791,71 @@ describe Projects::MergeRequestsController do
end
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
it 'does not create a new commit' do
......
......@@ -41,6 +41,46 @@ describe ProjectsController do
end
end
end
describe "when project repository is disabled" do
render_views
before do
project.team << [user, :developer]
project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
end
it 'shows wiki homepage' do
get :show, namespace_id: project.namespace.path, id: project.path
expect(response).to render_template('projects/_wiki')
end
it 'shows issues list page if wiki is disabled' do
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
get :show, namespace_id: project.namespace.path, id: project.path
expect(response).to render_template('projects/issues/_issues')
end
it 'shows customize workflow page if wiki and issues are disabled' do
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
get :show, namespace_id: project.namespace.path, id: project.path
expect(response).to render_template("projects/_customize_workflow")
end
it 'shows activity if enabled by user' do
user.update_attribute(:project_view, 'activity')
get :show, namespace_id: project.namespace.path, id: project.path
expect(response).to render_template("projects/_activity")
end
end
end
context "project with empty repo" do
......
......@@ -45,6 +45,7 @@ FactoryGirl.define do
snippets_access_level ProjectFeature::ENABLED
issues_access_level ProjectFeature::ENABLED
merge_requests_access_level ProjectFeature::ENABLED
repository_access_level ProjectFeature::ENABLED
end
after(:create) do |project, evaluator|
......@@ -55,6 +56,7 @@ FactoryGirl.define do
snippets_access_level: evaluator.snippets_access_level,
issues_access_level: evaluator.issues_access_level,
merge_requests_access_level: evaluator.merge_requests_access_level,
repository_access_level: evaluator.repository_access_level
)
end
end
......
......@@ -12,29 +12,139 @@ feature 'Merge request conflict resolution', js: true, feature: true do
end
end
context 'when a merge request can be resolved in the UI' do
let(:merge_request) { create_merge_request('conflict-resolvable') }
shared_examples "conflicts are resolved in Interactive mode" do
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
project.team << [user, :developer]
login_as(user)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'shows a link to the conflict resolution page' do
expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
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
expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
end
context 'in Inline view mode' do
before { click_link('conflicts', href: /\/conflicts\Z/) }
include_examples "conflicts are resolved in Interactive mode"
include_examples "conflicts are resolved in Edit inline mode"
end
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
context 'visiting the conflicts resolution page' do
before { click_link('conflicts', href: /\/conflicts\Z/) }
context 'the conflict contain markers' do
let(:merge_request) { create_merge_request('conflict-contains-conflict-markers') }
it 'shows the conflicts' do
begin
expect(find('#conflicts')).to have_content('popen.rb')
rescue Capybara::Poltergeist::JavascriptError
retry
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
......@@ -42,7 +152,6 @@ feature 'Merge request conflict resolution', js: true, feature: true do
UNRESOLVABLE_CONFLICTS = {
'conflict-too-large' => 'when the conflicts contain a large 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-non-utf8' => 'when the conflicts contain a non-UTF-8 file',
}
......
......@@ -2,8 +2,11 @@ require 'spec_helper'
include WaitForAjax
describe 'Edit Project Settings', feature: true do
include WaitForAjax
let(:member) { create(:user) }
let!(:project) { create(:project, :public, path: 'gitlab', name: 'sample') }
let!(:issue) { create(:issue, project: project) }
let(:non_member) { create(:user) }
describe 'project features visibility selectors', js: true do
......@@ -119,4 +122,31 @@ describe 'Edit Project Settings', feature: true do
end
end
end
describe 'repository visibility', js: true do
before do
project.team << [member, :master]
login_as(member)
visit edit_namespace_project_path(project.namespace, project)
end
it "disables repository related features" do
select "Disabled", from: "project_project_feature_attributes_repository_access_level"
expect(find(".edit-project")).to have_selector("select.disabled", count: 2)
end
it "shows empty features project homepage" do
select "Disabled", from: "project_project_feature_attributes_repository_access_level"
select "Disabled", from: "project_project_feature_attributes_issues_access_level"
select "Disabled", from: "project_project_feature_attributes_wiki_access_level"
click_button "Save changes"
wait_for_ajax
visit namespace_project_path(project.namespace, project)
expect(page).to have_content "Customize your workflow!"
end
end
end
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
......@@ -24,11 +24,12 @@ feature "Pipelines settings", feature: true do
context 'for master' do
given(:role) { :master }
scenario 'be allowed to change' do
scenario 'be allowed to change', js: true do
fill_in('Test coverage parsing', with: 'coverage_regex')
click_on 'Save changes'
expect(page.status_code).to eq(200)
expect(page).to have_button('Save changes', disabled: false)
expect(page).to have_field('Test coverage parsing', with: 'coverage_regex')
end
end
......
......@@ -51,6 +51,18 @@ feature 'Users', feature: true, js: true do
expect(current_path).to eq user_path(user)
expect(page).to have_text(user.name)
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
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"}
}
}
]
}
}
}
......@@ -257,5 +257,16 @@ FILE
it 'includes the blob icon for the file' do
expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o')
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
......@@ -307,6 +307,7 @@ ProjectFeature:
- wiki_access_level
- snippets_access_level
- builds_access_level
- repository_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
......
......@@ -88,24 +88,38 @@ describe Ci::Pipeline, models: true do
context 'no failed builds' do
before do
FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'success'
create_build('rspec', 'success')
end
it 'be not retryable' do
it 'is not retryable' do
is_expected.to be_falsey
end
context 'one canceled job' do
before do
create_build('rubocop', 'canceled')
end
it 'is retryable' do
is_expected.to be_truthy
end
end
end
context 'with failed builds' do
before do
FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'running'
FactoryGirl.create :ci_build, name: "rubocop", pipeline: pipeline, status: 'failed'
create_build('rspec', 'running')
create_build('rubocop', 'failed')
end
it 'be retryable' do
it 'is retryable' do
is_expected.to be_truthy
end
end
def create_build(name, status)
create(:ci_build, name: name, status: status, pipeline: pipeline)
end
end
describe '#stages' do
......
......@@ -13,7 +13,7 @@ describe Issue::Metrics, models: true do
metrics = subject.metrics
expect(metrics).to be_present
expect(metrics.first_associated_with_milestone_at).to be_within(1.second).of(time)
expect(metrics.first_associated_with_milestone_at).to be_like_time(time)
end
it "does not record the second time an issue is associated with a milestone" do
......@@ -24,7 +24,7 @@ describe Issue::Metrics, models: true do
metrics = subject.metrics
expect(metrics).to be_present
expect(metrics.first_associated_with_milestone_at).to be_within(1.second).of(time)
expect(metrics.first_associated_with_milestone_at).to be_like_time(time)
end
end
......@@ -36,7 +36,7 @@ describe Issue::Metrics, models: true do
metrics = subject.metrics
expect(metrics).to be_present
expect(metrics.first_added_to_board_at).to be_within(1.second).of(time)
expect(metrics.first_added_to_board_at).to be_like_time(time)
end
it "does not record the second time an issue is associated with a list label" do
......@@ -48,7 +48,7 @@ describe Issue::Metrics, models: true do
metrics = subject.metrics
expect(metrics).to be_present
expect(metrics.first_added_to_board_at).to be_within(1.second).of(time)
expect(metrics.first_added_to_board_at).to be_like_time(time)
end
end
end
......
......@@ -12,7 +12,7 @@ describe MergeRequest::Metrics, models: true do
metrics = subject.metrics
expect(metrics).to be_present
expect(metrics.merged_at).to be_within(1.second).of(time)
expect(metrics.merged_at).to be_like_time(time)
end
end
end
......@@ -1155,12 +1155,6 @@ describe MergeRequest, models: true do
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
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
merge_request = create_merge_request('conflict-missing-side')
......@@ -1172,6 +1166,12 @@ describe MergeRequest, models: true do
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
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
describe "#forked_source_project_missing?" do
......
......@@ -5,7 +5,7 @@ describe ProjectFeature do
let(:user) { create(:user) }
describe '#feature_available?' do
let(:features) { %w(issues wiki builds merge_requests snippets) }
let(:features) { %w(issues wiki builds merge_requests snippets repository) }
context 'when features are disabled' do
it "returns false" do
......@@ -64,6 +64,27 @@ describe ProjectFeature do
end
end
context 'repository related features' do
before do
project.project_feature.update_attributes(
merge_requests_access_level: ProjectFeature::DISABLED,
builds_access_level: ProjectFeature::DISABLED,
repository_access_level: ProjectFeature::PRIVATE
)
end
it "does not allow repository related features have higher level" do
features = %w(builds merge_requests)
project_feature = project.project_feature
features.each do |feature|
field = "#{feature}_access_level".to_sym
project_feature.update_attribute(field, ProjectFeature::ENABLED)
expect(project_feature.valid?).to be_falsy
end
end
end
describe '#*_enabled?' do
let(:features) { %w(wiki builds merge_requests) }
......
......@@ -694,7 +694,7 @@ describe API::API, api: true do
title: 'new issue', labels: 'label, label2', created_at: creation_time
expect(response).to have_http_status(201)
expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
end
end
......@@ -895,7 +895,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response['labels']).to include 'label3'
expect(Time.parse(json_response['updated_at'])).to be_within(1.second).of(update_time)
expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
end
end
end
......
......@@ -217,7 +217,7 @@ describe API::API, api: true do
expect(response).to have_http_status(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
end
......
......@@ -15,27 +15,27 @@ describe UsersController, "routing" do
end
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
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
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
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
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
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
......
......@@ -201,7 +201,7 @@ describe CreateDeploymentService, services: true do
time = Time.now
Timecop.freeze(time) { service.execute }
expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time)
expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time)
end
it "doesn't set the time if the deploy's environment is not 'production'" do
......@@ -227,13 +227,13 @@ describe CreateDeploymentService, services: true do
time = Time.now
Timecop.freeze(time) { service.execute }
expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time)
expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time)
# Current deploy
service = described_class.new(project, user, params)
Timecop.freeze(time + 12.hours) { service.execute }
expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time)
expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time)
end
end
......
......@@ -364,7 +364,7 @@ describe GitPushService, services: true do
it 'sets the metric for referenced issues' do
execute_service(project, user, @oldrev, @newrev, @ref)
expect(issue.reload.metrics.first_mentioned_in_commit_at).to be_within(1.second).of(commit_time)
expect(issue.reload.metrics.first_mentioned_in_commit_at).to be_like_time(commit_time)
end
it 'does not set the metric for non-referenced issues' do
......
......@@ -24,15 +24,26 @@ describe MergeRequests::ResolveService do
end
describe '#execute' do
context 'with valid params' do
context 'with section params' do
let(:params) do
{
sections: {
'2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
},
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: {
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
}
}
],
commit_message: 'This is a commit message!'
}
end
......@@ -49,7 +60,7 @@ describe MergeRequests::ResolveService do
it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id)).
to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
'75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b'])
'824be604a34828eb682305f0d963056cfac87b2d'])
end
end
......@@ -74,8 +85,96 @@ describe MergeRequests::ResolveService do
end
end
context 'when a resolution is missing' do
let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } }
context 'with content and sections params' do
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) }
it 'raises a MissingResolution error' do
expect { service.execute(merge_request) }.
to raise_error(Gitlab::Conflict::File::MissingResolution)
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
......@@ -83,5 +182,27 @@ describe MergeRequests::ResolveService do
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
RSpec::Matchers.define :be_like_time do |expected|
match do |actual|
expect(actual).to be_within(1.second).of(expected)
end
description do
"within one second of #{expected}"
end
failure_message do |actual|
"expected #{actual} to be within one second of #{expected}"
end
end
......@@ -27,10 +27,10 @@ module TestEnv
'expand-collapse-lines' => '238e82d',
'video' => '8879059',
'crlf-diff' => '5938907',
'conflict-start' => '75284c7',
'conflict-start' => '824be60',
'conflict-resolvable' => '1450cd6',
'conflict-binary-file' => '259a6fb',
'conflict-contains-conflict-markers' => '5e0964c',
'conflict-contains-conflict-markers' => '78a3086',
'conflict-missing-side' => 'eb227b3',
'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f',
......
......@@ -6,28 +6,48 @@ describe ExpireBuildInstanceArtifactsWorker do
let(:worker) { described_class.new }
describe '#perform' do
before { build }
subject! { worker.perform(build.id) }
before do
worker.perform(build.id)
end
context 'with expired artifacts' do
let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now - 7.days) }
let(:artifacts_expiry) { { artifacts_expire_at: Time.now - 7.days } }
it 'does expire' do
expect(build.reload.artifacts_expired?).to be_truthy
end
context 'when associated project is valid' do
let(:build) do
create(:ci_build, :artifacts, artifacts_expiry)
end
it 'does remove files' do
expect(build.reload.artifacts_file.exists?).to be_falsey
it 'does expire' do
expect(build.reload.artifacts_expired?).to be_truthy
end
it 'does remove files' do
expect(build.reload.artifacts_file.exists?).to be_falsey
end
it 'does nullify artifacts_file column' do
expect(build.reload.artifacts_file_identifier).to be_nil
end
end
it 'does nullify artifacts_file column' do
expect(build.reload.artifacts_file_identifier).to be_nil
context 'when associated project was removed' do
let(:build) do
create(:ci_build, :artifacts, artifacts_expiry) do |build|
build.project.delete
end
end
it 'does not remove artifacts' do
expect(build.reload.artifacts_file.exists?).to be_truthy
end
end
end
context 'with not yet expired artifacts' do
let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days) }
let(:build) do
create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days)
end
it 'does not expire' do
expect(build.reload.artifacts_expired?).to be_falsey
......
......@@ -23,7 +23,7 @@ describe PipelineMetricsWorker do
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)
expect(merge_request.reload.metrics.latest_build_started_at).to be_like_time(pipeline.started_at)
end
it 'clears the build end time' do
......@@ -39,7 +39,7 @@ describe PipelineMetricsWorker do
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)
expect(merge_request.reload.metrics.latest_build_finished_at).to be_like_time(pipeline.finished_at)
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