Skip to content

Commit 5b1428b

Browse files
authored
Merge pull request #390 from intersystems/fix-production-conflicts
Fix production conflicts automatically (and more sync tweaks)
2 parents 78d37a4 + d4923f0 commit 5b1428b

File tree

8 files changed

+472
-64
lines changed

8 files changed

+472
-64
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Items mapped from database other than namespace's default routine database are now ignored by default when exporting or adding files
1414
- New setting to configure whether mapped items should be should be treated as read-only
1515
- Added a basic mode to automatically perform functionality expected in basic use cases (#349)
16-
- New sync operation for basic mode that fetches, pulls, commits and then pushes (#349)
16+
- New sync operation for basic mode that fetches, pulls, commits, pushes, rebases, and pushes again (#349)
17+
- "Sync" operation in basic mode automatically resolves the class of merge conflict common in production classes where multiple independent items are added in different feature branches
1718
- Now skips files belonging to other git enabled packages in `##class(SourceControl.Git.Change).RefreshUncommitted()` (#347)
1819
- Added a new "Branch" parameter to `##class(SourceControl.Git.PullEventHandler)` (#351)
1920
- Command-line utility to do a baseline export of items in a namespace
@@ -26,7 +27,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2627
- Menu items names are properly translated from internal name in VSCode, Management Portal (#372)
2728
- Now has proper locking behavior in `##class(SourceControl.Git.WebUIDriver).HandleRequest()`(#385)
2829
- Git operations from the WebUI now don't unlock the session if they aren't read-only
30+
- Syncing only prompts users for a commit message if there are uncommitted files (#390)
2931
- WebUI works properly for users with %Developer without needing to add further SQL privileges (#365)
32+
- Fixed `<UNDEFINED>` error running Import All (#380)
33+
- Discarding changes now recompiles - critical for productions and some other cases (#387)
3034

3135
## [2.3.1] - 2024-04-30
3236

cls/SourceControl/Git/Extension.cls

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,10 @@ Method OnSourceMenuItem(name As %String, ByRef Enabled As %String, ByRef Display
159159
} else {
160160
set Enabled = -1
161161
}
162-
if (name '= "") {
162+
163+
if (name = "Status") {
164+
set DisplayName = ..LocalizeName(name)_" (branch: "_##class(SourceControl.Git.Utils).GetCurrentBranch()_")"
165+
} if (name '= "") {
163166
set DisplayName = ..LocalizeName(name)
164167
}
165168
quit $$$OK

cls/SourceControl/Git/PullEventHandler.cls

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,28 @@ Method OnPull() As %Status [ Abstract ]
2020
{
2121
}
2222

23+
/// <var>files</var> is an integer-subscripted array of <class>SourceControl.Git.Modification</class> objects.
24+
ClassMethod ForModifications(ByRef files) As %Status
25+
{
26+
set event = $classmethod(##class(SourceControl.Git.Utils).PullEventClass(),"%New")
27+
set event.LocalRoot = ##class(SourceControl.Git.Utils).TempFolder()
28+
merge event.ModifiedFiles = files
29+
quit event.OnPull()
2330
}
2431

32+
/// <var>InternalName</var> may be a comma-delimited string or $ListBuild list
33+
ClassMethod ForInternalNames(InternalName As %String) As %Status
34+
{
35+
set list = $select($listvalid(InternalName):InternalName,1:$ListFromString(InternalName))
36+
set pointer = 0
37+
while $listnext(list,pointer,InternalName) {
38+
set mod = ##class(SourceControl.Git.Modification).%New()
39+
set mod.internalName = InternalName
40+
set mod.externalName = ##class(SourceControl.Git.Utils).FullExternalName(InternalName)
41+
set mod.changeType = "M"
42+
set files($i(files)) = mod
43+
}
44+
quit ..ForModifications(.files)
45+
}
46+
47+
}

cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Method OnPull() As %Status
3535
write !, "Nothing to compile."
3636
quit $$$OK
3737
}
38-
quit $system.OBJ.CompileList(.compilelist, "cukb")
38+
quit $system.OBJ.CompileList(.compilelist, "ck")
3939
}
4040

4141
Method DeleteFile(item As %String) As %Status
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
Include (%occInclude, %occErrors, %occKeyword, %occReference, %occSAX)
2+
3+
Class SourceControl.Git.Util.ProductionConflictResolver Extends %RegisteredObject
4+
{
5+
6+
Property logStream As %Stream.Object [ Private ];
7+
8+
Property productionFile As %String [ Private ];
9+
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+
Do ..logStream.Rewind()
54+
Do ..logStream.ReadLine()
55+
Set productionLine = ..logStream.ReadLine()
56+
Set ..productionFile = $Piece(productionLine,"Merge conflict in ",2)
57+
If ..productionFile = "" {
58+
$$$ThrowStatus($$$ERROR($$$GeneralError,"Message did not reflect merge conflict on a single file."))
59+
}
60+
If '..logStream.AtEnd {
61+
$$$ThrowStatus($$$ERROR($$$GeneralError,"Multiple files had merge conflicts; cannot resolve intelligently."))
62+
}
63+
Set internalName = ##class(SourceControl.Git.Utils).NameToInternalName(..productionFile)
64+
If ($Piece(internalName,".",*) '= "CLS") {
65+
$$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict is not a class."))
66+
}
67+
Set ..productionClassname = $Piece(internalName,".",1,*-1)
68+
If '($$$comClassDefined(..productionClassname) && $ClassMethod(..productionClassname,"%Extends","Ens.Production")) {
69+
$$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict is not an interoperability production."))
70+
}
71+
}
72+
73+
Method Resolve() [ Private ]
74+
{
75+
Set filePath = ##class(SourceControl.Git.Utils).TempFolder()_..productionFile
76+
Set file = ##class(%Stream.FileCharacter).%OpenId(filePath,,.sc)
77+
$$$ThrowOnError(sc)
78+
79+
Do ..ResolveStream(file) // Throws exception on failure
80+
81+
$$$ThrowOnError(##class(SourceControl.Git.Utils).ImportItem(..productionClassname_".CLS",1))
82+
$$$ThrowOnError($System.OBJ.Compile(..productionClassname,"ck"))
83+
84+
// TODO: if we add multiple resolvers, move this to the end.
85+
set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "add", ..productionFile)
86+
if (code '= 0) {
87+
$$$ThrowStatus($$$ERROR($$$GeneralError,"git add reported failure"))
88+
}
89+
set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "commit", "--no-edit")
90+
if (code '= 0) {
91+
$$$ThrowStatus($$$ERROR($$$GeneralError,"git commit reported failure"))
92+
}
93+
94+
set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "rebase", "--continue")
95+
if (code '= 0) {
96+
$$$ThrowStatus($$$ERROR($$$GeneralError,"git rebase --continue reported failure"))
97+
}
98+
99+
set ..resolved = 1
100+
}
101+
102+
/// Non-private to support unit testing
103+
ClassMethod ResolveStream(stream As %Stream.Object)
104+
{
105+
// File may have:
106+
/*
107+
<<<<<<< HEAD
108+
<Item Name="Demo7" Category="" ClassName="EnsLib.CloudStorage.BusinessOperation" PoolSize="1" Enabled="false" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
109+
=======
110+
<Item Name="Demo5" Category="" ClassName="EnsLib.AmazonCloudWatch.MetricAlarmOperation" PoolSize="1" Enabled="false" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
111+
>>>>>>> 607d1f6 (modified src/HCC/Connect/Production.cls add Demo5)
112+
</Item>
113+
*/
114+
115+
// If:
116+
// * We have one such marker (<<<<<<< / ======= / >>>>>>>)
117+
// * The line after >>>>>> is "</Item>"
118+
// Then:
119+
// * We can replace ======= with "</Item>"
120+
121+
Set copy = ##class(%Stream.TmpCharacter).%New()
122+
Set markerCount = 0
123+
Set postCloseMarker = 0
124+
While 'stream.AtEnd {
125+
Set line = stream.ReadLine()
126+
Set start = $Extract(line,1,7)
127+
If start = "<<<<<<<" {
128+
Set markerCount = markerCount + 1
129+
Continue
130+
} ElseIf (start = ">>>>>>>") {
131+
Set postCloseMarker = 1
132+
Continue
133+
} ElseIf (start = "=======") {
134+
Do copy.WriteLine(" </Item>")
135+
Continue
136+
} ElseIf postCloseMarker {
137+
If $ZStrip(line,"<>W") '= "</Item>" {
138+
$$$ThrowStatus($$$ERROR($$$GeneralError,"The type of conflict encountered is not handled; user must resolve manually."))
139+
}
140+
Set postCloseMarker = 0
141+
}
142+
Do copy.WriteLine(line)
143+
}
144+
145+
If markerCount > 1 {
146+
$$$ThrowStatus($$$ERROR($$$GeneralError,"Multiple conflicts found, cannot resolve automatically."))
147+
} ElseIf markerCount = 0 {
148+
$$$ThrowStatus($$$ERROR($$$GeneralError,"No conflict markers found in file"))
149+
}
150+
151+
$$$ThrowOnError(stream.CopyFromAndSave(copy))
152+
153+
Quit 1
154+
}
155+
156+
}

0 commit comments

Comments
 (0)