Skip to content

feat: auto-resolve for common business rule case #577

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
157 changes: 3 additions & 154 deletions cls/SourceControl/Git/Util/ProductionConflictResolver.cls
Original file line number Diff line number Diff line change
@@ -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 = "</Item>";

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
<Item Name="Demo7" Category="" ClassName="EnsLib.CloudStorage.BusinessOperation" PoolSize="1" Enabled="false" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
=======
<Item Name="Demo5" Category="" ClassName="EnsLib.AmazonCloudWatch.MetricAlarmOperation" PoolSize="1" Enabled="false" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
>>>>>>> 607d1f6 (modified src/HCC/Connect/Production.cls add Demo5)
</Item>
*/

// If:
// * We have one such marker (<<<<<<< / ======= / >>>>>>>)
// * The line after >>>>>> is "</Item>"
// Then:
// * We can replace ======= with "</Item>"

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(" </Item>")
Continue
} ElseIf postCloseMarker {
If $ZStrip(line,"<>W") '= "</Item>" {
$$$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
}

}
121 changes: 121 additions & 0 deletions cls/SourceControl/Git/Util/ResolutionManager.cls
Original file line number Diff line number Diff line change
@@ -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"))
}

}

7 changes: 7 additions & 0 deletions cls/SourceControl/Git/Util/RuleConflictResolver.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Class SourceControl.Git.Util.RuleConflictResolver Extends SourceControl.Git.Util.XMLConflictResolver
{

Parameter ExpectedConflictTag = "</rule>";

}

64 changes: 64 additions & 0 deletions cls/SourceControl/Git/Util/XMLConflictResolver.cls
Original file line number Diff line number Diff line change
@@ -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
<Item Name="Demo7" Category="" ClassName="EnsLib.CloudStorage.BusinessOperation" PoolSize="1" Enabled="false" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
=======
<Item Name="Demo5" Category="" ClassName="EnsLib.AmazonCloudWatch.MetricAlarmOperation" PoolSize="1" Enabled="false" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
>>>>>>> 607d1f6 (modified src/HCC/Connect/Production.cls add Demo5)
</Item>
*/

// If:
// * We have one such marker (<<<<<<< / ======= / >>>>>>>)
// * The line after >>>>>> is "</Item>"
// Then:
// * We can replace ======= with "</Item>"

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
}

}

3 changes: 2 additions & 1 deletion cls/SourceControl/Git/Utils.cls
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2980,3 +2980,4 @@ ClassMethod GetFavoriteNamespaces(ByRef favNamespaces As %DynamicArray, ByRef no
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading