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 6 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
3 changes: 2 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 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
3 changes: 1 addition & 2 deletions 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 Expand Up @@ -71,4 +71,3 @@ Method DeleteFile(item As %String) As %Status
}

}

155 changes: 155 additions & 0 deletions cls/SourceControl/Git/Util/ProductionConflictResolver.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
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") {
isc-tleavitt marked this conversation as resolved.
Show resolved Hide resolved
$$$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", "-m", "Auto-resolved conflict on production class")
isc-tleavitt marked this conversation as resolved.
Show resolved Hide resolved
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
}

}
82 changes: 64 additions & 18 deletions cls/SourceControl/Git/Utils.cls
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,9 @@ ClassMethod AfterUserAction(Type As %Integer, Name As %String, InternalName As %
do ..Sync(Msg)
set Reload = 1
}
} elseif (menuItemName = "GitWebUI") {
// Always force reload as many things could have possibly changed.
set Reload = 1
}
quit $$$OK
}
Expand Down Expand Up @@ -400,14 +403,49 @@ ClassMethod StageAddedFiles()
}

/// Merges the files from the configured branch as part of the Sync operation
ClassMethod MergeDefaultRemoteBranch()
/// Returns true if this resulted in durable changes to the local git repo
ClassMethod MergeDefaultRemoteBranch(Output alert As %String = "") As %Boolean
{
set rebased = 0
set settings = ##class(SourceControl.Git.Settings).%New()
set defaultMergeBranch = settings.defaultMergeBranch
if defaultMergeBranch '= "" {
do ..RunGitWithArgs(.errStream, .outStream, "rebase", defaultMergeBranch)
do ..RunGitWithArgs(.errStream, .outStream, "fetch", "origin", defaultMergeBranch_":"_defaultMergeBranch)
do ..PrintStreams(errStream, outStream)

// Start a transaction so code changes can be rolled back
set initTLevel = $TLevel
try {
TSTART
set code = ..RunGitWithArgs(.errStream, .outStream, "rebase", defaultMergeBranch)
if (code '= 0) {
$$$ThrowStatus($$$ERROR($$$GeneralError,"git rebase reported failure"))
}
set rebased = 1
TCOMMIT
} 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)
if resolver.resolved {
set rebased = 1
TCOMMIT
write " success!"
} else {
write " unable to resolve - "_resolver.errorMessage
}
}
while $TLevel > initTLevel {
TROLLBACK 1
}
if 'rebased {
do ..RunGitCommand("rebase",.errStream, .outStream,"--abort")
do ..PrintStreams(errStream, outStream)
set alert = "WARNING: Remote branch '"_defaultMergeBranch_"' could not be merged due to conflicts. Changes have been pushed to '"_..GetCurrentBranch()_"' and must be resolved in your git remote."
write !,alert,!
}
}
quit rebased
}

/// Converts the DynamicArray into a list and calls the SourceControl.Git.Change RemoveUncommitted method on the newly created list
Expand All @@ -424,7 +462,7 @@ ClassMethod ClearUncommitted(filesWithActions) As %Status
quit $$$OK
}

ClassMethod Sync(Msg As %String) As %Status
ClassMethod Sync(Msg As %String, Output alert As %String) As %Status
{
write !, "Syncing local repository...", !
do ..StageAddedFiles()
Expand All @@ -435,20 +473,24 @@ ClassMethod Sync(Msg As %String) As %Status
do ..Fetch()
do ..Pull()
do ..SyncCommit(Msg)
do ..Push()
do ..MergeDefaultRemoteBranch()
do ..Push()

do ..Push(,1)
if ..MergeDefaultRemoteBranch(.alert) {
do ..Push(,1)
}
}

quit $$$OK
}

