diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ef54a1a..3f94008e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Warning on sync page if other users have unstaged changes (#493) - Added "Export System Default Settings" menu item (#544) - IRIS Business Intelligence items are mapped to the /dfi subdirectory by default (#428) +- Intelligent merge conflict auto-resolution works for the common Business Rule case as well (#391) ### Fixed - Fixed display of other users' username in workspace view on Unix (#530) diff --git a/cls/SourceControl/Git/Util/ProductionConflictResolver.cls b/cls/SourceControl/Git/Util/ProductionConflictResolver.cls index 1debada2..3b3c4d1c 100644 --- a/cls/SourceControl/Git/Util/ProductionConflictResolver.cls +++ b/cls/SourceControl/Git/Util/ProductionConflictResolver.cls @@ -1,162 +1,11 @@ Include (%occInclude, %occErrors, %occKeyword, %occReference, %occSAX) -Class SourceControl.Git.Util.ProductionConflictResolver Extends %RegisteredObject +Class SourceControl.Git.Util.ProductionConflictResolver Extends SourceControl.Git.Util.XMLConflictResolver { -Property logStream As %Stream.Object [ Private ]; +Parameter ExpectedConflictTag = ""; -Property productionFile As %String [ Private ]; +Parameter OutputIndent = " "; -Property productionClassname As %Dictionary.CacheClassname [ Private ]; - -Property errorStatus As %Status [ InitialExpression = 1, Private ]; - -/// API property: whether or not the conflict was resolved -Property resolved As %Boolean [ InitialExpression = 0 ]; - -/// API property: error message if resolved is false -Property errorMessage As %String [ Calculated ]; - -Method errorMessageGet() As %String -{ - If $$$ISERR(..errorStatus) { - Do $System.Status.DecomposeStatus(..errorStatus,.components) - If $Get(components(1,"code")) = $$$GeneralError { - Quit $Get(components(1,"param",1)) - } Else { - Set ex = ##class(%Exception.StatusException).CreateFromStatus(..errorStatus) - Do ex.Log() - Quit "an internal error occurred and has been logged." - } - } Else { - Quit "" - } -} - -ClassMethod FromLog(pOutStream As %Stream.Object) As SourceControl.Git.Util.ProductionConflictResolver -{ - Set inst = ..%New() - Try { - Set inst.logStream = pOutStream - Do inst.ConsumeStream() - Do inst.Resolve() - } Catch e { - Set inst.resolved = 0 - Set inst.errorStatus = e.AsStatus() - } - Do inst.logStream.Rewind() // Finally - Quit inst -} - -Method ConsumeStream() [ Private ] -{ - Set conflicts = 0 - Do ..logStream.Rewind() - Do ..logStream.ReadLine() - while '..logStream.AtEnd { - Set conflictLine = ..logStream.ReadLine() - If $Extract(conflictLine,1,8) = "CONFLICT" { - Set conflicts($i(conflicts)) = $Piece(conflictLine,"Merge conflict in ",2) - } - } - If (conflicts = 0) { - $$$ThrowStatus($$$ERROR($$$GeneralError,"Message did not reflect merge conflict on a single file.")) - } - If conflicts '= 1 { - $$$ThrowStatus($$$ERROR($$$GeneralError,"Multiple files had merge conflicts; cannot resolve intelligently.")) - } - Set ..productionFile = conflicts(1) - Set internalName = ##class(SourceControl.Git.Utils).NameToInternalName(..productionFile) - If ($Piece(internalName,".",*) '= "CLS") { - $$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict is not a class.")) - } - Set ..productionClassname = $Piece(internalName,".",1,*-1) - If '($$$comClassDefined(..productionClassname) && $ClassMethod(..productionClassname,"%Extends","Ens.Production")) { - $$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict is not an interoperability production.")) - } } -Method Resolve() [ Private ] -{ - Set filePath = ##class(SourceControl.Git.Utils).TempFolder()_..productionFile - Set file = ##class(%Stream.FileCharacter).%OpenId(filePath,,.sc) - $$$ThrowOnError(sc) - - Do ..ResolveStream(file) // Throws exception on failure - - $$$ThrowOnError(##class(SourceControl.Git.Utils).ImportItem(..productionClassname_".CLS",1)) - $$$ThrowOnError($System.OBJ.Compile(..productionClassname,"ck")) - - // TODO: if we add multiple resolvers, move this to the end. - set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "add", ..productionFile) - if (code '= 0) { - $$$ThrowStatus($$$ERROR($$$GeneralError,"git add reported failure")) - } - set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "commit", "--no-edit") - if (code '= 0) { - $$$ThrowStatus($$$ERROR($$$GeneralError,"git commit reported failure")) - } - - set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "rebase", "--continue") - if (code '= 0) { - $$$ThrowStatus($$$ERROR($$$GeneralError,"git rebase --continue reported failure")) - } - - set ..resolved = 1 -} - -/// Non-private to support unit testing -ClassMethod ResolveStream(stream As %Stream.Object) -{ - // File may have: - /* - <<<<<<< HEAD - - ======= - - >>>>>>> 607d1f6 (modified src/HCC/Connect/Production.cls add Demo5) - - */ - - // If: - // * We have one such marker (<<<<<<< / ======= / >>>>>>>) - // * The line after >>>>>> is "" - // Then: - // * We can replace ======= with "" - - Set copy = ##class(%Stream.TmpCharacter).%New() - Set markerCount = 0 - Set postCloseMarker = 0 - While 'stream.AtEnd { - Set line = stream.ReadLine() - Set start = $Extract(line,1,7) - If start = "<<<<<<<" { - Set markerCount = markerCount + 1 - Continue - } ElseIf (start = ">>>>>>>") { - Set postCloseMarker = 1 - Continue - } ElseIf (start = "=======") { - Do copy.WriteLine(" ") - Continue - } ElseIf postCloseMarker { - If $ZStrip(line,"<>W") '= "" { - $$$ThrowStatus($$$ERROR($$$GeneralError,"The type of conflict encountered is not handled; user must resolve manually.")) - } - Set postCloseMarker = 0 - } - Do copy.WriteLine(line) - } - - If markerCount > 1 { - $$$ThrowStatus($$$ERROR($$$GeneralError,"Multiple conflicts found, cannot resolve automatically.")) - } ElseIf markerCount = 0 { - $$$ThrowStatus($$$ERROR($$$GeneralError,"No conflict markers found in file")) - } - - $$$ThrowOnError(stream.CopyFromAndSave(copy)) - - Quit 1 -} - -} diff --git a/cls/SourceControl/Git/Util/ResolutionManager.cls b/cls/SourceControl/Git/Util/ResolutionManager.cls new file mode 100644 index 00000000..a36c310c --- /dev/null +++ b/cls/SourceControl/Git/Util/ResolutionManager.cls @@ -0,0 +1,121 @@ +Include (%occInclude, %occErrors, %occKeyword, %occReference, %occSAX) + +Class SourceControl.Git.Util.ResolutionManager Extends %RegisteredObject +{ + +Property logStream As %Stream.Object [ Private ]; + +Property errorStatus As %Status [ InitialExpression = 1, Private ]; + +/// API property: whether or not the conflict was resolved +Property resolved As %Boolean [ InitialExpression = 0 ]; + +/// API property: error message if resolved is false +Property errorMessage As %String [ Calculated ]; + +Method errorMessageGet() As %String +{ + If $$$ISERR(..errorStatus) { + Do $System.Status.DecomposeStatus(..errorStatus,.components) + If $Get(components(1,"code")) = $$$GeneralError { + Quit $Get(components(1,"param",1)) + } Else { + Set ex = ##class(%Exception.StatusException).CreateFromStatus(..errorStatus) + Do ex.Log() + Quit "an internal error occurred and has been logged." + } + } Else { + Quit "" + } +} + +ClassMethod FromLog(pOutStream As %Stream.Object) As SourceControl.Git.Util.ProductionConflictResolver +{ + Set inst = ..%New() + Try { + Set inst.logStream = pOutStream + Do inst.ConsumeStream() + } Catch e { + Set inst.resolved = 0 + Set inst.errorStatus = e.AsStatus() + } + Do inst.logStream.Rewind() // Finally + Quit inst +} + +Method ConsumeStream() [ Private ] +{ + Set conflicts = 0 + Do ..logStream.Rewind() + Do ..logStream.ReadLine() + while '..logStream.AtEnd { + Set conflictLine = ..logStream.ReadLine() + If $Extract(conflictLine,1,8) = "CONFLICT" { + Set conflicts($i(conflicts)) = $Piece(conflictLine,"Merge conflict in ",2) + } + } + If (conflicts = 0) { + $$$ThrowStatus($$$ERROR($$$GeneralError,"Message did not reflect merge conflict on a single file.")) + } + For i=1:1:conflicts { + Set targetFile = conflicts(i) + Write !,"Attempting intelligent auto-merge for: "_targetFile + Set internalName = ##class(SourceControl.Git.Utils).NameToInternalName(targetFile) + If ($Piece(internalName,".",*) '= "CLS") { + $$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict is not a class.")) + } + + Set targetClass = $Piece(internalName,".",1,*-1) + If '$$$comClassDefined(targetClass) { + $$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict not a known class.")) + } + + Set resolverClass = $Select( + $classmethod(targetClass,"%Extends","Ens.Production"):"SourceControl.Git.Util.ProductionConflictResolver", + $classmethod(targetClass,"%Extends","Ens.Rule.Definition"):"SourceControl.Git.Util.RuleConflictResolver", + 1:"" + ) + + If (resolverClass = "") { + $$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict not a class type that supports automatic resolution.")) + } + + do ..ResolveClass(targetClass, targetFile, resolverClass) + + set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "add", targetFile) + if (code '= 0) { + $$$ThrowStatus($$$ERROR($$$GeneralError,"git add reported failure")) + } + } + + set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "commit", "--no-edit") + if (code '= 0) { + $$$ThrowStatus($$$ERROR($$$GeneralError,"git commit reported failure")) + } + + set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "rebase", "--continue") + if (code '= 0) { + // Could hit a second+ conflict in the same rebase; attempt to resolve the next one too. + set resolver = ..FromLog(outStream) + set ..resolved = resolver.resolved + set ..errorStatus = resolver.errorStatus + } else { + set ..resolved = 1 + } +} + +Method ResolveClass(className As %String, fileName As %String, resolverClass As %Dictionary.Classname) [ Private ] +{ + Set filePath = ##class(SourceControl.Git.Utils).TempFolder()_fileName + Set file = ##class(%Stream.FileCharacter).%OpenId(filePath,,.sc) + $$$ThrowOnError(sc) + + Set resolver = $classmethod(resolverClass,"%New") + Do resolver.ResolveStream(file) // Throws exception on failure + + $$$ThrowOnError(##class(SourceControl.Git.Utils).ImportItem(className_".CLS",1)) + $$$ThrowOnError($System.OBJ.Compile(className,"ck")) +} + +} + diff --git a/cls/SourceControl/Git/Util/RuleConflictResolver.cls b/cls/SourceControl/Git/Util/RuleConflictResolver.cls new file mode 100644 index 00000000..1e05fbc7 --- /dev/null +++ b/cls/SourceControl/Git/Util/RuleConflictResolver.cls @@ -0,0 +1,7 @@ +Class SourceControl.Git.Util.RuleConflictResolver Extends SourceControl.Git.Util.XMLConflictResolver +{ + +Parameter ExpectedConflictTag = ""; + +} + diff --git a/cls/SourceControl/Git/Util/XMLConflictResolver.cls b/cls/SourceControl/Git/Util/XMLConflictResolver.cls new file mode 100644 index 00000000..5294221e --- /dev/null +++ b/cls/SourceControl/Git/Util/XMLConflictResolver.cls @@ -0,0 +1,64 @@ +Include (%occInclude, %occErrors, %occKeyword, %occReference, %occSAX) + +Class SourceControl.Git.Util.XMLConflictResolver Extends %RegisteredObject +{ + +Parameter ExpectedConflictTag; + +Parameter OutputIndent; + +Method ResolveStream(stream As %Stream.Object) +{ + // File may have: + /* + <<<<<<< HEAD + + ======= + + >>>>>>> 607d1f6 (modified src/HCC/Connect/Production.cls add Demo5) + + */ + + // If: + // * We have one such marker (<<<<<<< / ======= / >>>>>>>) + // * The line after >>>>>> is "" + // Then: + // * We can replace ======= with "" + + Set copy = ##class(%Stream.TmpCharacter).%New() + Set markerCount = 0 + Set postCloseMarker = 0 + While 'stream.AtEnd { + Set line = stream.ReadLine() + Set start = $Extract(line,1,7) + If start = "<<<<<<<" { + Set markerCount = markerCount + 1 + Continue + } ElseIf (start = ">>>>>>>") { + Set postCloseMarker = 1 + Continue + } ElseIf (start = "=======") { + Do copy.WriteLine(..#OutputIndent_..#ExpectedConflictTag) + Continue + } ElseIf postCloseMarker { + If $ZStrip(line,"<>W") '= ..#ExpectedConflictTag { + $$$ThrowStatus($$$ERROR($$$GeneralError,"The type of conflict encountered is not handled; user must resolve manually.")) + } + Set postCloseMarker = 0 + } + Do copy.WriteLine(line) + } + + If markerCount > 1 { + $$$ThrowStatus($$$ERROR($$$GeneralError,"Multiple conflicts found, cannot resolve automatically.")) + } ElseIf markerCount = 0 { + $$$ThrowStatus($$$ERROR($$$GeneralError,"No conflict markers found in file")) + } + + $$$ThrowOnError(stream.CopyFromAndSave(copy)) + + Quit 1 +} + +} + diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 27387cd0..1add44bd 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -507,7 +507,7 @@ ClassMethod MergeDefaultRemoteBranch(Output alert As %String = "") As %Boolean } catch e { // "rebase" may throw an exception due to errors syncing to IRIS. In that case, roll back and keep going to abort the rebase. write !,"Attempting to resolve differences in production definition..." - set resolver = ##class(SourceControl.Git.Util.ProductionConflictResolver).FromLog(outStream) + set resolver = ##class(SourceControl.Git.Util.ResolutionManager).FromLog(outStream) if resolver.resolved { set rebased = 1 TCOMMIT @@ -2980,3 +2980,4 @@ ClassMethod GetFavoriteNamespaces(ByRef favNamespaces As %DynamicArray, ByRef no } } + diff --git a/test/UnitTest/SourceControl/Git/ProductionConflictResolve.cls b/test/UnitTest/SourceControl/Git/ProductionConflictResolve.cls index 683b83b2..bd3914e9 100644 --- a/test/UnitTest/SourceControl/Git/ProductionConflictResolve.cls +++ b/test/UnitTest/SourceControl/Git/ProductionConflictResolve.cls @@ -29,7 +29,7 @@ Method DoResolveTest(index As %Integer) } $$$ThrowOnError(resolved.%Save()) - Do ##class(SourceControl.Git.Util.ProductionConflictResolver).ResolveStream(file) + Do ##class(SourceControl.Git.Util.ProductionConflictResolver).%New().ResolveStream(file) Do $$$AssertFilesSame(file.Filename,resolved.Filename) }