[BUG] svn merge silently misplaces a hunk with mixed CRLF/LF EOLs (reproduces in 1.15.0-rc3)

_玖 <[email protected]>
Newsgroups gmane.comp.version-control.subversion.user
Message-ID <CAGjirShNtzC6S8sxaocc61mB=Zue7WvJy9w-dQHMZ1H9SfExZA@mail.gmail.com>
Hello,

I found a reproducible svn merge issue which may be related to SVN-3373:
https://issues.apache.org/jira/browse/SVN-3373

When the merge target file contains a mixed CRLF/LF function block and has no
svn:eol-style property, svn merge silently inserts a one-line change after the
function's closing "end" statement instead of inside the function. No conflict
or warning is produced.

I reproduced this with command-line clients on Windows 10 x64:
- Subversion 1.14.5 (r1922182)
- locally built Subversion 1.15.0-rc3

The attached PowerShell script is named repro.ps1.txt solely to avoid Gmail
attachment restrictions. Please rename it to repro.ps1 before running it.

The script creates a new local file:// repository and reproduces the problem
without using project source code, private URLs, a GUI client, an external
merge tool, a server hook, or an existing working copy.

Expected behavior: apply the line inside the function, or report a conflict.
Actual behavior: the line is silently placed after the function's "end"
statement.

The same fixture succeeds when the target file is normalized to CRLF before
the merge (run the script with -NormalizeBranchEol).

Could this be considered a mixed-EOL variant of SVN-3373? If so, I would be
happy to add the reproduction to that issue.

Regards,
Rinne Ohara
repro.ps1.txt (text/plain, 6 KB)
<#
Creates an isolated local SVN repository and reproduces a wrong-context merge.
No network access, project files, or existing SVN working copies are used.
#>
[CmdletBinding()]
param(
    [string]$WorkRoot = (Join-Path $env:TEMP ("svn-mixed-eol-merge-repro-" + [guid]::NewGuid().ToString("N"))),
    [switch]$NormalizeBranchEol,
    [string]$SvnExecutable = "svn",
    [string]$SvnAdminExecutable = "svnadmin"
)

$ErrorActionPreference = "Stop";

function Invoke-Svn {
    param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Arguments)

    & $script:szSvnCommand @Arguments;
    if ($LASTEXITCODE -ne 0) {
        throw "svn command failed with exit code ${LASTEXITCODE}: svn $($Arguments -join ' ')";
    }
}

function Write-Utf8NoBom {
    param(
        [Parameter(Mandatory = $true)][string]$Path,
        [Parameter(Mandatory = $true)][string]$Text
    )

    [System.IO.File]::WriteAllText($Path, $Text, [System.Text.UTF8Encoding]::new($false));
}

function Get-IndexOfLine {
    param(
        [Parameter(Mandatory = $true)][AllowEmptyString()][string[]]$Lines,
        [Parameter(Mandatory = $true)][string]$Text,
        [int]$StartIndex = 0
    )

    for ($nIndex = $StartIndex; $nIndex -lt $Lines.Length; $nIndex++) {
        if ($Lines[$nIndex] -eq $Text) {
            return $nIndex;
        }
    }

    return -1;
}

try {
    $script:szSvnCommand = (Get-Command $SvnExecutable -CommandType Application -ErrorAction Stop).Source;
    $script:szSvnAdminCommand = (Get-Command $SvnAdminExecutable -CommandType Application -ErrorAction Stop).Source;
}
catch {
    throw "Could not resolve svn or svnadmin. Pass -SvnExecutable and -SvnAdminExecutable if they are not on PATH.";
}

if (Test-Path -LiteralPath $WorkRoot) {
    throw "Refusing to overwrite existing path: $WorkRoot";
}

$null = New-Item -ItemType Directory -Path $WorkRoot;
$szRepoPath = Join-Path $WorkRoot "repo";
$szTrunkWc = Join-Path $WorkRoot "wc-trunk";
$szBranchWc = Join-Path $WorkRoot "wc-branch";
$szRepoUrl = (New-Object System.Uri($szRepoPath)).AbsoluteUri.TrimEnd('/');
$szFileName = "sample.lua";
$szTrunkFile = Join-Path $szTrunkWc $szFileName;
$szBranchFile = Join-Path $szBranchWc $szFileName;
$szCrlf = "`r`n";
$szLf = "`n";

# r2: A completely CRLF-terminated baseline.
$tbFunctionLines = @(
    "function CustomerActionPrepareFinish()",
    "    self.nStartTime = GetTime();",
    "    self:_SwitchAIState(true);",
    "    self:StartUICountDown();",
    "    self:_AddActorEnterBuffStateToAll();",
    "",
    "    Log(`"Prepare finished`");",
    "end"
);
$szBaseText = (@("-- Fixture header", "") + $tbFunctionLines + @("", "-- Fixture trailer")) -join $szCrlf;
$szBaseText += $szCrlf;

# r4: The branch retains identical text but the function block is LF-terminated.
# The header/trailer intentionally remain CRLF-terminated, so this is a mixed-EOL file.
$szMixedText = "-- Fixture header${szCrlf}${szCrlf}";
$szMixedText += ($tbFunctionLines -join $szLf) + $szLf;
$szMixedText += "${szCrlf}-- Fixture trailer${szCrlf}";
if ($NormalizeBranchEol) {
    $szMixedText = $szBaseText;
}

# r5: The trunk inserts a call inside the CRLF version of the function.
$tbTrunkFunctionLines = @(
    "function CustomerActionPrepareFinish()",
    "    self.nStartTime = GetTime();",
    "    self:_SwitchAIState(true);",
    "    self:StartUICountDown();",
    "    self:_AddActorEnterBuffStateToAll();",
    "    self:_ChangePlayersFightStateHelper(true);",
    "",
    "    Log(`"Prepare finished`");",
    "end"
);
$szTrunkText = (@("-- Fixture header", "") + $tbTrunkFunctionLines + @("", "-- Fixture trailer")) -join $szCrlf;
$szTrunkText += $szCrlf;

& $script:szSvnAdminCommand create $szRepoPath;
if ($LASTEXITCODE -ne 0) {
    throw "svnadmin create failed with exit code $LASTEXITCODE.";
}

Invoke-Svn mkdir "${szRepoUrl}/trunk" "${szRepoUrl}/branches" -m "Create fixture layout";
Invoke-Svn checkout "${szRepoUrl}/trunk" $szTrunkWc;
Write-Utf8NoBom -Path $szTrunkFile -Text $szBaseText;
Invoke-Svn add $szTrunkFile;
Invoke-Svn commit $szTrunkWc -m "Add CRLF baseline";
Invoke-Svn copy "${szRepoUrl}/trunk" "${szRepoUrl}/branches/release" -m "Create release branch";
Invoke-Svn checkout "${szRepoUrl}/branches/release" $szBranchWc;

Write-Utf8NoBom -Path $szBranchFile -Text $szMixedText;
if (-not $NormalizeBranchEol) {
    Invoke-Svn commit $szBranchWc -m "Make only the branch function block LF-terminated";
}

Write-Utf8NoBom -Path $szTrunkFile -Text $szTrunkText;
Invoke-Svn commit $szTrunkWc -m "Insert helper call in trunk function";

$nMergeRevision = if ($NormalizeBranchEol) { 4 } else { 5 };
Invoke-Svn update $szBranchWc;
Set-Location $szBranchWc;
Invoke-Svn merge -c $nMergeRevision "${szRepoUrl}/trunk" .;

$tbResultLines = [System.IO.File]::ReadAllLines($szBranchFile);
$nFunctionLine = Get-IndexOfLine -Lines $tbResultLines -Text "function CustomerActionPrepareFinish()";
$nFunctionEndLine = Get-IndexOfLine -Lines $tbResultLines -Text "end" -StartIndex $nFunctionLine;
$nInsertedCallLine = Get-IndexOfLine -Lines $tbResultLines -Text "    self:_ChangePlayersFightStateHelper(true);";

if ($nFunctionLine -lt 0 -or $nFunctionEndLine -lt 0 -or $nInsertedCallLine -lt 0) {
    throw "The expected function or inserted call was not found.";
}

Write-Output "SVN executable: $script:szSvnCommand";
Write-Output "SVN version: $(& $script:szSvnCommand --version --quiet)";
Write-Output "Fixture path: $WorkRoot";
Write-Output "Function starts at line $($nFunctionLine + 1); first end is line $($nFunctionEndLine + 1); inserted call is line $($nInsertedCallLine + 1).";

if ($NormalizeBranchEol) {
    if ($nInsertedCallLine -lt $nFunctionEndLine) {
        Write-Output "CONTROL PASSED: CRLF normalization kept the inserted call inside the function.";
        exit 0;
    }

    throw "CONTROL FAILED: CRLF normalization did not keep the inserted call inside the function.";
}

if ($nInsertedCallLine -gt $nFunctionEndLine) {
    Write-Output "REPRODUCED: svn merge inserted the call after the function's end statement.";
    exit 0;
}

throw "NOT REPRODUCED: the inserted call remained inside the function.";
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.