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)
}