Skip to content

Commit 81e6f6a

Browse files
authored
Merge pull request #577 from intersystems/resolution-improvements
feat: auto-resolve for common business rule case
2 parents 390bfd3 + f39b2ff commit 81e6f6a

File tree

7 files changed

+199
-156
lines changed

7 files changed

+199
-156
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Warning on sync page if other users have unstaged changes (#493)
1717
- Added "Export System Default Settings" menu item (#544)
1818
- IRIS Business Intelligence items are mapped to the /dfi subdirectory by default (#428)
19+
- Intelligent merge conflict auto-resolution works for the common Business Rule case as well (#391)
1920

2021
### Fixed
2122
- Fixed display of other users' username in workspace view on Unix (#530)
Original file line numberDiff line numberDiff line change
@@ -1,162 +1,11 @@
11
Include (%occInclude, %occErrors, %occKeyword, %occReference, %occSAX)
22

3-
Class SourceControl.Git.Util.ProductionConflictResolver Extends %RegisteredObject
3+
Class SourceControl.Git.Util.ProductionConflictResolver Extends SourceControl.Git.Util.XMLConflictResolver
44
{
55

6-
Property logStream As %Stream.Object [ Private ];
6+
Parameter ExpectedConflictTag = "</Item>";
77

8-
Property productionFile As %String [ Private ];
8+
Parameter OutputIndent = " ";
99

10-
Property productionClassname As %Dictionary.CacheClassname [ Private ];
11-
12-
Property errorStatus As %Status [ InitialExpression = 1, Private ];
13-
14-
/// API property: whether or not the conflict was resolved
15-
Property resolved As %Boolean [ InitialExpression = 0 ];
16-
17-
/// API property: error message if resolved is false
18-
Property errorMessage As %String [ Calculated ];
19-
20-
Method errorMessageGet() As %String
21-
{
22-
If $$$ISERR(..errorStatus) {
23-
Do $System.Status.DecomposeStatus(..errorStatus,.components)
24-
If $Get(components(1,"code")) = $$$GeneralError {
25-
Quit $Get(components(1,"param",1))
26-
} Else {
27-
Set ex = ##class(%Exception.StatusException).CreateFromStatus(..errorStatus)
28-
Do ex.Log()
29-
Quit "an internal error occurred and has been logged."
30-
}
31-
} Else {
32-
Quit ""
33-
}
34-
}
35-
36-
ClassMethod FromLog(pOutStream As %Stream.Object) As SourceControl.Git.Util.ProductionConflictResolver
37-
{
38-
Set inst = ..%New()
39-
Try {
40-
Set inst.logStream = pOutStream
41-
Do inst.ConsumeStream()
42-
Do inst.Resolve()
43-
} Catch e {
44-
Set inst.resolved = 0
45-
Set inst.errorStatus = e.AsStatus()
46-
}
47-
Do inst.logStream.Rewind() // Finally
48-
Quit inst
49-
}
50-
51-
Method ConsumeStream() [ Private ]
52-
{
53-
Set conflicts = 0
54-
Do ..logStream.Rewind()
55-
Do ..logStream.ReadLine()
56-
while '..logStream.AtEnd {
57-
Set conflictLine = ..logStream.ReadLine()
58-
If $Extract(conflictLine,1,8) = "CONFLICT" {
59-
Set conflicts($i(conflicts)) = $Piece(conflictLine,"Merge conflict in ",2)
60-
}
61-
}
62-
If (conflicts = 0) {
63-
$$$ThrowStatus($$$ERROR($$$GeneralError,"Message did not reflect merge conflict on a single file."))
64-
}
65-
If conflicts '= 1 {
66-
$$$ThrowStatus($$$ERROR($$$GeneralError,"Multiple files had merge conflicts; cannot resolve intelligently."))
67-
}
68-
Set ..productionFile = conflicts(1)
69-
Set internalName = ##class(SourceControl.Git.Utils).NameToInternalName(..productionFile)
70-
If ($Piece(internalName,".",*) '= "CLS") {
71-
$$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict is not a class."))
72-
}
73-
Set ..productionClassname = $Piece(internalName,".",1,*-1)
74-
If '($$$comClassDefined(..productionClassname) && $ClassMethod(..productionClassname,"%Extends","Ens.Production")) {
75-
$$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict is not an interoperability production."))
76-
}
7710
}
7811

79-
Method Resolve() [ Private ]
80-
{
81-
Set filePath = ##class(SourceControl.Git.Utils).TempFolder()_..productionFile
82-
Set file = ##class(%Stream.FileCharacter).%OpenId(filePath,,.sc)
83-
$$$ThrowOnError(sc)
84-
85-
Do ..ResolveStream(file) // Throws exception on failure
86-
87-
$$$ThrowOnError(##class(SourceControl.Git.Utils).ImportItem(..productionClassname_".CLS",1))
88-
$$$ThrowOnError($System.OBJ.Compile(..productionClassname,"ck"))
89-
90-
// TODO: if we add multiple resolvers, move this to the end.
91-
set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "add", ..productionFile)
92-
if (code '= 0) {
93-
$$$ThrowStatus($$$ERROR($$$GeneralError,"git add reported failure"))
94-
}
95-
set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "commit", "--no-edit")
96-
if (code '= 0) {
97-
$$$ThrowStatus($$$ERROR($$$GeneralError,"git commit reported failure"))
98-
}
99-
100-
set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "rebase", "--continue")
101-
if (code '= 0) {
102-
$$$ThrowStatus($$$ERROR($$$GeneralError,"git rebase --continue reported failure"))
103-
}
104-
105-
set ..resolved = 1
106-
}
107-
108-
/// Non-private to support unit testing
109-
ClassMethod ResolveStream(stream As %Stream.Object)
110-
{
111-
// File may have:
112-
/*
113-
<<<<<<< HEAD
114-
<Item Name="Demo7" Category="" ClassName="EnsLib.CloudStorage.BusinessOperation" PoolSize="1" Enabled="false" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
115-
=======
116-
<Item Name="Demo5" Category="" ClassName="EnsLib.AmazonCloudWatch.MetricAlarmOperation" PoolSize="1" Enabled="false" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
117-
>>>>>>> 607d1f6 (modified src/HCC/Connect/Production.cls add Demo5)
118-
</Item>
119-
*/
120-
121-
// If:
122-
// * We have one such marker (<<<<<<< / ======= / >>>>>>>)
123-
// * The line after >>>>>> is "</Item>"
124-
// Then:
125-
// * We can replace ======= with "</Item>"
126-
127-
Set copy = ##class(%Stream.TmpCharacter).%New()
128-
Set markerCount = 0
129-
Set postCloseMarker = 0
130-
While 'stream.AtEnd {
131-
Set line = stream.ReadLine()
132-
Set start = $Extract(line,1,7)
133-
If start = "<<<<<<<" {
134-
Set markerCount = markerCount + 1
135-
Continue
136-
} ElseIf (start = ">>>>>>>") {
137-
Set postCloseMarker = 1
138-
Continue
139-
} ElseIf (start = "=======") {
140-
Do copy.WriteLine(" </Item>")
141-
Continue
142-
} ElseIf postCloseMarker {
143-
If $ZStrip(line,"<>W") '= "</Item>" {
144-
$$$ThrowStatus($$$ERROR($$$GeneralError,"The type of conflict encountered is not handled; user must resolve manually."))
145-
}
146-
Set postCloseMarker = 0
147-
}
148-
Do copy.WriteLine(line)
149-
}
150-
151-
If markerCount > 1 {
152-
$$$ThrowStatus($$$ERROR($$$GeneralError,"Multiple conflicts found, cannot resolve automatically."))
153-
} ElseIf markerCount = 0 {
154-
$$$ThrowStatus($$$ERROR($$$GeneralError,"No conflict markers found in file"))
155-
}
156-
157-
$$$ThrowOnError(stream.CopyFromAndSave(copy))
158-
159-
Quit 1
160-
}
161-
162-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
Include (%occInclude, %occErrors, %occKeyword, %occReference, %occSAX)
2+
3+
Class SourceControl.Git.Util.ResolutionManager Extends %RegisteredObject
4+
{
5+
6+
Property logStream As %Stream.Object [ Private ];
7+
8+
Property errorStatus As %Status [ InitialExpression = 1, Private ];
9+
10+
/// API property: whether or not the conflict was resolved
11+
Property resolved As %Boolean [ InitialExpression = 0 ];
12+
13+
/// API property: error message if resolved is false
14+
Property errorMessage As %String [ Calculated ];
15+
16+
Method errorMessageGet() As %String
17+
{
18+
If $$$ISERR(..errorStatus) {
19+
Do $System.Status.DecomposeStatus(..errorStatus,.components)
20+
If $Get(components(1,"code")) = $$$GeneralError {
21+
Quit $Get(components(1,"param",1))
22+
} Else {
23+
Set ex = ##class(%Exception.StatusException).CreateFromStatus(..errorStatus)
24+
Do ex.Log()
25+
Quit "an internal error occurred and has been logged."
26+
}
27+
} Else {
28+
Quit ""
29+
}
30+
}
31+
32+
ClassMethod FromLog(pOutStream As %Stream.Object) As SourceControl.Git.Util.ProductionConflictResolver
33+
{
34+
Set inst = ..%New()
35+
Try {
36+
Set inst.logStream = pOutStream
37+
Do inst.ConsumeStream()
38+
} Catch e {
39+
Set inst.resolved = 0
40+
Set inst.errorStatus = e.AsStatus()
41+
}
42+
Do inst.logStream.Rewind() // Finally
43+
Quit inst
44+
}
45+
46+
Method ConsumeStream() [ Private ]
47+
{
48+
Set conflicts = 0
49+
Do ..logStream.Rewind()
50+
Do ..logStream.ReadLine()
51+
while '..logStream.AtEnd {
52+
Set conflictLine = ..logStream.ReadLine()
53+
If $Extract(conflictLine,1,8) = "CONFLICT" {
54+
Set conflicts($i(conflicts)) = $Piece(conflictLine,"Merge conflict in ",2)
55+
}
56+
}
57+
If (conflicts = 0) {
58+
$$$ThrowStatus($$$ERROR($$$GeneralError,"Message did not reflect merge conflict on a single file."))
59+
}
60+
For i=1:1:conflicts {
61+
Set targetFile = conflicts(i)
62+
Write !,"Attempting intelligent auto-merge for: "_targetFile
63+
Set internalName = ##class(SourceControl.Git.Utils).NameToInternalName(targetFile)
64+
If ($Piece(internalName,".",*) '= "CLS") {
65+
$$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict is not a class."))
66+
}
67+
68+
Set targetClass = $Piece(internalName,".",1,*-1)
69+
If '$$$comClassDefined(targetClass) {
70+
$$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict not a known class."))
71+
}
72+
73+
Set resolverClass = $Select(
74+
$classmethod(targetClass,"%Extends","Ens.Production"):"SourceControl.Git.Util.ProductionConflictResolver",
75+
$classmethod(targetClass,"%Extends","Ens.Rule.Definition"):"SourceControl.Git.Util.RuleConflictResolver",
76+
1:""
77+
)
78+
79+
If (resolverClass = "") {
80+
$$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict not a class type that supports automatic resolution."))
81+
}
82+
83+
do ..ResolveClass(targetClass, targetFile, resolverClass)
84+
85+
set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "add", targetFile)
86+
if (code '= 0) {
87+
$$$ThrowStatus($$$ERROR($$$GeneralError,"git add reported failure"))
88+
}
89+
}
90+
91+
set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "commit", "--no-edit")
92+
if (code '= 0) {
93+
$$$ThrowStatus($$$ERROR($$$GeneralError,"git commit reported failure"))
94+
}
95+
96+
set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "rebase", "--continue")
97+
if (code '= 0) {
98+
// Could hit a second+ conflict in the same rebase; attempt to resolve the next one too.
99+
set resolver = ..FromLog(outStream)
100+
set ..resolved = resolver.resolved
101+
set ..errorStatus = resolver.errorStatus
102+
} else {
103+
set ..resolved = 1
104+
}
105+
}
106+
107+
Method ResolveClass(className As %String, fileName As %String, resolverClass As %Dictionary.Classname) [ Private ]
108+
{
109+
Set filePath = ##class(SourceControl.Git.Utils).TempFolder()_fileName
110+
Set file = ##class(%Stream.FileCharacter).%OpenId(filePath,,.sc)
111+
$$$ThrowOnError(sc)
112+
113+
Set resolver = $classmethod(resolverClass,"%New")
114+
Do resolver.ResolveStream(file) // Throws exception on failure
115+
116+
$$$ThrowOnError(##class(SourceControl.Git.Utils).ImportItem(className_".CLS",1))
117+
$$$ThrowOnError($System.OBJ.Compile(className,"ck"))
118+
}
119+
120+
}
121+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Class SourceControl.Git.Util.RuleConflictResolver Extends SourceControl.Git.Util.XMLConflictResolver
2+
{
3+
4+
Parameter ExpectedConflictTag = "</rule>";
5+
6+
}
7+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
Include (%occInclude, %occErrors, %occKeyword, %occReference, %occSAX)
2+
3+
Class SourceControl.Git.Util.XMLConflictResolver Extends %RegisteredObject
4+
{
5+
6+
Parameter ExpectedConflictTag;
7+
8+
Parameter OutputIndent;
9+
10+
Method ResolveStream(stream As %Stream.Object)
11+
{
12+
// File may have:
13+
/*
14+
<<<<<<< HEAD
15+
<Item Name="Demo7" Category="" ClassName="EnsLib.CloudStorage.BusinessOperation" PoolSize="1" Enabled="false" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
16+
=======
17+
<Item Name="Demo5" Category="" ClassName="EnsLib.AmazonCloudWatch.MetricAlarmOperation" PoolSize="1" Enabled="false" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
18+
>>>>>>> 607d1f6 (modified src/HCC/Connect/Production.cls add Demo5)
19+
</Item>
20+
*/
21+
22+
// If:
23+
// * We have one such marker (<<<<<<< / ======= / >>>>>>>)
24+
// * The line after >>>>>> is "</Item>"
25+
// Then:
26+
// * We can replace ======= with "</Item>"
27+
28+
Set copy = ##class(%Stream.TmpCharacter).%New()
29+
Set markerCount = 0
30+
Set postCloseMarker = 0
31+
While 'stream.AtEnd {
32+
Set line = stream.ReadLine()
33+
Set start = $Extract(line,1,7)
34+
If start = "<<<<<<<" {
35+
Set markerCount = markerCount + 1
36+
Continue
37+
} ElseIf (start = ">>>>>>>") {
38+
Set postCloseMarker = 1
39+
Continue
40+
} ElseIf (start = "=======") {
41+
Do copy.WriteLine(..#OutputIndent_..#ExpectedConflictTag)
42+
Continue
43+
} ElseIf postCloseMarker {
44+
If $ZStrip(line,"<>W") '= ..#ExpectedConflictTag {
45+
$$$ThrowStatus($$$ERROR($$$GeneralError,"The type of conflict encountered is not handled; user must resolve manually."))
46+
}
47+
Set postCloseMarker = 0
48+
}
49+
Do copy.WriteLine(line)
50+
}
51+
52+
If markerCount > 1 {
53+
$$$ThrowStatus($$$ERROR($$$GeneralError,"Multiple conflicts found, cannot resolve automatically."))
54+
} ElseIf markerCount = 0 {
55+
$$$ThrowStatus($$$ERROR($$$GeneralError,"No conflict markers found in file"))
56+
}
57+
58+
$$$ThrowOnError(stream.CopyFromAndSave(copy))
59+
60+
Quit 1
61+
}
62+
63+
}
64+

