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: ...@@ -22,7 +22,7 @@ before_script:
- bundle --version - bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"' - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
- retry gem install knapsack - retry gem install knapsack
- '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql'
stages: stages:
- prepare - prepare
......
...@@ -2,6 +2,8 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -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) ## 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) - Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
- Respond with 404 Not Found for non-existent tags (Linus Thiel) - Respond with 404 Not Found for non-existent tags (Linus Thiel)
- Truncate long labels with ellipsis in labels page - 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. ...@@ -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 - Update runner version only when updating contacted_at
- Add link from system note to compare with previous version - Add link from system note to compare with previous version
- Use gitlab-shell v3.6.6 - Use gitlab-shell v3.6.6
- Ability to resolve merge request conflicts with editor !6374
- Add `/projects/visible` API endpoint (Ben Boeckel) - Add `/projects/visible` API endpoint (Ben Boeckel)
- Fix centering of custom header logos (Ashley Dumaine) - Fix centering of custom header logos (Ashley Dumaine)
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) - 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 - Updating verbiage on git basics to be more intuitive
- Clarify documentation for Runners API (Gennady Trafimenkov) - Clarify documentation for Runners API (Gennady Trafimenkov)
- The instrumentation for Banzai::Renderer has been restored - The instrumentation for Banzai::Renderer has been restored
- Change user & group landing page routing from /u/:username to /:username - Change user & group landing page routing from /u/:username to /:username
- Prevent running GfmAutocomplete setup for each diff note !6569
- Added documentation for .gitattributes files - Added documentation for .gitattributes files
- Move Pipeline Metrics to separate worker - Move Pipeline Metrics to separate worker
- AbstractReferenceFilter caches project_refs on RequestStore when active - 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. ...@@ -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 - Update Gitlab Shell to fix some problems with moving projects between storages
- Cache rendered markdown in the database, rather than Redis - Cache rendered markdown in the database, rather than Redis
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Do not alter 'force_remove_source_branch' options on MergeRequest unless specified
- Simplify Mentionable concern instance methods - Simplify Mentionable concern instance methods
- API: Ability to retrieve version information (Robert Schilling) - API: Ability to retrieve version information (Robert Schilling)
- Fix permission for setting an issue's due date - Fix permission for setting an issue's due date
...@@ -54,6 +56,7 @@ Please view this file on the master branch, on stable branches it's out of 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) - Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API - Allow the Koding integration to be configured through the API
- Add new issue button to each list on Issues Board - 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 - Added soft wrap button to repository file/blob editor
- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms) - Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
- Show the time ago a merge request was deployed to an environment - Show the time ago a merge request was deployed to an environment
...@@ -63,6 +66,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -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) - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
- Remove redundant mixins (ClemMakesApps) - Remove redundant mixins (ClemMakesApps)
- Added 'Download' button to the Snippets page (Justin DiPierro) - 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) - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) - 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 - 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. ...@@ -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 - Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496 - Take filters in account in issuable counters. !6496
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
- Prevent flash alert text from being obscured when container is fluid
- Append issue template to existing description !6149 (Joseph Frazier) - Append issue template to existing description !6149 (Joseph Frazier)
- Trending projects now only show public projects and the list of projects is cached for a day - Trending projects now only show public projects and the list of projects is cached for a day
- Memoize Gitlab Shell's secret token (!6599, Justin DiPierro) - Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
- Revoke button in Applications Settings underlines on hover. - Revoke button in Applications Settings underlines on hover.
- Use higher size on Gitlab::Redis connection pool on Sidekiq servers - Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree
- Revert avoid touching file system on Build#artifacts? - Revert avoid touching file system on Build#artifacts?
- Stop using a Redis lease when updating the project activity timestamp whenever a new event is created - Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
- Add disabled delete button to protected branches (ClemMakesApps) - Add disabled delete button to protected branches (ClemMakesApps)
...@@ -95,7 +97,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -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) - Replace bootstrap caret with fontawesome caret (ClemMakesApps)
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533 - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile - Add organization field to user profile
- Ignore deployment for statistics in Cycle Analytics, except in staging and production stages - Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being.
- Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts) - Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts)
- Fix deploy status responsiveness error !6633 - Fix deploy status responsiveness error !6633
- Make searching for commits case insensitive - Make searching for commits case insensitive
...@@ -107,6 +109,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -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 - 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) - 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 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 - Fix Pipeline list commit column width should be adjusted
- Close todos when accepting merge requests via the API !6486 (tonygambone) - 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) - 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. ...@@ -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 - Grouped pipeline dropdown is a scrollable container
- Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi) - Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
- Fixes padding in all clipboard icons that have .btn class - 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 - Fix a typo in doc/api/labels.md
- API: all unknown routing will be handled with 404 Not Found - API: all unknown routing will be handled with 404 Not Found
- Add docs for request profiling - 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. ...@@ -126,8 +128,15 @@ Please view this file on the master branch, on stable branches it's out of date.
## 8.12.7 ## 8.12.7
- Use gitlab-markup gem instead of github-markup to fix `.rst` file rendering. !6659 - Prevent running `GfmAutocomplete` setup for each diff note. !6569
- Fix GFM autocomplete setup being called several times - Fix long commit messages overflow viewport in file tree. !6573
- Use `gitlab-markup` gem instead of `github-markup` to fix `.rst` file rendering. !6659
- Prevent flash alert text from being obscured when container is fluid. !6694
- Fix due date being displayed as `NaN` in Safari. !6797
- Fix JS bug with select2 because of missing `data-field` attribute in select box. !6812
- Do not alter `force_remove_source_branch` options on MergeRequest unless specified. !6817
- Fix GFM autocomplete setup being called several times. !6840
- Handle case where deployment ref no longer exists. !6855
## 8.12.6 ## 8.12.6
......
...@@ -262,8 +262,6 @@ group :development do ...@@ -262,8 +262,6 @@ group :development do
# thin instead webrick # thin instead webrick
gem 'thin', '~> 1.7.0' gem 'thin', '~> 1.7.0'
gem 'activerecord_sane_schema_dumper', '0.2'
end end
group :development, :test do group :development, :test do
...@@ -310,6 +308,8 @@ group :development, :test do ...@@ -310,6 +308,8 @@ group :development, :test do
gem 'license_finder', '~> 2.1.0', require: false gem 'license_finder', '~> 2.1.0', require: false
gem 'knapsack', '~> 1.11.0' gem 'knapsack', '~> 1.11.0'
gem 'activerecord_sane_schema_dumper', '0.2'
end end
group :test do group :test do
......
...@@ -101,9 +101,6 @@ ...@@ -101,9 +101,6 @@
new ZenMode(); new ZenMode();
new MergedButtons(); new MergedButtons();
break; break;
case "projects:merge_requests:conflicts":
window.mcui = new MergeConflictResolver()
break;
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
Issuable.init(); Issuable.init();
......
const HEAD_HEADER_TEXT = 'HEAD//our changes';
const ORIGIN_HEADER_TEXT = 'origin//their changes';
const HEAD_BUTTON_TITLE = 'Use ours';
const ORIGIN_BUTTON_TITLE = 'Use theirs';
class MergeConflictDataProvider {
getInitialData() {
// TODO: remove reliance on jQuery and DOM state introspection
const diffViewType = $.cookie('diff_view');
const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited');
return {
isLoading : true,
hasError : false,
isParallel : diffViewType === 'parallel',
diffViewType : diffViewType,
fixedLayout : fixedLayout,
isSubmitting : false,
conflictsData : {},
resolutionData : {}
}
}
decorateData(vueInstance, data) {
this.vueInstance = vueInstance;
if (data.type === 'error') {
vueInstance.hasError = true;
data.errorMessage = data.message;
}
else {
data.shortCommitSha = data.commit_sha.slice(0, 7);
data.commitMessage = data.commit_message;
this.setParallelLines(data);
this.setInlineLines(data);
this.updateResolutionsData(data);
}
vueInstance.conflictsData = data;
vueInstance.isSubmitting = false;
const conflictsText = this.getConflictsCount() > 1 ? 'conflicts' : 'conflict';
vueInstance.conflictsData.conflictsText = conflictsText;
}
updateResolutionsData(data) {
const vi = this.vueInstance;
data.files.forEach( (file) => {
file.sections.forEach( (section) => {
if (section.conflict) {
vi.$set(`resolutionData['${section.id}']`, false);
}
});
});
}
setParallelLines(data) {
data.files.forEach( (file) => {
file.filePath = this.getFilePath(file);
file.iconClass = `fa-${file.blob_icon}`;
file.blobPath = file.blob_path;
file.parallelLines = [];
const linesObj = { left: [], right: [] };
file.sections.forEach( (section) => {
const { conflict, lines, id } = section;
if (conflict) {
linesObj.left.push(this.getOriginHeaderLine(id));
linesObj.right.push(this.getHeadHeaderLine(id));
}
lines.forEach( (line) => {
const { type } = line;
if (conflict) {
if (type === 'old') {
linesObj.left.push(this.getLineForParallelView(line, id, 'conflict'));
}
else if (type === 'new') {
linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true));
}
}
else {
const lineType = type || 'context';
linesObj.left.push (this.getLineForParallelView(line, id, lineType));
linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
}
});
this.checkLineLengths(linesObj);
});
for (let i = 0, len = linesObj.left.length; i < len; i++) {
file.parallelLines.push([
linesObj.right[i],
linesObj.left[i]
]);
}
});
}
checkLineLengths(linesObj) {
let { left, right } = linesObj;
if (left.length !== right.length) {
if (left.length > right.length) {
const diff = left.length - right.length;
for (let i = 0; i < diff; i++) {
right.push({ lineType: 'emptyLine', richText: '' });
}
}
else {
const diff = right.length - left.length;
for (let i = 0; i < diff; i++) {
left.push({ lineType: 'emptyLine', richText: '' });
}
}
}
}
setInlineLines(data) {
data.files.forEach( (file) => {
file.iconClass = `fa-${file.blob_icon}`;
file.blobPath = file.blob_path;
file.filePath = this.getFilePath(file);
file.inlineLines = []
file.sections.forEach( (section) => {
let currentLineType = 'new';
const { conflict, lines, id } = section;
if (conflict) {
file.inlineLines.push(this.getHeadHeaderLine(id));
}
lines.forEach( (line) => {
const { type } = line;
if ((type === 'new' || type === 'old') && currentLineType !== type) {
currentLineType = type;
file.inlineLines.push({ lineType: 'emptyLine', richText: '' });
}
this.decorateLineForInlineView(line, id, conflict);
file.inlineLines.push(line);
})
if (conflict) {
file.inlineLines.push(this.getOriginHeaderLine(id));
}
});
});
}
handleSelected(sectionId, selection) {
const vi = this.vueInstance;
vi.resolutionData[sectionId] = selection;
vi.conflictsData.files.forEach( (file) => {
file.inlineLines.forEach( (line) => {
if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
this.markLine(line, selection);
}
});
file.parallelLines.forEach( (lines) => {
const left = lines[0];
const right = lines[1];
const hasSameId = right.id === sectionId || left.id === sectionId;
const isLeftMatch = left.hasConflict || left.isHeader;
const isRightMatch = right.hasConflict || right.isHeader;
if (hasSameId && (isLeftMatch || isRightMatch)) {
this.markLine(left, selection);
this.markLine(right, selection);
}
})
});
}
updateViewType(newType) {
const vi = this.vueInstance;
if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) {
return;
}
vi.diffViewType = newType;
vi.isParallel = newType === 'parallel';
$.cookie('diff_view', newType, {
path: (gon && gon.relative_url_root) || '/'
});
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', !vi.isParallel && vi.fixedLayout);
}
markLine(line, selection) {
if (selection === 'head' && line.isHead) {
line.isSelected = true;
line.isUnselected = false;
}
else if (selection === 'origin' && line.isOrigin) {
line.isSelected = true;
line.isUnselected = false;
}
else {
line.isSelected = false;
line.isUnselected = true;
}
}
getConflictsCount() {
return Object.keys(this.vueInstance.resolutionData).length;
}
getResolvedCount() {
let count = 0;
const data = this.vueInstance.resolutionData;
for (const id in data) {
const resolution = data[id];
if (resolution) {
count++;
}
}
return count;
}
isReadyToCommit() {
const { conflictsData, isSubmitting } = this.vueInstance
const allResolved = this.getConflictsCount() === this.getResolvedCount();
const hasCommitMessage = $.trim(conflictsData.commitMessage).length;
return !isSubmitting && hasCommitMessage && allResolved;
}
getCommitButtonText() {
const initial = 'Commit conflict resolution';
const inProgress = 'Committing...';
const vue = this.vueInstance;
return vue ? vue.isSubmitting ? inProgress : initial : initial;
}
decorateLineForInlineView(line, id, conflict) {
const { type } = line;
line.id = id;
line.hasConflict = conflict;
line.isHead = type === 'new';
line.isOrigin = type === 'old';
line.hasMatch = type === 'match';
line.richText = line.rich_text;
line.isSelected = false;
line.isUnselected = false;
}
getLineForParallelView(line, id, lineType, isHead) {
const { old_line, new_line, rich_text } = line;
const hasConflict = lineType === 'conflict';
return {
id,
lineType,
hasConflict,
isHead : hasConflict && isHead,
isOrigin : hasConflict && !isHead,
hasMatch : lineType === 'match',
lineNumber : isHead ? new_line : old_line,
section : isHead ? 'head' : 'origin',
richText : rich_text,
isSelected : false,
isUnselected : false
}
}
getHeadHeaderLine(id) {
return {
id : id,
richText : HEAD_HEADER_TEXT,
buttonTitle : HEAD_BUTTON_TITLE,
type : 'new',
section : 'head',
isHeader : true,
isHead : true,
isSelected : false,
isUnselected: false
}
}
getOriginHeaderLine(id) {
return {
id : id,
richText : ORIGIN_HEADER_TEXT,
buttonTitle : ORIGIN_BUTTON_TITLE,
type : 'old',
section : 'origin',
isHeader : true,
isOrigin : true,
isSelected : false,
isUnselected: false
}
}
handleFailedRequest(vueInstance, data) {
vueInstance.hasError = true;
vueInstance.conflictsData.errorMessage = 'Something went wrong!';
}
getCommitData() {
return {
commit_message: this.vueInstance.conflictsData.commitMessage,
sections: this.vueInstance.resolutionData
}
}
getFilePath(file) {
const { old_path, new_path } = file;
return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
}
}
//= require vue
class MergeConflictResolver {
constructor() {
this.dataProvider = new MergeConflictDataProvider()
this.initVue()
}
initVue() {
const that = this;
this.vue = new Vue({
el : '#conflicts',
name : 'MergeConflictResolver',
data : this.dataProvider.getInitialData(),
created : this.fetchData(),
computed : this.setComputedProperties(),
methods : {
handleSelected(sectionId, selection) {
that.dataProvider.handleSelected(sectionId, selection);
},
handleViewTypeChange(newType) {
that.dataProvider.updateViewType(newType);
},
commit() {
that.commit();
}
}
})
}
setComputedProperties() {
const dp = this.dataProvider;
return {
conflictsCount() { return dp.getConflictsCount() },
resolvedCount() { return dp.getResolvedCount() },
readyToCommit() { return dp.isReadyToCommit() },
commitButtonText() { return dp.getCommitButtonText() }
}
}
fetchData() {
const dp = this.dataProvider;
$.get($('#conflicts').data('conflictsPath'))
.done((data) => {
dp.decorateData(this.vue, data);
})
.error((data) => {
dp.handleFailedRequest(this.vue, data);
})
.always(() => {
this.vue.isLoading = false;
this.vue.$nextTick(() => {
$('#conflicts .js-syntax-highlight').syntaxHighlight();
});
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout);
})
}
commit() {
this.vue.isSubmitting = true;
$.post($('#conflicts').data('resolveConflictsPath'), this.dataProvider.getCommitData())
.done((data) => {
window.location.href = data.redirect_to;
})
.error(() => {
this.vue.isSubmitting = false;
new Flash('Something went wrong!');
});
}
}
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.diffFileEditor = Vue.extend({
props: {
file: Object,
onCancelDiscardConfirmation: Function,
onAcceptDiscardConfirmation: Function
},
data() {
return {
saved: false,
loading: false,
fileLoaded: false,
originalContent: '',
}
},
computed: {
classObject() {
return {
'saved': this.saved,
'is-loading': this.loading
};
}
},
watch: {
['file.showEditor'](val) {
this.resetEditorContent();
if (!val || this.fileLoaded || this.loading) {
return;
}
this.loadEditor();
}
},
ready() {
if (this.file.loadEditor) {
this.loadEditor();
}
},
methods: {
loadEditor() {
this.loading = true;
$.get(this.file.content_path)
.done((file) => {
let content = this.$el.querySelector('pre');
let fileContent = document.createTextNode(file.content);
content.textContent = fileContent.textContent;
this.originalContent = file.content;
this.fileLoaded = true;
this.editor = ace.edit(content);
this.editor.$blockScrolling = Infinity; // Turn off annoying warning
this.editor.getSession().setMode(`ace/mode/${file.blob_ace_mode}`);
this.editor.on('change', () => {
this.saveDiffResolution();
});
this.saveDiffResolution();
})
.fail(() => {
new Flash('Failed to load the file, please try again.');
})
.always(() => {
this.loading = false;
});
},
saveDiffResolution() {
this.saved = true;
// This probably be better placed in the data provider
this.file.content = this.editor.getValue();
this.file.resolveEditChanged = this.file.content !== this.originalContent;
this.file.promptDiscardConfirmation = false;
},
resetEditorContent() {
if (this.fileLoaded) {
this.editor.setValue(this.originalContent, -1);
}
},
cancelDiscardConfirmation(file) {
this.onCancelDiscardConfirmation(file);
},
acceptDiscardConfirmation(file) {
this.onAcceptDiscardConfirmation(file);
}
}
});
})(window.gl || (window.gl = {}));
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.inlineConflictLines = Vue.extend({
props: {
file: Object
},
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
});
})(window.gl || (window.gl = {}));
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLine = Vue.extend({
props: {
file: Object,
line: Object
},
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
template: '#parallel-conflict-line'
});
})(window.gl || (window.gl = {}));
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLines = Vue.extend({
props: {
file: Object
},
mixins: [global.mergeConflicts.utils],
components: {
'parallel-conflict-line': gl.mergeConflicts.parallelConflictLine
}
});
})(window.gl || (window.gl = {}));
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
class mergeConflictsService {
constructor(options) {
this.conflictsPath = options.conflictsPath;
this.resolveConflictsPath = options.resolveConflictsPath;
}
fetchConflictsData() {
return $.ajax({
dataType: 'json',
url: this.conflictsPath
});
}
submitResolveConflicts(data) {
return $.ajax({
url: this.resolveConflictsPath,
data: JSON.stringify(data),
contentType: 'application/json',
dataType: 'json',
method: 'POST'
});
}
};
global.mergeConflicts.mergeConflictsService = mergeConflictsService;
})(window.gl || (window.gl = {}));
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
const diffViewType = $.cookie('diff_view');
const HEAD_HEADER_TEXT = 'HEAD//our changes';
const ORIGIN_HEADER_TEXT = 'origin//their changes';
const HEAD_BUTTON_TITLE = 'Use ours';
const ORIGIN_BUTTON_TITLE = 'Use theirs';
const INTERACTIVE_RESOLVE_MODE = 'interactive';
const EDIT_RESOLVE_MODE = 'edit';
const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE;
const VIEW_TYPES = {
INLINE: 'inline',
PARALLEL: 'parallel'
};
const CONFLICT_TYPES = {
TEXT: 'text',
TEXT_EDITOR: 'text-editor'
};
global.mergeConflicts.mergeConflictsStore = {
state: {
isLoading: true,
hasError: false,
isSubmitting: false,
isParallel: diffViewType === VIEW_TYPES.PARALLEL,
diffViewType: diffViewType,
conflictsData: {}
},
setConflictsData(data) {
this.decorateFiles(data.files);
this.state.conflictsData = {
files: data.files,
commitMessage: data.commit_message,
sourceBranch: data.source_branch,
targetBranch: data.target_branch,
commitMessage: data.commit_message,
shortCommitSha: data.commit_sha.slice(0, 7),
};
},
decorateFiles(files) {
files.forEach((file) => {
file.content = '';
file.resolutionData = {};
file.promptDiscardConfirmation = false;
file.resolveMode = DEFAULT_RESOLVE_MODE;
file.filePath = this.getFilePath(file);
file.iconClass = `fa-${file.blob_icon}`;
file.blobPath = file.blob_path;
if (file.type === CONFLICT_TYPES.TEXT) {
file.showEditor = false;
file.loadEditor = false;
this.setInlineLine(file);
this.setParallelLine(file);
} else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) {
file.showEditor = true;
file.loadEditor = true;
}
});
},
setInlineLine(file) {
file.inlineLines = [];
file.sections.forEach((section) => {
let currentLineType = 'new';
const { conflict, lines, id } = section;
if (conflict) {
file.inlineLines.push(this.getHeadHeaderLine(id));
}
lines.forEach((line) => {
const { type } = line;
if ((type === 'new' || type === 'old') && currentLineType !== type) {
currentLineType = type;
file.inlineLines.push({ lineType: 'emptyLine', richText: '' });
}
this.decorateLineForInlineView(line, id, conflict);
file.inlineLines.push(line);
})
if (conflict) {
file.inlineLines.push(this.getOriginHeaderLine(id));
}
});
},
setParallelLine(file) {
file.parallelLines = [];
const linesObj = { left: [], right: [] };
file.sections.forEach((section) => {
const { conflict, lines, id } = section;
if (conflict) {
linesObj.left.push(this.getOriginHeaderLine(id));
linesObj.right.push(this.getHeadHeaderLine(id));
}
lines.forEach((line) => {
const { type } = line;
if (conflict) {
if (type === 'old') {
linesObj.left.push(this.getLineForParallelView(line, id, 'conflict'));
} else if (type === 'new') {
linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true));
}
} else {
const lineType = type || 'context';
linesObj.left.push (this.getLineForParallelView(line, id, lineType));
linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
}
});
this.checkLineLengths(linesObj);
});
for (let i = 0, len = linesObj.left.length; i < len; i++) {
file.parallelLines.push([
linesObj.right[i],
linesObj.left[i]
]);
}
},
setLoadingState(state) {
this.state.isLoading = state;
},
setErrorState(state) {
this.state.hasError = state;
},
setFailedRequest(message) {
this.state.hasError = true;
this.state.conflictsData.errorMessage = message;
},
getConflictsCount() {
if (!this.state.conflictsData.files.length) {
return 0;
}
const files = this.state.conflictsData.files;
let count = 0;
files.forEach((file) => {
if (file.type === CONFLICT_TYPES.TEXT) {
file.sections.forEach((section) => {
if (section.conflict) {
count++;
}
});
} else {
count++;
}
});
return count;
},
getConflictsCountText() {
const count = this.getConflictsCount();
const text = count ? 'conflicts' : 'conflict';
return `${count} ${text}`;
},
setViewType(viewType) {
this.state.diffView = viewType;
this.state.isParallel = viewType === VIEW_TYPES.PARALLEL;
$.cookie('diff_view', viewType, {
path: gon.relative_url_root || '/'
});
},
getHeadHeaderLine(id) {
return {
id: id,
richText: HEAD_HEADER_TEXT,
buttonTitle: HEAD_BUTTON_TITLE,
type: 'new',
section: 'head',
isHeader: true,
isHead: true,
isSelected: false,
isUnselected: false
};
},
decorateLineForInlineView(line, id, conflict) {
const { type } = line;
line.id = id;
line.hasConflict = conflict;
line.isHead = type === 'new';
line.isOrigin = type === 'old';
line.hasMatch = type === 'match';
line.richText = line.rich_text;
line.isSelected = false;
line.isUnselected = false;
},
getLineForParallelView(line, id, lineType, isHead) {
const { old_line, new_line, rich_text } = line;
const hasConflict = lineType === 'conflict';
return {
id,
lineType,
hasConflict,
isHead: hasConflict && isHead,
isOrigin: hasConflict && !isHead,
hasMatch: lineType === 'match',
lineNumber: isHead ? new_line : old_line,
section: isHead ? 'head' : 'origin',
richText: rich_text,
isSelected: false,
isUnselected: false
};
},
getOriginHeaderLine(id) {
return {
id: id,
richText: ORIGIN_HEADER_TEXT,
buttonTitle: ORIGIN_BUTTON_TITLE,
type: 'old',
section: 'origin',
isHeader: true,
isOrigin: true,
isSelected: false,
isUnselected: false
};
},
getFilePath(file) {
const { old_path, new_path } = file;
return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
},
checkLineLengths(linesObj) {
let { left, right } = linesObj;
if (left.length !== right.length) {
if (left.length > right.length) {
const diff = left.length - right.length;
for (let i = 0; i < diff; i++) {
right.push({ lineType: 'emptyLine', richText: '' });
}
} else {
const diff = right.length - left.length;
for (let i = 0; i < diff; i++) {
left.push({ lineType: 'emptyLine', richText: '' });
}
}
}
},
setPromptConfirmationState(file, state) {
file.promptDiscardConfirmation = state;
},
setFileResolveMode(file, mode) {
if (mode === INTERACTIVE_RESOLVE_MODE) {
file.showEditor = false;
} else if (mode === EDIT_RESOLVE_MODE) {
// Restore Interactive mode when switching to Edit mode
file.showEditor = true;
file.loadEditor = true;
file.resolutionData = {};
this.restoreFileLinesState(file);
}
file.resolveMode = mode;
},
restoreFileLinesState(file) {
file.inlineLines.forEach((line) => {
if (line.hasConflict || line.isHeader) {
line.isSelected = false;
line.isUnselected = false;
}
});
file.parallelLines.forEach((lines) => {
const left = lines[0];
const right = lines[1];
const isLeftMatch = left.hasConflict || left.isHeader;
const isRightMatch = right.hasConflict || right.isHeader;
if (isLeftMatch || isRightMatch) {
left.isSelected = false;
left.isUnselected = false;
right.isSelected = false;
right.isUnselected = false;
}
});
},
isReadyToCommit() {
const files = this.state.conflictsData.files;
const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length;
let unresolved = 0;
for (let i = 0, l = files.length; i < l; i++) {
let file = files[i];
if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
let numberConflicts = 0;
let resolvedConflicts = Object.keys(file.resolutionData).length
// We only check for conflicts type 'text'
// since conflicts `text_editor` can´t be resolved in interactive mode
if (file.type === CONFLICT_TYPES.TEXT) {
for (let j = 0, k = file.sections.length; j < k; j++) {
if (file.sections[j].conflict) {
numberConflicts++;
}
}
if (resolvedConflicts !== numberConflicts) {
unresolved++;
}
}
} else if (file.resolveMode === EDIT_RESOLVE_MODE) {
// Unlikely to happen since switching to Edit mode saves content automatically.
// Checking anyway in case the save strategy changes in the future
if (!file.content) {
unresolved++;
continue;
}
}
}
return !this.state.isSubmitting && hasCommitMessage && !unresolved;
},
getCommitButtonText() {
const initial = 'Commit conflict resolution';
const inProgress = 'Committing...';
return this.state ? this.state.isSubmitting ? inProgress : initial : initial;
},
getCommitData() {
let commitData = {};
commitData = {
commit_message: this.state.conflictsData.commitMessage,
files: []
};
this.state.conflictsData.files.forEach((file) => {
let addFile;
addFile = {
old_path: file.old_path,
new_path: file.new_path
};
if (file.type === CONFLICT_TYPES.TEXT) {
// Submit only one data for type of editing
if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
addFile.sections = file.resolutionData;
} else if (file.resolveMode === EDIT_RESOLVE_MODE) {
addFile.content = file.content;
}
} else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) {
addFile.content = file.content;
}
commitData.files.push(addFile);
});
return commitData;
},
handleSelected(file, sectionId, selection) {
Vue.set(file.resolutionData, sectionId, selection);
file.inlineLines.forEach((line) => {
if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
this.markLine(line, selection);
}
});
file.parallelLines.forEach((lines) => {
const left = lines[0];
const right = lines[1];
const hasSameId = right.id === sectionId || left.id === sectionId;
const isLeftMatch = left.hasConflict || left.isHeader;
const isRightMatch = right.hasConflict || right.isHeader;
if (hasSameId && (isLeftMatch || isRightMatch)) {
this.markLine(left, selection);
this.markLine(right, selection);
}
});
},
markLine(line, selection) {
if (selection === 'head' && line.isHead) {
line.isSelected = true;
line.isUnselected = false;
} else if (selection === 'origin' && line.isOrigin) {
line.isSelected = true;
line.isUnselected = false;
} else {
line.isSelected = false;
line.isUnselected = true;
}
},
setSubmitState(state) {
this.state.isSubmitting = state;
},
fileTextTypePresent() {
return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT);
}
};
})(window.gl || (window.gl = {}));
//= require vue
//= require ./merge_conflict_store
//= require ./merge_conflict_service
//= require ./mixins/line_conflict_utils
//= require ./mixins/line_conflict_actions
//= require ./components/diff_file_editor
//= require ./components/inline_conflict_lines
//= require ./components/parallel_conflict_line
//= require ./components/parallel_conflict_lines
$(() => {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
const conflictsEl = document.querySelector('#conflicts');
const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({
conflictsPath: conflictsEl.dataset.conflictsPath,
resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath
});
gl.MergeConflictsResolverApp = new Vue({
el: '#conflicts',
data: mergeConflictsStore.state,
components: {
'diff-file-editor': gl.mergeConflicts.diffFileEditor,
'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines,
'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines
},
computed: {
conflictsCountText() { return mergeConflictsStore.getConflictsCountText() },
readyToCommit() { return mergeConflictsStore.isReadyToCommit() },
commitButtonText() { return mergeConflictsStore.getCommitButtonText() },
showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent() }
},
created() {
mergeConflictsService
.fetchConflictsData()
.done((data) => {
if (data.type === 'error') {
mergeConflictsStore.setFailedRequest(data.message);
} else {
mergeConflictsStore.setConflictsData(data);
}
})
.error(() => {
mergeConflictsStore.setFailedRequest();
})
.always(() => {
mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => {
$(conflictsEl.querySelectorAll('.js-syntax-highlight')).syntaxHighlight();
});
});
},
methods: {
handleViewTypeChange(viewType) {
mergeConflictsStore.setViewType(viewType);
},
onClickResolveModeButton(file, mode) {
if (mode === INTERACTIVE_RESOLVE_MODE && file.resolveEditChanged) {
mergeConflictsStore.setPromptConfirmationState(file, true);
return;
}
mergeConflictsStore.setFileResolveMode(file, mode);
},
acceptDiscardConfirmation(file) {
mergeConflictsStore.setPromptConfirmationState(file, false);
mergeConflictsStore.setFileResolveMode(file, INTERACTIVE_RESOLVE_MODE);
},
cancelDiscardConfirmation(file) {
mergeConflictsStore.setPromptConfirmationState(file, false);
},
commit() {
mergeConflictsStore.setSubmitState(true);
mergeConflictsService
.submitResolveConflicts(mergeConflictsStore.getCommitData())
.done((data) => {
window.location.href = data.redirect_to;
})
.error(() => {
mergeConflictsStore.setSubmitState(false);
new Flash('Failed to save merge conflicts resolutions. Please try again!');
});
}
}
})
});
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.actions = {
methods: {
handleSelected(file, sectionId, selection) {
gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection);
}
}
};
})(window.gl || (window.gl = {}));
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.utils = {
methods: {
lineCssClass(line) {
return {
'head': line.isHead,
'origin': line.isOrigin,
'match': line.hasMatch,
'selected': line.isSelected,
'unselected': line.isUnselected
};
}
}
};
})(window.gl || (window.gl = {}));
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide') graphCollapsed ? $btnText.text('Hide') : $btnText.text('Expand')
} }
addMarginToBuildColumns() { addMarginToBuildColumns() {
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
function ProjectFindFile(element1, options) { function ProjectFindFile(element1, options) {
this.element = element1; this.element = element1;
this.options = options; this.options = options;
this.goToBlob = bind(this.goToBlob, this);
this.goToTree = bind(this.goToTree, this); this.goToTree = bind(this.goToTree, this);
this.selectRowDown = bind(this.selectRowDown, this); this.selectRowDown = bind(this.selectRowDown, this);
this.selectRowUp = bind(this.selectRowUp, this); this.selectRowUp = bind(this.selectRowUp, this);
...@@ -154,6 +155,14 @@ ...@@ -154,6 +155,14 @@
return location.href = this.options.treeUrl; return location.href = this.options.treeUrl;
}; };
ProjectFindFile.prototype.goToBlob = function() {
var $link = this.element.find(".tree-item.selected .tree-item-file-name a");
if ($link.length) {
$link.get(0).click();
}
};
return ProjectFindFile; return ProjectFindFile;
})(); })();
......
...@@ -4,9 +4,8 @@ ...@@ -4,9 +4,8 @@
this.ProjectNew = (function() { this.ProjectNew = (function() {
function ProjectNew() { function ProjectNew() {
this.toggleSettings = bind(this.toggleSettings, this); this.toggleSettings = bind(this.toggleSettings, this);
this.$selects = $('.features select').filter(function () { this.$selects = $('.features select');
return $(this).data('field'); this.$repoSelects = this.$selects.filter('.js-repo-select');
});
$('.project-edit-container').on('ajax:before', (function(_this) { $('.project-edit-container').on('ajax:before', (function(_this) {
return function() { return function() {
...@@ -16,6 +15,7 @@ ...@@ -16,6 +15,7 @@
})(this)); })(this));
this.toggleSettings(); this.toggleSettings();
this.toggleSettingsOnclick(); this.toggleSettingsOnclick();
this.toggleRepoVisibility();
} }
ProjectNew.prototype.toggleSettings = function() { ProjectNew.prototype.toggleSettings = function() {
...@@ -43,6 +43,38 @@ ...@@ -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; return ProjectNew;
})(); })();
......
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
this.renderState(); this.renderState();
return $.ajax({ return $.ajax({
type: 'GET', type: 'GET',
url: `/u/${username}/exists`, url: `/users/${username}/exists`,
dataType: 'json', dataType: 'json',
success: (res) => this.setAvailabilityState(res.exists) success: (res) => this.setAvailabilityState(res.exists)
}); });
......
...@@ -45,40 +45,38 @@ ...@@ -45,40 +45,38 @@
} }
h1 { h1 {
font-size: 2em; font-size: 1.75em;
font-weight: 600; font-weight: 600;
margin: 1em 0 10px; margin: 16px 0 10px;
padding: 0 0 0.3em; padding: 0 0 0.3em;
border-bottom: 1px solid $btn-default-border; border-bottom: 1px solid $white-dark;
color: $gl-gray-dark; color: $gl-gray-dark;
} }
h2 { h2 {
font-size: 1.6em; font-size: 1.5em;
font-weight: 600; font-weight: 600;
margin: 1em 0 10px; margin: 16px 0 10px;
padding-bottom: 0.3em;
border-bottom: 1px solid $btn-default-border;
color: $gl-gray-dark; color: $gl-gray-dark;
} }
h3 { h3 {
margin: 1em 0 10px; margin: 16px 0 10px;
font-size: 1.4em; font-size: 1.3em;
} }
h4 { h4 {
margin: 1em 0 10px; margin: 16px 0 10px;
font-size: 1.25em; font-size: 1.2em;
} }
h5 { h5 {
margin: 1em 0 10px; margin: 16px 0 10px;
font-size: 1em; font-size: 1em;
} }
h6 { h6 {
margin: 1em 0 10px; margin: 16px 0 10px;
font-size: 0.95em; font-size: 0.95em;
} }
...@@ -87,12 +85,12 @@ ...@@ -87,12 +85,12 @@
font-size: inherit; font-size: inherit;
padding: 8px 21px; padding: 8px 21px;
margin: 12px 0; margin: 12px 0;
border-left: 3px solid #e7e9ed; border-left: 3px solid $white-dark;
} }
blockquote:dir(rtl) { blockquote:dir(rtl) {
border-left: 0; border-left: 0;
border-right: 3px solid #e7e9ed; border-right: 3px solid $white-dark;
} }
blockquote p { blockquote p {
......
...@@ -56,6 +56,7 @@ $border-gray-light: #dcdcdc; ...@@ -56,6 +56,7 @@ $border-gray-light: #dcdcdc;
$border-gray-normal: #d7d7d7; $border-gray-normal: #d7d7d7;
$border-gray-dark: #c6cacf; $border-gray-dark: #c6cacf;
$border-green-extra-light: #9adb84;
$border-green-light: #2faa60; $border-green-light: #2faa60;
$border-green-normal: #2ca05b; $border-green-normal: #2ca05b;
$border-green-dark: #279654; $border-green-dark: #279654;
......
...@@ -20,9 +20,11 @@ ...@@ -20,9 +20,11 @@
.detail-page-description { .detail-page-description {
.title { .title {
margin: 0; margin: 0 0 16px;
font-size: 23px; font-size: 2em;
color: $gl-gray-dark; color: $gl-gray-dark;
padding: 0 0 0.3em;
border-bottom: 1px solid $white-dark;
} }
.description { .description {
......
...@@ -237,4 +237,51 @@ $colors: ( ...@@ -237,4 +237,51 @@ $colors: (
.btn-success .fa-spinner { .btn-success .fa-spinner {
color: #fff; color: #fff;
} }
.editor-wrap {
&.is-loading {
.editor {
display: none;
}
.loading {
display: block;
}
}
&.saved {
.editor {
border-top: solid 2px $border-green-extra-light;
}
}
.editor {
pre {
height: 350px;
border: none;
border-radius: 0;
margin-bottom: 0;
}
}
.loading {
display: none;
}
}
.discard-changes-alert {
background-color: $background-color;
text-align: right;
padding: $gl-padding-top $gl-padding;
color: $gl-text-color;
.discard-actions {
display: inline-block;
margin-left: 10px;
}
}
.resolve-conflicts-form {
padding-top: $gl-padding;
}
} }
...@@ -429,13 +429,6 @@ ...@@ -429,13 +429,6 @@
} }
} }
.merge-request-details {
.title {
margin-bottom: 20px;
}
}
.merge-request-tabs { .merge-request-tabs {
background-color: #fff; background-color: #fff;
......
...@@ -761,62 +761,6 @@ pre.light-well { ...@@ -761,62 +761,6 @@ pre.light-well {
.dropdown-menu { .dropdown-menu {
width: 300px; 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 { .clearable-input {
...@@ -855,3 +799,30 @@ pre.light-well { ...@@ -855,3 +799,30 @@ pre.light-well {
border-bottom-right-radius: 0; 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 @@ ...@@ -169,4 +169,8 @@
margin-top: 11px; margin-top: 11px;
position: relative; position: relative;
z-index: 2; z-index: 2;
.download-button {
margin-left: $btn-side-margin;
}
} }
...@@ -118,7 +118,12 @@ class ApplicationController < ActionController::Base ...@@ -118,7 +118,12 @@ class ApplicationController < ActionController::Base
end end
def render_404 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 end
def no_cache_headers def no_cache_headers
......
...@@ -9,15 +9,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -9,15 +9,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check, :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines, :merge, :merge_check,
:ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
] ]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs] before_action :define_commit_vars, only: [:diffs]
before_action :define_diff_comment_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :apply_diff_view_cookie!, only: [:new_diffs] before_action :apply_diff_view_cookie!, only: [:new_diffs]
before_action :build_merge_request, only: [:new, :new_diffs] before_action :build_merge_request, only: [:new, :new_diffs]
...@@ -33,7 +33,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -33,7 +33,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authenticate_user!, only: [:assign_related_issues] before_action :authenticate_user!, only: [:assign_related_issues]
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts] before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts]
def index def index
@merge_requests = merge_requests_collection @merge_requests = merge_requests_collection
...@@ -170,6 +170,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -170,6 +170,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
end end
def conflict_for_path
return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
file = @merge_request.conflicts.file_for_path(params[:old_path], params[:new_path])
return render_404 unless file
render json: file, full_content: true
end
def resolve_conflicts def resolve_conflicts
return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui? return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
...@@ -184,7 +194,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -184,7 +194,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.' flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'
render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) } render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) }
rescue Gitlab::Conflict::File::MissingResolution => e rescue Gitlab::Conflict::ResolutionError => e
render status: :bad_request, json: { message: e.message } render status: :bad_request, json: { message: e.message }
end end
end end
......
class ProjectsController < Projects::ApplicationController class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath include ExtractsPath
before_action :authenticate_user!, except: [:show, :activity, :refs] before_action :authenticate_user!, except: [:show, :activity, :refs]
...@@ -103,16 +104,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -103,16 +104,7 @@ class ProjectsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
@notification_setting = current_user.notification_settings_for(@project) if current_user @notification_setting = current_user.notification_settings_for(@project) if current_user
render_landing_page
if @project.repository_exists?
if @project.empty_repo?
render 'projects/empty'
else
render :show
end
else
render 'projects/no_repo'
end
end end
format.atom do format.atom do
...@@ -285,6 +277,26 @@ class ProjectsController < Projects::ApplicationController ...@@ -285,6 +277,26 @@ class ProjectsController < Projects::ApplicationController
private 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 def determine_layout
if [:new, :create].include?(action_name.to_sym) if [:new, :create].include?(action_name.to_sym)
'application' 'application'
...@@ -308,7 +320,8 @@ class ProjectsController < Projects::ApplicationController ...@@ -308,7 +320,8 @@ class ProjectsController < Projects::ApplicationController
project_feature_attributes: project_feature_attributes:
[ [
:issues_access_level, :builds_access_level, :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 module AwardEmojiHelper
def toggle_award_url(awardable) def toggle_award_url(awardable)
if @project return url_for([:toggle_award_emoji, awardable]) unless @project
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
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 else
url_for([:toggle_award_emoji, awardable]) url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
end end
end end
end end
...@@ -50,6 +50,20 @@ module PreferencesHelper ...@@ -50,6 +50,20 @@ module PreferencesHelper
end end
def default_project_view 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
end end
...@@ -134,16 +134,35 @@ module ProjectsHelper ...@@ -134,16 +134,35 @@ module ProjectsHelper
options = project_feature_options options = project_feature_options
if @project.private? if @project.private?
level = @project.project_feature.send(field)
options.delete('Everyone with access') 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 end
options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field)) 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 end
private 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) def get_project_nav_tabs(project, current_user)
nav_tabs = [:home] nav_tabs = [:home]
...@@ -155,12 +174,8 @@ module ProjectsHelper ...@@ -155,12 +174,8 @@ module ProjectsHelper
nav_tabs << :merge_requests nav_tabs << :merge_requests
end end
if can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
end
if can?(current_user, :read_build, project) if can?(current_user, :read_build, project)
nav_tabs << :builds nav_tabs << :pipelines
end end
if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project) if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
...@@ -435,4 +450,8 @@ module ProjectsHelper ...@@ -435,4 +450,8 @@ module ProjectsHelper
'Everyone with access' => ProjectFeature::ENABLED 'Everyone with access' => ProjectFeature::ENABLED
} }
end end
def project_child_container_class(view_path)
view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
end
end end
...@@ -154,7 +154,7 @@ module Ci ...@@ -154,7 +154,7 @@ module Ci
def retryable? def retryable?
builds.latest.any? do |build| builds.latest.any? do |build|
build.failed? && build.retryable? (build.failed? || build.canceled?) && build.retryable?
end end
end end
......
...@@ -871,7 +871,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -871,7 +871,7 @@ class MergeRequest < ActiveRecord::Base
# files. # files.
conflicts.files.each(&:lines) conflicts.files.each(&:lines)
@conflicts_can_be_resolved_in_ui = conflicts.files.length > 0 @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
rescue Rugged::OdbError, Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing
@conflicts_can_be_resolved_in_ui = false @conflicts_can_be_resolved_in_ui = false
end end
end end
......
...@@ -13,23 +13,26 @@ class ProjectFeature < ActiveRecord::Base ...@@ -13,23 +13,26 @@ class ProjectFeature < ActiveRecord::Base
# Enabled: enabled for everyone able to access the project # Enabled: enabled for everyone able to access the project
# #
# Permision levels # Permission levels
DISABLED = 0 DISABLED = 0
PRIVATE = 10 PRIVATE = 10
ENABLED = 20 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 # Default scopes force us to unscope here since a service may need to check
# permissions for a project in pending_delete # permissions for a project in pending_delete
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
belongs_to :project, -> { unscope(where: :pending_delete) } belongs_to :project, -> { unscope(where: :pending_delete) }
validate :repository_children_level
default_value_for :builds_access_level, value: ENABLED, allows_nil: false default_value_for :builds_access_level, value: ENABLED, allows_nil: false
default_value_for :issues_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 :merge_requests_access_level, value: ENABLED, allows_nil: false
default_value_for :snippets_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 :wiki_access_level, value: ENABLED, allows_nil: false
default_value_for :repository_access_level, value: ENABLED, allows_nil: false
def feature_available?(feature, user) def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature) raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
...@@ -57,6 +60,18 @@ class ProjectFeature < ActiveRecord::Base ...@@ -57,6 +60,18 @@ class ProjectFeature < ActiveRecord::Base
private 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) def get_permission(user, level)
case level case level
when DISABLED when DISABLED
......
...@@ -162,11 +162,13 @@ class ProjectPolicy < BasePolicy ...@@ -162,11 +162,13 @@ class ProjectPolicy < BasePolicy
end end
def disabled_features! def disabled_features!
repository_enabled = project.feature_available?(:repository, user)
unless project.feature_available?(:issues, user) unless project.feature_available?(:issues, user)
cannot!(*named_abilities(:issue)) cannot!(*named_abilities(:issue))
end end
unless project.feature_available?(:merge_requests, user) unless project.feature_available?(:merge_requests, user) && repository_enabled
cannot!(*named_abilities(:merge_request)) cannot!(*named_abilities(:merge_request))
end end
...@@ -183,13 +185,21 @@ class ProjectPolicy < BasePolicy ...@@ -183,13 +185,21 @@ class ProjectPolicy < BasePolicy
cannot!(*named_abilities(:wiki)) cannot!(*named_abilities(:wiki))
end end
unless project.feature_available?(:builds, user) unless project.feature_available?(:builds, user) && repository_enabled
cannot!(*named_abilities(:build)) cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline)) cannot!(*named_abilities(:pipeline))
cannot!(*named_abilities(:environment)) cannot!(*named_abilities(:environment))
cannot!(*named_abilities(:deployment)) cannot!(*named_abilities(:deployment))
end 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 unless project.container_registry_enabled
cannot!(*named_abilities(:container_image)) cannot!(*named_abilities(:container_image))
end end
......
module MergeRequests module MergeRequests
class ResolveService < MergeRequests::BaseService class ResolveService < MergeRequests::BaseService
class MissingFiles < Gitlab::Conflict::ResolutionError
end
attr_accessor :conflicts, :rugged, :merge_index, :merge_request attr_accessor :conflicts, :rugged, :merge_index, :merge_request
def execute(merge_request) def execute(merge_request)
...@@ -10,8 +13,16 @@ module MergeRequests ...@@ -10,8 +13,16 @@ module MergeRequests
fetch_their_commit! fetch_their_commit!
conflicts.files.each do |file| params[:files].each do |file_params|
write_resolved_file_to_index(file, params[:sections]) conflict_file = merge_request.conflicts.file_for_path(file_params[:old_path], file_params[:new_path])
write_resolved_file_to_index(conflict_file, file_params)
end
unless merge_index.conflicts.empty?
missing_files = merge_index.conflicts.map { |file| file[:ours][:path] }
raise MissingFiles, "Missing resolutions for the following files: #{missing_files.join(', ')}"
end end
commit_params = { commit_params = {
...@@ -23,8 +34,13 @@ module MergeRequests ...@@ -23,8 +34,13 @@ module MergeRequests
project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params) project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params)
end end
def write_resolved_file_to_index(file, resolutions) def write_resolved_file_to_index(file, params)
new_file = file.resolve_lines(resolutions).map(&:text).join("\n") new_file = if params[:sections]
file.resolve_lines(params[:sections]).map(&:text).join("\n")
elsif params[:content]
file.resolve_content(params[:content])
end
our_path = file.our_path our_path = file.our_path
merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode) merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
......
.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 @@ ...@@ -22,5 +22,6 @@
= render 'projects/buttons/star' = render 'projects/buttons/star'
= render 'projects/buttons/fork' = render 'projects/buttons/fork'
.project-clone-holder - if @project.feature_available?(:repository, current_user)
= render "shared/clone_panel" .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) - if !project.empty_repo? && can?(current_user, :download_code, project)
%span{class: 'hidden-xs hidden-sm'} %span{class: 'hidden-xs hidden-sm download-button'}
.dropdown.inline .dropdown.inline
%button.btn{ 'data-toggle' => 'dropdown' } %button.btn{ 'data-toggle' => 'dropdown' }
= icon('download') = icon('download')
......
...@@ -50,24 +50,39 @@ ...@@ -50,24 +50,39 @@
.form_group.prepend-top-20 .form_group.prepend-top-20
.row .row
.col-md-9 .col-md-9
= feature_fields.label :issues_access_level, "Issues", class: 'label-light' = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
%span.help-block Lightweight issue tracking system for this project %span.help-block Push files to be stored in this project
.col-md-3 .col-md-3.js-repo-access-level
= project_feature_access_select(:issues_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 .row
.col-md-9 .col-md-9
= feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
%span.help-block Submit changes to be merged upstream %span.help-block Share code pastes with others out of Git repository
.col-md-3 .col-md-3
= project_feature_access_select(:merge_requests_access_level) = project_feature_access_select(:snippets_access_level)
.row .row
.col-md-9 .col-md-9
= feature_fields.label :builds_access_level, "Builds", class: 'label-light' = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
%span.help-block Submit Test and deploy your changes before merge %span.help-block Lightweight issue tracking system for this project
.col-md-3 .col-md-3
= project_feature_access_select(:builds_access_level) = project_feature_access_select(:issues_access_level)
.row .row
.col-md-9 .col-md-9
...@@ -76,24 +91,17 @@ ...@@ -76,24 +91,17 @@
.col-md-3 .col-md-3
= project_feature_access_select(:wiki_access_level) = 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? - if Gitlab.config.lfs.enabled && current_user.admin?
.row .checkbox
.col-md-9 = f.label :lfs_enabled do
= f.label :lfs_enabled, 'LFS', class: 'label-light' = f.check_box :lfs_enabled
%span.help-block %strong LFS
%br
%span.descr
Git Large File Storage Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') = 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 .form-group
.checkbox .checkbox
= f.label :container_registry_enabled do = f.label :container_registry_enabled do
......
%ul.content-list.issues-list.issuable-list %ul.content-list.issues-list.issuable-list
= render @issues = render partial: "projects/issues/issue", collection: @issues
- if @issues.blank? - if @issues.blank?
%li %li
.nothing-here-block No issues to show .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" - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js')
= page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/show/mr_title" = render "projects/merge_requests/show/mr_title"
.merge-request-details.issuable-details .merge-request-details.issuable-details
...@@ -24,6 +20,21 @@ ...@@ -24,6 +20,21 @@
= render partial: "projects/merge_requests/conflicts/commit_stats" = render partial: "projects/merge_requests/conflicts/commit_stats"
.files-wrapper{"v-if" => "!isLoading && !hasError"} .files-wrapper{"v-if" => "!isLoading && !hasError"}
= render partial: "projects/merge_requests/conflicts/parallel_view", locals: { class_bindings: class_bindings } .files
= render partial: "projects/merge_requests/conflicts/inline_view", locals: { class_bindings: class_bindings } .diff-file.file-holder.conflict{"v-for" => "file in conflictsData.files"}
.file-title
%i.fa.fa-fw{":class" => "file.iconClass"}
%strong {{file.filePath}}
= render partial: 'projects/merge_requests/conflicts/file_actions'
.diff-content.diff-wrap-lines
.diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
= render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines"
.diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
= render partial: "projects/merge_requests/conflicts/components/parallel_conflict_lines"
%div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"}
= render partial: "projects/merge_requests/conflicts/components/diff_file_editor"
= render partial: "projects/merge_requests/conflicts/submit_form" = render partial: "projects/merge_requests/conflicts/submit_form"
-# Components
= render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line'
.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"} .content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"}
.inline-parallel-buttons .inline-parallel-buttons{"v-if" => "showDiffViewTypeSwitcher"}
.btn-group .btn-group
%a.btn{ | %button.btn{":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')"}
":class" => "{'active': !isParallel}", |
"@click" => "handleViewTypeChange('inline')"}
Inline Inline
%a.btn{ | %button.btn{":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')"}
":class" => "{'active': isParallel}", |
"@click" => "handleViewTypeChange('parallel')"}
Side-by-side Side-by-side
.js-toggle-container .js-toggle-container
.commit-stat-summary .commit-stat-summary
Showing Showing
%strong.cred {{conflictsCount}} {{conflictsData.conflictsText}} %strong.cred {{conflictsCountText}}
between between
%strong {{conflictsData.source_branch}} %strong {{conflictsData.sourceBranch}}
and and
%strong {{conflictsData.target_branch}} %strong {{conflictsData.targetBranch}}
.file-actions
.btn-group{"v-if" => "file.type === 'text'"}
%button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }",
'@click' => "onClickResolveModeButton(file, 'interactive')",
type: 'button' }
Interactive mode
%button.btn{ ':class' => "{ 'active': file.resolveMode == 'edit' }",
'@click' => "onClickResolveModeButton(file, 'edit')",
type: 'button' }
Edit inline
%a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
View file @{{conflictsData.shortCommitSha}}
.files{"v-show" => "!isParallel"}
.diff-file.file-holder.conflict.inline-view{"v-for" => "file in conflictsData.files"}
.file-title
%i.fa.fa-fw{":class" => "file.iconClass"}
%strong {{file.filePath}}
.file-actions
%a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
View file @{{conflictsData.shortCommitSha}}
.diff-content.diff-wrap-lines
.diff-wrap-lines.code.file-content.js-syntax-highlight
%table
%tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
%template{"v-if" => "!line.isHeader"}
%td.diff-line-num.new_line{":class" => class_bindings}
%a {{line.new_line}}
%td.diff-line-num.old_line{":class" => class_bindings}
%a {{line.old_line}}
%td.line_content{":class" => class_bindings}
{{{line.richText}}}
%template{"v-if" => "line.isHeader"}
%td.diff-line-num.header{":class" => class_bindings}
%td.diff-line-num.header{":class" => class_bindings}
%td.line_content.header{":class" => class_bindings}
%strong {{{line.richText}}}
%button.btn{"@click" => "handleSelected(line.id, line.section)"}
{{line.buttonTitle}}
.files{"v-show" => "isParallel"}
.diff-file.file-holder.conflict.parallel-view{"v-for" => "file in conflictsData.files"}
.file-title
%i.fa.fa-fw{":class" => "file.iconClass"}
%strong {{file.filePath}}
.file-actions
%a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
View file @{{conflictsData.shortCommitSha}}
.diff-content.diff-wrap-lines
.diff-wrap-lines.code.file-content.js-syntax-highlight
%table
%tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
%template{"v-for" => "line in section"}
%template{"v-if" => "line.isHeader"}
%td.diff-line-num.header{":class" => class_bindings}
%td.line_content.header{":class" => class_bindings}
%strong {{line.richText}}
%button.btn{"@click" => "handleSelected(line.id, line.section)"}
{{line.buttonTitle}}
%template{"v-if" => "!line.isHeader"}
%td.diff-line-num.old_line{":class" => class_bindings}
{{line.lineNumber}}
%td.line_content.parallel{":class" => class_bindings}
{{{line.richText}}}
.content-block.oneline-block.files-changed .form-horizontal.resolve-conflicts-form
%strong.resolved-count {{resolvedCount}} .form-group
of %label.col-sm-2.control-label{ "for" => "commit-message" }
%strong.total-count {{conflictsCount}} Commit message
conflicts have been resolved .col-sm-10
.commit-message-container
.commit-message-container.form-group .max-width-marker
.max-width-marker %textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" }
%textarea.form-control.js-commit-message{"v-model" => "conflictsData.commitMessage"} .form-group
{{{conflictsData.commitMessage}}} .col-sm-offset-2.col-sm-10
.row
%button{type: "button", class: "btn btn-success js-submit-button", ":disabled" => "!readyToCommit", "@click" => "commit()"} .col-xs-6
%span {{commitButtonText}} %button{ type: "button", class: "btn btn-success js-submit-button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
%span {{commitButtonText}}
= link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel" .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 @@ ...@@ -7,7 +7,7 @@
.col-lg-9 .col-lg-9
%h5.prepend-top-0 %h5.prepend-top-0
Pipelines 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 %fieldset.builds-feature
- unless @repository.gitlab_ci_yml - unless @repository.gitlab_ci_yml
.form-group .form-group
......
...@@ -12,72 +12,74 @@ ...@@ -12,72 +12,74 @@
= render 'projects/last_push' = render 'projects/last_push'
= render "home_panel" = render "home_panel"
%nav.project-stats{ class: (container_class) } - if @project.feature_available?(:repository, current_user)
%ul.nav %nav.project-stats{ class: container_class }
%li %ul.nav
= 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
%li %li
= link_to 'Readme', readme_path(@project) = link_to project_files_path(@project) do
Files (#{repository_size})
- if @repository.changelog
%li %li
= link_to 'Changelog', changelog_path(@project) = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
#{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)})
- if @repository.license_blob
%li %li
= link_to license_short_name(@project), license_path(@project) = link_to namespace_project_branches_path(@project.namespace, @project) do
#{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
- if @repository.contribution_guide
%li %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 - if default_project_view != 'readme' && @repository.readme
%li %li
= link_to 'CI configuration', ci_configuration_path(@project) = link_to 'Readme', readme_path(@project)
- if current_user && can_push_branch?(@project, @project.default_branch) - if @repository.changelog
- unless @repository.changelog %li
%li.missing = link_to 'Changelog', changelog_path(@project)
= link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
Add Changelog - if @repository.license_blob
- unless @repository.license_blob %li
%li.missing = link_to license_short_name(@project), license_path(@project)
= link_to add_special_file_path(@project, file_name: 'LICENSE') do
Add License - if @repository.contribution_guide
- unless @repository.contribution_guide %li
%li.missing = link_to 'Contribution guide', contribution_guide_path(@project)
= link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
Add Contribution guide - if @repository.gitlab_ci_yml
- unless @repository.gitlab_ci_yml %li
%li.missing = link_to 'CI configuration', ci_configuration_path(@project)
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
Set Up CI - if current_user && can_push_branch?(@project, @project.default_branch)
- unless @repository.changelog
%li.project-repo-buttons-right %li.missing
.project-repo-buttons.project-right-buttons = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
- if current_user Add Changelog
= render 'shared/members/access_request_buttons', source: @project - unless @repository.license_blob
= render "projects/buttons/koding" %li.missing
= link_to add_special_file_path(@project, file_name: 'LICENSE') do
= render 'projects/buttons/download', project: @project, ref: @ref Add License
= render 'projects/buttons/dropdown' - unless @repository.contribution_guide
%li.missing
= render 'shared/notifications/button', notification_setting: @notification_setting = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
- if @repository.commit Add Contribution guide
.project-last-commit{ class: container_class } - unless @repository.gitlab_ci_yml
= render 'projects/last_commit', commit: @repository.commit, project: @project %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 } %div{ class: container_class }
- if @project.archived? - if @project.archived?
...@@ -86,5 +88,7 @@ ...@@ -86,5 +88,7 @@
= icon("exclamation-triangle fw") = icon("exclamation-triangle fw")
Archived project! Repository is read-only Archived project! Repository is read-only
%div{class: "project-show-#{default_project_view}"} - view_path = default_project_view
= render default_project_view
%div{ class: project_child_container_class(view_path) }
= render view_path
...@@ -2,10 +2,14 @@ class ExpireBuildInstanceArtifactsWorker ...@@ -2,10 +2,14 @@ class ExpireBuildInstanceArtifactsWorker
include Sidekiq::Worker include Sidekiq::Worker
def perform(build_id) def perform(build_id)
build = Ci::Build.with_expired_artifacts.reorder(nil).find_by(id: build_id) build = Ci::Build
return unless 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! build.erase_artifacts!
end end
end end
...@@ -89,6 +89,7 @@ module Gitlab ...@@ -89,6 +89,7 @@ module Gitlab
config.assets.precompile << "profile/profile_bundle.js" config.assets.precompile << "profile/profile_bundle.js"
config.assets.precompile << "diff_notes/diff_notes_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js"
config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "boards/boards_bundle.js"
config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
config.assets.precompile << "boards/test_utils/simulate_drag.js" config.assets.precompile << "boards/test_utils/simulate_drag.js"
config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js"
config.assets.precompile << "snippet/snippet_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js"
......
...@@ -67,6 +67,7 @@ if Gitlab::Metrics.enabled? ...@@ -67,6 +67,7 @@ if Gitlab::Metrics.enabled?
['app', 'finders'] => ['app', 'finders'], ['app', 'finders'] => ['app', 'finders'],
['app', 'mailers', 'emails'] => ['app', 'mailers'], ['app', 'mailers', 'emails'] => ['app', 'mailers'],
['app', 'services', '**'] => ['app', 'services'], ['app', 'services', '**'] => ['app', 'services'],
['lib', 'gitlab', 'conflicts'] => ['lib'],
['lib', 'gitlab', 'diff'] => ['lib'], ['lib', 'gitlab', 'diff'] => ['lib'],
['lib', 'gitlab', 'email', 'message'] => ['lib'], ['lib', 'gitlab', 'email', 'message'] => ['lib'],
['lib', 'gitlab', 'checks'] => ['lib'] ['lib', 'gitlab', 'checks'] => ['lib']
......
...@@ -267,6 +267,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: ...@@ -267,6 +267,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
get :commits get :commits
get :diffs get :diffs
get :conflicts get :conflicts
get :conflict_for_path
get :builds get :builds
get :pipelines get :pipelines
get :merge_check get :merge_check
......
require 'constraints/user_url_constrainer' require 'constraints/user_url_constrainer'
get '/u/:username', to: redirect('/%{username}'),
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
registrations: :registrations, registrations: :registrations,
passwords: :passwords, passwords: :passwords,
...@@ -23,7 +20,7 @@ constraints(UserUrlConstrainer.new) do ...@@ -23,7 +20,7 @@ constraints(UserUrlConstrainer.new) do
end end
end end
scope(path: 'u/:username', scope(path: 'users/:username',
as: :user, as: :user,
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
controller: :users) do controller: :users) do
...@@ -36,3 +33,12 @@ scope(path: 'u/:username', ...@@ -36,3 +33,12 @@ scope(path: 'u/:username',
get :exists get :exists
get '/', to: redirect('/%{username}') get '/', to: redirect('/%{username}')
end end
# Compatibility with old routing
# TODO (dzaporozhets): remove in 10.0
get '/u/:username', to: redirect('/%{username}'), constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
# TODO (dzaporozhets): remove in 9.0
get '/u/:username/groups', to: redirect('/users/%{username}/groups'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
get '/u/:username/projects', to: redirect('/users/%{username}/projects'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
get '/u/:username/snippets', to: redirect('/users/%{username}/snippets'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
get '/u/:username/contributed', to: redirect('/users/%{username}/contributed'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
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 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161007133303) do ActiveRecord::Schema.define(version: 20161012180455) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -830,6 +830,7 @@ ActiveRecord::Schema.define(version: 20161007133303) do ...@@ -830,6 +830,7 @@ ActiveRecord::Schema.define(version: 20161007133303) do
t.integer "builds_access_level" t.integer "builds_access_level"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "repository_access_level", default: 20, null: false
end end
add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree
......
...@@ -43,7 +43,7 @@ Example Response: ...@@ -43,7 +43,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/root" "web_url": "http://gitlab.example.com/root"
}, },
"created_at": "2016-06-15T10:09:34.206Z", "created_at": "2016-06-15T10:09:34.206Z",
"updated_at": "2016-06-15T10:09:34.206Z", "updated_at": "2016-06-15T10:09:34.206Z",
...@@ -59,7 +59,7 @@ Example Response: ...@@ -59,7 +59,7 @@ Example Response:
"id": 26, "id": 26,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/user4" "web_url": "http://gitlab.example.com/user4"
}, },
"created_at": "2016-06-15T10:09:34.177Z", "created_at": "2016-06-15T10:09:34.177Z",
"updated_at": "2016-06-15T10:09:34.177Z", "updated_at": "2016-06-15T10:09:34.177Z",
...@@ -103,7 +103,7 @@ Example Response: ...@@ -103,7 +103,7 @@ Example Response:
"id": 26, "id": 26,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/user4" "web_url": "http://gitlab.example.com/user4"
}, },
"created_at": "2016-06-15T10:09:34.177Z", "created_at": "2016-06-15T10:09:34.177Z",
"updated_at": "2016-06-15T10:09:34.177Z", "updated_at": "2016-06-15T10:09:34.177Z",
...@@ -146,7 +146,7 @@ Example Response: ...@@ -146,7 +146,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/root" "web_url": "http://gitlab.example.com/root"
}, },
"created_at": "2016-06-17T17:47:29.266Z", "created_at": "2016-06-17T17:47:29.266Z",
"updated_at": "2016-06-17T17:47:29.266Z", "updated_at": "2016-06-17T17:47:29.266Z",
...@@ -190,7 +190,7 @@ Example Response: ...@@ -190,7 +190,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/root" "web_url": "http://gitlab.example.com/root"
}, },
"created_at": "2016-06-17T17:47:29.266Z", "created_at": "2016-06-17T17:47:29.266Z",
"updated_at": "2016-06-17T17:47:29.266Z", "updated_at": "2016-06-17T17:47:29.266Z",
...@@ -238,7 +238,7 @@ Example Response: ...@@ -238,7 +238,7 @@ Example Response:
"id": 26, "id": 26,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/user4" "web_url": "http://gitlab.example.com/user4"
}, },
"created_at": "2016-06-15T10:09:34.197Z", "created_at": "2016-06-15T10:09:34.197Z",
"updated_at": "2016-06-15T10:09:34.197Z", "updated_at": "2016-06-15T10:09:34.197Z",
...@@ -279,7 +279,7 @@ Example Response: ...@@ -279,7 +279,7 @@ Example Response:
"id": 26, "id": 26,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/user4" "web_url": "http://gitlab.example.com/user4"
}, },
"created_at": "2016-06-15T10:09:34.197Z", "created_at": "2016-06-15T10:09:34.197Z",
"updated_at": "2016-06-15T10:09:34.197Z", "updated_at": "2016-06-15T10:09:34.197Z",
...@@ -319,7 +319,7 @@ Example Response: ...@@ -319,7 +319,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/root" "web_url": "http://gitlab.example.com/root"
}, },
"created_at": "2016-06-17T19:59:55.888Z", "created_at": "2016-06-17T19:59:55.888Z",
"updated_at": "2016-06-17T19:59:55.888Z", "updated_at": "2016-06-17T19:59:55.888Z",
...@@ -362,7 +362,7 @@ Example Response: ...@@ -362,7 +362,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/u/root" "web_url": "http://gitlab.example.com/root"
}, },
"created_at": "2016-06-17T19:59:55.888Z", "created_at": "2016-06-17T19:59:55.888Z",
"updated_at": "2016-06-17T19:59:55.888Z", "updated_at": "2016-06-17T19:59:55.888Z",
......
...@@ -64,7 +64,7 @@ Example of response ...@@ -64,7 +64,7 @@ Example of response
"state": "active", "state": "active",
"twitter": "", "twitter": "",
"username": "root", "username": "root",
"web_url": "http://gitlab.dev/u/root", "web_url": "http://gitlab.dev/root",
"website_url": "" "website_url": ""
} }
}, },
...@@ -108,7 +108,7 @@ Example of response ...@@ -108,7 +108,7 @@ Example of response
"state": "active", "state": "active",
"twitter": "", "twitter": "",
"username": "root", "username": "root",
"web_url": "http://gitlab.dev/u/root", "web_url": "http://gitlab.dev/root",
"website_url": "" "website_url": ""
} }
} }
...@@ -212,7 +212,7 @@ Example of response ...@@ -212,7 +212,7 @@ Example of response
"state": "active", "state": "active",
"twitter": "", "twitter": "",
"username": "root", "username": "root",
"web_url": "http://gitlab.dev/u/root", "web_url": "http://gitlab.dev/root",
"website_url": "" "website_url": ""
} }
} }
...@@ -279,7 +279,7 @@ Example of response ...@@ -279,7 +279,7 @@ Example of response
"state": "active", "state": "active",
"twitter": "", "twitter": "",
"username": "root", "username": "root",
"web_url": "http://gitlab.dev/u/root", "web_url": "http://gitlab.dev/root",
"website_url": "" "website_url": ""
} }
} }
......
...@@ -288,7 +288,7 @@ Example response: ...@@ -288,7 +288,7 @@ Example response:
```json ```json
{ {
"author" : { "author" : {
"web_url" : "https://gitlab.example.com/u/thedude", "web_url" : "https://gitlab.example.com/thedude",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
"username" : "thedude", "username" : "thedude",
"state" : "active", "state" : "active",
...@@ -343,7 +343,7 @@ Example response: ...@@ -343,7 +343,7 @@ Example response:
"author" : { "author" : {
"username" : "thedude", "username" : "thedude",
"state" : "active", "state" : "active",
"web_url" : "https://gitlab.example.com/u/thedude", "web_url" : "https://gitlab.example.com/thedude",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
"id" : 28, "id" : 28,
"name" : "Jeff Lebowski" "name" : "Jeff Lebowski"
...@@ -370,7 +370,7 @@ Example response: ...@@ -370,7 +370,7 @@ Example response:
"id" : 28, "id" : 28,
"name" : "Jeff Lebowski", "name" : "Jeff Lebowski",
"username" : "thedude", "username" : "thedude",
"web_url" : "https://gitlab.example.com/u/thedude", "web_url" : "https://gitlab.example.com/thedude",
"state" : "active", "state" : "active",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png" "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png"
}, },
...@@ -408,7 +408,7 @@ Example response: ...@@ -408,7 +408,7 @@ Example response:
```json ```json
{ {
"author" : { "author" : {
"web_url" : "https://gitlab.example.com/u/thedude", "web_url" : "https://gitlab.example.com/thedude",
"name" : "Jeff Lebowski", "name" : "Jeff Lebowski",
"avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
"username" : "thedude", "username" : "thedude",
......
...@@ -56,7 +56,7 @@ Example of response ...@@ -56,7 +56,7 @@ Example of response
"state": "active", "state": "active",
"twitter": "", "twitter": "",
"username": "root", "username": "root",
"web_url": "http://localhost:3000/u/root", "web_url": "http://localhost:3000/root",
"website_url": "" "website_url": ""
} }
}, },
...@@ -75,7 +75,7 @@ Example of response ...@@ -75,7 +75,7 @@ Example of response
"name": "Administrator", "name": "Administrator",
"state": "active", "state": "active",
"username": "root", "username": "root",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
} }
}, },
{ {
...@@ -114,7 +114,7 @@ Example of response ...@@ -114,7 +114,7 @@ Example of response
"state": "active", "state": "active",
"twitter": "", "twitter": "",
"username": "root", "username": "root",
"web_url": "http://localhost:3000/u/root", "web_url": "http://localhost:3000/root",
"website_url": "" "website_url": ""
} }
}, },
...@@ -133,7 +133,7 @@ Example of response ...@@ -133,7 +133,7 @@ Example of response
"name": "Administrator", "name": "Administrator",
"state": "active", "state": "active",
"username": "root", "username": "root",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
} }
} }
] ]
...@@ -169,7 +169,7 @@ Example of response ...@@ -169,7 +169,7 @@ Example of response
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"environment": { "environment": {
"id": 9, "id": 9,
...@@ -193,7 +193,7 @@ Example of response ...@@ -193,7 +193,7 @@ Example of response
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root", "web_url": "http://localhost:3000/root",
"created_at": "2016-08-11T07:09:20.351Z", "created_at": "2016-08-11T07:09:20.351Z",
"is_admin": true, "is_admin": true,
"bio": null, "bio": null,
......
...@@ -46,7 +46,7 @@ Example response: ...@@ -46,7 +46,7 @@ Example response:
"author" : { "author" : {
"state" : "active", "state" : "active",
"id" : 18, "id" : 18,
"web_url" : "https://gitlab.example.com/u/eileen.lowe", "web_url" : "https://gitlab.example.com/eileen.lowe",
"name" : "Alexandra Bashirian", "name" : "Alexandra Bashirian",
"avatar_url" : null, "avatar_url" : null,
"username" : "eileen.lowe" "username" : "eileen.lowe"
...@@ -67,7 +67,7 @@ Example response: ...@@ -67,7 +67,7 @@ Example response:
"state" : "active", "state" : "active",
"id" : 1, "id" : 1,
"name" : "Administrator", "name" : "Administrator",
"web_url" : "https://gitlab.example.com/u/root", "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null, "avatar_url" : null,
"username" : "root" "username" : "root"
}, },
...@@ -134,7 +134,7 @@ Example response: ...@@ -134,7 +134,7 @@ Example response:
}, },
"author" : { "author" : {
"state" : "active", "state" : "active",
"web_url" : "https://gitlab.example.com/u/root", "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null, "avatar_url" : null,
"username" : "root", "username" : "root",
"id" : 1, "id" : 1,
...@@ -145,7 +145,7 @@ Example response: ...@@ -145,7 +145,7 @@ Example response:
"iid" : 1, "iid" : 1,
"assignee" : { "assignee" : {
"avatar_url" : null, "avatar_url" : null,
"web_url" : "https://gitlab.example.com/u/lennie", "web_url" : "https://gitlab.example.com/lennie",
"state" : "active", "state" : "active",
"username" : "lennie", "username" : "lennie",
"id" : 9, "id" : 9,
...@@ -215,7 +215,7 @@ Example response: ...@@ -215,7 +215,7 @@ Example response:
}, },
"author" : { "author" : {
"state" : "active", "state" : "active",
"web_url" : "https://gitlab.example.com/u/root", "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null, "avatar_url" : null,
"username" : "root", "username" : "root",
"id" : 1, "id" : 1,
...@@ -226,7 +226,7 @@ Example response: ...@@ -226,7 +226,7 @@ Example response:
"iid" : 1, "iid" : 1,
"assignee" : { "assignee" : {
"avatar_url" : null, "avatar_url" : null,
"web_url" : "https://gitlab.example.com/u/lennie", "web_url" : "https://gitlab.example.com/lennie",
"state" : "active", "state" : "active",
"username" : "lennie", "username" : "lennie",
"id" : 9, "id" : 9,
...@@ -281,7 +281,7 @@ Example response: ...@@ -281,7 +281,7 @@ Example response:
}, },
"author" : { "author" : {
"state" : "active", "state" : "active",
"web_url" : "https://gitlab.example.com/u/root", "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null, "avatar_url" : null,
"username" : "root", "username" : "root",
"id" : 1, "id" : 1,
...@@ -292,7 +292,7 @@ Example response: ...@@ -292,7 +292,7 @@ Example response:
"iid" : 1, "iid" : 1,
"assignee" : { "assignee" : {
"avatar_url" : null, "avatar_url" : null,
"web_url" : "https://gitlab.example.com/u/lennie", "web_url" : "https://gitlab.example.com/lennie",
"state" : "active", "state" : "active",
"username" : "lennie", "username" : "lennie",
"id" : 9, "id" : 9,
...@@ -357,7 +357,7 @@ Example response: ...@@ -357,7 +357,7 @@ Example response:
"name" : "Alexandra Bashirian", "name" : "Alexandra Bashirian",
"avatar_url" : null, "avatar_url" : null,
"state" : "active", "state" : "active",
"web_url" : "https://gitlab.example.com/u/eileen.lowe", "web_url" : "https://gitlab.example.com/eileen.lowe",
"id" : 18, "id" : 18,
"username" : "eileen.lowe" "username" : "eileen.lowe"
}, },
...@@ -414,7 +414,7 @@ Example response: ...@@ -414,7 +414,7 @@ Example response:
"username" : "eileen.lowe", "username" : "eileen.lowe",
"id" : 18, "id" : 18,
"state" : "active", "state" : "active",
"web_url" : "https://gitlab.example.com/u/eileen.lowe" "web_url" : "https://gitlab.example.com/eileen.lowe"
}, },
"state" : "closed", "state" : "closed",
"title" : "Issues with auth", "title" : "Issues with auth",
...@@ -500,7 +500,7 @@ Example response: ...@@ -500,7 +500,7 @@ Example response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/axel.block" "web_url": "https://gitlab.example.com/axel.block"
}, },
"author": { "author": {
"name": "Kris Steuber", "name": "Kris Steuber",
...@@ -508,7 +508,7 @@ Example response: ...@@ -508,7 +508,7 @@ Example response:
"id": 10, "id": 10,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/solon.cremin" "web_url": "https://gitlab.example.com/solon.cremin"
}, },
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/11", "web_url": "http://example.com/example/example/issues/11",
...@@ -557,7 +557,7 @@ Example response: ...@@ -557,7 +557,7 @@ Example response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/axel.block" "web_url": "https://gitlab.example.com/axel.block"
}, },
"author": { "author": {
"name": "Kris Steuber", "name": "Kris Steuber",
...@@ -565,7 +565,7 @@ Example response: ...@@ -565,7 +565,7 @@ Example response:
"id": 10, "id": 10,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/solon.cremin" "web_url": "https://gitlab.example.com/solon.cremin"
}, },
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/11", "web_url": "http://example.com/example/example/issues/11",
...@@ -614,7 +614,7 @@ Example response: ...@@ -614,7 +614,7 @@ Example response:
"id": 21, "id": 21,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/keyon" "web_url": "https://gitlab.example.com/keyon"
}, },
"author": { "author": {
"name": "Vivian Hermann", "name": "Vivian Hermann",
...@@ -622,7 +622,7 @@ Example response: ...@@ -622,7 +622,7 @@ Example response:
"id": 11, "id": 11,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/orville" "web_url": "https://gitlab.example.com/orville"
}, },
"subscribed": false, "subscribed": false,
"due_date": null, "due_date": null,
...@@ -669,7 +669,7 @@ Example response: ...@@ -669,7 +669,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"action_name": "marked", "action_name": "marked",
"target_type": "Issue", "target_type": "Issue",
...@@ -700,7 +700,7 @@ Example response: ...@@ -700,7 +700,7 @@ Example response:
"id": 14, "id": 14,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/francisca" "web_url": "https://gitlab.example.com/francisca"
}, },
"author": { "author": {
"name": "Maxie Medhurst", "name": "Maxie Medhurst",
...@@ -708,7 +708,7 @@ Example response: ...@@ -708,7 +708,7 @@ Example response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/craig_rutherford" "web_url": "https://gitlab.example.com/craig_rutherford"
}, },
"subscribed": true, "subscribed": true,
"user_notes_count": 7, "user_notes_count": 7,
......
...@@ -24,7 +24,7 @@ Parameters: ...@@ -24,7 +24,7 @@ Parameters:
"id": 25, "id": 25,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/cfa35b8cd2ec278026357769582fa563?s=40\u0026d=identicon", "avatar_url": "http://www.gravatar.com/avatar/cfa35b8cd2ec278026357769582fa563?s=40\u0026d=identicon",
"web_url": "http://localhost:3000/u/john_smith", "web_url": "http://localhost:3000/john_smith",
"created_at": "2015-09-03T07:24:01.670Z", "created_at": "2015-09-03T07:24:01.670Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
......
...@@ -621,7 +621,7 @@ Example response when the GitLab issue tracker is used: ...@@ -621,7 +621,7 @@ Example response when the GitLab issue tracker is used:
"author" : { "author" : {
"state" : "active", "state" : "active",
"id" : 18, "id" : 18,
"web_url" : "https://gitlab.example.com/u/eileen.lowe", "web_url" : "https://gitlab.example.com/eileen.lowe",
"name" : "Alexandra Bashirian", "name" : "Alexandra Bashirian",
"avatar_url" : null, "avatar_url" : null,
"username" : "eileen.lowe" "username" : "eileen.lowe"
...@@ -642,7 +642,7 @@ Example response when the GitLab issue tracker is used: ...@@ -642,7 +642,7 @@ Example response when the GitLab issue tracker is used:
"state" : "active", "state" : "active",
"id" : 1, "id" : 1,
"name" : "Administrator", "name" : "Administrator",
"web_url" : "https://gitlab.example.com/u/root", "web_url" : "https://gitlab.example.com/root",
"avatar_url" : null, "avatar_url" : null,
"username" : "root" "username" : "root"
}, },
...@@ -711,7 +711,7 @@ Example response: ...@@ -711,7 +711,7 @@ Example response:
"id": 19, "id": 19,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/leila" "web_url": "https://gitlab.example.com/leila"
}, },
"assignee": { "assignee": {
"name": "Celine Wehner", "name": "Celine Wehner",
...@@ -719,7 +719,7 @@ Example response: ...@@ -719,7 +719,7 @@ Example response:
"id": 16, "id": 16,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/carli" "web_url": "https://gitlab.example.com/carli"
}, },
"source_project_id": 5, "source_project_id": 5,
"target_project_id": 5, "target_project_id": 5,
...@@ -787,7 +787,7 @@ Example response: ...@@ -787,7 +787,7 @@ Example response:
"id": 19, "id": 19,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/leila" "web_url": "https://gitlab.example.com/leila"
}, },
"assignee": { "assignee": {
"name": "Celine Wehner", "name": "Celine Wehner",
...@@ -795,7 +795,7 @@ Example response: ...@@ -795,7 +795,7 @@ Example response:
"id": 16, "id": 16,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/carli" "web_url": "https://gitlab.example.com/carli"
}, },
"source_project_id": 5, "source_project_id": 5,
"target_project_id": 5, "target_project_id": 5,
...@@ -858,7 +858,7 @@ Example response: ...@@ -858,7 +858,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"action_name": "marked", "action_name": "marked",
"target_type": "MergeRequest", "target_type": "MergeRequest",
...@@ -881,7 +881,7 @@ Example response: ...@@ -881,7 +881,7 @@ Example response:
"id": 14, "id": 14,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/francisca" "web_url": "https://gitlab.example.com/francisca"
}, },
"assignee": { "assignee": {
"name": "Dr. Gabrielle Strosin", "name": "Dr. Gabrielle Strosin",
...@@ -889,7 +889,7 @@ Example response: ...@@ -889,7 +889,7 @@ Example response:
"id": 4, "id": 4,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/733005fcd7e6df12d2d8580171ccb966?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/733005fcd7e6df12d2d8580171ccb966?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/barrett.krajcik" "web_url": "https://gitlab.example.com/barrett.krajcik"
}, },
"source_project_id": 3, "source_project_id": 3,
"target_project_id": 3, "target_project_id": 3,
......
...@@ -143,7 +143,7 @@ Example Response: ...@@ -143,7 +143,7 @@ Example Response:
"state": "active", "state": "active",
"created_at": "2013-09-30T13:46:01Z", "created_at": "2013-09-30T13:46:01Z",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/pipin" "web_url": "https://gitlab.example.com/pipin"
}, },
"created_at": "2016-04-05T22:10:44.164Z", "created_at": "2016-04-05T22:10:44.164Z",
"system": false, "system": false,
...@@ -268,7 +268,7 @@ Example Response: ...@@ -268,7 +268,7 @@ Example Response:
"state": "active", "state": "active",
"created_at": "2013-09-30T13:46:01Z", "created_at": "2013-09-30T13:46:01Z",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/pipin" "web_url": "https://gitlab.example.com/pipin"
}, },
"created_at": "2016-04-06T16:51:53.239Z", "created_at": "2016-04-06T16:51:53.239Z",
"system": false, "system": false,
...@@ -398,7 +398,7 @@ Example Response: ...@@ -398,7 +398,7 @@ Example Response:
"state": "active", "state": "active",
"created_at": "2013-09-30T13:46:01Z", "created_at": "2013-09-30T13:46:01Z",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/pipin" "web_url": "https://gitlab.example.com/pipin"
}, },
"created_at": "2016-04-05T22:11:59.923Z", "created_at": "2016-04-05T22:11:59.923Z",
"system": false, "system": false,
......
...@@ -34,7 +34,7 @@ Example of response ...@@ -34,7 +34,7 @@ Example of response
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2016-08-16T10:23:19.007Z", "created_at": "2016-08-16T10:23:19.007Z",
"updated_at": "2016-08-16T10:23:19.216Z", "updated_at": "2016-08-16T10:23:19.216Z",
...@@ -57,7 +57,7 @@ Example of response ...@@ -57,7 +57,7 @@ Example of response
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2016-08-16T10:23:21.184Z", "created_at": "2016-08-16T10:23:21.184Z",
"updated_at": "2016-08-16T10:23:21.314Z", "updated_at": "2016-08-16T10:23:21.314Z",
...@@ -103,7 +103,7 @@ Example of response ...@@ -103,7 +103,7 @@ Example of response
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2016-08-11T11:28:34.085Z", "created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z", "updated_at": "2016-08-11T11:32:35.169Z",
...@@ -148,7 +148,7 @@ Response: ...@@ -148,7 +148,7 @@ Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2016-08-11T11:28:34.085Z", "created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z", "updated_at": "2016-08-11T11:32:35.169Z",
...@@ -193,7 +193,7 @@ Response: ...@@ -193,7 +193,7 @@ Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2016-08-11T11:28:34.085Z", "created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z", "updated_at": "2016-08-11T11:32:35.169Z",
......
...@@ -465,7 +465,7 @@ Parameters: ...@@ -465,7 +465,7 @@ Parameters:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "root" "author_username": "root"
}, },
...@@ -482,7 +482,7 @@ Parameters: ...@@ -482,7 +482,7 @@ Parameters:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "john", "author_username": "john",
"data": { "data": {
...@@ -528,7 +528,7 @@ Parameters: ...@@ -528,7 +528,7 @@ Parameters:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "root" "author_username": "root"
}, },
...@@ -552,7 +552,7 @@ Parameters: ...@@ -552,7 +552,7 @@ Parameters:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2015-12-04T10:33:56.698Z", "created_at": "2015-12-04T10:33:56.698Z",
"system": false, "system": false,
...@@ -567,7 +567,7 @@ Parameters: ...@@ -567,7 +567,7 @@ Parameters:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "root" "author_username": "root"
} }
......
...@@ -44,7 +44,7 @@ Example Response: ...@@ -44,7 +44,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"action_name": "marked", "action_name": "marked",
"target_type": "MergeRequest", "target_type": "MergeRequest",
...@@ -67,7 +67,7 @@ Example Response: ...@@ -67,7 +67,7 @@ Example Response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/craig_rutherford" "web_url": "https://gitlab.example.com/craig_rutherford"
}, },
"assignee": { "assignee": {
"name": "Administrator", "name": "Administrator",
...@@ -75,7 +75,7 @@ Example Response: ...@@ -75,7 +75,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"source_project_id": 2, "source_project_id": 2,
"target_project_id": 2, "target_project_id": 2,
...@@ -117,7 +117,7 @@ Example Response: ...@@ -117,7 +117,7 @@ Example Response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/craig_rutherford" "web_url": "https://gitlab.example.com/craig_rutherford"
}, },
"action_name": "assigned", "action_name": "assigned",
"target_type": "MergeRequest", "target_type": "MergeRequest",
...@@ -140,7 +140,7 @@ Example Response: ...@@ -140,7 +140,7 @@ Example Response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/craig_rutherford" "web_url": "https://gitlab.example.com/craig_rutherford"
}, },
"assignee": { "assignee": {
"name": "Administrator", "name": "Administrator",
...@@ -148,7 +148,7 @@ Example Response: ...@@ -148,7 +148,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"source_project_id": 2, "source_project_id": 2,
"target_project_id": 2, "target_project_id": 2,
...@@ -215,7 +215,7 @@ Example Response: ...@@ -215,7 +215,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"action_name": "marked", "action_name": "marked",
"target_type": "MergeRequest", "target_type": "MergeRequest",
...@@ -238,7 +238,7 @@ Example Response: ...@@ -238,7 +238,7 @@ Example Response:
"id": 12, "id": 12,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/craig_rutherford" "web_url": "https://gitlab.example.com/craig_rutherford"
}, },
"assignee": { "assignee": {
"name": "Administrator", "name": "Administrator",
...@@ -246,7 +246,7 @@ Example Response: ...@@ -246,7 +246,7 @@ Example Response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root" "web_url": "https://gitlab.example.com/root"
}, },
"source_project_id": 2, "source_project_id": 2,
"target_project_id": 2, "target_project_id": 2,
......
...@@ -20,7 +20,7 @@ GET /users ...@@ -20,7 +20,7 @@ GET /users
"name": "John Smith", "name": "John Smith",
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
"web_url": "http://localhost:3000/u/john_smith" "web_url": "http://localhost:3000/john_smith"
}, },
{ {
"id": 2, "id": 2,
...@@ -28,7 +28,7 @@ GET /users ...@@ -28,7 +28,7 @@ GET /users
"name": "Jack Smith", "name": "Jack Smith",
"state": "blocked", "state": "blocked",
"avatar_url": "http://gravatar.com/../e32131cd8.jpeg", "avatar_url": "http://gravatar.com/../e32131cd8.jpeg",
"web_url": "http://localhost:3000/u/jack_smith" "web_url": "http://localhost:3000/jack_smith"
} }
] ]
``` ```
...@@ -48,7 +48,7 @@ GET /users ...@@ -48,7 +48,7 @@ GET /users
"name": "John Smith", "name": "John Smith",
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
"web_url": "http://localhost:3000/u/john_smith", "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
...@@ -81,7 +81,7 @@ GET /users ...@@ -81,7 +81,7 @@ GET /users
"name": "Jack Smith", "name": "Jack Smith",
"state": "blocked", "state": "blocked",
"avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg", "avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg",
"web_url": "http://localhost:3000/u/jack_smith", "web_url": "http://localhost:3000/jack_smith",
"created_at": "2012-05-23T08:01:01Z", "created_at": "2012-05-23T08:01:01Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
...@@ -141,7 +141,7 @@ Parameters: ...@@ -141,7 +141,7 @@ Parameters:
"name": "John Smith", "name": "John Smith",
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
"web_url": "http://localhost:3000/u/john_smith", "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
...@@ -172,7 +172,7 @@ Parameters: ...@@ -172,7 +172,7 @@ Parameters:
"name": "John Smith", "name": "John Smith",
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
"web_url": "http://localhost:3000/u/john_smith", "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
...@@ -293,7 +293,7 @@ GET /user ...@@ -293,7 +293,7 @@ GET /user
"name": "John Smith", "name": "John Smith",
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
"web_url": "http://localhost:3000/u/john_smith", "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
...@@ -665,7 +665,7 @@ Example response: ...@@ -665,7 +665,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "root" "author_username": "root"
}, },
...@@ -682,7 +682,7 @@ Example response: ...@@ -682,7 +682,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "john", "author_username": "john",
"data": { "data": {
...@@ -728,7 +728,7 @@ Example response: ...@@ -728,7 +728,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "root" "author_username": "root"
}, },
...@@ -752,7 +752,7 @@ Example response: ...@@ -752,7 +752,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"created_at": "2015-12-04T10:33:56.698Z", "created_at": "2015-12-04T10:33:56.698Z",
"system": false, "system": false,
...@@ -767,7 +767,7 @@ Example response: ...@@ -767,7 +767,7 @@ Example response:
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/root"
}, },
"author_username": "root" "author_username": "root"
} }
......
...@@ -254,5 +254,5 @@ impact on runtime performance, and as such, using a constant instead of ...@@ -254,5 +254,5 @@ impact on runtime performance, and as such, using a constant instead of
referencing an object directly may even slow code down. referencing an object directly may even slow code down.
[#15607]: https://gitlab.com/gitlab-org/gitlab-ce/issues/15607 [#15607]: https://gitlab.com/gitlab-org/gitlab-ce/issues/15607
[yorickpeterse]: https://gitlab.com/u/yorickpeterse [yorickpeterse]: https://gitlab.com/yorickpeterse
[anti-pattern]: https://en.wikipedia.org/wiki/Anti-pattern [anti-pattern]: https://en.wikipedia.org/wiki/Anti-pattern
doc/raketasks/backup_hrz.png

8.7 KB | W: | H:

doc/raketasks/backup_hrz.png

31 KB | W: | H:

doc/raketasks/backup_hrz.png
doc/raketasks/backup_hrz.png
doc/raketasks/backup_hrz.png
doc/raketasks/backup_hrz.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
include IconsHelper include IconsHelper
class MissingResolution < StandardError class MissingResolution < ResolutionError
end end
CONTEXT_LINES = 3 CONTEXT_LINES = 3
...@@ -21,12 +21,34 @@ module Gitlab ...@@ -21,12 +21,34 @@ module Gitlab
@match_line_headers = {} @match_line_headers = {}
end end
def content
merge_file_result[:data]
end
def our_blob
@our_blob ||= repository.blob_at(merge_request.diff_refs.head_sha, our_path)
end
def type
lines unless @type
@type.inquiry
end
# Array of Gitlab::Diff::Line objects # Array of Gitlab::Diff::Line objects
def lines def lines
@lines ||= Gitlab::Conflict::Parser.new.parse(merge_file_result[:data], return @lines if defined?(@lines)
begin
@type = 'text'
@lines = Gitlab::Conflict::Parser.new.parse(content,
our_path: our_path, our_path: our_path,
their_path: their_path, their_path: their_path,
parent_file: self) parent_file: self)
rescue Gitlab::Conflict::Parser::ParserError
@type = 'text-editor'
@lines = nil
end
end end
def resolve_lines(resolution) def resolve_lines(resolution)
...@@ -53,6 +75,14 @@ module Gitlab ...@@ -53,6 +75,14 @@ module Gitlab
end.compact end.compact
end end
def resolve_content(resolution)
if resolution == content
raise MissingResolution, "Resolved content has no changes for file #{our_path}"
end
resolution
end
def highlight_lines! def highlight_lines!
their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n") their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n") our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
...@@ -170,21 +200,39 @@ module Gitlab ...@@ -170,21 +200,39 @@ module Gitlab
match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}" match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}"
end end
def as_json(opts = nil) def as_json(opts = {})
{ json_hash = {
old_path: their_path, old_path: their_path,
new_path: our_path, new_path: our_path,
blob_icon: file_type_icon_class('file', our_mode, our_path), blob_icon: file_type_icon_class('file', our_mode, our_path),
blob_path: namespace_project_blob_path(merge_request.project.namespace, blob_path: namespace_project_blob_path(merge_request.project.namespace,
merge_request.project, merge_request.project,
::File.join(merge_request.diff_refs.head_sha, our_path)), ::File.join(merge_request.diff_refs.head_sha, our_path))
sections: sections
} }
json_hash.tap do |json_hash|
if opts[:full_content]
json_hash[:content] = content
json_hash[:blob_ace_mode] = our_blob && our_blob.language.try(:ace_mode)
else
json_hash[:sections] = sections if type.text?
json_hash[:type] = type
json_hash[:content_path] = content_path
end
end
end
def content_path
conflict_for_path_namespace_project_merge_request_path(merge_request.project.namespace,
merge_request.project,
merge_request,
old_path: their_path,
new_path: our_path)
end end
# Don't try to print merge_request or repository. # Don't try to print merge_request or repository.
def inspect def inspect
instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode].map do |instance_variable| instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode, :type].map do |instance_variable|
value = instance_variable_get("@#{instance_variable}") value = instance_variable_get("@#{instance_variable}")
"#{instance_variable}=\"#{value}\"" "#{instance_variable}=\"#{value}\""
......
...@@ -30,6 +30,10 @@ module Gitlab ...@@ -30,6 +30,10 @@ module Gitlab
end end
end end
def file_for_path(old_path, new_path)
files.find { |file| file.their_path == old_path && file.our_path == new_path }
end
def as_json(opts = nil) def as_json(opts = nil)
{ {
target_branch: merge_request.target_branch, target_branch: merge_request.target_branch,
......
module Gitlab module Gitlab
module Conflict module Conflict
class Parser class Parser
class ParserError < StandardError class UnresolvableError < StandardError
end end
class UnexpectedDelimiter < ParserError class UnmergeableFile < UnresolvableError
end end
class MissingEndDelimiter < ParserError class UnsupportedEncoding < UnresolvableError
end
# Recoverable errors - the conflict can be resolved in an editor, but not with
# sections.
class ParserError < StandardError
end end
class UnmergeableFile < ParserError class UnexpectedDelimiter < ParserError
end end
class UnsupportedEncoding < ParserError class MissingEndDelimiter < ParserError
end end
def parse(text, our_path:, their_path:, parent_file: nil) def parse(text, our_path:, their_path:, parent_file: nil)
......
module Gitlab
module Conflict
class ResolutionError < StandardError
end
end
end
...@@ -125,6 +125,10 @@ module Gitlab ...@@ -125,6 +125,10 @@ module Gitlab
repository.blob_at(commit.id, file_path) repository.blob_at(commit.id, file_path)
end end
def cache_key
"#{file_path}-#{new_file}-#{deleted_file}-#{renamed_file}"
end
end end
end end
end end
...@@ -35,16 +35,16 @@ module Gitlab ...@@ -35,16 +35,16 @@ module Gitlab
# for the highlighted ones, so we just skip their execution. # for the highlighted ones, so we just skip their execution.
# If the highlighted diff files lines are not cached we calculate and cache them. # 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. # hashes that represent serialized diff lines.
# #
def cache_highlight!(diff_file) def cache_highlight!(diff_file)
file_path = diff_file.file_path item_key = diff_file.cache_key
if highlight_cache[file_path] if highlight_cache[item_key]
highlight_diff_file_from_cache!(diff_file, highlight_cache[file_path]) highlight_diff_file_from_cache!(diff_file, highlight_cache[item_key])
else 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
end end
......
...@@ -570,7 +570,7 @@ describe Projects::MergeRequestsController do ...@@ -570,7 +570,7 @@ describe Projects::MergeRequestsController do
context 'when the conflicts cannot be resolved in the UI' do context 'when the conflicts cannot be resolved in the UI' do
before do before do
allow_any_instance_of(Gitlab::Conflict::Parser). allow_any_instance_of(Gitlab::Conflict::Parser).
to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnexpectedDelimiter) to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
get :conflicts, get :conflicts,
namespace_id: merge_request_with_conflicts.project.namespace.to_param, namespace_id: merge_request_with_conflicts.project.namespace.to_param,
...@@ -597,6 +597,10 @@ describe Projects::MergeRequestsController do ...@@ -597,6 +597,10 @@ describe Projects::MergeRequestsController do
format: 'json' format: 'json'
end end
it 'matches the schema' do
expect(response).to match_response_schema('conflicts')
end
it 'includes meta info about the MR' do it 'includes meta info about the MR' do
expect(json_response['commit_message']).to include('Merge branch') expect(json_response['commit_message']).to include('Merge branch')
expect(json_response['commit_sha']).to match(/\h{40}/) expect(json_response['commit_sha']).to match(/\h{40}/)
...@@ -658,26 +662,97 @@ describe Projects::MergeRequestsController do ...@@ -658,26 +662,97 @@ describe Projects::MergeRequestsController do
end end
end end
describe 'GET conflict_for_path' do
let(:json_response) { JSON.parse(response.body) }
def conflict_for_path(path)
get :conflict_for_path,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
project_id: merge_request_with_conflicts.project.to_param,
id: merge_request_with_conflicts.iid,
old_path: path,
new_path: path,
format: 'json'
end
context 'when the conflicts cannot be resolved in the UI' do
before do
allow_any_instance_of(Gitlab::Conflict::Parser).
to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
conflict_for_path('files/ruby/regex.rb')
end
it 'returns a 404 status code' do
expect(response).to have_http_status(:not_found)
end
end
context 'when the file does not exist cannot be resolved in the UI' do
before { conflict_for_path('files/ruby/regexp.rb') }
it 'returns a 404 status code' do
expect(response).to have_http_status(:not_found)
end
end
context 'with an existing file' do
let(:path) { 'files/ruby/regex.rb' }
before { conflict_for_path(path) }
it 'returns a 200 status code' do
expect(response).to have_http_status(:ok)
end
it 'returns the file in JSON format' do
content = merge_request_with_conflicts.conflicts.file_for_path(path, path).content
expect(json_response).to include('old_path' => path,
'new_path' => path,
'blob_icon' => 'file-text-o',
'blob_path' => a_string_ending_with(path),
'blob_ace_mode' => 'ruby',
'content' => content)
end
end
end
context 'POST resolve_conflicts' do context 'POST resolve_conflicts' do
let(:json_response) { JSON.parse(response.body) } let(:json_response) { JSON.parse(response.body) }
let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha } let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
def resolve_conflicts(sections) def resolve_conflicts(files)
post :resolve_conflicts, post :resolve_conflicts,
namespace_id: merge_request_with_conflicts.project.namespace.to_param, namespace_id: merge_request_with_conflicts.project.namespace.to_param,
project_id: merge_request_with_conflicts.project.to_param, project_id: merge_request_with_conflicts.project.to_param,
id: merge_request_with_conflicts.iid, id: merge_request_with_conflicts.iid,
format: 'json', format: 'json',
sections: sections, files: files,
commit_message: 'Commit message' commit_message: 'Commit message'
end end
context 'with valid params' do context 'with valid params' do
before do before do
resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head', resolved_files = [
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', {
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', 'new_path' => 'files/ruby/popen.rb',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin') '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 end
it 'creates a new commit on the branch' do it 'creates a new commit on the branch' do
...@@ -692,7 +767,23 @@ describe Projects::MergeRequestsController do ...@@ -692,7 +767,23 @@ describe Projects::MergeRequestsController do
context 'when sections are missing' do context 'when sections are missing' do
before do before do
resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head') resolved_files = [
{
'new_path' => 'files/ruby/popen.rb',
'old_path' => 'files/ruby/popen.rb',
'sections' => {
'2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head'
}
}, {
'new_path' => 'files/ruby/regex.rb',
'old_path' => 'files/ruby/regex.rb',
'sections' => {
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head'
}
}
]
resolve_conflicts(resolved_files)
end end
it 'returns a 400 error' do it 'returns a 400 error' do
...@@ -700,7 +791,71 @@ describe Projects::MergeRequestsController do ...@@ -700,7 +791,71 @@ describe Projects::MergeRequestsController do
end end
it 'has a message with the name of the first missing section' do it 'has a message with the name of the first missing section' do
expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9') expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21')
end
it 'does not create a new commit' do
expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
end
end
context 'when files are missing' do
before do
resolved_files = [
{
'new_path' => 'files/ruby/regex.rb',
'old_path' => 'files/ruby/regex.rb',
'sections' => {
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
}
}
]
resolve_conflicts(resolved_files)
end
it 'returns a 400 error' do
expect(response).to have_http_status(:bad_request)
end
it 'has a message with the name of the missing file' do
expect(json_response['message']).to include('files/ruby/popen.rb')
end
it 'does not create a new commit' do
expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
end
end
context 'when a file has identical content to the conflict' do
before do
resolved_files = [
{
'new_path' => 'files/ruby/popen.rb',
'old_path' => 'files/ruby/popen.rb',
'content' => merge_request_with_conflicts.conflicts.file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb').content
}, {
'new_path' => 'files/ruby/regex.rb',
'old_path' => 'files/ruby/regex.rb',
'sections' => {
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
}
}
]
resolve_conflicts(resolved_files)
end
it 'returns a 400 error' do
expect(response).to have_http_status(:bad_request)
end
it 'has a message with the path of the problem file' do
expect(json_response['message']).to include('files/ruby/popen.rb')
end end
it 'does not create a new commit' do it 'does not create a new commit' do
......
...@@ -41,6 +41,46 @@ describe ProjectsController do ...@@ -41,6 +41,46 @@ describe ProjectsController do
end end
end 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 end
context "project with empty repo" do context "project with empty repo" do
......
...@@ -45,6 +45,7 @@ FactoryGirl.define do ...@@ -45,6 +45,7 @@ FactoryGirl.define do
snippets_access_level ProjectFeature::ENABLED snippets_access_level ProjectFeature::ENABLED
issues_access_level ProjectFeature::ENABLED issues_access_level ProjectFeature::ENABLED
merge_requests_access_level ProjectFeature::ENABLED merge_requests_access_level ProjectFeature::ENABLED
repository_access_level ProjectFeature::ENABLED
end end
after(:create) do |project, evaluator| after(:create) do |project, evaluator|
...@@ -55,6 +56,7 @@ FactoryGirl.define do ...@@ -55,6 +56,7 @@ FactoryGirl.define do
snippets_access_level: evaluator.snippets_access_level, snippets_access_level: evaluator.snippets_access_level,
issues_access_level: evaluator.issues_access_level, issues_access_level: evaluator.issues_access_level,
merge_requests_access_level: evaluator.merge_requests_access_level, merge_requests_access_level: evaluator.merge_requests_access_level,
repository_access_level: evaluator.repository_access_level
) )
end end
end end
......
...@@ -12,29 +12,139 @@ feature 'Merge request conflict resolution', js: true, feature: true do ...@@ -12,29 +12,139 @@ feature 'Merge request conflict resolution', js: true, feature: true do
end end
end end
context 'when a merge request can be resolved in the UI' do shared_examples "conflicts are resolved in Interactive mode" do
let(:merge_request) { create_merge_request('conflict-resolvable') } it 'conflicts are resolved in Interactive mode' do
within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do
click_button 'Use ours'
end
within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
all('button', text: 'Use ours').each do |button|
button.click
end
end
click_button 'Commit conflict resolution'
wait_for_ajax
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
click_on 'Changes'
wait_for_ajax
within find('.diff-file', text: 'files/ruby/popen.rb') do
expect(page).to have_selector('.line_content.new', text: "vars = { 'PWD' => path }")
expect(page).to have_selector('.line_content.new', text: "options = { chdir: path }")
end
within find('.diff-file', text: 'files/ruby/regex.rb') do
expect(page).to have_selector('.line_content.new', text: "def username_regexp")
expect(page).to have_selector('.line_content.new', text: "def project_name_regexp")
expect(page).to have_selector('.line_content.new', text: "def path_regexp")
expect(page).to have_selector('.line_content.new', text: "def archive_formats_regexp")
expect(page).to have_selector('.line_content.new', text: "def git_reference_regexp")
expect(page).to have_selector('.line_content.new', text: "def default_regexp")
end
end
end
shared_examples "conflicts are resolved in Edit inline mode" do
it 'conflicts are resolved in Edit inline mode' do
expect(find('#conflicts')).to have_content('popen.rb')
within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do
click_button 'Edit inline'
wait_for_ajax
execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("One morning");')
end
within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
click_button 'Edit inline'
wait_for_ajax
execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");')
end
click_button 'Commit conflict resolution'
wait_for_ajax
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
click_on 'Changes'
wait_for_ajax
expect(page).to have_content('One morning')
expect(page).to have_content('Gregor Samsa woke from troubled dreams')
end
end
context 'can be resolved in the UI' do
before do before do
project.team << [user, :developer] project.team << [user, :developer]
login_as(user) login_as(user)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end end
it 'shows a link to the conflict resolution page' do context 'the conflicts are resolvable' do
expect(page).to have_link('conflicts', href: /\/conflicts\Z/) 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 end
context 'visiting the conflicts resolution page' do context 'the conflict contain markers' do
before { click_link('conflicts', href: /\/conflicts\Z/) } let(:merge_request) { create_merge_request('conflict-contains-conflict-markers') }
it 'shows the conflicts' do before do
begin visit namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(find('#conflicts')).to have_content('popen.rb') click_link('conflicts', href: /\/conflicts\Z/)
rescue Capybara::Poltergeist::JavascriptError end
retry
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 end
click_button 'Commit conflict resolution'
wait_for_ajax
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
click_on 'Changes'
wait_for_ajax
find('.click-to-expand').click
wait_for_ajax
expect(page).to have_content('Gregor Samsa woke from troubled dreams')
end end
end end
end end
...@@ -42,7 +152,6 @@ feature 'Merge request conflict resolution', js: true, feature: true do ...@@ -42,7 +152,6 @@ feature 'Merge request conflict resolution', js: true, feature: true do
UNRESOLVABLE_CONFLICTS = { UNRESOLVABLE_CONFLICTS = {
'conflict-too-large' => 'when the conflicts contain a large file', 'conflict-too-large' => 'when the conflicts contain a large file',
'conflict-binary-file' => 'when the conflicts contain a binary file', 'conflict-binary-file' => 'when the conflicts contain a binary file',
'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers',
'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another', 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another',
'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file', 'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file',
} }
......
...@@ -2,8 +2,11 @@ require 'spec_helper' ...@@ -2,8 +2,11 @@ require 'spec_helper'
include WaitForAjax include WaitForAjax
describe 'Edit Project Settings', feature: true do describe 'Edit Project Settings', feature: true do
include WaitForAjax
let(:member) { create(:user) } let(:member) { create(:user) }
let!(:project) { create(:project, :public, path: 'gitlab', name: 'sample') } let!(:project) { create(:project, :public, path: 'gitlab', name: 'sample') }
let!(:issue) { create(:issue, project: project) }
let(:non_member) { create(:user) } let(:non_member) { create(:user) }
describe 'project features visibility selectors', js: true do describe 'project features visibility selectors', js: true do
...@@ -119,4 +122,31 @@ describe 'Edit Project Settings', feature: true do ...@@ -119,4 +122,31 @@ describe 'Edit Project Settings', feature: true do
end end
end 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 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 ...@@ -24,11 +24,12 @@ feature "Pipelines settings", feature: true do
context 'for master' do context 'for master' do
given(:role) { :master } 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') fill_in('Test coverage parsing', with: 'coverage_regex')
click_on 'Save changes' click_on 'Save changes'
expect(page.status_code).to eq(200) 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') expect(page).to have_field('Test coverage parsing', with: 'coverage_regex')
end end
end end
......
...@@ -51,6 +51,18 @@ feature 'Users', feature: true, js: true do ...@@ -51,6 +51,18 @@ feature 'Users', feature: true, js: true do
expect(current_path).to eq user_path(user) expect(current_path).to eq user_path(user)
expect(page).to have_text(user.name) expect(page).to have_text(user.name)
end end
scenario '/u/user1/groups redirects to user groups page' do
visit '/u/user1/groups'
expect(current_path).to eq user_groups_path(user)
end
scenario '/u/user1/projects redirects to user projects page' do
visit '/u/user1/projects'
expect(current_path).to eq user_projects_path(user)
end
end end
feature 'username validation' do feature 'username validation' do
......
{
"type": "object",
"required": [
"commit_message",
"commit_sha",
"source_branch",
"target_branch",
"files"
],
"properties": {
"commit_message": {"type": "string"},
"commit_sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
"source_branch": {"type": "string"},
"target_branch": {"type": "string"},
"files": {
"type": "array",
"items": {
"oneOf": [
{ "$ref": "#/definitions/conflict-text-with-sections" },
{ "$ref": "#/definitions/conflict-text-for-editor" }
]
}
}
},
"definitions": {
"conflict-base": {
"type": "object",
"required": [
"old_path",
"new_path",
"blob_icon",
"blob_path"
],
"properties": {
"old_path": {"type": "string"},
"new_path": {"type": "string"},
"blob_icon": {"type": "string"},
"blob_path": {"type": "string"}
}
},
"conflict-text-for-editor": {
"allOf": [
{"$ref": "#/definitions/conflict-base"},
{
"type": "object",
"required": [
"type",
"content_path"
],
"properties": {
"type": {"type": {"enum": ["text-editor"]}},
"content_path": {"type": "string"}
}
}
]
},
"conflict-text-with-sections": {
"allOf": [
{"$ref": "#/definitions/conflict-base"},
{
"type": "object",
"required": [
"type",
"content_path",
"sections"
],
"properties": {
"type": {"type": {"enum": ["text"]}},
"content_path": {"type": "string"},
"sections": {
"type": "array",
"items": {
"oneOf": [
{ "$ref": "#/definitions/section-context" },
{ "$ref": "#/definitions/section-conflict" }
]
}
}
}
}
]
},
"section-base": {
"type": "object",
"required": [
"conflict",
"lines"
],
"properties": {
"conflict": {"type": "boolean"},
"lines": {
"type": "array",
"items": {
"type": "object",
"required": [
"old_line",
"new_line",
"text",
"rich_text"
],
"properties": {
"type": {"type": "string"},
"old_line": {"type": "string"},
"new_line": {"type": "string"},
"text": {"type": "string"},
"rich_text": {"type": "string"}
}
}
}
}
},
"section-context": {
"allOf": [
{"$ref": "#/definitions/section-base"},
{
"type": "object",
"properties": {
"conflict": {"enum": [false]}
}
}
]
},
"section-conflict": {
"allOf": [
{"$ref": "#/definitions/section-base"},
{
"type": "object",
"required": ["id"],
"properties": {
"conflict": {"enum": [true]},
"id": {"type": "string"}
}
}
]
}
}
}
...@@ -257,5 +257,16 @@ FILE ...@@ -257,5 +257,16 @@ FILE
it 'includes the blob icon for the file' do it 'includes the blob icon for the file' do
expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o') expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o')
end end
context 'with the full_content option passed' do
it 'includes the full content of the conflict' do
expect(conflict_file.as_json(full_content: true)).to have_key(:content)
end
it 'includes the detected language of the conflict file' do
expect(conflict_file.as_json(full_content: true)[:blob_ace_mode]).
to eq('ruby')
end
end
end end
end end
...@@ -307,6 +307,7 @@ ProjectFeature: ...@@ -307,6 +307,7 @@ ProjectFeature:
- wiki_access_level - wiki_access_level
- snippets_access_level - snippets_access_level
- builds_access_level - builds_access_level
- repository_access_level
- created_at - created_at
- updated_at - updated_at
ProtectedBranch::MergeAccessLevel: ProtectedBranch::MergeAccessLevel:
......
...@@ -88,24 +88,38 @@ describe Ci::Pipeline, models: true do ...@@ -88,24 +88,38 @@ describe Ci::Pipeline, models: true do
context 'no failed builds' do context 'no failed builds' do
before do before do
FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'success' create_build('rspec', 'success')
end end
it 'be not retryable' do it 'is not retryable' do
is_expected.to be_falsey is_expected.to be_falsey
end 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 end
context 'with failed builds' do context 'with failed builds' do
before do before do
FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'running' create_build('rspec', 'running')
FactoryGirl.create :ci_build, name: "rubocop", pipeline: pipeline, status: 'failed' create_build('rubocop', 'failed')
end end
it 'be retryable' do it 'is retryable' do
is_expected.to be_truthy is_expected.to be_truthy
end end
end end
def create_build(name, status)
create(:ci_build, name: name, status: status, pipeline: pipeline)
end
end end
describe '#stages' do describe '#stages' do
......
...@@ -13,7 +13,7 @@ describe Issue::Metrics, models: true do ...@@ -13,7 +13,7 @@ describe Issue::Metrics, models: true do
metrics = subject.metrics metrics = subject.metrics
expect(metrics).to be_present 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
it "does not record the second time an issue is associated with a milestone" do 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 ...@@ -24,7 +24,7 @@ describe Issue::Metrics, models: true do
metrics = subject.metrics metrics = subject.metrics
expect(metrics).to be_present 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
end end
...@@ -36,7 +36,7 @@ describe Issue::Metrics, models: true do ...@@ -36,7 +36,7 @@ describe Issue::Metrics, models: true do
metrics = subject.metrics metrics = subject.metrics
expect(metrics).to be_present 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
it "does not record the second time an issue is associated with a list label" do 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 ...@@ -48,7 +48,7 @@ describe Issue::Metrics, models: true do
metrics = subject.metrics metrics = subject.metrics
expect(metrics).to be_present 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 end
end end
......
...@@ -12,7 +12,7 @@ describe MergeRequest::Metrics, models: true do ...@@ -12,7 +12,7 @@ describe MergeRequest::Metrics, models: true do
metrics = subject.metrics metrics = subject.metrics
expect(metrics).to be_present 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 end
end end
...@@ -1155,12 +1155,6 @@ describe MergeRequest, models: true do ...@@ -1155,12 +1155,6 @@ describe MergeRequest, models: true do
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
end end
it 'returns a falsey value when the conflicts contain a file with ambiguous conflict markers' do
merge_request = create_merge_request('conflict-contains-conflict-markers')
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
end
it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do
merge_request = create_merge_request('conflict-missing-side') merge_request = create_merge_request('conflict-missing-side')
...@@ -1172,6 +1166,12 @@ describe MergeRequest, models: true do ...@@ -1172,6 +1166,12 @@ describe MergeRequest, models: true do
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
end end
it 'returns a truthy value when the conflicts have to be resolved in an editor' do
merge_request = create_merge_request('conflict-contains-conflict-markers')
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
end
end end
describe "#forked_source_project_missing?" do describe "#forked_source_project_missing?" do
......
...@@ -5,7 +5,7 @@ describe ProjectFeature do ...@@ -5,7 +5,7 @@ describe ProjectFeature do
let(:user) { create(:user) } let(:user) { create(:user) }
describe '#feature_available?' do 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 context 'when features are disabled' do
it "returns false" do it "returns false" do
...@@ -64,6 +64,27 @@ describe ProjectFeature do ...@@ -64,6 +64,27 @@ describe ProjectFeature do
end end
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 describe '#*_enabled?' do
let(:features) { %w(wiki builds merge_requests) } let(:features) { %w(wiki builds merge_requests) }
......
...@@ -694,7 +694,7 @@ describe API::API, api: true do ...@@ -694,7 +694,7 @@ describe API::API, api: true do
title: 'new issue', labels: 'label, label2', created_at: creation_time title: 'new issue', labels: 'label, label2', created_at: creation_time
expect(response).to have_http_status(201) 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 end
end end
...@@ -895,7 +895,7 @@ describe API::API, api: true do ...@@ -895,7 +895,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['labels']).to include 'label3' 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 end
end end
......
...@@ -217,7 +217,7 @@ describe API::API, api: true do ...@@ -217,7 +217,7 @@ describe API::API, api: true do
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
expect(json_response['body']).to eq('hi!') expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username) 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
end end
......
...@@ -15,27 +15,27 @@ describe UsersController, "routing" do ...@@ -15,27 +15,27 @@ describe UsersController, "routing" do
end end
it "to #groups" do it "to #groups" do
expect(get("/u/User/groups")).to route_to('users#groups', username: 'User') expect(get("/users/User/groups")).to route_to('users#groups', username: 'User')
end end
it "to #projects" do it "to #projects" do
expect(get("/u/User/projects")).to route_to('users#projects', username: 'User') expect(get("/users/User/projects")).to route_to('users#projects', username: 'User')
end end
it "to #contributed" do it "to #contributed" do
expect(get("/u/User/contributed")).to route_to('users#contributed', username: 'User') expect(get("/users/User/contributed")).to route_to('users#contributed', username: 'User')
end end
it "to #snippets" do it "to #snippets" do
expect(get("/u/User/snippets")).to route_to('users#snippets', username: 'User') expect(get("/users/User/snippets")).to route_to('users#snippets', username: 'User')
end end
it "to #calendar" do it "to #calendar" do
expect(get("/u/User/calendar")).to route_to('users#calendar', username: 'User') expect(get("/users/User/calendar")).to route_to('users#calendar', username: 'User')
end end
it "to #calendar_activities" do it "to #calendar_activities" do
expect(get("/u/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User') expect(get("/users/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User')
end end
end end
......
...@@ -201,7 +201,7 @@ describe CreateDeploymentService, services: true do ...@@ -201,7 +201,7 @@ describe CreateDeploymentService, services: true do
time = Time.now time = Time.now
Timecop.freeze(time) { service.execute } 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 end
it "doesn't set the time if the deploy's environment is not 'production'" do it "doesn't set the time if the deploy's environment is not 'production'" do
...@@ -227,13 +227,13 @@ describe CreateDeploymentService, services: true do ...@@ -227,13 +227,13 @@ describe CreateDeploymentService, services: true do
time = Time.now time = Time.now
Timecop.freeze(time) { service.execute } 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 # Current deploy
service = described_class.new(project, user, params) service = described_class.new(project, user, params)
Timecop.freeze(time + 12.hours) { service.execute } 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
end end
......
...@@ -364,7 +364,7 @@ describe GitPushService, services: true do ...@@ -364,7 +364,7 @@ describe GitPushService, services: true do
it 'sets the metric for referenced issues' do it 'sets the metric for referenced issues' do
execute_service(project, user, @oldrev, @newrev, @ref) 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 end
it 'does not set the metric for non-referenced issues' do it 'does not set the metric for non-referenced issues' do
......
...@@ -24,15 +24,26 @@ describe MergeRequests::ResolveService do ...@@ -24,15 +24,26 @@ describe MergeRequests::ResolveService do
end end
describe '#execute' do describe '#execute' do
context 'with valid params' do context 'with section params' do
let(:params) do let(:params) do
{ {
sections: { files: [
'2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head', {
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', old_path: 'files/ruby/popen.rb',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', new_path: 'files/ruby/popen.rb',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' 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!' commit_message: 'This is a commit message!'
} }
end end
...@@ -49,7 +60,7 @@ describe MergeRequests::ResolveService do ...@@ -49,7 +60,7 @@ describe MergeRequests::ResolveService do
it 'creates a commit with the correct parents' do it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id)). expect(merge_request.source_branch_head.parents.map(&:id)).
to eq(['1450cd639e0bc6721eb02800169e464f212cde06', to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
'75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b']) '824be604a34828eb682305f0d963056cfac87b2d'])
end end
end end
...@@ -74,8 +85,96 @@ describe MergeRequests::ResolveService do ...@@ -74,8 +85,96 @@ describe MergeRequests::ResolveService do
end end
end end
context 'when a resolution is missing' do context 'with content and sections params' do
let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } } let(:popen_content) { "class Popen\nend" }
let(:params) do
{
files: [
{
old_path: 'files/ruby/popen.rb',
new_path: 'files/ruby/popen.rb',
content: popen_content
}, {
old_path: 'files/ruby/regex.rb',
new_path: 'files/ruby/regex.rb',
sections: {
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
}
}
],
commit_message: 'This is a commit message!'
}
end
before do
MergeRequests::ResolveService.new(project, user, params).execute(merge_request)
end
it 'creates a commit with the message' do
expect(merge_request.source_branch_head.message).to eq(params[:commit_message])
end
it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id)).
to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
'824be604a34828eb682305f0d963056cfac87b2d'])
end
it 'sets the content to the content given' do
blob = merge_request.source_project.repository.blob_at(merge_request.source_branch_head.sha,
'files/ruby/popen.rb')
expect(blob.data).to eq(popen_content)
end
end
context 'when a resolution section is missing' do
let(:invalid_params) do
{
files: [
{
old_path: 'files/ruby/popen.rb',
new_path: 'files/ruby/popen.rb',
content: ''
}, {
old_path: 'files/ruby/regex.rb',
new_path: 'files/ruby/regex.rb',
sections: { '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head' }
}
],
commit_message: 'This is a commit message!'
}
end
let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
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) } let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
it 'raises a MissingResolution error' do it 'raises a MissingResolution error' do
...@@ -83,5 +182,27 @@ describe MergeRequests::ResolveService do ...@@ -83,5 +182,27 @@ describe MergeRequests::ResolveService do
to raise_error(Gitlab::Conflict::File::MissingResolution) to raise_error(Gitlab::Conflict::File::MissingResolution)
end end
end end
context 'when a file is missing' do
let(:invalid_params) do
{
files: [
{
old_path: 'files/ruby/popen.rb',
new_path: 'files/ruby/popen.rb',
content: ''
}
],
commit_message: 'This is a commit message!'
}
end
let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
it 'raises a MissingFiles error' do
expect { service.execute(merge_request) }.
to raise_error(MergeRequests::ResolveService::MissingFiles)
end
end
end end
end end
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 ...@@ -27,10 +27,10 @@ module TestEnv
'expand-collapse-lines' => '238e82d', 'expand-collapse-lines' => '238e82d',
'video' => '8879059', 'video' => '8879059',
'crlf-diff' => '5938907', 'crlf-diff' => '5938907',
'conflict-start' => '75284c7', 'conflict-start' => '824be60',
'conflict-resolvable' => '1450cd6', 'conflict-resolvable' => '1450cd6',
'conflict-binary-file' => '259a6fb', 'conflict-binary-file' => '259a6fb',
'conflict-contains-conflict-markers' => '5e0964c', 'conflict-contains-conflict-markers' => '78a3086',
'conflict-missing-side' => 'eb227b3', 'conflict-missing-side' => 'eb227b3',
'conflict-non-utf8' => 'd0a293c', 'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f', 'conflict-too-large' => '39fa04f',
......
...@@ -6,28 +6,48 @@ describe ExpireBuildInstanceArtifactsWorker do ...@@ -6,28 +6,48 @@ describe ExpireBuildInstanceArtifactsWorker do
let(:worker) { described_class.new } let(:worker) { described_class.new }
describe '#perform' do describe '#perform' do
before { build } before do
worker.perform(build.id)
subject! { worker.perform(build.id) } end
context 'with expired artifacts' do 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 context 'when associated project is valid' do
expect(build.reload.artifacts_expired?).to be_truthy let(:build) do
end create(:ci_build, :artifacts, artifacts_expiry)
end
it 'does remove files' do it 'does expire' do
expect(build.reload.artifacts_file.exists?).to be_falsey 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 end
it 'does nullify artifacts_file column' do context 'when associated project was removed' do
expect(build.reload.artifacts_file_identifier).to be_nil 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
end end
context 'with not yet expired artifacts' do 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 it 'does not expire' do
expect(build.reload.artifacts_expired?).to be_falsey expect(build.reload.artifacts_expired?).to be_falsey
......
...@@ -23,7 +23,7 @@ describe PipelineMetricsWorker do ...@@ -23,7 +23,7 @@ describe PipelineMetricsWorker do
it 'records the build start time' do it 'records the build start time' do
subject 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 end
it 'clears the build end time' do it 'clears the build end time' do
...@@ -39,7 +39,7 @@ describe PipelineMetricsWorker do ...@@ -39,7 +39,7 @@ describe PipelineMetricsWorker do
it 'records the build end time' do it 'records the build end time' do
subject 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 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