Merge branch 'mc-ui'
# Conflicts: # app/controllers/projects/merge_requests_controller.rb
This commit is contained in:
		
						commit
						095fcfc447
					
				| 
						 | 
				
			
			@ -88,6 +88,8 @@
 | 
			
		|||
          new ZenMode();
 | 
			
		||||
          new MergedButtons();
 | 
			
		||||
          break;
 | 
			
		||||
        case "projects:merge_requests:conflicts":
 | 
			
		||||
          window.mcui = new MergeConflictResolver()
 | 
			
		||||
        case 'projects:merge_requests:index':
 | 
			
		||||
          shortcut_handler = new ShortcutsNavigation();
 | 
			
		||||
          Issuable.init();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,341 @@
 | 
			
		|||
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() {
 | 
			
		||||
    const diffViewType = $.cookie('diff_view');
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      isLoading      : true,
 | 
			
		||||
      hasError       : false,
 | 
			
		||||
      isParallel     : diffViewType === 'parallel',
 | 
			
		||||
      diffViewType   : diffViewType,
 | 
			
		||||
      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.diffView || !(newType === 'parallel' || newType === 'inline')) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    vi.diffView   = newType;
 | 
			
		||||
    vi.isParallel = newType === 'parallel';
 | 
			
		||||
    $.cookie('diff_view', newType); // TODO: Make sure that cookie path added.
 | 
			
		||||
    $('.content-wrapper .container-fluid').toggleClass('container-limited');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  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}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,85 @@
 | 
			
		|||