ClassMethod Push(remote As %String = "origin") As %Status
ClassMethod Push(remote As %String = "origin", force As %Boolean = 0) As %Status
{
do ##class(SourceControl.Git.Utils).RunGitCommandWithInput("branch",,.errStream,.outstream,"--show-current")
set branchName = outstream.ReadLine(outstream.Size)
do ..RunGitWithArgs(.errStream, .outStream, "push", remote, branchName)
if (force) {
set args($i(args)) = "--force"
}
set args($i(args)) = remote
set args($i(args)) = branchName
do ..RunGitWithArgs(.errStream, .outStream, "push", args...)
do ..PrintStreams(errStream, outStream)
quit $$$OK
}
Expand Down Expand Up @@ -1231,6 +1273,7 @@ ClassMethod ImportCSPFile(InternalName As %String) As %Status
ClassMethod ListItemsInFiles(ByRef itemList, ByRef err) As %Status
{
#define DoNotLoad 1
set res = $$$OK

set mappingFileType = $order($$$SourceMapping(""))
while (mappingFileType '= "") {
Expand Down Expand Up @@ -1565,13 +1608,16 @@ ClassMethod RunGitCommandWithInput(command As %String, inFile As %String = "", O

if (command = "checkout"){
set syncIrisWithDiff = 1
set diffCompare = args(args)
if $data(args) && $data(args(args),diffCompare) {
// no-op
}
} elseif (command = "merge") || (command = "rebase") || (command = "pull"){
set syncIrisWithCommand = 1
set diffCompare = args(args)
if $data(args) && $data(args(args),diffCompare) {
// no-op
}
}


for i=1:1:$get(args) {
if ($data(args(i))) {
set newArgs($increment(newArgs)) = args(i)
Expand Down Expand Up @@ -1601,7 +1647,7 @@ ClassMethod RunGitCommandWithInput(command As %String, inFile As %String = "", O
}
do ..RunGitCommand("fetch", .errorStream, .outputStream)
kill errorStream, outputStream
do ##class(SourceControl.Git.Utils).RunGitCommandWithInput("diff",,.errorStream,.outputStream, diffBase_".."_diffCompare, "--name-status")
do ##class(SourceControl.Git.Utils).RunGitCommandWithInput("diff",,.errorStream,.outputStream, diffBase_$Case(diffCompare,"":"",:"..")_diffCompare, "--name-status")
while (outputStream.AtEnd = 0) {
set file = outputStream.ReadLine()
set modification = ##class(SourceControl.Git.Modification).%New()
Expand Down Expand Up @@ -1646,10 +1692,12 @@ ClassMethod RunGitCommandWithInput(command As %String, inFile As %String = "", O
for stream=errStream,outStream {
set stream.RemoveOnClose = 1
}
do ..PrintStreams(errStream, outStream)

if syncIrisWithDiff {
do ..PrintStreams(errStream, outStream)
$$$ThrowOnError(..SyncIrisWithRepoThroughDiff(.files))
} elseif syncIrisWithCommand {
do ..PrintStreams(errStream, outStream)
$$$ThrowOnError(..SyncIrisWithRepoThroughCommand(.outStream))
}
quit returnCode
Expand Down Expand Up @@ -1704,7 +1752,7 @@ ClassMethod SyncIrisWithRepoThroughDiff(ByRef files) As %Status
set key = $order(files(""))
set deletedFiles = ""
set addedFiles = ""
while(key '= "") {
while (key '= "") {
set modification = files(key)
if (modification.changeType = "D"){
if (modification.internalName '= "") {
Expand All @@ -1715,7 +1763,6 @@ ClassMethod SyncIrisWithRepoThroughDiff(ByRef files) As %Status
if (modification.internalName '= "") {
set addedFiles = addedFiles_","_modification.internalName
set files(key) = modification

}
}
set key = $order(files(key))
Expand Down Expand Up @@ -2383,4 +2430,3 @@ ClassMethod BaselineExport(pCommitMessage = "", pPushToRemote = "") As %Status
}

}

1 change: 1 addition & 0 deletions cls/_zpkg/isc/sc/git/Socket.cls
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,4 @@ Method SendJSON(pObject As %DynamicAbstractObject)
}

}

Loading
Loading