From 74a6b77305352c9707aacd8f5a07f85222176719 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 21 Apr 2016 22:09:13 -0400 Subject: [PATCH 1/5] Convert Conflict parser to a function. --- lib/conflict.coffee | 234 +++++++++++++++----------------------------- 1 file changed, 79 insertions(+), 155 deletions(-) diff --git a/lib/conflict.coffee b/lib/conflict.coffee index 4f3ba23..668599b 100644 --- a/lib/conflict.coffee +++ b/lib/conflict.coffee @@ -5,159 +5,97 @@ _ = require 'underscore-plus' {Side, OurSide, TheirSide, BaseSide} = require './side' {Navigator} = require './navigator' -CONFLICT_REGEX = /// - ^<{7}\ (.+)\r?\n([^]*?) - # Base side may contain nested conflict markers. - # Refer: http://stackoverflow.com/questions/16990657/git-merge-diff3-style-need-explanation - (?:\|{7}\ (.+)\r?\n((?:(?:<{7}[^]*?>{7})|[^])*?))? - ={7}\r?\n([^]*?) - >{7}\ (.+)(?:\r?\n)? - ///mg - -INVALID = null +CONFLICT_START_REGEX = /^<{7} (.+)\r?\n/g + +# Side positions TOP = 'top' BASE = 'base' -MIDDLE = 'middle' BOTTOM = 'bottom' +# Common options used to construct markers. +options = + persistent: false + invalidate: 'never' + # Private: ConflictParser discovers git conflict markers in a corpus of text and constructs Conflict # instances that mark the correct lines. # -class ConflictParser +parseConflict = (state, editor, row) -> + previousSide = null - # Common options used to construct markers. - options = - persistent: false - invalidate: 'never' + d = (x) -> console.log "#{x} - #{editor.lineTextForBufferRow row}" - # Private: Initialize a parser to operate on a specific TextEditor. - # - # state [MergeState] - Repository-wide conflict resolution state. - # editor [TextEditor] - An editor containing text that may include one or more conflicts. - # - constructor: (@state, @editor) -> - @position = INVALID + # Mark and construct a Side that begins with a banner and description as its first line. + markHeaderSide = (position, sideKlass) -> + sideRowStart = row + description = sideDescription() + advanceToBoundary() + sideRowEnd = row - # Private: Begin handling the result of a CONFLICT_REGEX match. - # - # m [Array] - The match object returned from CONFLICT_REGEX. - # - start: (@m) -> - @startRow = @m.range.start.row - @endRow = @m.range.end.row + bannerMarker = editor.markBufferRange([[sideRowStart, 0], [sideRowStart + 1, 0]], options) + previousSide.followingMarker = bannerMarker if previousSide? - @chunks = @m.match - @chunks.shift() + textRange = [[sideRowStart + 1, 0], [sideRowEnd, 0]] + textMarker = editor.markBufferRange(textRange, options) + text = editor.getTextInBufferRange(textRange) - @currentRow = @startRow - @position = TOP - @previousSide = null + previousSide = new sideKlass(text, description, textMarker, bannerMarker, position) - # Private: Complete handling of an individual CONFLICT_REGEX match. - # - finish: -> - @previousSide.followingMarker = @previousSide.refBannerMarker + # Mark and construct a Side with a banner and description as its last line. + markFooterSide = (position, sideKlass) -> + sideRowStart = row + advanceToBoundary() + description = sideDescription() + row += 1 # Advance past the boundary line. + sideRowEnd = row - # Private: Mark the current lines as "ours". - # - # Returns [Side] marking the current conflict's side. - # - markOurs: -> @_markHunk OurSide + textRange = [[sideRowStart, 0], [sideRowEnd - 1, 0]] + textMarker = editor.markBufferRange(textRange, options) + text = editor.getTextInBufferRange(textRange) - # Private: Mark the current lines as "base". - # - # Returns [Side] marking the base of conflict, or null if no base conflict marker is found. - # - markBase: -> @_markHunk BaseSide + bannerMarker = editor.markBufferRange([[sideRowEnd - 1, 0], [sideRowEnd, 0]], options) + previousSide.followingMarker = bannerMarker if previousSide? - # Private: Mark the current lines as a separator. - # - # Returns [Navigator] containing a marker to the separator line. - # - markSeparator: -> - unless @position is MIDDLE - throw new Error("Unexpected position for separator: #{@position}") - @position = BOTTOM + previousSide = new sideKlass(text, description, textMarker, bannerMarker, position) + previousSide.followingMarker = bannerMarker + previousSide - sepRowStart = @currentRow - sepRowEnd = @_advance 1 + maybeMarkBase = -> if isAtSeparator() then null else markHeaderSide(BASE, BaseSide) - marker = @editor.markBufferRange( - [[sepRowStart, 0], [sepRowEnd, 0]], @options - ) + markSeparator = -> + sepRowStart = row + row += 1 + sepRowEnd = row - # @previousSide should always be populated because @position is MIDDLE. - @previousSide.followingMarker = marker + marker = editor.markBufferRange([[sepRowStart, 0], [sepRowEnd, 0]], options) + previousSide.followingMarker = marker + previousSide = new Navigator(marker) - new Navigator(marker) + sideDescription = -> editor.lineTextForBufferRow(row).match(/^[<|>]{7} (.*)$/)[1] - # Private: Mark the current lines as "theirs". - # - # Returns [Side] marking the current conflict's side. - # - markTheirs: -> @_markHunk TheirSide + isAtBoundary = -> /^[<|=>]{7}/.test editor.lineTextForBufferRow(row) - # Private: Mark the current lines and construct a Side of the appropriate class. - # - # sideKlass [Class] Side subclass to construct. - # returns [sideKlass] marking the current lines. - # - _markHunk: (sideKlass) -> - sidePosition = @position - switch @position - when TOP - ref = @chunks.shift() - text = @chunks.shift() - lines = text.split /\n/ - - bannerRowStart = @currentRow - bannerRowEnd = rowStart = @_advance 1 - rowEnd = @_advance lines.length - 1 - - @position = BASE - when BASE - @position = MIDDLE - - ref = @chunks.shift() - text = @chunks.shift() - # base is optional - return null unless text - lines = text.split /\n/ - - bannerRowStart = @currentRow - bannerRowEnd = rowStart = @_advance 1 - rowEnd = @_advance lines.length - 1 - when BOTTOM - text = @chunks.shift() - ref = @chunks.shift() - lines = text.split /\n/ - - rowStart = @currentRow - bannerRowStart = rowEnd = @_advance lines.length - 1 - bannerRowEnd = @_advance 1 - - @position = INVALID - else - throw new Error("Unexpected position for side: #{@position}") - - bannerMarker = @editor.markBufferRange( - [[bannerRowStart, 0], [bannerRowEnd, 0]], @options - ) - marker = @editor.markBufferRange( - [[rowStart, 0], [rowEnd, 0]], @options - ) - - @previousSide.followingMarker = bannerMarker if sidePosition is BASE - - side = new sideKlass(text, ref, marker, bannerMarker, sidePosition) - @previousSide = side - side - - # Private: Advance the row counter. - # - # rowCount [Integer] The number of rows to advance. - # - _advance: (rowCount) -> @currentRow += rowCount + isAtSeparator = -> /^={7}$/.test editor.lineTextForBufferRow(row) + + advanceToBoundary = -> + row += 1 + until isAtBoundary() + row += 1 + + if state.isRebase + theirs = markHeaderSide(TOP, TheirSide) + base = maybeMarkBase() + nav = markSeparator() + ours = markFooterSide(BOTTOM, OurSide) + else + ours = markHeaderSide(TOP, OurSide) + base = maybeMarkBase() + nav = markSeparator() + theirs = markFooterSide(BOTTOM, TheirSide) + + conflict = new Conflict(ours, theirs, base, nav, state) + + return { conflict: conflict, endRow: row } # Public: Model an individual conflict parsed from git's automatic conflict resolution output. # @@ -231,33 +169,19 @@ class Conflict # return [Array] A (possibly empty) collection of parsed Conflicts. # @all: (state, editor) -> - results = [] - previous = null - marker = new ConflictParser(state, editor) - - editor.getBuffer().scan CONFLICT_REGEX, (m) -> - marker.start m - - if state.isRebase - theirs = marker.markTheirs() - base = marker.markBase() - nav = marker.markSeparator() - ours = marker.markOurs() - else - ours = marker.markOurs() - base = marker.markBase() - nav = marker.markSeparator() - theirs = marker.markTheirs() - - marker.finish() + conflicts = [] + lastRow = -1 - c = new Conflict(ours, theirs, base, nav, state) - results.push c + editor.getBuffer().scan CONFLICT_START_REGEX, (m) -> + conflictStartRow = m.range.start.row + return if conflictStartRow < lastRow - nav.linkToPrevious previous - previous = c + result = parseConflict state, editor, conflictStartRow + result.conflict.navigator.linkToPrevious conflicts[conflicts.length - 1] if conflicts.length > 0 + conflicts.push result.conflict + lastRow = result.endRow - results + conflicts module.exports = Conflict: Conflict From dc211400cf418055fd76208e4e389c0f6970f501 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 22 Apr 2016 22:16:56 -0400 Subject: [PATCH 2/5] Handle recursive diff3 output. --- lib/conflict.coffee | 65 ++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/lib/conflict.coffee b/lib/conflict.coffee index 668599b..8cdc76d 100644 --- a/lib/conflict.coffee +++ b/lib/conflict.coffee @@ -23,15 +23,34 @@ options = parseConflict = (state, editor, row) -> previousSide = null - d = (x) -> console.log "#{x} - #{editor.lineTextForBufferRow row}" - # Mark and construct a Side that begins with a banner and description as its first line. markHeaderSide = (position, sideKlass) -> sideRowStart = row description = sideDescription() - advanceToBoundary() + row += 1 + advanceToBoundary('|=') sideRowEnd = row + createHeaderSide(sideRowStart, sideRowEnd, description, sideKlass, position) + + maybeMarkBase = -> + return null if isAtBoundary('=') + + sideRowStart = row + description = sideDescription() + row += 1 + + b = advanceToBoundary('<=') + until b is '=' + # Embedded recursive conflict within the base. Advance beyond it. + row = parseConflict(state, editor, row).endRow + b = advanceToBoundary('<=') + + sideRowEnd = row + + createHeaderSide(sideRowStart, sideRowEnd, description, BaseSide, BASE) + + createHeaderSide = (sideRowStart, sideRowEnd, description, sideKlass, position) -> bannerMarker = editor.markBufferRange([[sideRowStart, 0], [sideRowStart + 1, 0]], options) previousSide.followingMarker = bannerMarker if previousSide? @@ -41,12 +60,21 @@ parseConflict = (state, editor, row) -> previousSide = new sideKlass(text, description, textMarker, bannerMarker, position) + markSeparator = -> + sepRowStart = row + row += 1 + sepRowEnd = row + + marker = editor.markBufferRange([[sepRowStart, 0], [sepRowEnd, 0]], options) + previousSide.followingMarker = marker + previousSide = new Navigator(marker) + # Mark and construct a Side with a banner and description as its last line. markFooterSide = (position, sideKlass) -> sideRowStart = row - advanceToBoundary() + advanceToBoundary('>') description = sideDescription() - row += 1 # Advance past the boundary line. + row += 1 sideRowEnd = row textRange = [[sideRowStart, 0], [sideRowEnd - 1, 0]] @@ -60,27 +88,20 @@ parseConflict = (state, editor, row) -> previousSide.followingMarker = bannerMarker previousSide - maybeMarkBase = -> if isAtSeparator() then null else markHeaderSide(BASE, BaseSide) - - markSeparator = -> - sepRowStart = row - row += 1 - sepRowEnd = row - - marker = editor.markBufferRange([[sepRowStart, 0], [sepRowEnd, 0]], options) - previousSide.followingMarker = marker - previousSide = new Navigator(marker) - sideDescription = -> editor.lineTextForBufferRow(row).match(/^[<|>]{7} (.*)$/)[1] - isAtBoundary = -> /^[<|=>]{7}/.test editor.lineTextForBufferRow(row) + isAtBoundary = (boundaryKinds = '<|=>') -> + line = editor.lineTextForBufferRow(row) + for b in boundaryKinds + return b if line.startsWith(b.repeat(7)) + null - isAtSeparator = -> /^={7}$/.test editor.lineTextForBufferRow(row) - - advanceToBoundary = -> - row += 1 - until isAtBoundary() + advanceToBoundary = (boundaryKinds = '<|=>') -> + b = isAtBoundary(boundaryKinds) + until b row += 1 + b = isAtBoundary(boundaryKinds) + b if state.isRebase theirs = markHeaderSide(TOP, TheirSide) From 021910a3a7e0d2b572ac0258cf7dc1253ff52963 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sat, 23 Apr 2016 21:57:18 -0400 Subject: [PATCH 3/5] Rewrite Conflict in ES2015 --- lib/conflict.coffee | 208 ------------------------- lib/conflict.js | 371 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 371 insertions(+), 208 deletions(-) delete mode 100644 lib/conflict.coffee create mode 100644 lib/conflict.js diff --git a/lib/conflict.coffee b/lib/conflict.coffee deleted file mode 100644 index 8cdc76d..0000000 --- a/lib/conflict.coffee +++ /dev/null @@ -1,208 +0,0 @@ -{$} = require 'space-pen' -{Emitter} = require 'atom' -_ = require 'underscore-plus' - -{Side, OurSide, TheirSide, BaseSide} = require './side' -{Navigator} = require './navigator' - -CONFLICT_START_REGEX = /^<{7} (.+)\r?\n/g - -# Side positions -TOP = 'top' -BASE = 'base' -BOTTOM = 'bottom' - -# Common options used to construct markers. -options = - persistent: false - invalidate: 'never' - -# Private: ConflictParser discovers git conflict markers in a corpus of text and constructs Conflict -# instances that mark the correct lines. -# -parseConflict = (state, editor, row) -> - previousSide = null - - # Mark and construct a Side that begins with a banner and description as its first line. - markHeaderSide = (position, sideKlass) -> - sideRowStart = row - description = sideDescription() - row += 1 - advanceToBoundary('|=') - sideRowEnd = row - - createHeaderSide(sideRowStart, sideRowEnd, description, sideKlass, position) - - maybeMarkBase = -> - return null if isAtBoundary('=') - - sideRowStart = row - description = sideDescription() - row += 1 - - b = advanceToBoundary('<=') - until b is '=' - # Embedded recursive conflict within the base. Advance beyond it. - row = parseConflict(state, editor, row).endRow - b = advanceToBoundary('<=') - - sideRowEnd = row - - createHeaderSide(sideRowStart, sideRowEnd, description, BaseSide, BASE) - - createHeaderSide = (sideRowStart, sideRowEnd, description, sideKlass, position) -> - bannerMarker = editor.markBufferRange([[sideRowStart, 0], [sideRowStart + 1, 0]], options) - previousSide.followingMarker = bannerMarker if previousSide? - - textRange = [[sideRowStart + 1, 0], [sideRowEnd, 0]] - textMarker = editor.markBufferRange(textRange, options) - text = editor.getTextInBufferRange(textRange) - - previousSide = new sideKlass(text, description, textMarker, bannerMarker, position) - - markSeparator = -> - sepRowStart = row - row += 1 - sepRowEnd = row - - marker = editor.markBufferRange([[sepRowStart, 0], [sepRowEnd, 0]], options) - previousSide.followingMarker = marker - previousSide = new Navigator(marker) - - # Mark and construct a Side with a banner and description as its last line. - markFooterSide = (position, sideKlass) -> - sideRowStart = row - advanceToBoundary('>') - description = sideDescription() - row += 1 - sideRowEnd = row - - textRange = [[sideRowStart, 0], [sideRowEnd - 1, 0]] - textMarker = editor.markBufferRange(textRange, options) - text = editor.getTextInBufferRange(textRange) - - bannerMarker = editor.markBufferRange([[sideRowEnd - 1, 0], [sideRowEnd, 0]], options) - previousSide.followingMarker = bannerMarker if previousSide? - - previousSide = new sideKlass(text, description, textMarker, bannerMarker, position) - previousSide.followingMarker = bannerMarker - previousSide - - sideDescription = -> editor.lineTextForBufferRow(row).match(/^[<|>]{7} (.*)$/)[1] - - isAtBoundary = (boundaryKinds = '<|=>') -> - line = editor.lineTextForBufferRow(row) - for b in boundaryKinds - return b if line.startsWith(b.repeat(7)) - null - - advanceToBoundary = (boundaryKinds = '<|=>') -> - b = isAtBoundary(boundaryKinds) - until b - row += 1 - b = isAtBoundary(boundaryKinds) - b - - if state.isRebase - theirs = markHeaderSide(TOP, TheirSide) - base = maybeMarkBase() - nav = markSeparator() - ours = markFooterSide(BOTTOM, OurSide) - else - ours = markHeaderSide(TOP, OurSide) - base = maybeMarkBase() - nav = markSeparator() - theirs = markFooterSide(BOTTOM, TheirSide) - - conflict = new Conflict(ours, theirs, base, nav, state) - - return { conflict: conflict, endRow: row } - -# Public: Model an individual conflict parsed from git's automatic conflict resolution output. -# -class Conflict - - # Private: Initialize a new Conflict with its constituent Sides, Navigator, and the MergeState - # it belongs to. - # - # ours [Side] the lines of this conflict that the current user contributed (by our best guess). - # theirs [Side] the lines of this conflict that another contributor created. - # base [Side] the lines of merge base of this conflict. Optional. - # navigator [Navigator] maintains references to surrounding Conflicts in the original file. - # state [MergeState] repository-wide information about the current merge. - # - constructor: (@ours, @theirs, @base, @navigator, @state) -> - @emitter = new Emitter - - @ours.conflict = this - @theirs.conflict = this - @base?.conflict = this - @navigator.conflict = this - @resolution = null - - # Public: Has this conflict been resolved in any way? - # - # Return [Boolean] - # - isResolved: -> @resolution? - - # Public: Attach an event handler to be notified when this conflict is resolved. - # - # callback [Function] - # - onDidResolveConflict: (callback) -> - @emitter.on 'resolve-conflict', callback - - # Public: Specify which Side is to be kept. Note that either side may have been modified by the - # user prior to resolution. Notify any subscribers. - # - # side [Side] our changes or their changes. - # - resolveAs: (side) -> - @resolution = side - @emitter.emit 'resolve-conflict' - - # Public: Locate the position that the editor should scroll to in order to make this conflict - # visible. - # - # Return [Point] buffer coordinates - # - scrollTarget: -> @ours.marker.getTailBufferPosition() - - # Public: Audit all Marker instances owned by subobjects within this Conflict. - # - # Return [Array] - # - markers: -> - _.flatten [@ours.markers(), @theirs.markers(), @base?.markers() ? [], @navigator.markers()], true - - # Public: Console-friendly identification of this conflict. - # - # Return [String] that distinguishes this conflict from others. - # - toString: -> "[conflict: #{@ours} #{@theirs}]" - - # Public: Parse any conflict markers in a TextEditor's buffer and return a Conflict that contains - # markers corresponding to each. - # - # state [MergeState] Repository-wide state of the merge. - # editor [TextEditor] The editor to search. - # return [Array] A (possibly empty) collection of parsed Conflicts. - # - @all: (state, editor) -> - conflicts = [] - lastRow = -1 - - editor.getBuffer().scan CONFLICT_START_REGEX, (m) -> - conflictStartRow = m.range.start.row - return if conflictStartRow < lastRow - - result = parseConflict state, editor, conflictStartRow - result.conflict.navigator.linkToPrevious conflicts[conflicts.length - 1] if conflicts.length > 0 - conflicts.push result.conflict - lastRow = result.endRow - - conflicts - -module.exports = - Conflict: Conflict diff --git a/lib/conflict.js b/lib/conflict.js new file mode 100644 index 0000000..9895450 --- /dev/null +++ b/lib/conflict.js @@ -0,0 +1,371 @@ +'use babel' + +import {Emitter} from 'atom' +import _ from 'underscore-plus' + +import {Side, OurSide, TheirSide, BaseSide} from './side' +import {Navigator} from './navigator' + +// Public: Model an individual conflict parsed from git's automatic conflict resolution output. +export class Conflict { + + /* + * Private: Initialize a new Conflict with its constituent Sides, Navigator, and the MergeState + * it belongs to. + * + * ours [Side] the lines of this conflict that the current user contributed (by our best guess). + * theirs [Side] the lines of this conflict that another contributor created. + * base [Side] the lines of merge base of this conflict. Optional. + * navigator [Navigator] maintains references to surrounding Conflicts in the original file. + * state [MergeState] repository-wide information about the current merge. + */ + constructor (ours, theirs, base, navigator, merge) { + this.ours = ours + this.theirs = theirs + this.base = base + this.navigator = navigator + this.merge = merge + + this.emitter = new Emitter() + + // Populate back-references + this.ours.conflict = this + this.theirs.conflict = this + if (this.base) { + this.base.conflict = this + } + this.navigator.conflict = this + + // Begin unresolved + this.resolution = null + } + + /* + * Public: Has this conflict been resolved in any way? + * + * Return [Boolean] + */ + isResolved() { + return this.resolution !== null + } + + /* + * Public: Attach an event handler to be notified when this conflict is resolved. + * + * callback [Function] + */ + onDidResolveConflict (callback) { + return this.emitter.on('resolve-conflict', callback) + } + + /* + * Public: Specify which Side is to be kept. Note that either side may have been modified by the + * user prior to resolution. Notify any subscribers. + * + * side [Side] our changes or their changes. + */ + resolveAs (side) { + this.resolution = side + this.emitter.emit('resolve-conflict') + } + + /* + * Public: Locate the position that the editor should scroll to in order to make this conflict + * visible. + * + * Return [Point] buffer coordinates + */ + scrollTarget () { + return this.ours.marker.getTailBufferPosition() + } + + /* + * Public: Audit all Marker instances owned by subobjects within this Conflict. + * + * Return [Array] + */ + markers () { + const ms = [this.ours.markers(), this.theirs.markers(), this.navigator.markers()] + if (this.baseSide) { + ms.push(this.baseSide.markers()) + } + return _.flatten(ms, true) + } + + /* + * Public: Console-friendly identification of this conflict. + * + * Return [String] that distinguishes this conflict from others. + */ + toString () { + return `[conflict: ${this.ours} ${this.theirs}]` + } + + /* + * Public: Parse any conflict markers in a TextEditor's buffer and return a Conflict that contains + * markers corresponding to each. + * + * merge [MergeState] Repository-wide state of the merge. + * editor [TextEditor] The editor to search. + * return [Array] A (possibly empty) collection of parsed Conflicts. + */ + static all (merge, editor) { + const conflicts = [] + let lastRow = -1 + + editor.getBuffer().scan(CONFLICT_START_REGEX, (m) => { + conflictStartRow = m.range.start.row + if (conflictStartRow < lastRow) { + // Match within an already-parsed conflict. + return + } + + const visitor = new ConflictVisitor(merge, editor) + lastRow = parseConflict(merge, editor, conflictStartRow, visitor) + const conflict = visitor.conflict() + + if (conflicts.length > 0) { + conflict.navigator.linkToPrevious(conflicts[conflicts.length - 1]) + } + + conflicts.push(conflict) + }) + + return conflicts + } +} + +// Regular expression that matches the beginning of a potential conflict. +const CONFLICT_START_REGEX = /^<{7} (.+)\r?\n/g + +// Side positions. +const TOP = 'top' +const BASE = 'base' +const BOTTOM = 'bottom' + +// Options used to initialize markers. +const options = { + persistent: false, + invalidate: 'never' +} + +/* + * Private: conflict parser visitor that ignores all events. + */ +class NoopVisitor { + + visitOurSide (position, bannerRow, textRowStart, textRowEnd) { } + + visitBaseSide (position, bannerRow, textRowStart, textRowEnd) { } + + visitSeparator (sepRowStart, sepRowEnd) { } + + visitTheirSide (position, bannerRow, textRowStart, textRowEnd) { } + +} + +/* + * Private: conflict parser visitor that marks each buffer range and assembles a Conflict from the + * pieces. + */ +class ConflictVisitor { + + /* + * merge - [MergeState] passed to each instantiated Side. + * editor - [TextEditor] displaying the conflicting text. + */ + constructor (merge, editor) { + this.merge = merge + this.editor = editor + this.previousSide = null + + this.ourSide = null + this.baseSide = null + this.navigator = null + } + + /* + * position - [String] one of TOP or BOTTOM. + * bannerRow - [Integer] of the buffer row that contains our side's banner. + * textRowStart - [Integer] of the first buffer row that contain this side's text. + * textRowEnd - [Integer] of the first buffer row beyond the extend of this side's text. + */ + visitOurSide (position, bannerRow, textRowStart, textRowEnd) { + this.ourSide = this.markSide(position, OurSide, bannerRow, textRowStart, textRowEnd) + } + + /* + * bannerRow - [Integer] the buffer row that contains our side's banner. + * textRowStart - [Integer] first buffer row that contain this side's text. + * textRowEnd - [Integer] first buffer row beyond the extend of this side's text. + */ + visitBaseSide (bannerRow, textRowStart, textRowEnd) { + this.baseSide = this.markSide(BASE, BaseSide, bannerRow, textRowStart, textRowEnd) + } + + /* + * sepRowStart - [Integer] buffer row that contains the "=======" separator. + * sepRowEnd - [Integer] the buffer row after the separator. + */ + visitSeparator (sepRowStart, sepRowEnd) { + const marker = this.editor.markBufferRange([[sepRowStart, 0], [sepRowEnd, 0]], options) + this.previousSide.followingMarker = marker + + this.navigator = new Navigator(marker) + this.previousSide = this.navigator + } + + /* + * position - [String] Always BASE; accepted for consistency. + * bannerRow - [Integer] the buffer row that contains our side's banner. + * textRowStart - [Integer] first buffer row that contain this side's text. + * textRowEnd - [Integer] first buffer row beyond the extend of this side's text. + */ + visitTheirSide (position, bannerRow, textRowStart, textRowEnd) { + this.theirSide = this.markSide(position, TheirSide, bannerRow, textRowStart, textRowEnd) + } + + markSide (position, sideKlass, bannerRow, textRowStart, textRowEnd) { + const description = this.sideDescription(bannerRow) + + const bannerMarker = this.editor.markBufferRange([[bannerRow, 0], [bannerRow + 1, 0]], options) + + if (this.previousSide) { + this.previousSide.followingMarker = bannerMarker + } + + const textRange = [[textRowStart, 0], [textRowEnd, 0]] + const textMarker = this.editor.markBufferRange(textRange, options) + const text = this.editor.getTextInBufferRange(textRange) + + const side = new sideKlass(text, description, textMarker, bannerMarker, position) + this.previousSide = side + return side + } + + /* + * Parse the banner description for the current side from a banner row. + */ + sideDescription (bannerRow) { + return this.editor.lineTextForBufferRow(bannerRow).match(/^[<|>]{7} (.*)$/)[1] + } + + conflict () { + this.previousSide.followingMarker = this.previousSide.refBannerMarker + + return new Conflict(this.ourSide, this.theirSide, this.baseSide, this.navigator, this.merge) + } + +} + +/* + * Private: parseConflict discovers git conflict markers in a corpus of text and constructs Conflict + * instances that mark the correct lines. + * + * Returns [Integer] the buffer row after the final <<<<<< boundary. + */ +const parseConflict = function (merge, editor, row, visitor) { + let lastBoundary = null + + // Visit a side that begins with a banner and description as its first line. + const visitHeaderSide = (position, visitMethod) => { + const sideRowStart = row + row += 1 + advanceToBoundary('|=') + const sideRowEnd = row + + visitor[visitMethod](position, sideRowStart, sideRowStart + 1, sideRowEnd) + } + + // Visit the base side from diff3 output, if one is present, then visit the separator. + const visitBaseAndSeparator = () => { + if (lastBoundary === '|') { + visitBaseSide() + } + + visitSeparator() + } + + // Visit a base side from diff3 output. + const visitBaseSide = () => { + const sideRowStart = row + row += 1 + + let b = advanceToBoundary('<=') + while (b === '<') { + // Embedded recursive conflict within a base side, caused by a criss-cross merge. + // Advance beyond it without marking anything. + row = parseConflict(merge, editor, row, new NoopVisitor()) + b = advanceToBoundary('<=') + } + + const sideRowEnd = row + + visitor.visitBaseSide(sideRowStart, sideRowStart + 1, sideRowEnd) + } + + // Visit a "========" separator. + const visitSeparator = () => { + const sepRowStart = row + row += 1 + const sepRowEnd = row + + visitor.visitSeparator(sepRowStart, sepRowEnd) + } + + // Vidie a side with a banner and description as its last line. + const visitFooterSide = (position, visitMethod) => { + const sideRowStart = row + const b = advanceToBoundary('>') + row += 1 + sideRowEnd = row + + visitor[visitMethod](position, sideRowEnd - 1, sideRowStart, sideRowEnd - 1) + } + + // Determine if the current row is a side boundary. + // + // boundaryKinds - [String] any combination of <, |, =, or > to limit the kinds of boundary + // detected. + // + // Returns the matching boundaryKinds character, or `null` if none match. + const isAtBoundary = (boundaryKinds = '<|=>') => { + const line = editor.lineTextForBufferRow(row) + for (b of boundaryKinds) { + if (line.startsWith(b.repeat(7))) { + return b + } + } + return null + } + + // Increment the current row until the current line matches one of the provided boundary kinds, + // or until there are no more lines in the editor. + // + // boundaryKinds - [String] any combination of <, |, =, or > to limit the kinds of boundaries + // that halt the progression. + // + // Returns the matching boundaryKinds character, or 'null' if there are no matches to the end of + // the editor. + const advanceToBoundary = (boundaryKinds = '<|=>') => { + let b = isAtBoundary(boundaryKinds) + while (b === null && row < editor.getLineCount()) { + row += 1 + b = isAtBoundary(boundaryKinds) + } + lastBoundary = b + return b + } + + if (!merge.isRebase) { + visitHeaderSide(TOP, 'visitOurSide') + visitBaseAndSeparator() + visitFooterSide(BOTTOM, 'visitTheirSide') + } else { + visitHeaderSide(TOP, 'visitTheirSide') + visitBaseAndSeparator() + visitFooterSide(BOTTOM, 'visitOurSide') + } + + return row +} From b61a2ceef67ecbed9b06752bacceb9b0112c2f64 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 24 Apr 2016 20:02:32 -0400 Subject: [PATCH 4/5] Fail gracefully for unterminated conflicts. --- lib/conflict.js | 28 ++++++++++++++++++++------- spec/conflict-spec.coffee | 16 +++++++++++++++ spec/fixtures/corrupted-2way-diff.txt | 6 ++++++ spec/fixtures/corrupted-3way-diff.txt | 21 ++++++++++++++++++++ 4 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 spec/fixtures/corrupted-2way-diff.txt create mode 100644 spec/fixtures/corrupted-3way-diff.txt diff --git a/lib/conflict.js b/lib/conflict.js index 9895450..795f176 100644 --- a/lib/conflict.js +++ b/lib/conflict.js @@ -121,14 +121,22 @@ export class Conflict { } const visitor = new ConflictVisitor(merge, editor) - lastRow = parseConflict(merge, editor, conflictStartRow, visitor) - const conflict = visitor.conflict() - if (conflicts.length > 0) { - conflict.navigator.linkToPrevious(conflicts[conflicts.length - 1]) - } + try { + lastRow = parseConflict(merge, editor, conflictStartRow, visitor) + const conflict = visitor.conflict() + + if (conflicts.length > 0) { + conflict.navigator.linkToPrevious(conflicts[conflicts.length - 1]) + } + conflicts.push(conflict) + } catch (e) { + if (!e.parserState) throw e - conflicts.push(conflict) + if (!atom.inSpecMode()) { + console.error(`Unable to parse conflict: ${e.message}\n${e.stack}`) + } + } }) return conflicts @@ -349,10 +357,16 @@ const parseConflict = function (merge, editor, row, visitor) { // the editor. const advanceToBoundary = (boundaryKinds = '<|=>') => { let b = isAtBoundary(boundaryKinds) - while (b === null && row < editor.getLineCount()) { + while (b === null) { row += 1 + if (row > editor.getLastBufferRow()) { + const e = new Error('Unterminated conflict side') + e.parserState = true + throw e + } b = isAtBoundary(boundaryKinds) } + lastBoundary = b return b } diff --git a/spec/conflict-spec.coffee b/spec/conflict-spec.coffee index 0c719eb..9213ccc 100644 --- a/spec/conflict-spec.coffee +++ b/spec/conflict-spec.coffee @@ -87,6 +87,22 @@ describe "Conflict", -> expect(util.rowRangeFrom cs[1].ours.marker).toEqual([14, 15]) expect(util.rowRangeFrom cs[1].theirs.marker).toEqual([16, 17]) + describe 'with corrupted diffs', -> + + it 'handles corrupted diff output', -> + util.openPath 'corrupted-2way-diff.txt', (editorView) -> + cs = Conflict.all({}, editorView.getModel()) + expect(cs.length).toBe(0) + + it 'handles corrupted diff3 output', -> + util.openPath 'corrupted-3way-diff.txt', (editorView) -> + cs = Conflict.all({}, editorView.getModel()) + + expect(cs.length).toBe(1) + expect(util.rowRangeFrom cs[0].ours.marker).toEqual([13, 14]) + expect(util.rowRangeFrom cs[0].base.marker).toEqual([15, 16]) + expect(util.rowRangeFrom cs[0].theirs.marker).toEqual([17, 18]) + describe 'when rebasing', -> [conflict] = [] diff --git a/spec/fixtures/corrupted-2way-diff.txt b/spec/fixtures/corrupted-2way-diff.txt new file mode 100644 index 0000000..bbd1682 --- /dev/null +++ b/spec/fixtures/corrupted-2way-diff.txt @@ -0,0 +1,6 @@ +<<<<<<< HEAD +These are my changes +======= +These are your changes + +Oops, deleted the end marker. diff --git a/spec/fixtures/corrupted-3way-diff.txt b/spec/fixtures/corrupted-3way-diff.txt new file mode 100644 index 0000000..c257546 --- /dev/null +++ b/spec/fixtures/corrupted-3way-diff.txt @@ -0,0 +1,21 @@ +This is a file containing a corrupted diff3 patch. + +<<<<<<< HEAD +These are my changes +||||||| merged common ancestors +These are original texts +Oops, we never get a separator +These are your changes +>>>>>>> master + +It also contains a valid one. + +<<<<<<< HEAD +These are my changes +||||||| merged common ancestors +These are original texts +======= +These are your changes +>>>>>>> master + +And some text after the end. From ae70931cc8f5824b355dcf1fbde3f20b255c4132 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sun, 24 Apr 2016 20:05:49 -0400 Subject: [PATCH 5/5] CHANGELOG entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfc5abe..7e1bec2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.4.1 + +- Rewrite the Conflict parser as a proper recursive descent parser. [#229](https://github.com/smashwilson/merge-conflicts/pull/229) + ## 1.4.0 - Handle three-way merge markers. [#219](https://github.com/smashwilson/merge-conflicts/pull/219)