//= 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();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (this.vue.diffViewType === 'parallel') {
 | 
			
		||||
          $('.content-wrapper .container-fluid').removeClass('container-limited');
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  commit() {
 | 
			
		||||
    this.vue.isSubmitting = true;
 | 
			
		||||
 | 
			
		||||
    $.post($('#conflicts').data('resolveConflictsPath'), this.dataProvider.getCommitData())
 | 
			
		||||
      .done((data) => {
 | 
			
		||||
        window.location.href = data.redirect_to;
 | 
			
		||||
      })
 | 
			
		||||
      .error(() => {
 | 
			
		||||
        new Flash('Something went wrong!');
 | 
			
		||||
      })
 | 
			
		||||
      .always(() => {
 | 
			
		||||
        this.vue.isSubmitting = false;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +53,7 @@
 | 
			
		|||
          return function(data) {
 | 
			
		||||
            var callback, urlSuffix;
 | 
			
		||||
            if (data.state === "merged") {
 | 
			
		||||
              urlSuffix = deleteSourceBranch ? '?delete_source=true' : '';
 | 
			
		||||
              urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
 | 
			
		||||
              return window.location.href = window.location.pathname + urlSuffix;
 | 
			
		||||
            } else if (data.merge_error) {
 | 
			
		||||
              return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,3 +20,8 @@
 | 
			
		|||
    .turn-off { display: block; }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[v-cloak] {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -124,3 +124,8 @@
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin dark-diff-match-line {
 | 
			
		||||
  color: rgba(255, 255, 255, 0.3);
 | 
			
		||||
  background: rgba(255, 255, 255, 0.1);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,10 @@
 | 
			
		|||
 | 
			
		||||
  // Diff line
 | 
			
		||||
  .line_holder {
 | 
			
		||||
    &.match .line_content {
 | 
			
		||||
      @include dark-diff-match-line;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    td.diff-line-num.hll:not(.empty-cell),
 | 
			
		||||
    td.line_content.hll:not(.empty-cell) {
 | 
			
		||||
      background-color: #557;
 | 
			
		||||
| 
						 | 
				
			
			@ -36,8 +40,7 @@
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    .line_content.match {
 | 
			
		||||
      color: rgba(255, 255, 255, 0.3);
 | 
			
		||||
      background: rgba(255, 255, 255, 0.1);
 | 
			
		||||
      @include dark-diff-match-line;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,10 @@
 | 
			
		|||
 | 
			
		||||
  // Diff line
 | 
			
		||||
  .line_holder {
 | 
			
		||||
    &.match .line_content {
 | 
			
		||||
      @include dark-diff-match-line;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    td.diff-line-num.hll:not(.empty-cell),
 | 
			
		||||
    td.line_content.hll:not(.empty-cell) {
 | 
			
		||||
      background-color: #49483e;
 | 
			
		||||
| 
						 | 
				
			
			@ -36,8 +40,7 @@
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    .line_content.match {
 | 
			
		||||
      color: rgba(255, 255, 255, 0.3);
 | 
			
		||||
      background: rgba(255, 255, 255, 0.1);
 | 
			
		||||
      @include dark-diff-match-line;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,10 @@
 | 
			
		|||
 | 
			
		||||
  // Diff line
 | 
			
		||||
  .line_holder {
 | 
			
		||||
    &.match .line_content {
 | 
			
		||||
      @include dark-diff-match-line;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    td.diff-line-num.hll:not(.empty-cell),
 | 
			
		||||
    td.line_content.hll:not(.empty-cell) {
 | 
			
		||||
      background-color: #174652;
 | 
			
		||||
| 
						 | 
				
			
			@ -36,8 +40,7 @@
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    .line_content.match {
 | 
			
		||||
      color: rgba(255, 255, 255, 0.3);
 | 
			
		||||
      background: rgba(255, 255, 255, 0.1);
 | 
			
		||||
      @include dark-diff-match-line;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,10 @@
 | 
			
		|||
/* https://gist.github.com/qguv/7936275 */
 | 
			
		||||
 | 
			
		||||
@mixin matchLine {
 | 
			
		||||
  color: $black-transparent;
 | 
			
		||||
  background: rgba(255, 255, 255, 0.4);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.code.solarized-light {
 | 
			
		||||
  // Line numbers
 | 
			
		||||
  .line-numbers, .diff-line-num {
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +27,10 @@
 | 
			
		|||
 | 
			
		||||
  // Diff line
 | 
			
		||||
  .line_holder {
 | 
			
		||||
    &.match .line_content {
 | 
			
		||||
      @include matchLine;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    td.diff-line-num.hll:not(.empty-cell),
 | 
			
		||||
    td.line_content.hll:not(.empty-cell) {
 | 
			
		||||
      background-color: #ddd8c5;
 | 
			
		||||
| 
						 | 
				
			
			@ -36,8 +46,7 @@
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    .line_content.match {
 | 
			
		||||
      color: $black-transparent;
 | 
			
		||||
      background: rgba(255, 255, 255, 0.4);
 | 
			
		||||
      @include matchLine;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,10 @@
 | 
			
		|||
/* https://github.com/aahan/pygments-github-style */
 | 
			
		||||
 | 
			
		||||
@mixin matchLine {
 | 
			
		||||
  color: $black-transparent;
 | 
			
		||||
  background-color: $match-line;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.code.white {
 | 
			
		||||
  // Line numbers
 | 
			
		||||
  .line-numbers, .diff-line-num {
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +28,10 @@
 | 
			
		|||
  // Diff line
 | 
			
		||||
  .line_holder {
 | 
			
		||||
 | 
			
		||||
    &.match .line_content {
 | 
			
		||||
      @include matchLine;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .diff-line-num {
 | 
			
		||||
      &.old {
 | 
			
		||||
        background-color: $line-number-old;
 | 
			
		||||
| 
						 | 
				
			
			@ -57,8 +67,7 @@
 | 
			
		|||
      }
 | 
			
		||||
 | 
			
		||||
      &.match {
 | 
			
		||||
        color: $black-transparent;
 | 
			
		||||
        background-color: $match-line;
 | 
			
		||||
        @include matchLine;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.hll:not(.empty-cell) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,238 @@
 | 
			
		|||
$colors: (
 | 
			
		||||
  white_header_head_neutral   : #e1fad7,
 | 
			
		||||
  white_line_head_neutral     : #effdec,
 | 
			
		||||
  white_button_head_neutral   : #9adb84,
 | 
			
		||||
 | 
			
		||||
  white_header_head_chosen    : #baf0a8,
 | 
			
		||||
  white_line_head_chosen      : #e1fad7,
 | 
			
		||||
  white_button_head_chosen    : #52c22d,
 | 
			
		||||
 | 
			
		||||
  white_header_origin_neutral : #e0f0ff,
 | 
			
		||||
  white_line_origin_neutral   : #f2f9ff,
 | 
			
		||||
  white_button_origin_neutral : #87c2fa,
 | 
			
		||||
 | 
			
		||||
  white_header_origin_chosen  : #add8ff,
 | 
			
		||||
  white_line_origin_chosen    : #e0f0ff,
 | 
			
		||||
  white_button_origin_chosen  : #268ced,
 | 
			
		||||
 | 
			
		||||
  white_header_not_chosen     : #f0f0f0,
 | 
			
		||||
  white_line_not_chosen       : #f9f9f9,
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  dark_header_head_neutral   : rgba(#3f3, .2),
 | 
			
		||||
  dark_line_head_neutral     : rgba(#3f3, .1),
 | 
			
		||||
  dark_button_head_neutral   : #40874f,
 | 
			
		||||
 | 
			
		||||
  dark_header_head_chosen    : rgba(#3f3, .33),
 | 
			
		||||
  dark_line_head_chosen      : rgba(#3f3, .2),
 | 
			
		||||
  dark_button_head_chosen    : #258537,
 | 
			
		||||
 | 
			
		||||
  dark_header_origin_neutral : rgba(#2878c9, .4),
 | 
			
		||||
  dark_line_origin_neutral   : rgba(#2878c9, .3),
 | 
			
		||||
  dark_button_origin_neutral : #2a5c8c,
 | 
			
		||||
 | 
			
		||||
  dark_header_origin_chosen  : rgba(#2878c9, .6),
 | 
			
		||||
  dark_line_origin_chosen    : rgba(#2878c9, .4),
 | 
			
		||||
  dark_button_origin_chosen  : #1d6cbf,
 | 
			
		||||
 | 
			
		||||
  dark_header_not_chosen     : rgba(#fff, .25),
 | 
			
		||||
  dark_line_not_chosen       : rgba(#fff, .1),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  monokai_header_head_neutral   : rgba(#a6e22e, .25),
 | 
			
		||||
  monokai_line_head_neutral     : rgba(#a6e22e, .1),
 | 
			
		||||
  monokai_button_head_neutral   : #376b20,
 | 
			
		||||
 | 
			
		||||
  monokai_header_head_chosen    : rgba(#a6e22e, .4),
 | 
			
		||||
  monokai_line_head_chosen      : rgba(#a6e22e, .25),
 | 
			
		||||
  monokai_button_head_chosen    : #39800d,
 | 
			
		||||
 | 
			
		||||
  monokai_header_origin_neutral : rgba(#60d9f1, .35),
 | 
			
		||||
  monokai_line_origin_neutral   : rgba(#60d9f1, .15),
 | 
			
		||||
  monokai_button_origin_neutral : #38848c,
 | 
			
		||||
 | 
			
		||||
  monokai_header_origin_chosen  : rgba(#60d9f1, .5),
 | 
			
		||||
  monokai_line_origin_chosen    : rgba(#60d9f1, .35),
 | 
			
		||||
  monokai_button_origin_chosen  : #3ea4b2,
 | 
			
		||||
 | 
			
		||||
  monokai_header_not_chosen     : rgba(#76715d, .24),
 | 
			
		||||
  monokai_line_not_chosen       : rgba(#76715d, .1),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  solarized_light_header_head_neutral   : rgba(#859900, .37),
 | 
			
		||||
  solarized_light_line_head_neutral     : rgba(#859900, .2),
 | 
			
		||||
  solarized_light_button_head_neutral   : #afb262,
 | 
			
		||||
 | 
			
		||||
  solarized_light_header_head_chosen    : rgba(#859900, .5),
 | 
			
		||||
  solarized_light_line_head_chosen      : rgba(#859900, .37),
 | 
			
		||||
  solarized_light_button_head_chosen    : #94993d,
 | 
			
		||||
 | 
			
		||||
  solarized_light_header_origin_neutral : rgba(#2878c9, .37),
 | 
			
		||||
  solarized_light_line_origin_neutral   : rgba(#2878c9, .15),
 | 
			
		||||
  solarized_light_button_origin_neutral : #60a1bf,
 | 
			
		||||
 | 
			
		||||
  solarized_light_header_origin_chosen  : rgba(#2878c9, .6),
 | 
			
		||||
  solarized_light_line_origin_chosen    : rgba(#2878c9, .37),
 | 
			
		||||
  solarized_light_button_origin_chosen  : #2482b2,
 | 
			
		||||
 | 
			
		||||
  solarized_light_header_not_chosen     : rgba(#839496, .37),
 | 
			
		||||
  solarized_light_line_not_chosen       : rgba(#839496, .2),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  solarized_dark_header_head_neutral   : rgba(#859900, .35),
 | 
			
		||||
  solarized_dark_line_head_neutral     : rgba(#859900, .15),
 | 
			
		||||
  solarized_dark_button_head_neutral   : #376b20,
 | 
			
		||||
 | 
			
		||||
  solarized_dark_header_head_chosen    : rgba(#859900, .5),
 | 
			
		||||
  solarized_dark_line_head_chosen      : rgba(#859900, .35),
 | 
			
		||||
  solarized_dark_button_head_chosen    : #39800d,
 | 
			
		||||
 | 
			
		||||
  solarized_dark_header_origin_neutral : rgba(#2878c9, .35),
 | 
			
		||||
  solarized_dark_line_origin_neutral   : rgba(#2878c9, .15),
 | 
			
		||||
  solarized_dark_button_origin_neutral : #086799,
 | 
			
		||||
 | 
			
		||||
  solarized_dark_header_origin_chosen  : rgba(#2878c9, .6),
 | 
			
		||||
  solarized_dark_line_origin_chosen    : rgba(#2878c9, .35),
 | 
			
		||||
  solarized_dark_button_origin_chosen  : #0082cc,
 | 
			
		||||
 | 
			
		||||
  solarized_dark_header_not_chosen     : rgba(#839496, .25),
 | 
			
		||||
  solarized_dark_line_not_chosen       : rgba(#839496, .15)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@mixin color-scheme($color) {
 | 
			
		||||
  .header.line_content, .diff-line-num {
 | 
			
		||||
    &.origin {
 | 
			
		||||
      background-color: map-get($colors, #{$color}_header_origin_neutral);
 | 
			
		||||
      border-color: map-get($colors, #{$color}_header_origin_neutral);
 | 
			
		||||
 | 
			
		||||
      button {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_button_origin_neutral);
 | 
			
		||||
        border-color: darken(map-get($colors, #{$color}_button_origin_neutral), 15);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.selected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_header_origin_chosen);
 | 
			
		||||
        border-color: map-get($colors, #{$color}_header_origin_chosen);
 | 
			
		||||
 | 
			
		||||
        button {
 | 
			
		||||
          background-color: map-get($colors, #{$color}_button_origin_chosen);
 | 
			
		||||
          border-color: darken(map-get($colors, #{$color}_button_origin_chosen), 15);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.unselected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_header_not_chosen);
 | 
			
		||||
        border-color: map-get($colors, #{$color}_header_not_chosen);
 | 
			
		||||
 | 
			
		||||
        button {
 | 
			
		||||
          background-color: lighten(map-get($colors, #{$color}_button_origin_neutral), 15);
 | 
			
		||||
          border-color: map-get($colors, #{$color}_button_origin_neutral);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &.head {
 | 
			
		||||
      background-color: map-get($colors, #{$color}_header_head_neutral);
 | 
			
		||||
      border-color: map-get($colors, #{$color}_header_head_neutral);
 | 
			
		||||
 | 
			
		||||
      button {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_button_head_neutral);
 | 
			
		||||
        border-color: darken(map-get($colors, #{$color}_button_head_neutral), 15);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.selected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_header_head_chosen);
 | 
			
		||||
        border-color: map-get($colors, #{$color}_header_head_chosen);
 | 
			
		||||
 | 
			
		||||
        button {
 | 
			
		||||
          background-color: map-get($colors, #{$color}_button_head_chosen);
 | 
			
		||||
          border-color: darken(map-get($colors, #{$color}_button_head_chosen), 15);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.unselected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_header_not_chosen);
 | 
			
		||||
        border-color: map-get($colors, #{$color}_header_not_chosen);
 | 
			
		||||
 | 
			
		||||
        button {
 | 
			
		||||
          background-color: lighten(map-get($colors, #{$color}_button_head_neutral), 15);
 | 
			
		||||
          border-color: map-get($colors, #{$color}_button_head_neutral);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .line_content {
 | 
			
		||||
    &.origin {
 | 
			
		||||
      background-color: map-get($colors, #{$color}_line_origin_neutral);
 | 
			
		||||
 | 
			
		||||
      &.selected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_line_origin_chosen);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.unselected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_line_not_chosen);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &.head {
 | 
			
		||||
      background-color: map-get($colors, #{$color}_line_head_neutral);
 | 
			
		||||
 | 
			
		||||
      &.selected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_line_head_chosen);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.unselected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_line_not_chosen);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#conflicts {
 | 
			
		||||
 | 
			
		||||
  .white {
 | 
			
		||||
    @include color-scheme('white')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .dark {
 | 
			
		||||
    @include color-scheme('dark')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .monokai {
 | 
			
		||||
    @include color-scheme('monokai')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .solarized-light {
 | 
			
		||||
    @include color-scheme('solarized_light')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .solarized-dark {
 | 
			
		||||
    @include color-scheme('solarized_dark')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .diff-wrap-lines .line_content {
 | 
			
		||||
    white-space: normal;
 | 
			
		||||
    min-height: 19px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .line_content.header {
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    button {
 | 
			
		||||
      border-radius: 2px;
 | 
			
		||||
      font-size: 10px;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      right: 10px;
 | 
			
		||||
      padding: 0;
 | 
			
		||||
      outline: none;
 | 
			
		||||
      color: #fff;
 | 
			
		||||
      width: 75px; // static width to make 2 buttons have same width
 | 
			
		||||
      height: 19px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .btn-success .fa-spinner {
 | 
			
		||||
    color: #fff;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -9,15 +9,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 | 
			
		|||
 | 
			
		||||
  before_action :module_enabled
 | 
			
		||||
  before_action :merge_request, only: [
 | 
			
		||||
    :edit, :update, :show, :diffs, :commits, :builds, :pipelines, :merge, :merge_check,
 | 
			
		||||
    :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip
 | 
			
		||||
    :edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check,
 | 
			
		||||
    :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts
 | 
			
		||||
  ]
 | 
			
		||||
  before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines]
 | 
			
		||||
  before_action :define_show_vars, only: [:show, :diffs, :commits, :builds, :pipelines]
 | 
			
		||||
  before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines]
 | 
			
		||||
  before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
 | 
			
		||||
  before_action :define_commit_vars, only: [:diffs]
 | 
			
		||||
  before_action :define_diff_comment_vars, only: [:diffs]
 | 
			
		||||
  before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :pipelines]
 | 
			
		||||
  before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
 | 
			
		||||
 | 
			
		||||
  # Allow read any merge_request
 | 
			
		||||
  before_action :authorize_read_merge_request!
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +28,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 | 
			
		|||
  # Allow modify merge_request
 | 
			
		||||
  before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
 | 
			
		||||
 | 
			
		||||
  before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
 | 
			
		||||
 | 
			
		||||
  def index
 | 
			
		||||
    terms = params['issue_search']
 | 
			
		||||
    @merge_requests = merge_requests_collection
 | 
			
		||||
| 
						 | 
				
			
			@ -130,6 +132,47 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def conflicts
 | 
			
		||||
    respond_to do |format|
 | 
			
		||||
      format.html { define_discussion_vars }
 | 
			
		||||
 | 
			
		||||
      format.json do
 | 
			
		||||
        if @merge_request.conflicts_can_be_resolved_in_ui?
 | 
			
		||||
          render json: @merge_request.conflicts
 | 
			
		||||
        elsif @merge_request.can_be_merged?
 | 
			
		||||
          render json: {
 | 
			
		||||
            message: 'The merge conflicts for this merge request have already been resolved. Please return to the merge request.',
 | 
			
		||||
            type: 'error'
 | 
			
		||||
          }
 | 
			
		||||
        else
 | 
			
		||||
          render json: {
 | 
			
		||||
            message: 'The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally.',
 | 
			
		||||
            type: 'error'
 | 
			
		||||
          }
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def resolve_conflicts
 | 
			
		||||
    return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
 | 
			
		||||
 | 
			
		||||
    if @merge_request.can_be_merged?
 | 
			
		||||
      render status: :bad_request, json: { message: 'The merge conflicts for this merge request have already been resolved.' }
 | 
			
		||||
      return
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    begin
 | 
			
		||||
      MergeRequests::ResolveService.new(@merge_request.source_project, current_user, params).execute(@merge_request)
 | 
			
		||||
 | 
			
		||||
      flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'
 | 
			
		||||
 | 
			
		||||
      render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) }
 | 
			
		||||
    rescue Gitlab::Conflict::File::MissingResolution => e
 | 
			
		||||
      render status: :bad_request, json: { message: e.message }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def builds
 | 
			
		||||
    respond_to do |format|
 | 
			
		||||
      format.html do
 | 
			
		||||
| 
						 | 
				
			
			@ -351,6 +394,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 | 
			
		|||
    return render_404 unless can?(current_user, :admin_merge_request, @merge_request)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def authorize_can_resolve_conflicts!
 | 
			
		||||
    return render_404 unless @merge_request.conflicts_can_be_resolved_by?(current_user)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def module_enabled
 | 
			
		||||
    return render_404 unless @project.merge_requests_enabled
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -425,7 +472,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 | 
			
		|||
      noteable_id: @merge_request.id
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @use_legacy_diff_notes = !@merge_request.support_new_diff_notes?
 | 
			
		||||
    @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
 | 
			
		||||
    @grouped_diff_discussions = @merge_request.notes.inc_author_project_award_emoji.grouped_diff_discussions
 | 
			
		||||
 | 
			
		||||
    Banzai::NoteRenderer.render(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,7 @@ module NavHelper
 | 
			
		|||
      current_path?('merge_requests#diffs') ||
 | 
			
		||||
      current_path?('merge_requests#commits') ||
 | 
			
		||||
      current_path?('merge_requests#builds') ||
 | 
			
		||||
      current_path?('merge_requests#conflicts') ||
 | 
			
		||||
      current_path?('issues#show')
 | 
			
		||||
      if cookies[:collapsed_gutter] == 'true'
 | 
			
		||||
        "page-gutter right-sidebar-collapsed"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,7 +75,7 @@ class DiffNote < Note
 | 
			
		|||
  private
 | 
			
		||||
 | 
			
		||||
  def supported?
 | 
			
		||||
    !self.for_merge_request? || self.noteable.support_new_diff_notes?
 | 
			
		||||
    !self.for_merge_request? || self.noteable.has_complete_diff_refs?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def noteable_diff_refs
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -701,12 +701,12 @@ class MergeRequest < ActiveRecord::Base
 | 
			
		|||
    merge_commit
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def support_new_diff_notes?
 | 
			
		||||
  def has_complete_diff_refs?
 | 
			
		||||
    diff_sha_refs && diff_sha_refs.complete?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update_diff_notes_positions(old_diff_refs:, new_diff_refs:)
 | 
			
		||||
    return unless support_new_diff_notes?
 | 
			
		||||
    return unless has_complete_diff_refs?
 | 
			
		||||
    return if new_diff_refs == old_diff_refs
 | 
			
		||||
 | 
			
		||||
    active_diff_notes = self.notes.diff_notes.select do |note|
 | 
			
		||||
| 
						 | 
				
			
			@ -734,4 +734,26 @@ class MergeRequest < ActiveRecord::Base
 | 
			
		|||
  def keep_around_commit
 | 
			
		||||
    project.repository.keep_around(self.merge_commit_sha)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def conflicts
 | 
			
		||||
    @conflicts ||= Gitlab::Conflict::FileCollection.new(self)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def conflicts_can_be_resolved_by?(user)
 | 
			
		||||
    access = ::Gitlab::UserAccess.new(user, project: source_project)
 | 
			
		||||
    access.can_push_to_branch?(source_branch)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def conflicts_can_be_resolved_in_ui?
 | 
			
		||||
    return @conflicts_can_be_resolved_in_ui if defined?(@conflicts_can_be_resolved_in_ui)
 | 
			
		||||
 | 
			
		||||
    return @conflicts_can_be_resolved_in_ui = false unless cannot_be_merged?
 | 
			
		||||
    return @conflicts_can_be_resolved_in_ui = false unless has_complete_diff_refs?
 | 
			
		||||
 | 
			
		||||
    begin
 | 
			
		||||
      @conflicts_can_be_resolved_in_ui = conflicts.files.each(&:lines)
 | 
			
		||||
    rescue Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing
 | 
			
		||||
      @conflicts_can_be_resolved_in_ui = false
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -869,6 +869,14 @@ class Repository
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def resolve_conflicts(user, branch, params)
 | 
			
		||||
    commit_with_hooks(user, branch) do
 | 
			
		||||
      committer = user_to_committer(user)
 | 
			
		||||
 | 
			
		||||
      Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def check_revert_content(commit, base_branch)
 | 
			
		||||
    source_sha = find_branch(base_branch).target.sha
 | 
			
		||||
    args       = [commit.id, source_sha]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
module MergeRequests
 | 
			
		||||
  class ResolveService < MergeRequests::BaseService
 | 
			
		||||
    attr_accessor :conflicts, :rugged, :merge_index
 | 
			
		||||
 | 
			
		||||
    def execute(merge_request)
 | 
			
		||||
      @conflicts = merge_request.conflicts
 | 
			
		||||
      @rugged = project.repository.rugged
 | 
			
		||||
      @merge_index = conflicts.merge_index
 | 
			
		||||
 | 
			
		||||
      conflicts.files.each do |file|
 | 
			
		||||
        write_resolved_file_to_index(file, params[:sections])
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      commit_params = {
 | 
			
		||||
        message: params[:commit_message] || conflicts.default_commit_message,
 | 
			
		||||
        parents: [conflicts.our_commit, conflicts.their_commit].map(&:oid),
 | 
			
		||||
        tree: merge_index.write_tree(rugged)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def write_resolved_file_to_index(file, resolutions)
 | 
			
		||||
      new_file = file.resolve_lines(resolutions).map(&:text).join("\n")
 | 
			
		||||
      our_path = file.our_path
 | 
			
		||||
 | 
			
		||||
      merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
 | 
			
		||||
      merge_index.conflict_remove(our_path)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
- 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"
 | 
			
		||||
= render "projects/merge_requests/show/mr_title"
 | 
			
		||||
 | 
			
		||||
.merge-request-details.issuable-details
 | 
			
		||||
  = render "projects/merge_requests/show/mr_box"
 | 
			
		||||
 | 
			
		||||
= render 'shared/issuable/sidebar', issuable: @merge_request
 | 
			
		||||
 | 
			
		||||
#conflicts{"v-cloak" => "true", data: { conflicts_path: conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request, format: :json),
 | 
			
		||||
    resolve_conflicts_path: resolve_conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request) } }
 | 
			
		||||
  .loading{"v-if" => "isLoading"}
 | 
			
		||||
    %i.fa.fa-spinner.fa-spin
 | 
			
		||||
 | 
			
		||||
  .nothing-here-block{"v-if" => "hasError"}
 | 
			
		||||
    {{conflictsData.errorMessage}}
 | 
			
		||||
 | 
			
		||||
  = render partial: "projects/merge_requests/conflicts/commit_stats"
 | 
			
		||||
 | 
			
		||||
  .files-wrapper{"v-if" => "!isLoading && !hasError"}
 | 
			
		||||
    = render partial: "projects/merge_requests/conflicts/parallel_view", locals: { class_bindings: class_bindings }
 | 
			
		||||
    = render partial: "projects/merge_requests/conflicts/inline_view", locals: { class_bindings: class_bindings }
 | 
			
		||||
    = render partial: "projects/merge_requests/conflicts/submit_form"
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"}
 | 
			
		||||
  .inline-parallel-buttons
 | 
			
		||||
    .btn-group
 | 
			
		||||
      %a.btn{ |
 | 
			
		||||
        ":class" => "{'active': !isParallel}", |
 | 
			
		||||
        "@click" => "handleViewTypeChange('inline')"}
 | 
			
		||||
        Inline
 | 
			
		||||
      %a.btn{ |
 | 
			
		||||
        ":class" => "{'active': isParallel}", |
 | 
			
		||||
        "@click" => "handleViewTypeChange('parallel')"}
 | 
			
		||||
        Side-by-side
 | 
			
		||||
 | 
			
		||||
  .js-toggle-container
 | 
			
		||||
    .commit-stat-summary
 | 
			
		||||
      Showing
 | 
			
		||||
      %strong.cred {{conflictsCount}} {{conflictsData.conflictsText}}
 | 
			
		||||
      between
 | 
			
		||||
      %strong {{conflictsData.source_branch}}
 | 
			
		||||
      and
 | 
			
		||||
      %strong {{conflictsData.target_branch}}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
.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}}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
.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}}}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
.content-block.oneline-block.files-changed
 | 
			
		||||
  %strong.resolved-count {{resolvedCount}}
 | 
			
		||||
  of
 | 
			
		||||
  %strong.total-count {{conflictsCount}}
 | 
			
		||||
  conflicts have been resolved
 | 
			
		||||
 | 
			
		||||
  .commit-message-container.form-group
 | 
			
		||||
    .max-width-marker
 | 
			
		||||
    %textarea.form-control.js-commit-message{"v-model" => "conflictsData.commitMessage"}
 | 
			
		||||
      {{{conflictsData.commitMessage}}}
 | 
			
		||||
 | 
			
		||||
  %button{type: "button", class: "btn btn-success js-submit-button", ":disabled" => "!readyToCommit", "@click" => "commit()"}
 | 
			
		||||
    %span {{commitButtonText}}
 | 
			
		||||
 | 
			
		||||
  = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel"
 | 
			
		||||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
      - if @merge_request.merge_event
 | 
			
		||||
        by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)}
 | 
			
		||||
        #{time_ago_with_tooltip(@merge_request.merge_event.created_at)}
 | 
			
		||||
    - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true')
 | 
			
		||||
    - if !@merge_request.source_branch_exists? || params[:deleted_source_branch]
 | 
			
		||||
      %p
 | 
			
		||||
        The changes were merged into
 | 
			
		||||
        #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,12 @@
 | 
			
		|||
.mr-state-widget
 | 
			
		||||
  = render 'projects/merge_requests/widget/heading'
 | 
			
		||||
  .mr-widget-body
 | 
			
		||||
    -# After conflicts are resolved, the user is redirected back to the MR page.
 | 
			
		||||
    -# There is a short window before background workers run and GitLab processes
 | 
			
		||||
    -# the new push and commits, during which it will think the conflicts still exist.
 | 
			
		||||
    -# We send this param to get the widget to treat the MR as having no more conflicts.
 | 
			
		||||
    - resolved_conflicts = params[:resolved_conflicts]
 | 
			
		||||
 | 
			
		||||
    - if @project.archived?
 | 
			
		||||
      = render 'projects/merge_requests/widget/open/archived'
 | 
			
		||||
    - elsif @merge_request.commits.blank?
 | 
			
		||||
| 
						 | 
				
			
			@ -9,7 +15,7 @@
 | 
			
		|||
      = render 'projects/merge_requests/widget/open/missing_branch'
 | 
			
		||||
    - elsif @merge_request.unchecked?
 | 
			
		||||
      = render 'projects/merge_requests/widget/open/check'
 | 
			
		||||
    - elsif @merge_request.cannot_be_merged?
 | 
			
		||||
    - elsif @merge_request.cannot_be_merged? && !resolved_conflicts
 | 
			
		||||
      = render 'projects/merge_requests/widget/open/conflicts'
 | 
			
		||||
    - elsif @merge_request.work_in_progress?
 | 
			
		||||
      = render 'projects/merge_requests/widget/open/wip'
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +25,7 @@
 | 
			
		|||
      = render 'projects/merge_requests/widget/open/not_allowed'
 | 
			
		||||
    - elsif !@merge_request.mergeable_ci_state? && @pipeline && @pipeline.failed?
 | 
			
		||||
      = render 'projects/merge_requests/widget/open/build_failed'
 | 
			
		||||
    - elsif @merge_request.can_be_merged?
 | 
			
		||||
    - elsif @merge_request.can_be_merged? || resolved_conflicts
 | 
			
		||||
      = render 'projects/merge_requests/widget/open/accept'
 | 
			
		||||
 | 
			
		||||
  - if mr_closes_issues.present?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,18 @@
 | 
			
		|||
  This merge request contains merge conflicts
 | 
			
		||||
 | 
			
		||||
%p
 | 
			
		||||
  Please resolve these conflicts or
 | 
			
		||||
  Please
 | 
			
		||||
  - if @merge_request.conflicts_can_be_resolved_by?(current_user)
 | 
			
		||||
    - if @merge_request.conflicts_can_be_resolved_in_ui?
 | 
			
		||||
      = link_to "resolve these conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
 | 
			
		||||
    - else
 | 
			
		||||
      %span.has-tooltip{title: "These conflicts cannot be resolved through GitLab"}
 | 
			
		||||
        resolve these conflicts locally
 | 
			
		||||
  - else
 | 
			
		||||
    resolve these conflicts
 | 
			
		||||
 | 
			
		||||
  or
 | 
			
		||||
 | 
			
		||||
  - if @merge_request.can_be_merged_via_command_line_by?(current_user)
 | 
			
		||||
    #{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}.
 | 
			
		||||
  - else
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -727,6 +727,7 @@ Rails.application.routes.draw do
 | 
			
		|||
          member do
 | 
			
		||||
            get :commits
 | 
			
		||||
            get :diffs
 | 
			
		||||
            get :conflicts
 | 
			
		||||
            get :builds
 | 
			
		||||
            get :pipelines
 | 
			
		||||
            get :merge_check
 | 
			
		||||
| 
						 | 
				
			
			@ -737,6 +738,7 @@ Rails.application.routes.draw do
 | 
			
		|||
            post :toggle_award_emoji
 | 
			
		||||
            post :remove_wip
 | 
			
		||||
            get :diff_for_path
 | 
			
		||||
            post :resolve_conflicts
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          collection do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 242 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 32 KiB  | 
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
# Merge conflict resolution
 | 
			
		||||
 | 
			
		||||
> [Introduced][ce-5479] in GitLab 8.11.
 | 
			
		||||
 | 
			
		||||
When a merge request has conflicts, GitLab may provide the option to resolve
 | 
			
		||||
those conflicts in the GitLab UI. (See
 | 
			
		||||
[conflicts available for resolution](#conflicts-available-for-resolution) for
 | 
			
		||||
more information on when this is available.) If this is an option, you will see
 | 
			
		||||
a **resolve these conflicts** link in the merge request widget:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
Clicking this will show a list of files with conflicts, with conflict sections
 | 
			
		||||
highlighted:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
Once all conflicts have been marked as using 'ours' or 'theirs', the conflict
 | 
			
		||||
can be resolved. This will perform a merge of the target branch of the merge
 | 
			
		||||
request into the source branch, resolving the conflicts using the options
 | 
			
		||||
chosen. If the source branch is `feature` and the target branch is `master`,
 | 
			
		||||
this is similar to performing `git checkout feature; git merge master` locally.
 | 
			
		||||
 | 
			
		||||
## Conflicts available for resolution
 | 
			
		||||
 | 
			
		||||
GitLab allows resolving conflicts in a file where all of the below are true:
 | 
			
		||||
 | 
			
		||||
- The file is text, not binary
 | 
			
		||||
- The file does not already contain conflict markers
 | 
			
		||||
- The file, with conflict markers added, is not over 200 KB in size
 | 
			
		||||
- The file exists under the same path in both branches
 | 
			
		||||
 | 
			
		||||
If any file with conflicts in that merge request does not meet all of these
 | 
			
		||||
criteria, the conflicts for that merge request cannot be resolved in the UI.
 | 
			
		||||
 | 
			
		||||
Additionally, GitLab does not detect conflicts in renames away from a path. For
 | 
			
		||||
example, this will not create a conflict: on branch `a`, doing `git mv file1
 | 
			
		||||
file2`; on branch `b`, doing `git mv file1 file3`. Instead, both files will be
 | 
			
		||||
present in the branch after the merge request is merged.
 | 
			
		||||
 | 
			
		||||
[ce-5479]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5479
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,186 @@
 | 
			
		|||
module Gitlab
 | 
			
		||||
  module Conflict
 | 
			
		||||
    class File
 | 
			
		||||
      include Gitlab::Routing.url_helpers
 | 
			
		||||
      include IconsHelper
 | 
			
		||||
 | 
			
		||||
      class MissingResolution < StandardError
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      CONTEXT_LINES = 3
 | 
			
		||||
 | 
			
		||||
      attr_reader :merge_file_result, :their_path, :our_path, :our_mode, :merge_request, :repository
 | 
			
		||||
 | 
			
		||||
      def initialize(merge_file_result, conflict, merge_request:)
 | 
			
		||||
        @merge_file_result = merge_file_result
 | 
			
		||||
        @their_path = conflict[:theirs][:path]
 | 
			
		||||
        @our_path = conflict[:ours][:path]
 | 
			
		||||
        @our_mode = conflict[:ours][:mode]
 | 
			
		||||
        @merge_request = merge_request
 | 
			
		||||
        @repository = merge_request.project.repository
 | 
			
		||||
        @match_line_headers = {}
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Array of Gitlab::Diff::Line objects
 | 
			
		||||
      def lines
 | 
			
		||||
        @lines ||= Gitlab::Conflict::Parser.new.parse(merge_file_result[:data],
 | 
			
		||||
                                                      our_path: our_path,
 | 
			
		||||
                                                      their_path: their_path,
 | 
			
		||||
                                                      parent_file: self)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def resolve_lines(resolution)
 | 
			
		||||
        section_id = nil
 | 
			
		||||
 | 
			
		||||
        lines.map do |line|
 | 
			
		||||
          unless line.type
 | 
			
		||||
            section_id = nil
 | 
			
		||||
            next line
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          section_id ||= line_code(line)
 | 
			
		||||
 | 
			
		||||
          case resolution[section_id]
 | 
			
		||||
          when 'head'
 | 
			
		||||
            next unless line.type == 'new'
 | 
			
		||||
          when 'origin'
 | 
			
		||||
            next unless line.type == 'old'
 | 
			
		||||
          else
 | 
			
		||||
            raise MissingResolution, "Missing resolution for section ID: #{section_id}"
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          line
 | 
			
		||||
        end.compact
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def highlight_lines!
 | 
			
		||||
        their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
 | 
			
		||||
        our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
 | 
			
		||||
 | 
			
		||||
        their_highlight = Gitlab::Highlight.highlight(their_path, their_file, repository: repository).lines
 | 
			
		||||
        our_highlight = Gitlab::Highlight.highlight(our_path, our_file, repository: repository).lines
 | 
			
		||||
 | 
			
		||||
        lines.each do |line|
 | 
			
		||||
          if line.type == 'old'
 | 
			
		||||
            line.rich_text = their_highlight[line.old_line - 1].try(:html_safe)
 | 
			
		||||
          else
 | 
			
		||||
            line.rich_text = our_highlight[line.new_line - 1].try(:html_safe)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def sections
 | 
			
		||||
        return @sections if @sections
 | 
			
		||||
 | 
			
		||||
        chunked_lines = lines.chunk { |line| line.type.nil? }.to_a
 | 
			
		||||
        match_line = nil
 | 
			
		||||
 | 
			
		||||
        sections_count = chunked_lines.size
 | 
			
		||||
 | 
			
		||||
        @sections = chunked_lines.flat_map.with_index do |(no_conflict, lines), i|
 | 
			
		||||
          section = nil
 | 
			
		||||
 | 
			
		||||
          # We need to reduce context sections to CONTEXT_LINES. Conflict sections are
 | 
			
		||||
          # always shown in full.
 | 
			
		||||
          if no_conflict
 | 
			
		||||
            conflict_before = i > 0
 | 
			
		||||
            conflict_after = (sections_count - i) > 1
 | 
			
		||||
 | 
			
		||||
            if conflict_before && conflict_after
 | 
			
		||||
              # Create a gap in a long context section.
 | 
			
		||||
              if lines.length > CONTEXT_LINES * 2
 | 
			
		||||
                head_lines = lines.first(CONTEXT_LINES)
 | 
			
		||||
                tail_lines = lines.last(CONTEXT_LINES)
 | 
			
		||||
 | 
			
		||||
                # Ensure any existing match line has text for all lines up to the last
 | 
			
		||||
                # line of its context.
 | 
			
		||||
                update_match_line_text(match_line, head_lines.last)
 | 
			
		||||
 | 
			
		||||
                # Insert a new match line after the created gap.
 | 
			
		||||
                match_line = create_match_line(tail_lines.first)
 | 
			
		||||
 | 
			
		||||
                section = [
 | 
			
		||||
                  { conflict: false, lines: head_lines },
 | 
			
		||||
                  { conflict: false, lines: tail_lines.unshift(match_line) }
 | 
			
		||||
                ]
 | 
			
		||||
              end
 | 
			
		||||
            elsif conflict_after
 | 
			
		||||
              tail_lines = lines.last(CONTEXT_LINES)
 | 
			
		||||
 | 
			
		||||
              # Create a gap and insert a match line at the start.
 | 
			
		||||
              if lines.length > tail_lines.length
 | 
			
		||||
                match_line = create_match_line(tail_lines.first)
 | 
			
		||||
 | 
			
		||||
                tail_lines.unshift(match_line)
 | 
			
		||||
              end
 | 
			
		||||
 | 
			
		||||
              lines = tail_lines
 | 
			
		||||
            elsif conflict_before
 | 
			
		||||
              # We're at the end of the file (no conflicts after), so just remove extra
 | 
			
		||||
              # trailing lines.
 | 
			
		||||
              lines = lines.first(CONTEXT_LINES)
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          # We want to update the match line's text every time unless we've already
 | 
			
		||||
          # created a gap and its corresponding match line.
 | 
			
		||||
          update_match_line_text(match_line, lines.last) unless section
 | 
			
		||||
 | 
			
		||||
          section ||= { conflict: !no_conflict, lines: lines }
 | 
			
		||||
          section[:id] = line_code(lines.first) unless no_conflict
 | 
			
		||||
          section
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def line_code(line)
 | 
			
		||||
        Gitlab::Diff::LineCode.generate(our_path, line.new_pos, line.old_pos)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def create_match_line(line)
 | 
			
		||||
        Gitlab::Diff::Line.new('', 'match', line.index, line.old_pos, line.new_pos)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Any line beginning with a letter, an underscore, or a dollar can be used in a
 | 
			
		||||
      # match line header. Only context sections can contain match lines, as match lines
 | 
			
		||||
      # have to exist in both versions of the file.
 | 
			
		||||
      def find_match_line_header(index)
 | 
			
		||||
        return @match_line_headers[index] if @match_line_headers.key?(index)
 | 
			
		||||
 | 
			
		||||
        @match_line_headers[index] = begin
 | 
			
		||||
          if index >= 0
 | 
			
		||||
            line = lines[index]
 | 
			
		||||
 | 
			
		||||
            if line.type.nil? && line.text.match(/\A[A-Za-z$_]/)
 | 
			
		||||
              " #{line.text}"
 | 
			
		||||
            else
 | 
			
		||||
              find_match_line_header(index - 1)
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Set the match line's text for the current line. A match line takes its start
 | 
			
		||||
      # position and context header (where present) from itself, and its end position from
 | 
			
		||||
      # the line passed in.
 | 
			
		||||
      def update_match_line_text(match_line, line)
 | 
			
		||||
        return unless match_line
 | 
			
		||||
 | 
			
		||||
        header = find_match_line_header(match_line.index - 1)
 | 
			
		||||
 | 
			
		||||
        match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}"
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def as_json(opts = nil)
 | 
			
		||||
        {
 | 
			
		||||
          old_path: their_path,
 | 
			
		||||
          new_path: our_path,
 | 
			
		||||
          blob_icon: file_type_icon_class('file', our_mode, our_path),
 | 
			
		||||
          blob_path: namespace_project_blob_path(merge_request.project.namespace,
 | 
			
		||||
                                                 merge_request.project,
 | 
			
		||||
                                                 ::File.join(merge_request.diff_refs.head_sha, our_path)),
 | 
			
		||||
          sections: sections
 | 
			
		||||
        }
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,57 @@
 | 
			
		|||
module Gitlab
 | 
			
		||||
  module Conflict
 | 
			
		||||
    class FileCollection
 | 
			
		||||
      class ConflictSideMissing < StandardError
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      attr_reader :merge_request, :our_commit, :their_commit
 | 
			
		||||
 | 
			
		||||
      def initialize(merge_request)
 | 
			
		||||
        @merge_request = merge_request
 | 
			
		||||
        @our_commit = merge_request.source_branch_head.raw.raw_commit
 | 
			
		||||
        @their_commit = merge_request.target_branch_head.raw.raw_commit
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def repository
 | 
			
		||||
        merge_request.project.repository
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def merge_index
 | 
			
		||||
        @merge_index ||= repository.rugged.merge_commits(our_commit, their_commit)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def files
 | 
			
		||||
        @files ||= merge_index.conflicts.map do |conflict|
 | 
			
		||||
          raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]
 | 
			
		||||
 | 
			
		||||
          Gitlab::Conflict::File.new(merge_index.merge_file(conflict[:ours][:path]),
 | 
			
		||||
                                     conflict,
 | 
			
		||||
                                     merge_request: merge_request)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def as_json(opts = nil)
 | 
			
		||||
        {
 | 
			
		||||
          target_branch: merge_request.target_branch,
 | 
			
		||||
          source_branch: merge_request.source_branch,
 | 
			
		||||
          commit_sha: merge_request.diff_head_sha,
 | 
			
		||||
          commit_message: default_commit_message,
 | 
			
		||||
          files: files
 | 
			
		||||
        }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def default_commit_message
 | 
			
		||||
        conflict_filenames = merge_index.conflicts.map do |conflict|
 | 
			
		||||
          "#   #{conflict[:ours][:path]}"
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        <<EOM.chomp
 | 
			
		||||
Merge branch '#{merge_request.target_branch}' into '#{merge_request.source_branch}'
 | 
			
		||||
 | 
			
		||||
# Conflicts:
 | 
			
		||||
#{conflict_filenames.join("\n")}
 | 
			
		||||
EOM
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
module Gitlab
 | 
			
		||||
  module Conflict
 | 
			
		||||
    class Parser
 | 
			
		||||
      class ParserError < StandardError
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      class UnexpectedDelimiter < ParserError
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      class MissingEndDelimiter < ParserError
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      class UnmergeableFile < ParserError
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def parse(text, our_path:, their_path:, parent_file: nil)
 | 
			
		||||
        raise UnmergeableFile if text.blank? # Typically a binary file
 | 
			
		||||
        raise UnmergeableFile if text.length > 102400
 | 
			
		||||
 | 
			
		||||
        line_obj_index = 0
 | 
			
		||||
        line_old = 1
 | 
			
		||||
        line_new = 1
 | 
			
		||||
        type = nil
 | 
			
		||||
        lines = []
 | 
			
		||||
        conflict_start = "<<<<<<< #{our_path}"
 | 
			
		||||
        conflict_middle = '======='
 | 
			
		||||
        conflict_end = ">>>>>>> #{their_path}"
 | 
			
		||||
 | 
			
		||||
        text.each_line.map do |line|
 | 
			
		||||
          full_line = line.delete("\n")
 | 
			
		||||
 | 
			
		||||
          if full_line == conflict_start
 | 
			
		||||
            raise UnexpectedDelimiter unless type.nil?
 | 
			
		||||
 | 
			
		||||
            type = 'new'
 | 
			
		||||
          elsif full_line == conflict_middle
 | 
			
		||||
            raise UnexpectedDelimiter unless type == 'new'
 | 
			
		||||
 | 
			
		||||
            type = 'old'
 | 
			
		||||
          elsif full_line == conflict_end
 | 
			
		||||
            raise UnexpectedDelimiter unless type == 'old'
 | 
			
		||||
 | 
			
		||||
            type = nil
 | 
			
		||||
          elsif line[0] == '\\'
 | 
			
		||||
            type = 'nonewline'
 | 
			
		||||
            lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
 | 
			
		||||
          else
 | 
			
		||||
            lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
 | 
			
		||||
            line_old += 1 if type != 'new'
 | 
			
		||||
            line_new += 1 if type != 'old'
 | 
			
		||||
 | 
			
		||||
            line_obj_index += 1
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        raise MissingEndDelimiter unless type.nil?
 | 
			
		||||
 | 
			
		||||
        lines
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -2,11 +2,13 @@ module Gitlab
 | 
			
		|||
  module Diff
 | 
			
		||||
    class Line
 | 
			
		||||
      attr_reader :type, :index, :old_pos, :new_pos
 | 
			
		||||
      attr_writer :rich_text
 | 
			
		||||
      attr_accessor :text
 | 
			
		||||
 | 
			
		||||
      def initialize(text, type, index, old_pos, new_pos)
 | 
			
		||||
      def initialize(text, type, index, old_pos, new_pos, parent_file: nil)
 | 
			
		||||
        @text, @type, @index = text, type, index
 | 
			
		||||
        @old_pos, @new_pos = old_pos, new_pos
 | 
			
		||||
        @parent_file = parent_file
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def self.init_from_hash(hash)
 | 
			
		||||
| 
						 | 
				
			
			@ -43,9 +45,25 @@ module Gitlab
 | 
			
		|||
        type == 'old'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def rich_text
 | 
			
		||||
        @parent_file.highlight_lines! if @parent_file && !@rich_text
 | 
			
		||||
 | 
			
		||||
        @rich_text
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def meta?
 | 
			
		||||
        type == 'match' || type == 'nonewline'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def as_json(opts = nil)
 | 
			
		||||
        {
 | 
			
		||||
          type: type,
 | 
			
		||||
          old_line: old_line,
 | 
			
		||||
          new_line: new_line,
 | 
			
		||||
          text: text,
 | 
			
		||||
          rich_text: rich_text || text
 | 
			
		||||
        }
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,11 @@ describe Projects::MergeRequestsController do
 | 
			
		|||
  let(:project) { create(:project) }
 | 
			
		||||
  let(:user)    { create(:user) }
 | 
			
		||||
  let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
 | 
			
		||||
  let(:merge_request_with_conflicts) do
 | 
			
		||||
    create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr|
 | 
			
		||||
      mr.mark_as_unmergeable
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  before do
 | 
			
		||||
    sign_in(user)
 | 
			
		||||
| 
						 | 
				
			
			@ -523,4 +528,135 @@ describe Projects::MergeRequestsController do
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'GET conflicts' do
 | 
			
		||||
    let(:json_response) { JSON.parse(response.body) }
 | 
			
		||||
 | 
			
		||||
    context 'when the conflicts cannot be resolved in the UI' do
 | 
			
		||||
      before do
 | 
			
		||||
        allow_any_instance_of(Gitlab::Conflict::Parser).
 | 
			
		||||
          to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnexpectedDelimiter)
 | 
			
		||||
 | 
			
		||||
        get :conflicts,
 | 
			
		||||
            namespace_id: merge_request_with_conflicts.project.namespace.to_param,
 | 
			
		||||
            project_id: merge_request_with_conflicts.project.to_param,
 | 
			
		||||
            id: merge_request_with_conflicts.iid,
 | 
			
		||||
            format: 'json'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns a 200 status code' do
 | 
			
		||||
        expect(response).to have_http_status(:ok)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns JSON with a message' do
 | 
			
		||||
        expect(json_response.keys).to contain_exactly('message', 'type')
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with valid conflicts' do
 | 
			
		||||
      before do
 | 
			
		||||
        get :conflicts,
 | 
			
		||||
            namespace_id: merge_request_with_conflicts.project.namespace.to_param,
 | 
			
		||||
            project_id: merge_request_with_conflicts.project.to_param,
 | 
			
		||||
            id: merge_request_with_conflicts.iid,
 | 
			
		||||
            format: 'json'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'includes meta info about the MR' do
 | 
			
		||||
        expect(json_response['commit_message']).to include('Merge branch')
 | 
			
		||||
        expect(json_response['commit_sha']).to match(/\h{40}/)
 | 
			
		||||
        expect(json_response['source_branch']).to eq(merge_request_with_conflicts.source_branch)
 | 
			
		||||
        expect(json_response['target_branch']).to eq(merge_request_with_conflicts.target_branch)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'includes each file that has conflicts' do
 | 
			
		||||
        filenames = json_response['files'].map { |file| file['new_path'] }
 | 
			
		||||
 | 
			
		||||
        expect(filenames).to contain_exactly('files/ruby/popen.rb', 'files/ruby/regex.rb')
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'splits files into sections with lines' do
 | 
			
		||||
        json_response['files'].each do |file|
 | 
			
		||||
          file['sections'].each do |section|
 | 
			
		||||
            expect(section).to include('conflict', 'lines')
 | 
			
		||||
 | 
			
		||||
            section['lines'].each do |line|
 | 
			
		||||
              if section['conflict']
 | 
			
		||||
                expect(line['type']).to be_in(['old', 'new'])
 | 
			
		||||
                expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer))
 | 
			
		||||
              else
 | 
			
		||||
                if line['type'].nil?
 | 
			
		||||
                  expect(line['old_line']).not_to eq(nil)
 | 
			
		||||
                  expect(line['new_line']).not_to eq(nil)
 | 
			
		||||
                else
 | 
			
		||||
                  expect(line['type']).to eq('match')
 | 
			
		||||
                  expect(line['old_line']).to eq(nil)
 | 
			
		||||
                  expect(line['new_line']).to eq(nil)
 | 
			
		||||
                end
 | 
			
		||||
              end
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'has unique section IDs across files' do
 | 
			
		||||
        section_ids = json_response['files'].flat_map do |file|
 | 
			
		||||
          file['sections'].map { |section| section['id'] }.compact
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        expect(section_ids.uniq).to eq(section_ids)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'POST resolve_conflicts' do
 | 
			
		||||
    let(:json_response) { JSON.parse(response.body) }
 | 
			
		||||
    let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
 | 
			
		||||
 | 
			
		||||
    def resolve_conflicts(sections)
 | 
			
		||||
      post :resolve_conflicts,
 | 
			
		||||
           namespace_id: merge_request_with_conflicts.project.namespace.to_param,
 | 
			
		||||
           project_id: merge_request_with_conflicts.project.to_param,
 | 
			
		||||
           id: merge_request_with_conflicts.iid,
 | 
			
		||||
           format: 'json',
 | 
			
		||||
           sections: sections,
 | 
			
		||||
           commit_message: 'Commit message'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with valid params' do
 | 
			
		||||
      before do
 | 
			
		||||
        resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
 | 
			
		||||
                          '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
 | 
			
		||||
                          '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
 | 
			
		||||
                          '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin')
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'creates a new commit on the branch' do
 | 
			
		||||
        expect(original_head_sha).not_to eq(merge_request_with_conflicts.source_branch_head.sha)
 | 
			
		||||
        expect(merge_request_with_conflicts.source_branch_head.message).to include('Commit message')
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns an OK response' do
 | 
			
		||||
        expect(response).to have_http_status(:ok)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when sections are missing' do
 | 
			
		||||
      before do
 | 
			
		||||
        resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head')
 | 
			
		||||
      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 first missing section' do
 | 
			
		||||
        expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9')
 | 
			
		||||
      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
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,72 @@
 | 
			
		|||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
feature 'Merge request conflict resolution', js: true, feature: true do
 | 
			
		||||
  include WaitForAjax
 | 
			
		||||
 | 
			
		||||
  let(:user) { create(:user) }
 | 
			
		||||
  let(:project) { create(:project) }
 | 
			
		||||
 | 
			
		||||
  def create_merge_request(source_branch)
 | 
			
		||||
    create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', source_project: project) do |mr|
 | 
			
		||||
      mr.mark_as_unmergeable
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'when a merge request can be resolved in the UI' do
 | 
			
		||||
    let(:merge_request) { create_merge_request('conflict-resolvable') }
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      project.team << [user, :developer]
 | 
			
		||||
      login_as(user)
 | 
			
		||||
 | 
			
		||||
      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'shows a link to the conflict resolution page' do
 | 
			
		||||
      expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'visiting the conflicts resolution page' do
 | 
			
		||||
      before { click_link('conflicts', href: /\/conflicts\Z/) }
 | 
			
		||||
 | 
			
		||||
      it 'shows the conflicts' do
 | 
			
		||||
        begin
 | 
			
		||||
          expect(find('#conflicts')).to have_content('popen.rb')
 | 
			
		||||
        rescue Capybara::Poltergeist::JavascriptError
 | 
			
		||||
          retry
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  UNRESOLVABLE_CONFLICTS = {
 | 
			
		||||
    'conflict-too-large' => 'when the conflicts contain a large file',
 | 
			
		||||
    'conflict-binary-file' => 'when the conflicts contain a binary file',
 | 
			
		||||
    'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers',
 | 
			
		||||
    'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  UNRESOLVABLE_CONFLICTS.each do |source_branch, description|
 | 
			
		||||
    context description do
 | 
			
		||||
      let(:merge_request) { create_merge_request(source_branch) }
 | 
			
		||||
 | 
			
		||||
      before do
 | 
			
		||||
        project.team << [user, :developer]
 | 
			
		||||
        login_as(user)
 | 
			
		||||
 | 
			
		||||
        visit namespace_project_merge_request_path(project.namespace, project, merge_request)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'does not show a link to the conflict resolution page' do
 | 
			
		||||
        expect(page).not_to have_link('conflicts', href: /\/conflicts\Z/)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'shows an error if the conflicts page is visited directly' do
 | 
			
		||||
        visit current_url + '/conflicts'
 | 
			
		||||
        wait_for_ajax
 | 
			
		||||
 | 
			
		||||
        expect(find('#conflicts')).to have_content('Please try to resolve them locally.')
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
describe Gitlab::Conflict::FileCollection, lib: true do
 | 
			
		||||
  let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start') }
 | 
			
		||||
  let(:file_collection) { Gitlab::Conflict::FileCollection.new(merge_request) }
 | 
			
		||||
 | 
			
		||||
  describe '#files' do
 | 
			
		||||
    it 'returns an array of Conflict::Files' do
 | 
			
		||||
      expect(file_collection.files).to all(be_an_instance_of(Gitlab::Conflict::File))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#default_commit_message' do
 | 
			
		||||
    it 'matches the format of the git CLI commit message' do
 | 
			
		||||
      expect(file_collection.default_commit_message).to eq(<<EOM.chomp)
 | 
			
		||||
Merge branch 'conflict-start' into 'conflict-resolvable'
 | 
			
		||||
 | 
			
		||||
# Conflicts:
 | 
			
		||||
#   files/ruby/popen.rb
 | 
			
		||||
#   files/ruby/regex.rb
 | 
			
		||||
EOM
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,261 @@
 | 
			
		|||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
describe Gitlab::Conflict::File, lib: true do
 | 
			
		||||
  let(:project) { create(:project) }
 | 
			
		||||
  let(:repository) { project.repository }
 | 
			
		||||
  let(:rugged) { repository.rugged }
 | 
			
		||||
  let(:their_commit) { rugged.branches['conflict-start'].target }
 | 
			
		||||
  let(:our_commit) { rugged.branches['conflict-resolvable'].target }
 | 
			
		||||
  let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) }
 | 
			
		||||
  let(:index) { rugged.merge_commits(our_commit, their_commit) }
 | 
			
		||||
  let(:conflict) { index.conflicts.last }
 | 
			
		||||
  let(:merge_file_result) { index.merge_file('files/ruby/regex.rb') }
 | 
			
		||||
  let(:conflict_file) { Gitlab::Conflict::File.new(merge_file_result, conflict, merge_request: merge_request) }
 | 
			
		||||
 | 
			
		||||
  describe '#resolve_lines' do
 | 
			
		||||
    let(:section_keys) { conflict_file.sections.map { |section| section[:id] }.compact }
 | 
			
		||||
 | 
			
		||||
    context 'when resolving everything to the same side' do
 | 
			
		||||
      let(:resolution_hash) { section_keys.map { |key| [key, 'head'] }.to_h }
 | 
			
		||||
      let(:resolved_lines) { conflict_file.resolve_lines(resolution_hash) }
 | 
			
		||||
      let(:expected_lines) { conflict_file.lines.reject { |line| line.type == 'old' } }
 | 
			
		||||
 | 
			
		||||
      it 'has the correct number of lines' do
 | 
			
		||||
        expect(resolved_lines.length).to eq(expected_lines.length)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'has content matching the chosen lines' do
 | 
			
		||||
        expect(resolved_lines.map(&:text)).to eq(expected_lines.map(&:text))
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with mixed resolutions' do
 | 
			
		||||
      let(:resolution_hash) do
 | 
			
		||||
        section_keys.map.with_index { |key, i| [key, i.even? ? 'head' : 'origin'] }.to_h
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      let(:resolved_lines) { conflict_file.resolve_lines(resolution_hash) }
 | 
			
		||||
 | 
			
		||||
      it 'has the correct number of lines' do
 | 
			
		||||
        file_lines = conflict_file.lines.reject { |line| line.type == 'new' }
 | 
			
		||||
 | 
			
		||||
        expect(resolved_lines.length).to eq(file_lines.length)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns a file containing only the chosen parts of the resolved sections' do
 | 
			
		||||
        expect(resolved_lines.chunk { |line| line.type || 'both' }.map(&:first)).
 | 
			
		||||
          to eq(['both', 'new', 'both', 'old', 'both', 'new', 'both'])
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'raises MissingResolution when passed a hash without resolutions for all sections' do
 | 
			
		||||
      empty_hash = section_keys.map { |key| [key, nil] }.to_h
 | 
			
		||||
      invalid_hash = section_keys.map { |key| [key, 'invalid'] }.to_h
 | 
			
		||||
 | 
			
		||||
      expect { conflict_file.resolve_lines({}) }.
 | 
			
		||||
        to raise_error(Gitlab::Conflict::File::MissingResolution)
 | 
			
		||||
 | 
			
		||||
      expect { conflict_file.resolve_lines(empty_hash) }.
 | 
			
		||||
        to raise_error(Gitlab::Conflict::File::MissingResolution)
 | 
			
		||||
 | 
			
		||||
      expect { conflict_file.resolve_lines(invalid_hash) }.
 | 
			
		||||
        to raise_error(Gitlab::Conflict::File::MissingResolution)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#highlight_lines!' do
 | 
			
		||||
    def html_to_text(html)
 | 
			
		||||
      CGI.unescapeHTML(ActionView::Base.full_sanitizer.sanitize(html)).delete("\n")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'modifies the existing lines' do
 | 
			
		||||
      expect { conflict_file.highlight_lines! }.to change { conflict_file.lines.map(&:instance_variables) }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'is called implicitly when rich_text is accessed on a line' do
 | 
			
		||||
      expect(conflict_file).to receive(:highlight_lines!).once.and_call_original
 | 
			
		||||
 | 
			
		||||
      conflict_file.lines.each(&:rich_text)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets the rich_text of the lines matching the text content' do
 | 
			
		||||
      conflict_file.lines.each do |line|
 | 
			
		||||
        expect(line.text).to eq(html_to_text(line.rich_text))
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#sections' do
 | 
			
		||||
    it 'only inserts match lines when there is a gap between sections' do
 | 
			
		||||
      conflict_file.sections.each_with_index do |section, i|
 | 
			
		||||
        previous_line_number = 0
 | 
			
		||||
        current_line_number = section[:lines].map(&:old_line).compact.min
 | 
			
		||||
 | 
			
		||||
        if i > 0
 | 
			
		||||
          previous_line_number = conflict_file.sections[i - 1][:lines].map(&:old_line).compact.last
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        if current_line_number == previous_line_number + 1
 | 
			
		||||
          expect(section[:lines].first.type).not_to eq('match')
 | 
			
		||||
        else
 | 
			
		||||
          expect(section[:lines].first.type).to eq('match')
 | 
			
		||||
          expect(section[:lines].first.text).to match(/\A@@ -#{current_line_number},\d+ \+\d+,\d+ @@ module Gitlab\Z/)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets conflict to false for sections with only unchanged lines' do
 | 
			
		||||
      conflict_file.sections.reject { |section| section[:conflict] }.each do |section|
 | 
			
		||||
        without_match = section[:lines].reject { |line| line.type == 'match' }
 | 
			
		||||
 | 
			
		||||
        expect(without_match).to all(have_attributes(type: nil))
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'only includes a maximum of CONTEXT_LINES (plus an optional match line) in context sections' do
 | 
			
		||||
      conflict_file.sections.reject { |section| section[:conflict] }.each do |section|
 | 
			
		||||
        without_match = section[:lines].reject { |line| line.type == 'match' }
 | 
			
		||||
 | 
			
		||||
        expect(without_match.length).to be <= Gitlab::Conflict::File::CONTEXT_LINES * 2
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets conflict to true for sections with only changed lines' do
 | 
			
		||||
      conflict_file.sections.select { |section| section[:conflict] }.each do |section|
 | 
			
		||||
        section[:lines].each do |line|
 | 
			
		||||
          expect(line.type).to be_in(['new', 'old'])
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'adds unique IDs to conflict sections, and not to other sections' do
 | 
			
		||||
      section_ids = []
 | 
			
		||||
 | 
			
		||||
      conflict_file.sections.each do |section|
 | 
			
		||||
        if section[:conflict]
 | 
			
		||||
          expect(section).to have_key(:id)
 | 
			
		||||
          section_ids << section[:id]
 | 
			
		||||
        else
 | 
			
		||||
          expect(section).not_to have_key(:id)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      expect(section_ids.uniq).to eq(section_ids)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with an example file' do
 | 
			
		||||
      let(:file) do
 | 
			
		||||
        <<FILE
 | 
			
		||||
  # Ensure there is no match line header here
 | 
			
		||||
  def username_regexp
 | 
			
		||||
    default_regexp
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
<<<<<<< files/ruby/regex.rb
 | 
			
		||||
def project_name_regexp
 | 
			
		||||
  /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
def name_regexp
 | 
			
		||||
  /\A[a-zA-Z0-9_\-\. ]*\z/
 | 
			
		||||
=======
 | 
			
		||||
def project_name_regex
 | 
			
		||||
  %r{\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z}
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
def name_regex
 | 
			
		||||
  %r{\A[a-zA-Z0-9_\-\. ]*\z}
 | 
			
		||||
>>>>>>> files/ruby/regex.rb
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
# Some extra lines
 | 
			
		||||
# To force a match line
 | 
			
		||||
# To be created
 | 
			
		||||
 | 
			
		||||
def path_regexp
 | 
			
		||||
  default_regexp
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
<<<<<<< files/ruby/regex.rb
 | 
			
		||||
def archive_formats_regexp
 | 
			
		||||
  /(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
 | 
			
		||||
=======
 | 
			
		||||
def archive_formats_regex
 | 
			
		||||
  %r{(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)}
 | 
			
		||||
>>>>>>> files/ruby/regex.rb
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
def git_reference_regexp
 | 
			
		||||
  # Valid git ref regexp, see:
 | 
			
		||||
  # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
 | 
			
		||||
  %r{
 | 
			
		||||
    (?!
 | 
			
		||||
       (?# doesn't begins with)
 | 
			
		||||
       \/|                    (?# rule #6)
 | 
			
		||||
       (?# doesn't contain)
 | 
			
		||||
       .*(?:
 | 
			
		||||
          [\/.]\.|            (?# rule #1,3)
 | 
			
		||||
          \/\/|               (?# rule #6)
 | 
			
		||||
          @\{|                (?# rule #8)
 | 
			
		||||
          \\                  (?# rule #9)
 | 
			
		||||
       )
 | 
			
		||||
    )
 | 
			
		||||
    [^\000-\040\177~^:?*\[]+  (?# rule #4-5)
 | 
			
		||||
    (?# doesn't end with)
 | 
			
		||||
    (?<!\.lock)               (?# rule #1)
 | 
			
		||||
    (?<![\/.])                (?# rule #6-7)
 | 
			
		||||
  }x
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
protected
 | 
			
		||||
 | 
			
		||||
<<<<<<< files/ruby/regex.rb
 | 
			
		||||
def default_regexp
 | 
			
		||||
  /\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
 | 
			
		||||
=======
 | 
			
		||||
def default_regex
 | 
			
		||||
  %r{\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z}
 | 
			
		||||
>>>>>>> files/ruby/regex.rb
 | 
			
		||||
end
 | 
			
		||||
FILE
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      let(:conflict_file) { Gitlab::Conflict::File.new({ data: file }, conflict, merge_request: merge_request) }
 | 
			
		||||
      let(:sections) { conflict_file.sections }
 | 
			
		||||
 | 
			
		||||
      it 'sets the correct match line headers' do
 | 
			
		||||
        expect(sections[0][:lines].first).to have_attributes(type: 'match', text: '@@ -3,14 +3,14 @@')
 | 
			
		||||
        expect(sections[3][:lines].first).to have_attributes(type: 'match', text: '@@ -19,26 +19,26 @@ def path_regexp')
 | 
			
		||||
        expect(sections[6][:lines].first).to have_attributes(type: 'match', text: '@@ -47,52 +47,52 @@ end')
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'does not add match lines where they are not needed' do
 | 
			
		||||
        expect(sections[1][:lines].first.type).not_to eq('match')
 | 
			
		||||
        expect(sections[2][:lines].first.type).not_to eq('match')
 | 
			
		||||
        expect(sections[4][:lines].first.type).not_to eq('match')
 | 
			
		||||
        expect(sections[5][:lines].first.type).not_to eq('match')
 | 
			
		||||
        expect(sections[7][:lines].first.type).not_to eq('match')
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'creates context sections of the correct length' do
 | 
			
		||||
        expect(sections[0][:lines].reject(&:type).length).to eq(3)
 | 
			
		||||
        expect(sections[2][:lines].reject(&:type).length).to eq(3)
 | 
			
		||||
        expect(sections[3][:lines].reject(&:type).length).to eq(3)
 | 
			
		||||
        expect(sections[5][:lines].reject(&:type).length).to eq(3)
 | 
			
		||||
        expect(sections[6][:lines].reject(&:type).length).to eq(3)
 | 
			
		||||
        expect(sections[8][:lines].reject(&:type).length).to eq(1)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#as_json' do
 | 
			
		||||
    it 'includes the blob path for the file' do
 | 
			
		||||
      expect(conflict_file.as_json[:blob_path]).
 | 
			
		||||
        to eq("/#{project.namespace.to_param}/#{merge_request.project.to_param}/blob/#{our_commit.oid}/files/ruby/regex.rb")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'includes the blob icon for the file' do
 | 
			
		||||
      expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o')
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,188 @@
 | 
			
		|||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
describe Gitlab::Conflict::Parser, lib: true do
 | 
			
		||||
  let(:parser) { Gitlab::Conflict::Parser.new }
 | 
			
		||||
 | 
			
		||||
  describe '#parse' do
 | 
			
		||||
    def parse_text(text)
 | 
			
		||||
      parser.parse(text, our_path: 'README.md', their_path: 'README.md')
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the file has valid conflicts' do
 | 
			
		||||
      let(:text) do
 | 
			
		||||
        <<CONFLICT
 | 
			
		||||
module Gitlab
 | 
			
		||||
  module Regexp
 | 
			
		||||
    extend self
 | 
			
		||||
 | 
			
		||||
    def username_regexp
 | 
			
		||||
      default_regexp
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
<<<<<<< files/ruby/regex.rb
 | 
			
		||||
    def project_name_regexp
 | 
			
		||||
      /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def name_regexp
 | 
			
		||||
      /\A[a-zA-Z0-9_\-\. ]*\z/
 | 
			
		||||
=======
 | 
			
		||||
    def project_name_regex
 | 
			
		||||
      %r{\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z}
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def name_regex
 | 
			
		||||
      %r{\A[a-zA-Z0-9_\-\. ]*\z}
 | 
			
		||||
>>>>>>> files/ruby/regex.rb
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def path_regexp
 | 
			
		||||
      default_regexp
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
<<<<<<< files/ruby/regex.rb
 | 
			
		||||
    def archive_formats_regexp
 | 
			
		||||
      /(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
 | 
			
		||||
=======
 | 
			
		||||
    def archive_formats_regex
 | 
			
		||||
      %r{(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)}
 | 
			
		||||
>>>>>>> files/ruby/regex.rb
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def git_reference_regexp
 | 
			
		||||
      # Valid git ref regexp, see:
 | 
			
		||||
      # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
 | 
			
		||||
      %r{
 | 
			
		||||
        (?!
 | 
			
		||||
           (?# doesn't begins with)
 | 
			
		||||
           \/|                    (?# rule #6)
 | 
			
		||||
           (?# doesn't contain)
 | 
			
		||||
           .*(?:
 | 
			
		||||
              [\/.]\.|            (?# rule #1,3)
 | 
			
		||||
              \/\/|               (?# rule #6)
 | 
			
		||||
              @\{|                (?# rule #8)
 | 
			
		||||
              \\                  (?# rule #9)
 | 
			
		||||
           )
 | 
			
		||||
        )
 | 
			
		||||
        [^\000-\040\177~^:?*\[]+  (?# rule #4-5)
 | 
			
		||||
        (?# doesn't end with)
 | 
			
		||||
        (?<!\.lock)               (?# rule #1)
 | 
			
		||||
        (?<![\/.])                (?# rule #6-7)
 | 
			
		||||
      }x
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    protected
 | 
			
		||||
 | 
			
		||||
<<<<<<< files/ruby/regex.rb
 | 
			
		||||
    def default_regexp
 | 
			
		||||
      /\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
 | 
			
		||||
=======
 | 
			
		||||
    def default_regex
 | 
			
		||||
      %r{\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z}
 | 
			
		||||
>>>>>>> files/ruby/regex.rb
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
CONFLICT
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      let(:lines) do
 | 
			
		||||
        parser.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb')
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'sets our lines as new lines' do
 | 
			
		||||
        expect(lines[8..13]).to all(have_attributes(type: 'new'))
 | 
			
		||||
        expect(lines[26..27]).to all(have_attributes(type: 'new'))
 | 
			
		||||
        expect(lines[56..57]).to all(have_attributes(type: 'new'))
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'sets their lines as old lines' do
 | 
			
		||||
        expect(lines[14..19]).to all(have_attributes(type: 'old'))
 | 
			
		||||
        expect(lines[28..29]).to all(have_attributes(type: 'old'))
 | 
			
		||||
        expect(lines[58..59]).to all(have_attributes(type: 'old'))
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'sets non-conflicted lines as both' do
 | 
			
		||||
        expect(lines[0..7]).to all(have_attributes(type: nil))
 | 
			
		||||
        expect(lines[20..25]).to all(have_attributes(type: nil))
 | 
			
		||||
        expect(lines[30..55]).to all(have_attributes(type: nil))
 | 
			
		||||
        expect(lines[60..62]).to all(have_attributes(type: nil))
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'sets consecutive line numbers for index, old_pos, and new_pos' do
 | 
			
		||||
        old_line_numbers = lines.select { |line| line.type != 'new' }.map(&:old_pos)
 | 
			
		||||
        new_line_numbers = lines.select { |line| line.type != 'old' }.map(&:new_pos)
 | 
			
		||||
 | 
			
		||||
        expect(lines.map(&:index)).to eq(0.upto(62).to_a)
 | 
			
		||||
        expect(old_line_numbers).to eq(1.upto(53).to_a)
 | 
			
		||||
        expect(new_line_numbers).to eq(1.upto(53).to_a)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the file contents include conflict delimiters' do
 | 
			
		||||
      it 'raises UnexpectedDelimiter when there is a non-start delimiter first' do
 | 
			
		||||
        expect { parse_text('=======') }.
 | 
			
		||||
          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
 | 
			
		||||
 | 
			
		||||
        expect { parse_text('>>>>>>> README.md') }.
 | 
			
		||||
          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
 | 
			
		||||
 | 
			
		||||
        expect { parse_text('>>>>>>> some-other-path.md') }.
 | 
			
		||||
          not_to raise_error
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'raises UnexpectedDelimiter when a start delimiter is followed by a non-middle delimiter' do
 | 
			
		||||
        start_text = "<<<<<<< README.md\n"
 | 
			
		||||
        end_text = "\n=======\n>>>>>>> README.md"
 | 
			
		||||
 | 
			
		||||
        expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }.
 | 
			
		||||
          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
 | 
			
		||||
 | 
			
		||||
        expect { parse_text(start_text + start_text + end_text) }.
 | 
			
		||||
          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
 | 
			
		||||
 | 
			
		||||
        expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
 | 
			
		||||
          not_to raise_error
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'raises UnexpectedDelimiter when a middle delimiter is followed by a non-end delimiter' do
 | 
			
		||||
        start_text = "<<<<<<< README.md\n=======\n"
 | 
			
		||||
        end_text = "\n>>>>>>> README.md"
 | 
			
		||||
 | 
			
		||||
        expect { parse_text(start_text + '=======' + end_text) }.
 | 
			
		||||
          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
 | 
			
		||||
 | 
			
		||||
        expect { parse_text(start_text + start_text + end_text) }.
 | 
			
		||||
          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
 | 
			
		||||
 | 
			
		||||
        expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
 | 
			
		||||
          not_to raise_error
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'raises MissingEndDelimiter when there is no end delimiter at the end' do
 | 
			
		||||
        start_text = "<<<<<<< README.md\n=======\n"
 | 
			
		||||
 | 
			
		||||
        expect { parse_text(start_text) }.
 | 
			
		||||
          to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
 | 
			
		||||
 | 
			
		||||
        expect { parse_text(start_text + '>>>>>>> some-other-path.md') }.
 | 
			
		||||
          to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'other file types' do
 | 
			
		||||
      it 'raises UnmergeableFile when lines is blank, indicating a binary file' do
 | 
			
		||||
        expect { parse_text('') }.
 | 
			
		||||
          to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
 | 
			
		||||
 | 
			
		||||
        expect { parse_text(nil) }.
 | 
			
		||||
          to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'raises UnmergeableFile when the file is over 100 KB' do
 | 
			
		||||
        expect { parse_text('a' * 102401) }.
 | 
			
		||||
          to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -783,4 +783,56 @@ describe MergeRequest, models: true do
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#conflicts_can_be_resolved_in_ui?' do
 | 
			
		||||
    def create_merge_request(source_branch)
 | 
			
		||||
      create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr|
 | 
			
		||||
        mr.mark_as_unmergeable
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns a falsey value when the MR can be merged without conflicts' do
 | 
			
		||||
      merge_request = create_merge_request('master')
 | 
			
		||||
      merge_request.mark_as_mergeable
 | 
			
		||||
 | 
			
		||||
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns a falsey value when the MR does not support new diff notes' do
 | 
			
		||||
      merge_request = create_merge_request('conflict-resolvable')
 | 
			
		||||
      merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
 | 
			
		||||
 | 
			
		||||
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns a falsey value when the conflicts contain a large file' do
 | 
			
		||||
      merge_request = create_merge_request('conflict-too-large')
 | 
			
		||||
 | 
			
		||||
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns a falsey value when the conflicts contain a binary file' do
 | 
			
		||||
      merge_request = create_merge_request('conflict-binary-file')
 | 
			
		||||
 | 
			
		||||
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns a falsey value when the conflicts contain a file with ambiguous conflict markers' do
 | 
			
		||||
      merge_request = create_merge_request('conflict-contains-conflict-markers')
 | 
			
		||||
 | 
			
		||||
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do
 | 
			
		||||
      merge_request = create_merge_request('conflict-missing-side')
 | 
			
		||||
 | 
			
		||||
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns a truthy value when the conflicts are resolvable in the UI' do
 | 
			
		||||
      merge_request = create_merge_request('conflict-resolvable')
 | 
			
		||||
 | 
			
		||||
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,25 +5,31 @@ module TestEnv
 | 
			
		|||
 | 
			
		||||
  # When developing the seed repository, comment out the branch you will modify.
 | 
			
		||||
  BRANCH_SHA = {
 | 
			
		||||
    'empty-branch'          => '7efb185',
 | 
			
		||||
    'ends-with.json'        => '98b0d8b3',
 | 
			
		||||
    'flatten-dir'           => 'e56497b',
 | 
			
		||||
    'feature'               => '0b4bc9a',
 | 
			
		||||
    'feature_conflict'      => 'bb5206f',
 | 
			
		||||
    'fix'                   => '48f0be4',
 | 
			
		||||
    'improve/awesome'       => '5937ac0',
 | 
			
		||||
    'markdown'              => '0ed8c6c',
 | 
			
		||||
    'lfs'                   => 'be93687',
 | 
			
		||||
    'master'                => '5937ac0',
 | 
			
		||||
    "'test'"                => 'e56497b',
 | 
			
		||||
    'orphaned-branch'       => '45127a9',
 | 
			
		||||
    'binary-encoding'       => '7b1cf43',
 | 
			
		||||
    'gitattributes'         => '5a62481',
 | 
			
		||||
    'expand-collapse-diffs' => '4842455',
 | 
			
		||||
    'expand-collapse-files' => '025db92',
 | 
			
		||||
    'expand-collapse-lines' => '238e82d',
 | 
			
		||||
    'video'                 => '8879059',
 | 
			
		||||
    'crlf-diff'             => '5938907'
 | 
			
		||||
    'empty-branch'                       => '7efb185',
 | 
			
		||||
    'ends-with.json'                     => '98b0d8b3',
 | 
			
		||||
    'flatten-dir'                        => 'e56497b',
 | 
			
		||||
    'feature'                            => '0b4bc9a',
 | 
			
		||||
    'feature_conflict'                   => 'bb5206f',
 | 
			
		||||
    'fix'                                => '48f0be4',
 | 
			
		||||
    'improve/awesome'                    => '5937ac0',
 | 
			
		||||
    'markdown'                           => '0ed8c6c',
 | 
			
		||||
    'lfs'                                => 'be93687',
 | 
			
		||||
    'master'                             => '5937ac0',
 | 
			
		||||
    "'test'"                             => 'e56497b',
 | 
			
		||||
    'orphaned-branch'                    => '45127a9',
 | 
			
		||||
    'binary-encoding'                    => '7b1cf43',
 | 
			
		||||
    'gitattributes'                      => '5a62481',
 | 
			
		||||
    'expand-collapse-diffs'              => '4842455',
 | 
			
		||||
    'expand-collapse-files'              => '025db92',
 | 
			
		||||
    'expand-collapse-lines'              => '238e82d',
 | 
			
		||||
    'video'                              => '8879059',
 | 
			
		||||
    'crlf-diff'                          => '5938907',
 | 
			
		||||
    'conflict-start'                     => '14fa46b',
 | 
			
		||||
    'conflict-resolvable'                => '1450cd6',
 | 
			
		||||
    'conflict-binary-file'               => '259a6fb',
 | 
			
		||||
    'conflict-contains-conflict-markers' => '5e0964c',
 | 
			
		||||
    'conflict-missing-side'              => 'eb227b3',
 | 
			
		||||
    'conflict-too-large'                 => '39fa04f',
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue