Skip to content
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

Fix production conflicts automatically (and more sync tweaks) #390

Merged
merged 19 commits into from
Jul 3, 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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Items mapped from database other than namespace's default routine database are now ignored by default when exporting or adding files
- New setting to configure whether mapped items should be should be treated as read-only
- Added a basic mode to automatically perform functionality expected in basic use cases (#349)
- New sync operation for basic mode that fetches, pulls, commits and then pushes (#349)
- New sync operation for basic mode that fetches, pulls, commits, pushes, rebases, and pushes again (#349)
- "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
- Now skips files belonging to other git enabled packages in `##class(SourceControl.Git.Change).RefreshUncommitted()` (#347)
- Added a new "Branch" parameter to `##class(SourceControl.Git.PullEventHandler)` (#351)
- Command-line utility to do a baseline export of items in a namespace
Expand All @@ -26,7 +27,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Menu items names are properly translated from internal name in VSCode, Management Portal (#372)
- Now has proper locking behavior in `##class(SourceControl.Git.WebUIDriver).HandleRequest()`(#385)
- Git operations from the WebUI now don't unlock the session if they aren't read-only
- Syncing only prompts users for a commit message if there are uncommitted files (#390)
- WebUI works properly for users with %Developer without needing to add further SQL privileges (#365)
- Fixed `<UNDEFINED>` error running Import All (#380)
- Discarding changes now recompiles - critical for productions and some other cases (#387)

## [2.3.1] - 2024-04-30

Expand Down
5 changes: 4 additions & 1 deletion cls/SourceControl/Git/Extension.cls
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ Method OnSourceMenuItem(name As %String, ByRef Enabled As %String, ByRef Display
} else {
set Enabled = -1
}
if (name '= "") {

if (name = "Status") {
set DisplayName = ..LocalizeName(name)_" (branch: "_##class(SourceControl.Git.Utils).GetCurrentBranch()_")"
} if (name '= "") {
set DisplayName = ..LocalizeName(name)
}
quit $$$OK
Expand Down
23 changes: 23 additions & 0 deletions cls/SourceControl/Git/PullEventHandler.cls
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,28 @@ Method OnPull() As %Status [ Abstract ]
{
}

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

/// <var>InternalName</var> may be a comma-delimited string or $ListBuild list
ClassMethod ForInternalNames(InternalName As %String) As %Status
{
set list = $select($listvalid(InternalName):InternalName,1:$ListFromString(InternalName))
set pointer = 0
while $listnext(list,pointer,InternalName) {
set mod = ##class(SourceControl.Git.Modification).%New()
set mod.internalName = InternalName
set mod.externalName = ##class(SourceControl.Git.Utils).FullExternalName(InternalName)
set mod.changeType = "M"
set files($i(files)) = mod
}
quit ..ForModifications(.files)
}

}
2 changes: 1 addition & 1 deletion cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Method OnPull() As %Status
write !, "Nothing to compile."
quit $$$OK
}
quit $system.OBJ.CompileList(.compilelist, "cukb")
quit $system.OBJ.CompileList(.compilelist, "ck")
}

Method DeleteFile(item As %String) As %Status
Expand Down
156 changes: 156 additions & 0 deletions cls/SourceControl/Git/Util/ProductionConflictResolver.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
Include (%occInclude, %occErrors, %occKeyword, %occReference, %occSAX)

Class SourceControl.Git.Util.ProductionConflictResolver Extends %RegisteredObject
{

Property logStream As %Stream.Object [ Private ];

Property productionFile As %String [ Private ];

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 ]
{
Do ..logStream.Rewind()
Do ..logStream.ReadLine()
Set productionLine = ..logStream.ReadLine()
Set ..productionFile = $Piece(productionLine,"Merge conflict in ",2)
If ..productionFile = "" {
$$$ThrowStatus($$$ERROR($$$GeneralError,"Message did not reflect merge conflict on a single file."))
}
If '..logStream.AtEnd {
$$$ThrowStatus($$$ERROR($$$GeneralError,"Multiple files had merge conflicts; cannot resolve intelligently."))
}
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"))
isc-tleavitt marked this conversation as resolved.
Show resolved Hide resolved
}

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
}

}
Loading
Loading