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
<#
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.";

Reply via email to