cls/SourceControl/Git/Utils.cls

+2-1
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ ClassMethod MergeDefaultRemoteBranch(Output alert As %String = "") As %Boolean
507507
} catch e {
508508
// "rebase" may throw an exception due to errors syncing to IRIS. In that case, roll back and keep going to abort the rebase.
509509
write !,"Attempting to resolve differences in production definition..."
510-
set resolver = ##class(SourceControl.Git.Util.ProductionConflictResolver).FromLog(outStream)
510+
set resolver = ##class(SourceControl.Git.Util.ResolutionManager).FromLog(outStream)
511511
if resolver.resolved {
512512
set rebased = 1
513513
TCOMMIT
@@ -2980,3 +2980,4 @@ ClassMethod GetFavoriteNamespaces(ByRef favNamespaces As %DynamicArray, ByRef no
29802980
}
29812981

29822982
}
2983+

test/UnitTest/SourceControl/Git/ProductionConflictResolve.cls

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Method DoResolveTest(index As %Integer)
2929
}
3030
$$$ThrowOnError(resolved.%Save())
3131

32-
Do ##class(SourceControl.Git.Util.ProductionConflictResolver).ResolveStream(file)
32+
Do ##class(SourceControl.Git.Util.ProductionConflictResolver).%New().ResolveStream(file)
3333

3434
Do $$$AssertFilesSame(file.Filename,resolved.Filename)
3535
}

0 commit comments

Comments
 (0)