diff --git a/hgext/fix.py b/hgext/fix.py --- a/hgext/fix.py +++ b/hgext/fix.py @@ -45,6 +45,11 @@ tool needs to operate on unchanged files, it should set the :skipclean suboption to false. +By default, adjacent blank lines will be excluded from the generated line +ranges. This can be suboptimal, especially when lines with adjacent blanks are +deleted and, thus, aren't included. Setting :adjacentblanks will include these +lines in diffs, even in the case of delete-only diffs. + The :pattern suboption determines which files will be passed through each configured tool. See :hg:`help patterns` for possible values. However, all patterns are relative to the repo root, even if that text says they are relative @@ -173,6 +178,7 @@ b'priority': 0, b'metadata': False, b'skipclean': True, + b'adjacentblanks': False, b'enabled': True, } @@ -479,7 +485,7 @@ return files -def lineranges(opts, path, basepaths, basectxs, fixctx, content2): +def lineranges(opts, fixer, path, basepaths, basectxs, fixctx, content2): """Returns the set of line ranges that should be fixed in a file Of the form [(10, 20), (30, 40)]. @@ -494,7 +500,7 @@ if opts.get(b'whole'): # Return a range containing all lines. Rely on the diff implementation's # idea of how many lines are in the file, instead of reimplementing it. - return difflineranges(b'', content2) + return difflineranges(fixer, b'', content2) rangeslist = [] for basectx in basectxs: @@ -504,7 +510,7 @@ content1 = basectx[basepath].data() else: content1 = b'' - rangeslist.extend(difflineranges(content1, content2)) + rangeslist.extend(difflineranges(fixer, content1, content2)) return unionranges(rangeslist) @@ -558,7 +564,7 @@ return unioned -def difflineranges(content1, content2): +def difflineranges(fixer, content1, content2): """Return list of line number ranges in content2 that differ from content1. Line numbers are 1-based. The numbers are the first and last line contained @@ -569,7 +575,7 @@ >>> from mercurial import pycompat >>> lines = lambda s: b'\\n'.join([c for c in pycompat.iterbytestr(s)]) - >>> difflineranges2 = lambda a, b: difflineranges(lines(a), lines(b)) + >>> difflineranges2 = lambda a, b: difflineranges(..., lines(a), lines(b)) >>> difflineranges2(b'', b'') [] >>> difflineranges2(b'a', b'') @@ -598,10 +604,22 @@ [(2, 4)] """ ranges = [] + blanks2 = tuple(line.isspace() for line in mdiff.splitnewlines(content2)) + adjacentblanks = fixer.shouldemitadjacentblanks() for lines, kind in mdiff.allblocks(content1, content2): firstline, lastline = lines[2:4] - if kind == b'!' and firstline != lastline: - ranges.append((firstline + 1, lastline)) + # Produce a line range whenever the two sources differ and EITHER there + # is an addition/modification OR :adjacentblanks is enabled and there is an + # adjacent blank line. + if kind == b'!' and (firstline != lastline or adjacentblanks and + any(blanks2[firstline - 1: lastline + 1])): + range_ = (firstline + 1, lastline) + if adjacentblanks: + range_ = ( + range_[0] - int(any(blanks2[firstline - 1: firstline])), + range_[1] + int(any(blanks2[lastline: lastline + 1])), + ) + ranges.append(range_) return ranges @@ -675,7 +693,7 @@ for fixername, fixer in pycompat.iteritems(fixers): if fixer.affects(opts, fixctx, path): ranges = lineranges( - opts, path, basepaths, basectxs, fixctx, newdata + opts, fixer, path, basepaths, basectxs, fixctx, newdata ) command = fixer.command(ui, path, ranges) if command is None: @@ -847,6 +865,7 @@ priority = ui.configint(b'fix', name + b':priority') metadata = ui.configbool(b'fix', name + b':metadata') skipclean = ui.configbool(b'fix', name + b':skipclean') + adjacentblanks = ui.configbool(b'fix', name + b':adjacentblanks') # Don't use a fixer if it has no pattern configured. It would be # dangerous to let it affect all files. It would be pointless to let it # affect no files. There is no reasonable subset of files to use as the @@ -863,7 +882,8 @@ ui.debug(b'ignoring disabled fixer tool: %s\n' % (name,)) else: fixers[name] = Fixer( - command, pattern, linerange, priority, metadata, skipclean + command, pattern, linerange, priority, metadata, skipclean, + adjacentblanks ) return collections.OrderedDict( sorted(fixers.items(), key=lambda item: item[1]._priority, reverse=True) @@ -883,7 +903,8 @@ """Wraps the raw config values for a fixer with methods""" def __init__( - self, command, pattern, linerange, priority, metadata, skipclean + self, command, pattern, linerange, priority, metadata, skipclean, + adjacentblanks ): self._command = command self._pattern = pattern @@ -891,6 +912,7 @@ self._priority = priority self._metadata = metadata self._skipclean = skipclean + self._adjacentblanks = adjacentblanks def affects(self, opts, fixctx, path): """Should this fixer run on the file at the given path and context?""" @@ -904,6 +926,10 @@ """Should the stdout of this fixer start with JSON and a null byte?""" return self._metadata + def shouldemitadjacentblanks(self): + """Should this fixer receive line ranges of blanks adjacent to diffs?""" + return self._adjacentblanks + def command(self, ui, path, ranges): """A shell command to use to invoke this fixer on the given file/lines diff --git a/tests/test-fix.t b/tests/test-fix.t --- a/tests/test-fix.t +++ b/tests/test-fix.t @@ -156,6 +156,11 @@ If such a tool needs to operate on unchanged files, it should set the :skipclean suboption to false. + By default, adjacent blank lines will be excluded from the generated line + ranges. This can be suboptimal, especially when lines with adjacent blanks are + deleted and, thus, aren't included. Setting :adjacentblanks will include these + lines in diffs, even in the case of delete-only diffs. + The :pattern suboption determines which files will be passed through each configured tool. See 'hg help patterns' for possible values. However, all patterns are relative to the repo root, even if that text says they are @@ -1453,6 +1458,42 @@ $ cd .. +Tools should be able to include adjacent blanks in generated line ranges when +set :adjacentblanks is enabled. Notably, this generates line ranges for +delete-only diffs which are normally excluded from line ranges. + + $ hg init adjacentblanks + $ cd adjacentblanks + + $ printf "a\nb\nc\n" > neither + $ printf "a\n\t\nb\n \nc\n" > both + $ printf "a\n\nb\nc\n" > above + $ printf "a\nb \n\nc\n" > below + $ hg commit -Aqm "base" + + $ printf "a\nc\n" > neither + $ printf "a\n\n\nc\n" > both + $ printf "a\n\nc\n" > above + $ printf "a\n\nc\n" > below + + $ hg fix --working-dir neither both above below \ + > --config "fix.downwithwhitespace:command=printf \"%s\n\"" \ + > --config 'fix.downwithwhitespace:linerange="{first} through {last}"' \ + > --config 'fix.downwithwhitespace:pattern=glob:**' \ + > --config 'fix.downwithwhitespace:skipclean=false' \ + > --config 'fix.downwithwhitespace:adjacentblanks=true' + + $ cat neither + + $ cat both + 2 through 3 + $ cat above + 2 through 2 + $ cat below + 2 through 2 + + $ cd .. + Test various cases around merges. We were previously dropping files if they were created on only the p2 side of the merge, so let's test permutations of: * added, was fixed