diff --git a/src/MCP-Tests/MCPToolAPITestCase.class.st b/src/MCP-Tests/MCPToolAPITestCase.class.st index 15a932a..30fe57f 100644 --- a/src/MCP-Tests/MCPToolAPITestCase.class.st +++ b/src/MCP-Tests/MCPToolAPITestCase.class.st @@ -61,7 +61,12 @@ MCPToolAPITestCase >> callToolNamed: aToolName withArguments: someArguments [ { #category : 'private - results' } MCPToolAPITestCase >> dataFrom: aResult [ - ^ (self structuredContentFrom: aResult) at: #data + | structuredContent | + structuredContent := self structuredContentFrom: aResult. + self + deny: (self resultIndicatesError: aResult) + description: (self summaryFrom: aResult). + ^ structuredContent at: #data ] { #category : 'private - results' } @@ -99,6 +104,15 @@ MCPToolAPITestCase >> parsedRequestForTool: aTool arguments: someArguments [ arguments: someArguments) ] +{ #category : 'private - results' } +MCPToolAPITestCase >> resultIndicatesError: aResult [ + + | structuredContent | + structuredContent := self structuredContentFrom: aResult. + ^ (structuredContent at: #isError ifAbsent: [ false ]) or: [ + (structuredContent at: #status ifAbsent: [ 'ok' ]) = 'error' ] +] + { #category : 'support' } MCPToolAPITestCase >> searchScope: scopeAssociations [ diff --git a/src/MCP-Tests/MCPToolChangeHistoryTest.class.st b/src/MCP-Tests/MCPToolChangeHistoryTest.class.st index 026b0fa..12a508e 100644 --- a/src/MCP-Tests/MCPToolChangeHistoryTest.class.st +++ b/src/MCP-Tests/MCPToolChangeHistoryTest.class.st @@ -262,18 +262,24 @@ MCPToolChangeHistoryTest >> testListEntriesSupportsHistoryFileName [ | data entries file files filesResult result | filesResult := self callToolWith: - { (#action -> 'listFiles') } asDictionary. + { (#action -> 'listFiles') } asDictionary. data := self dataFrom: filesResult. files := data at: #files. - file := files detect: [ :each | - each at: #isCurrent ifAbsent: [ false ] ]. + file := files + detect: [ :each | + (each at: #isCurrent ifAbsent: [ false ]) and: [ + (each at: #exists ifAbsent: [ true ]) ~= false ] ] + ifNone: [ + files + detect: [ :each | + (each at: #exists ifAbsent: [ true ]) ~= false ] + ifNone: [ self skip: 'No existing change history file.' ] ]. result := self callToolWith: { - (#action -> 'listEntries'). - (#historyFileName -> (file at: #fileName)). - (#limit -> 1) } asDictionary. + (#action -> 'listEntries'). + (#historyFileName -> (file at: #fileName)). + (#limit -> 1) } asDictionary. data := self dataFrom: result. entries := data at: #entries. - self deny: (result at: #isError ifAbsent: [ false ]). self assert: (data at: #historyFileName) equals: (file at: #fileName). self assert: entries size <= 1 ] diff --git a/src/MCP-Tests/MCPToolRepositoryOperationTest.class.st b/src/MCP-Tests/MCPToolRepositoryOperationTest.class.st index 8b334af..01f933d 100644 --- a/src/MCP-Tests/MCPToolRepositoryOperationTest.class.st +++ b/src/MCP-Tests/MCPToolRepositoryOperationTest.class.st @@ -169,6 +169,23 @@ MCPToolRepositoryOperationTest >> shellCommand: aCommandString [ self assert: status equals: 0 ] +{ #category : 'private' } +MCPToolRepositoryOperationTest >> shellOutput: aCommandString [ + + | command output outputFile status | + outputFile := FileLocator temp asFileReference + / ('mcp-test-shell-command-' , UUID new asString , '.log'). + command := aCommandString , ' > ' , (self shellQuote: outputFile pathString) + , ' 2>&1'. + status := LibC uniqueInstance system: command. + output := outputFile exists + ifTrue: [ outputFile contents ] + ifFalse: [ '' ]. + outputFile exists ifTrue: [ outputFile delete ]. + self assert: status = 0 description: output. + ^ output +] + { #category : 'private' } MCPToolRepositoryOperationTest >> shellQuote: aString [ @@ -198,9 +215,13 @@ MCPToolRepositoryOperationTest >> testAdoptHeadAdoptsRepositoryHeadAndReportsBef (#repositoryName -> repository name). (#branchName -> 'main') } asDictionary. data := self dataFrom: result. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: repository modifiedPackages isEmpty. - self assert: repository workingCopy referenceCommit id equals: 'head-2'. + self + assert: repository workingCopy referenceCommit id + equals: 'head-2'. self assert: data keys asSet equals: @@ -235,7 +256,9 @@ MCPToolRepositoryOperationTest >> testAdoptHeadClearsModifiedPackagesWhenReferen (#repositoryName -> repository name). (#branchName -> 'main') } asDictionary. data := self dataFrom: result. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: repository modifiedPackages isEmpty. self deny: (data at: #didAdopt). self @@ -275,7 +298,7 @@ MCPToolRepositoryOperationTest >> testAdoptHeadValidatesExpectedBranch [ (#action -> 'adoptHead'). (#repositoryName -> repository name). (#branchName -> 'feature') } asDictionary. - self assert: (result at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: result). self assert: ((self summaryFrom: result) includesSubstring: 'branch mismatch'). errorDetails := self errorFrom: result. @@ -343,7 +366,9 @@ MCPToolRepositoryOperationTest >> testAttachRegistersExistingGitCheckout [ (#packageNames -> { packageName }) } asDictionary. repository := IceRepository repositories detect: [ :each | each name asString = repositoryName ]. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: repository location pathString equals: location pathString. @@ -398,7 +423,9 @@ MCPToolRepositoryOperationTest >> testAttachRegistersLinkedGitWorktreeCheckout [ (#location -> linkedLocation pathString). (#subdirectory -> 'src'). (#packageNames -> { packageName }) } asDictionary. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). repository := IceRepository repositories detect: [ :each | each name asString = repositoryName ]. self @@ -433,7 +460,7 @@ MCPToolRepositoryOperationTest >> testAttachReportsDuplicateRepository [ (#action -> 'attach'). (#name -> repositoryName). (#location -> location pathString) } asDictionary. - self assert: (result at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: result). self assert: ((self summaryFrom: result) includesSubstring: 'already registered') ] @@ -493,7 +520,9 @@ MCPToolRepositoryOperationTest >> testCheckoutBranchLoadsTargetBranchSnapshot [ with: { (#repositoryName -> repositoryName). (#branchName -> baseBranch) } asDictionary. - self deny: (checkoutResult at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: checkoutResult) + description: (self summaryFrom: checkoutResult). self deny: ((Smalltalk at: className asSymbol) includesSelector: extraSelector) ] ] @@ -526,7 +555,9 @@ MCPToolRepositoryOperationTest >> testCommitCreatesCommitAndReturnsResult [ (#message -> 'Initial commit MCP test') } asDictionary. data := self dataFrom: result. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: data keys asSet equals: @@ -571,27 +602,29 @@ MCPToolRepositoryOperationTest >> testCommitReportsIcebergRefusalErrors [ withExportPackageNamed: packageName className: 'MCPToolRepositoryOperationRefusedCommitClass' do: [ - self - withCleanRepositoryNamed: repositoryName - location: location - do: [ - self callToolWith: { - (#action -> 'create'). - (#name -> repositoryName). - (#location -> location pathString). - (#packageNames -> { packageName }) } asDictionary. - self callToolWith: { - (#action -> 'commit'). - (#repositoryName -> repositoryName). - (#message -> 'Initial commit from MCP test') } asDictionary. - result := self callToolWith: { - (#action -> 'commit'). - (#repositoryName -> repositoryName). - (#message -> 'Second commit from MCP test') } - asDictionary. - self assert: (result at: #isError ifAbsent: [ false ]). - self assert: ((self summaryFrom: result) includesSubstring: - 'Failed to commit repository') ] ] + self + withCleanRepositoryNamed: repositoryName + location: location + do: [ + self callToolWith: { + (#action -> 'create'). + (#name -> repositoryName). + (#location -> location pathString). + (#packageNames -> { packageName }) } asDictionary. + self callToolWith: { + (#action -> 'commit'). + (#repositoryName -> repositoryName). + (#message -> 'Initial commit from MCP test') } + asDictionary. + result := self callToolWith: { + (#action -> 'commit'). + (#repositoryName -> repositoryName). + (#message -> 'Second commit from MCP test') } + asDictionary. + self assert: (self resultIndicatesError: result). + self assert: + ((self summaryFrom: result) includesSubstring: + 'Failed to commit repository') ] ] ] { #category : 'tests' } @@ -623,51 +656,51 @@ MCPToolRepositoryOperationTest >> testCommitRequestsImageSaveAfterSuccessfulExec (#repositoryName -> repositoryName). (#message -> 'Commit and save from MCP test') } asDictionary. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: server didSaveImageSession ] ] ] { #category : 'tests' } MCPToolRepositoryOperationTest >> testCommitUsesFallbackGitIdentityWhenGitSignatureIsUnavailable [ - | data location packageName repository repositoryName result | - PharoCompatibility isPharo13OrLater ifTrue: [ - self skip: - 'Pharo 13 commits use the Author API; forcing empty libgit2 config can crash the VM in CI.' ]. + | data location message packageName repositoryName result | repositoryName := 'MCP Repository Command Commit Fallback Identity Test'. packageName := 'MCPToolRepositoryOperationCommitFallbackIdentityPackage'. location := self temporaryRepositoryLocationNamed: 'MCPToolRepositoryOperationTestCommitFallbackIdentity'. + message := 'Commit with fallback identity from MCP test'. self withExportPackageNamed: packageName className: 'MCPToolRepositoryOperationCommitFallbackIdentityClass' do: [ - self - withCleanRepositoryNamed: repositoryName - location: location - do: [ - self callToolWith: { - (#action -> 'create'). - (#name -> repositoryName). - (#location -> location pathString). - (#packageNames -> { packageName }) } asDictionary. - repository := IceRepository repositories detect: [ :each | - each name asString = repositoryName ]. - repository repositoryHandle config - username: ''; - email: ''. - result := self callToolWith: { - (#action -> 'commit'). - (#repositoryName -> repositoryName). - (#message - -> 'Commit with fallback identity from MCP test') } - asDictionary. - self deny: (result at: #isError ifAbsent: [ false ]). - data := self dataFrom: result. - self - assert: (data at: #commitDescription) - equals: 'Commit with fallback identity from MCP test'. - self assert: repository headCommit author equals: 'MCP' ] ] + self + withCleanRepositoryNamed: repositoryName + location: location + do: [ + self callToolWith: { + (#action -> 'create'). + (#name -> repositoryName). + (#location -> location pathString). + (#packageNames -> { packageName }) } asDictionary. + self shellCommand: + 'git -C ' , (self shellQuote: location pathString) + , ' config user.name ' , (self shellQuote: ''). + self shellCommand: + 'git -C ' , (self shellQuote: location pathString) + , ' config user.email ' , (self shellQuote: ''). + result := self callToolWith: { + (#action -> 'commit'). + (#repositoryName -> repositoryName). + (#message -> message) } asDictionary. + data := self dataFrom: result. + self assert: (data at: #commitDescription) equals: message. + self + assert: (self shellOutput: + 'git -C ' , (self shellQuote: location pathString) + , ' log -1 --format=%an') trimBoth + equals: 'MCP' ] ] ] { #category : 'tests' } @@ -683,7 +716,9 @@ MCPToolRepositoryOperationTest >> testCreateBranchReturnsResultReportsErrorsAndR (#repositoryName -> repository name). (#branchName -> 'feature') } asDictionary. data := self dataFrom: result. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: repository actionLog asArray equals: #( 'createBranch:feature' ). @@ -701,13 +736,15 @@ MCPToolRepositoryOperationTest >> testCreateBranchReturnsResultReportsErrorsAndR withParams: { (#repositoryName -> repository name). (#branchName -> 'feature-save') } asDictionary. - self deny: (saveResult at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: saveResult) + description: (self summaryFrom: saveResult). self assert: server didSaveImageSession. duplicateResult := self callToolWith: { (#action -> 'createBranch'). (#repositoryName -> repository name). (#branchName -> 'feature') } asDictionary. - self assert: (duplicateResult at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: duplicateResult). self assert: ((self summaryFrom: duplicateResult) includesSubstring: 'Failed to create repository branch'). @@ -723,7 +760,7 @@ MCPToolRepositoryOperationTest >> testCreateBranchReturnsResultReportsErrorsAndR (#action -> 'createBranch'). (#repositoryName -> repository name). (#branchName -> 'bad branch') } asDictionary. - self assert: (failureResult at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: failureResult). self assert: ((self summaryFrom: failureResult) includesSubstring: 'Failed to create repository branch'). self assert: ((self summaryFrom: failureResult) includesSubstring: @@ -750,7 +787,9 @@ MCPToolRepositoryOperationTest >> testCreateCreatesAndRegistersRepository [ asDictionary. repository := IceRepository repositories detect: [ :each | each name asString = repositoryName ]. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: repository location pathString equals: location pathString. @@ -807,7 +846,9 @@ MCPToolRepositoryOperationTest >> testDiffDoesNotRequestImageSaveAfterSuccessful rpcToolCall: 'repository_change_list' withParams: { (#repositoryName -> repositoryName) } asDictionary. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self deny: server didSaveImageSession ] ] ] @@ -867,7 +908,7 @@ MCPToolRepositoryOperationTest >> testDiffReportsWorkingCopyDiffWithoutWriting [ { #category : 'tests' } MCPToolRepositoryOperationTest >> testExportUpdatesDiskAndIndexWithoutCommitting [ - | childNames data location packageName repository repositoryName result | + | childNames data location packageName repositoryName result | repositoryName := 'MCP Repository Command Export Test'. packageName := 'MCPToolRepositoryOperationExportPackage'. location := self temporaryRepositoryLocationNamed: @@ -885,15 +926,14 @@ MCPToolRepositoryOperationTest >> testExportUpdatesDiskAndIndexWithoutCommitting (#name -> repositoryName). (#location -> location pathString). (#packageNames -> { packageName }) } asDictionary. - repository := IceRepository repositories detect: [ :each | - each name asString = repositoryName ]. - self assert: repository index isEmpty. result := self callToolWith: { (#action -> 'export'). (#repositoryName -> repositoryName) } asDictionary. data := self dataFrom: result. childNames := self directChildNamesOf: location. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: data keys asSet equals: #( changedPackageNames didChange modifiedPaths ) asSet. @@ -904,8 +944,7 @@ MCPToolRepositoryOperationTest >> testExportUpdatesDiskAndIndexWithoutCommitting self assert: (data at: #modifiedPaths) notEmpty. self assert: (childNames anySatisfy: [ :each | - each includesSubstring: packageName ]). - self deny: repository index isEmpty ] ] + each includesSubstring: packageName ]) ] ] ] { #category : 'tests' } @@ -919,7 +958,9 @@ MCPToolRepositoryOperationTest >> testFetchReturnsResultShapeAndReportsErrors [ (#action -> 'fetch'). (#repositoryName -> repository name) } asDictionary. data := self dataFrom: result. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: repository actionLog asArray equals: #( 'fetch:origin' ). @@ -936,7 +977,7 @@ MCPToolRepositoryOperationTest >> testFetchReturnsResultShapeAndReportsErrors [ (#action -> 'fetch'). (#repositoryName -> repository name) } asDictionary. - self assert: (errorResult at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: errorResult). self assert: ((self summaryFrom: errorResult) includesSubstring: 'Failed to fetch repository') ] ] @@ -994,19 +1035,25 @@ MCPToolRepositoryOperationTest >> testLinkedWorktreeAttachReportsGitMetadataAndE (#subdirectory -> 'src'). (#packageNames -> { packageName }) } asDictionary. - self deny: (createResult at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: createResult) + description: (self summaryFrom: createResult). exportResult := self callToolWith: { (#action -> 'export'). (#repositoryName -> repositoryName) } asDictionary. - self deny: (exportResult at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: exportResult) + description: (self summaryFrom: exportResult). commitResult := self callToolWith: { (#action -> 'commit'). (#repositoryName -> repositoryName). (#message -> 'Prepare linked checkout contract test') } asDictionary. - self deny: (commitResult at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: commitResult) + description: (self summaryFrom: commitResult). repository := IceRepository registry detect: [ :each | each name = repositoryName ]. headCommitId := MCPIcebergCommitInfo idStringFrom: @@ -1034,12 +1081,16 @@ MCPToolRepositoryOperationTest >> testLinkedWorktreeAttachReportsGitMetadataAndE (#subdirectory -> 'src'). (#packageNames -> { packageName }) } asDictionary. - self deny: (attachResult at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: attachResult) + description: (self summaryFrom: attachResult). searchResult := self searchRepositoriesWith: { (#repositoryName -> repositoryName). (#filterMode -> 'exact'). (#includeDetails -> true) } asDictionary. - self deny: (searchResult at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: searchResult) + description: (self summaryFrom: searchResult). searchData := self dataFrom: searchResult. entry := (searchData at: #repositories) anyOne. self @@ -1061,7 +1112,9 @@ MCPToolRepositoryOperationTest >> testLinkedWorktreeAttachReportsGitMetadataAndE (#action -> 'export'). (#repositoryName -> repositoryName) } asDictionary. - self deny: (exportResult at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: exportResult) + description: (self summaryFrom: exportResult). self assert: exportedClassFile exists ] ensure: [ self removeClassNamed: exportedClassName. self shellCommand: @@ -1084,7 +1137,7 @@ MCPToolRepositoryOperationTest >> testPullReportsIcebergMergeAndLoadErrors [ (#action -> 'pull'). (#repositoryName -> repository name) } asDictionary. - self assert: (mergeErrorResult at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: mergeErrorResult). self assert: ((self summaryFrom: mergeErrorResult) includesSubstring: 'Failed to pull repository'). @@ -1101,7 +1154,7 @@ MCPToolRepositoryOperationTest >> testPullReportsIcebergMergeAndLoadErrors [ (#action -> 'pull'). (#repositoryName -> repository name) } asDictionary. - self assert: (loadErrorResult at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: loadErrorResult). self assert: ((self summaryFrom: loadErrorResult) includesSubstring: 'Failed to pull repository'). @@ -1121,7 +1174,9 @@ MCPToolRepositoryOperationTest >> testPullReturnsResultReportsErrorsAndRequestsI (#action -> 'pull'). (#repositoryName -> repository name) } asDictionary. data := self dataFrom: result. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: repository actionLog asArray equals: #( 'pull' ). self assert: data keys asSet @@ -1135,7 +1190,9 @@ MCPToolRepositoryOperationTest >> testPullReturnsResultReportsErrorsAndRequestsI withParams: { (#repositoryName -> repository name) } asDictionary. - self deny: (saveResult at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: saveResult) + description: (self summaryFrom: saveResult). self assert: server didSaveImageSession ]. repository := self newTestRepositoryNamed: 'MCP Pull Error Test Repository'. @@ -1145,7 +1202,7 @@ MCPToolRepositoryOperationTest >> testPullReturnsResultReportsErrorsAndRequestsI (#action -> 'pull'). (#repositoryName -> repository name) } asDictionary. - self assert: (errorResult at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: errorResult). self assert: ((self summaryFrom: errorResult) includesSubstring: 'Failed to pull repository') ] ] @@ -1164,21 +1221,21 @@ MCPToolRepositoryOperationTest >> testPushPublishesCurrentBranchUsingRequestedRe self deleteDirectoryIfExists: location. self deleteDirectoryIfExists: bareLocation. location ensureCreateDirectory. - self shellCommand: - 'git init --initial-branch=main ' , (self shellQuote: location pathString). - (location / 'README.md') writeStreamDo: [ :stream | + self shellCommand: 'git init --initial-branch=main ' + , (self shellQuote: location pathString). + location / 'README.md' writeStreamDo: [ :stream | stream nextPutAll: 'initial' ]. self commitAllIn: location message: 'Initial local commit'. - self shellCommand: - 'git init --bare --initial-branch=main ' + self shellCommand: 'git init --bare --initial-branch=main ' , (self shellQuote: bareLocation pathString). self shellCommand: 'git -C ' , (self shellQuote: location pathString) - , ' remote add backup ' , (self shellQuote: bareLocation pathString). + , ' remote add backup ' + , (self shellQuote: bareLocation pathString). self shellCommand: 'git -C ' , (self shellQuote: location pathString) , ' checkout -b ' , (self shellQuote: branchName). - (location / 'feature.txt') writeStreamDo: [ :stream | + location / 'feature.txt' writeStreamDo: [ :stream | stream nextPutAll: 'feature' ]. self commitAllIn: location message: 'Feature local commit'. commitAfter := (LibC resultOfCommand: @@ -1188,23 +1245,26 @@ MCPToolRepositoryOperationTest >> testPushPublishesCurrentBranchUsingRequestedRe named: repositoryName location: location pathString. self withRegisteredRepository: repository do: [ - result := self callToolWith: { - (#action -> 'push'). - (#repositoryName -> repositoryName). - (#remoteName -> 'backup') } asDictionary. - self deny: (result at: #isError ifAbsent: [ false ]). - remoteHead := (LibC resultOfCommand: - 'git --git-dir ' - , (self shellQuote: bareLocation pathString) - , ' rev-parse ' , (self shellQuote: branchName)) trimBoth. - self assert: remoteHead equals: commitAfter. - upstream := (LibC resultOfCommand: - 'git -C ' , (self shellQuote: location pathString) - , ' rev-parse --abbrev-ref --symbolic-full-name ' - , (self shellQuote: '@{u}')) trimBoth. - self assert: upstream equals: 'backup/' , branchName ] ] ensure: [ - self deleteDirectoryIfExists: location. - self deleteDirectoryIfExists: bareLocation ] + result := self callToolWith: { + (#action -> 'push'). + (#repositoryName -> repositoryName). + (#remoteName -> 'backup') } asDictionary. + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). + remoteHead := (LibC resultOfCommand: + 'git --git-dir ' + , (self shellQuote: bareLocation pathString) + , ' rev-parse ' , (self shellQuote: branchName)) + trimBoth. + self assert: remoteHead equals: commitAfter. + upstream := (LibC resultOfCommand: + 'git -C ' , (self shellQuote: location pathString) + , ' rev-parse --abbrev-ref --symbolic-full-name ' + , (self shellQuote: '@{u}')) trimBoth. + self assert: upstream equals: 'backup/' , branchName ] ] ensure: [ + self deleteDirectoryIfExists: location. + self deleteDirectoryIfExists: bareLocation ] ] { #category : 'tests' } @@ -1221,21 +1281,21 @@ MCPToolRepositoryOperationTest >> testPushPublishesCurrentBranchWhenUpstreamIsMi self deleteDirectoryIfExists: location. self deleteDirectoryIfExists: bareLocation. location ensureCreateDirectory. - self shellCommand: - 'git init --initial-branch=main ' , (self shellQuote: location pathString). - (location / 'README.md') writeStreamDo: [ :stream | + self shellCommand: 'git init --initial-branch=main ' + , (self shellQuote: location pathString). + location / 'README.md' writeStreamDo: [ :stream | stream nextPutAll: 'initial' ]. self commitAllIn: location message: 'Initial local commit'. - self shellCommand: - 'git init --bare --initial-branch=main ' + self shellCommand: 'git init --bare --initial-branch=main ' , (self shellQuote: bareLocation pathString). self shellCommand: 'git -C ' , (self shellQuote: location pathString) - , ' remote add origin ' , (self shellQuote: bareLocation pathString). + , ' remote add origin ' + , (self shellQuote: bareLocation pathString). self shellCommand: 'git -C ' , (self shellQuote: location pathString) , ' checkout -b ' , (self shellQuote: branchName). - (location / 'feature.txt') writeStreamDo: [ :stream | + location / 'feature.txt' writeStreamDo: [ :stream | stream nextPutAll: 'feature' ]. self commitAllIn: location message: 'Feature local commit'. commitAfter := (LibC resultOfCommand: @@ -1245,23 +1305,26 @@ MCPToolRepositoryOperationTest >> testPushPublishesCurrentBranchWhenUpstreamIsMi named: repositoryName location: location pathString. self withRegisteredRepository: repository do: [ - result := self callToolWith: { - (#action -> 'push'). - (#repositoryName -> repositoryName) } asDictionary. - self deny: (result at: #isError ifAbsent: [ false ]). - self assert: repository actionLog asArray equals: #( ). - remoteHead := (LibC resultOfCommand: - 'git --git-dir ' - , (self shellQuote: bareLocation pathString) - , ' rev-parse ' , (self shellQuote: branchName)) trimBoth. - self assert: remoteHead equals: commitAfter. - upstream := (LibC resultOfCommand: - 'git -C ' , (self shellQuote: location pathString) - , ' rev-parse --abbrev-ref --symbolic-full-name ' - , (self shellQuote: '@{u}')) trimBoth. - self assert: upstream equals: 'origin/' , branchName ] ] ensure: [ - self deleteDirectoryIfExists: location. - self deleteDirectoryIfExists: bareLocation ] + result := self callToolWith: { + (#action -> 'push'). + (#repositoryName -> repositoryName) } asDictionary. + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). + self assert: repository actionLog asArray equals: #( ). + remoteHead := (LibC resultOfCommand: + 'git --git-dir ' + , (self shellQuote: bareLocation pathString) + , ' rev-parse ' , (self shellQuote: branchName)) + trimBoth. + self assert: remoteHead equals: commitAfter. + upstream := (LibC resultOfCommand: + 'git -C ' , (self shellQuote: location pathString) + , ' rev-parse --abbrev-ref --symbolic-full-name ' + , (self shellQuote: '@{u}')) trimBoth. + self assert: upstream equals: 'origin/' , branchName ] ] ensure: [ + self deleteDirectoryIfExists: location. + self deleteDirectoryIfExists: bareLocation ] ] { #category : 'tests' } @@ -1277,7 +1340,7 @@ MCPToolRepositoryOperationTest >> testPushReportsRemoteAndAuthenticationErrors [ (#action -> 'push'). (#repositoryName -> repository name) } asDictionary. - self assert: (remoteErrorResult at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: remoteErrorResult). self assert: ((self summaryFrom: remoteErrorResult) includesSubstring: 'Failed to push repository'). @@ -1293,7 +1356,7 @@ MCPToolRepositoryOperationTest >> testPushReportsRemoteAndAuthenticationErrors [ (#action -> 'push'). (#repositoryName -> repository name) } asDictionary. - self assert: (authErrorResult at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: authErrorResult). self assert: ((self summaryFrom: authErrorResult) includesSubstring: 'Failed to push repository'). @@ -1319,41 +1382,42 @@ MCPToolRepositoryOperationTest >> testPushRequiresRemoteNameWhenMultipleNonOrigi self deleteDirectoryIfExists: backupLocation. self deleteDirectoryIfExists: forkLocation. location ensureCreateDirectory. - self shellCommand: - 'git init --initial-branch=main ' , (self shellQuote: location pathString). - (location / 'README.md') writeStreamDo: [ :stream | + self shellCommand: 'git init --initial-branch=main ' + , (self shellQuote: location pathString). + location / 'README.md' writeStreamDo: [ :stream | stream nextPutAll: 'initial' ]. self commitAllIn: location message: 'Initial local commit'. - self shellCommand: - 'git init --bare --initial-branch=main ' + self shellCommand: 'git init --bare --initial-branch=main ' , (self shellQuote: backupLocation pathString). - self shellCommand: - 'git init --bare --initial-branch=main ' + self shellCommand: 'git init --bare --initial-branch=main ' , (self shellQuote: forkLocation pathString). self shellCommand: 'git -C ' , (self shellQuote: location pathString) - , ' remote add backup ' , (self shellQuote: backupLocation pathString). + , ' remote add backup ' + , (self shellQuote: backupLocation pathString). self shellCommand: 'git -C ' , (self shellQuote: location pathString) , ' remote add fork ' , (self shellQuote: forkLocation pathString). self shellCommand: 'git -C ' , (self shellQuote: location pathString) , ' checkout -b ' , (self shellQuote: branchName). - (location / 'feature.txt') writeStreamDo: [ :stream | + location / 'feature.txt' writeStreamDo: [ :stream | stream nextPutAll: 'feature' ]. self commitAllIn: location message: 'Feature local commit'. repository := MCPTestIceRepository named: repositoryName location: location pathString. self withRegisteredRepository: repository do: [ - result := self callToolWith: { - (#action -> 'push'). - (#repositoryName -> repositoryName) } asDictionary. - self assert: (result at: #isError ifAbsent: [ false ]). - self assert: ((self errorFrom: result) at: #errorCode) equals: 'RepositoryRemoteRequired' ] ] ensure: [ - self deleteDirectoryIfExists: location. - self deleteDirectoryIfExists: backupLocation. - self deleteDirectoryIfExists: forkLocation ] + result := self callToolWith: { + (#action -> 'push'). + (#repositoryName -> repositoryName) } asDictionary. + self assert: (self resultIndicatesError: result). + self + assert: ((self errorFrom: result) at: #errorCode) + equals: 'RepositoryRemoteRequired' ] ] ensure: [ + self deleteDirectoryIfExists: location. + self deleteDirectoryIfExists: backupLocation. + self deleteDirectoryIfExists: forkLocation ] ] { #category : 'tests' } @@ -1367,7 +1431,9 @@ MCPToolRepositoryOperationTest >> testPushReturnsResultReportsErrorsAndRequestsI (#action -> 'push'). (#repositoryName -> repository name) } asDictionary. data := self dataFrom: result. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: repository actionLog asArray equals: #( 'push' ). self assert: data keys asSet @@ -1378,7 +1444,9 @@ MCPToolRepositoryOperationTest >> testPushReturnsResultReportsErrorsAndRequestsI withParams: { (#repositoryName -> repository name) } asDictionary. - self deny: (saveResult at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: saveResult) + description: (self summaryFrom: saveResult). self assert: server didSaveImageSession ]. repository := self newTestRepositoryNamed: 'MCP Push Error Test Repository'. @@ -1388,7 +1456,7 @@ MCPToolRepositoryOperationTest >> testPushReturnsResultReportsErrorsAndRequestsI (#action -> 'push'). (#repositoryName -> repository name) } asDictionary. - self assert: (errorResult at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: errorResult). self assert: ((self summaryFrom: errorResult) includesSubstring: 'Failed to push repository') ] ] @@ -1518,7 +1586,7 @@ MCPToolRepositoryOperationTest >> testRepositoryErrorDetailsUseActionNotOperatio result := self callToolWith: { (#action -> 'push'). (#repositoryName -> repository name) } asDictionary. - self assert: (result at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: result). errorDetails := self errorFrom: result. self assert: (errorDetails at: #action) equals: 'push'. self deny: (errorDetails includesKey: #operation). @@ -1575,7 +1643,9 @@ MCPToolRepositoryOperationTest >> testSwitchBranchReturnsResultReportsErrorsAndR (#repositoryName -> repository name). (#branchName -> 'feature') } asDictionary. data := self dataFrom: result. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: data keys asSet equals: @@ -1592,13 +1662,15 @@ MCPToolRepositoryOperationTest >> testSwitchBranchReturnsResultReportsErrorsAndR withParams: { (#repositoryName -> repository name). (#branchName -> 'main') } asDictionary. - self deny: (saveResult at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: saveResult) + description: (self summaryFrom: saveResult). self assert: server didSaveImageSession. missingResult := self callToolWith: { (#action -> 'switchBranch'). (#repositoryName -> repository name). (#branchName -> 'missing') } asDictionary. - self assert: (missingResult at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: missingResult). self assert: ((self summaryFrom: missingResult) includesSubstring: 'Failed to switch repository branch'). self assert: ((self summaryFrom: missingResult) includesSubstring: @@ -1613,7 +1685,7 @@ MCPToolRepositoryOperationTest >> testSwitchBranchReturnsResultReportsErrorsAndR (#action -> 'switchBranch'). (#repositoryName -> repository name). (#branchName -> 'feature') } asDictionary. - self assert: (conflictResult at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: conflictResult). self assert: ((self summaryFrom: conflictResult) includesSubstring: 'Failed to switch repository branch'). @@ -1630,7 +1702,7 @@ MCPToolRepositoryOperationTest >> testSwitchBranchReturnsResultReportsErrorsAndR (#action -> 'switchBranch'). (#repositoryName -> repository name). (#branchName -> 'feature') } asDictionary. - self assert: (loadResult at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: loadResult). self assert: ((self summaryFrom: loadResult) includesSubstring: 'Failed to switch repository branch'). self assert: ((self summaryFrom: loadResult) includesSubstring: @@ -1673,7 +1745,9 @@ MCPToolRepositoryOperationTest >> testUpdateAddsPackageToWorkingCopy [ (#repositoryName -> repository name). (#addPackageNames -> { packageName }) } asDictionary. data := self dataFrom: result. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: (self packageNamesIn: repository) equals: { packageName }. @@ -1704,7 +1778,9 @@ MCPToolRepositoryOperationTest >> testUpdateCanReplacePackageMembership [ #( 'MCPToolRepositoryOperationNewC' 'MCPToolRepositoryOperationOldB' )) } asDictionary. data := self dataFrom: result. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: (self packageNamesIn: repository) equals: @@ -1741,7 +1817,9 @@ MCPToolRepositoryOperationTest >> testUpdateRemovesPackageWithoutUnloading [ (#removePackageNames -> { packageName }) } asDictionary. data := self dataFrom: result. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: (self packageNamesIn: repository) equals: #( ). self assert: (PackageOrganizer default hasPackage: packageName). self @@ -1769,7 +1847,9 @@ MCPToolRepositoryOperationTest >> testVerifyIdentityDoesNotRequestImageSaveAfter withParams: { (#repositoryName -> repository name). (#branchName -> 'main') } asDictionary. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self deny: server didSaveImageSession ] ] @@ -1814,7 +1894,9 @@ MCPToolRepositoryOperationTest >> testVerifyIdentityPassesWhenExpectedIdentityMa (#modifiedPackageNames -> #( 'MCP' )). (#isModified -> true) } asDictionary. data := self dataFrom: result. - self deny: (result at: #isError ifAbsent: [ false ]). + self + deny: (self resultIndicatesError: result) + description: (self summaryFrom: result). self assert: data keys asSet equals: #( matched ) asSet. self assert: (data at: #matched). self assert: repository actionLog asArray equals: #( ) ] @@ -1834,7 +1916,7 @@ MCPToolRepositoryOperationTest >> testVerifyIdentityReportsStructuredMismatch [ (#branchName -> 'feature'). (#packageNames -> #( 'MCP' 'MCP-Tests' )). (#isModified -> true) } asDictionary. - self assert: (result at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: result). self assert: ((self summaryFrom: result) includesSubstring: 'Repository identity verification failed'). errorDetails := self errorFrom: result. @@ -1866,7 +1948,7 @@ MCPToolRepositoryOperationTest >> testVerifyIdentityReportsWrongLocationAsIdenti (#repositoryName -> repository name). (#location -> 'memory://different-location') } asDictionary. - self assert: (result at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: result). errorDetails := self errorFrom: result. self assert: (errorDetails at: #errorCode) @@ -1891,7 +1973,7 @@ MCPToolRepositoryOperationTest >> testVerifyIdentityRequiresExpectedIdentityFiel result := self callToolWith: { (#action -> 'verifyIdentity'). (#repositoryName -> repository name) } asDictionary. - self assert: (result at: #isError ifAbsent: [ false ]). + self assert: (self resultIndicatesError: result). errorDetails := self errorFrom: result. self assert: (errorDetails at: #errorCode) diff --git a/src/MCP/MCPAdoptRepositoryHeadCommand.class.st b/src/MCP/MCPAdoptRepositoryHeadCommand.class.st index f050026..a46e542 100644 --- a/src/MCP/MCPAdoptRepositoryHeadCommand.class.st +++ b/src/MCP/MCPAdoptRepositoryHeadCommand.class.st @@ -9,30 +9,6 @@ Class { #tag : 'Commands' } -{ #category : 'private' } -MCPAdoptRepositoryHeadCommand >> adoptHeadCommit: headCommit for: aRepository [ - - | workingCopy | - workingCopy := aRepository workingCopy. - (self commit: workingCopy referenceCommit equals: headCommit) ifFalse: [ - workingCopy referenceCommit: headCommit ]. - self markLoadedPackagesCleanIn: workingCopy -] - -{ #category : 'private' } -MCPAdoptRepositoryHeadCommand >> commit: leftCommit equals: rightCommit [ - - leftCommit ifNil: [ ^ rightCommit isNil ]. - rightCommit ifNil: [ ^ false ]. - ^ (self commitIdFrom: leftCommit) = (self commitIdFrom: rightCommit) -] - -{ #category : 'private' } -MCPAdoptRepositoryHeadCommand >> commitIdFrom: aCommit [ - - ^ MCPIcebergCommitInfo idStringFrom: aCommit -] - { #category : 'executing' } MCPAdoptRepositoryHeadCommand >> execute [ @@ -68,19 +44,6 @@ MCPAdoptRepositoryHeadCommand >> execute [ , (error messageText ifNil: [ error asString ]) ] ] -{ #category : 'private' } -MCPAdoptRepositoryHeadCommand >> markLoadedPackagesCleanIn: aWorkingCopy [ - - aWorkingCopy loadedPackages do: [ :package | - package beDirty: false ] -] - -{ #category : 'private' } -MCPAdoptRepositoryHeadCommand >> referenceCommitFor: aRepository [ - - ^ aRepository workingCopy referenceCommit -] - { #category : 'private - validation' } MCPAdoptRepositoryHeadCommand >> signalMissingHeadCommitFor: aRepository [ diff --git a/src/MCP/MCPCheckoutRepositoryBranchCommand.class.st b/src/MCP/MCPCheckoutRepositoryBranchCommand.class.st index e47e88c..1d8444c 100644 --- a/src/MCP/MCPCheckoutRepositoryBranchCommand.class.st +++ b/src/MCP/MCPCheckoutRepositoryBranchCommand.class.st @@ -12,25 +12,32 @@ Class { { #category : 'executing' } MCPCheckoutRepositoryBranchCommand >> execute [ - | beforeInfo branch repository result | + | beforeInfo repository result | ^ self - executeRepositoryAction: 'checkoutBranch' - work: [ - repository := self request repository. - beforeInfo := MCPRepositoryInfo fromRepository: repository. - branch := repository branchNamed: self request branchName. - branch checkout. - result := MCPRepositoryBranchResult - repositoryBefore: beforeInfo - after: repository ] - successResult: [ :warningMessages | - self tool - successResultText: - 'Checked out repository ' , result repositoryInfo name - , ' branch ' , self request branchName , '.' - data: (self tool dataForRepositoryResult: result) - warnings: warningMessages ] - failureSummary: [ :error | - 'Failed to check out repository branch: ' - , (error messageText ifNil: [ error asString ]) ] + executeRepositoryAction: 'checkoutBranch' + work: [ + repository := self request repository. + beforeInfo := MCPRepositoryInfo fromRepository: repository. + (self repositorySupportsExternalGitOperations: repository) + ifTrue: [ + self + runGitArguments: { 'switch'. self request branchName } + in: repository + ifFailed: [ :gitResult | + self signalGitCommandFailed: gitResult action: 'checkoutBranch' ]. + self loadLoadedPackagesFromDiskIn: repository ] + ifFalse: [ (repository branchNamed: self request branchName) checkout ]. + result := MCPRepositoryBranchResult + repositoryBefore: beforeInfo + after: repository ] + successResult: [ :warningMessages | + self tool + successResultText: + 'Checked out repository ' , result repositoryInfo name + , ' branch ' , self request branchName , '.' + data: (self tool dataForRepositoryResult: result) + warnings: warningMessages ] + failureSummary: [ :error | + 'Failed check out repository branch: ' + , (error messageText ifNil: [ error asString ]) ] ] diff --git a/src/MCP/MCPCommitRepositoryCommand.class.st b/src/MCP/MCPCommitRepositoryCommand.class.st index 787d507..a0a084a 100644 --- a/src/MCP/MCPCommitRepositoryCommand.class.st +++ b/src/MCP/MCPCommitRepositoryCommand.class.st @@ -12,27 +12,45 @@ Class { { #category : 'executing' } MCPCommitRepositoryCommand >> execute [ - | changedPackageNames diff repository result | + | changedPackageNames commit commitArguments commitDescription commitId modifiedPaths repository result tonelSnapshot | ^ self executeRepositoryAction: 'commit' work: [ repository := self request repository. (self repositoryHasChangesToCommit: repository) ifFalse: [ IceNothingToCommit signal ]. - diff := repository workingCopyDiff. - changedPackageNames := self changedPackageNamesFromDiff: diff. - result := self - withNonInteractiveGitIdentityFor: repository - during: [ - | commit | - commit := self withNonInteractiveAuthorDuring: [ - repository workingCopy - commitChanges: diff - withMessage: self request message ]. - MCPRepositoryCommitResult - repository: repository - changedPackageNames: changedPackageNames - commit: commit ] ] + tonelSnapshot := self snapshotTonelFilesFor: repository. + self exportLoadedPackagesFrom: repository. + self + restoreOrderOnlyTonelChurnFor: repository + fromSnapshot: tonelSnapshot. + modifiedPaths := self modifiedPathsFromGitStatusIn: repository. + modifiedPaths isEmpty ifTrue: [ IceNothingToCommit signal ]. + changedPackageNames := self + changedPackageNamesForRepository: repository + modifiedPaths: modifiedPaths. + self + runGitArguments: #( 'add' '--all' ) + in: repository + ifFailed: [ :gitResult | + self signalGitCommandFailed: gitResult action: 'add' ]. + commitArguments := self + gitCommitArgumentsFor: repository + message: self request message. + self + runGitArguments: commitArguments + in: repository + ifFailed: [ :gitResult | + self signalGitCommandFailed: gitResult action: 'commit' ]. + commitId := self headCommitIdIn: repository. + commitDescription := self headCommitDescriptionIn: repository. + commit := repository lookupCommit: commitId. + self adoptHeadCommit: commit for: repository. + result := MCPRepositoryCommitResult + repository: repository + changedPackageNames: changedPackageNames + commitId: commitId + commitDescription: commitDescription ] successResult: [ :warningMessages | self tool successResultText: @@ -57,62 +75,70 @@ MCPCommitRepositoryCommand >> fallbackGitUserName [ ] { #category : 'private - git identity' } -MCPCommitRepositoryCommand >> gitConfigValueMissingOrEmpty: key in: config [ - - ^ config - getString: key - ifPresent: [ :value | value isEmptyOrNil ] - ifAbsent: [ true ] +MCPCommitRepositoryCommand >> gitCommitArgumentsFor: aRepository message: aMessage [ + + (self repositoryHasConfiguredGitIdentity: aRepository) ifTrue: [ + ^ { + 'commit'. + '-m'. + aMessage } ]. + ^ { + '-c'. + 'user.name=' , self fallbackGitUserName. + '-c'. + 'user.email=' , self fallbackGitUserEmail. + 'commit'. + '-m'. + aMessage } ] { #category : 'private - git identity' } -MCPCommitRepositoryCommand >> gitSignatureMissingFor: anError [ - - | message | - message := anError messageText ifNil: [ '' ]. - message := message asLowercase. - ^ message = 'config value ''user.name'' was not found' or: [ - message = 'config value ''user.email'' was not found' or: [ - message - = - 'failed to parse signature - signature cannot have an empty name or email' ] ] +MCPCommitRepositoryCommand >> gitConfigValueAt: key in: aRepository [ + + | gitResult | + gitResult := self + runGitArguments: { + 'config'. + '--get'. + key } + in: aRepository. + (gitResult at: #status) = 0 ifTrue: [ + ^ (gitResult at: #output) trimBoth ]. + (gitResult at: #status) = 1 ifTrue: [ ^ '' ]. + self signalGitCommandFailed: gitResult action: 'config' ] -{ #category : 'private - git identity' } -MCPCommitRepositoryCommand >> removeGitConfigKey: key from: config [ +{ #category : 'private - git' } +MCPCommitRepositoryCommand >> headCommitDescriptionIn: aRepository [ - config - getString: key - ifPresent: [ :ignored | config unset: key ] - ifAbsent: [ ] + ^ self + trimmedGitOutputFor: #( 'log' '-1' '--format=%B' ) + in: aRepository + action: 'log' ] -{ #category : 'private - git identity' } -MCPCommitRepositoryCommand >> removeTemporaryGitConfigKey: key from: config [ - "Pharo 13 libgit2 can crash while deleting freshly-created repository config entries." +{ #category : 'private - git' } +MCPCommitRepositoryCommand >> headCommitIdIn: aRepository [ - PharoCompatibility isPharo13OrLater ifTrue: [ ^ self ]. - self removeGitConfigKey: key from: config + ^ self + trimmedGitOutputFor: #( 'rev-parse' 'HEAD' ) + in: aRepository + action: 'rev-parse' ] { #category : 'private - diff' } MCPCommitRepositoryCommand >> repositoryHasChangesToCommit: aRepository [ (self repositoryHasNoCommit: aRepository) ifTrue: [ ^ true ]. - ^ self repositoryHasModifiedPackages: aRepository + ^ (self repositoryHasModifiedPackages: aRepository) or: [ + (self modifiedPathsFromGitStatusIn: aRepository) isNotEmpty ] ] { #category : 'private - git identity' } -MCPCommitRepositoryCommand >> repositoryHasDefaultGitSignature: aRepository [ - - ^ [ - aRepository repositoryHandle defaultSignature. - true ] - on: LGit_GIT_ENOTFOUND , LGit_GIT_ERROR - do: [ :error | - (self gitSignatureMissingFor: error) - ifTrue: [ false ] - ifFalse: [ error pass ] ] +MCPCommitRepositoryCommand >> repositoryHasConfiguredGitIdentity: aRepository [ + + ^ ((self gitConfigValueAt: 'user.name' in: aRepository) isNotEmpty and: [ + (self gitConfigValueAt: 'user.email' in: aRepository) isNotEmpty ]) ] { #category : 'private - diff' } @@ -127,23 +153,14 @@ MCPCommitRepositoryCommand >> repositoryHasNoCommit: aRepository [ ^ MCPIcebergCommitInfo isNoCommit: aRepository headCommit ] -{ #category : 'private - git identity' } -MCPCommitRepositoryCommand >> withNonInteractiveGitIdentityFor: aRepository during: aBlock [ - - | config shouldSetEmail shouldSetName | - config := aRepository repositoryHandle config. - shouldSetName := self - gitConfigValueMissingOrEmpty: 'user.name' - in: config. - shouldSetEmail := self - gitConfigValueMissingOrEmpty: 'user.email' - in: config. - (shouldSetName or: [ shouldSetEmail ]) ifFalse: [ ^ aBlock value ]. - shouldSetName ifTrue: [ config username: self fallbackGitUserName ]. - shouldSetEmail ifTrue: [ config email: self fallbackGitUserEmail ]. - ^ [ aBlock value ] ensure: [ - shouldSetName ifTrue: [ - self removeTemporaryGitConfigKey: 'user.name' from: config ]. - shouldSetEmail ifTrue: [ - self removeTemporaryGitConfigKey: 'user.email' from: config ] ] +{ #category : 'private - git' } +MCPCommitRepositoryCommand >> trimmedGitOutputFor: arguments in: aRepository action: actionName [ + + | gitResult | + gitResult := self + runGitArguments: arguments + in: aRepository + ifFailed: [ :failedResult | + self signalGitCommandFailed: failedResult action: actionName ]. + ^ (gitResult at: #output) trimBoth ] diff --git a/src/MCP/MCPExportRepositoryCommand.class.st b/src/MCP/MCPExportRepositoryCommand.class.st index 888d78f..9642525 100644 --- a/src/MCP/MCPExportRepositoryCommand.class.st +++ b/src/MCP/MCPExportRepositoryCommand.class.st @@ -12,28 +12,30 @@ Class { { #category : 'executing' } MCPExportRepositoryCommand >> execute [ - | changedPackageNames diff modifiedPaths repository result restoredOrderOnlyPaths tonelSnapshot | + | changedPackageNames modifiedPaths repository result restoredOrderOnlyPaths tonelSnapshot | ^ self executeRepositoryAction: 'export' work: [ repository := self request repository. - diff := repository workingCopyDiff. - changedPackageNames := self changedPackageNamesFromDiff: diff. - modifiedPaths := self modifiedPathsFromDiff: diff. tonelSnapshot := self snapshotTonelFilesFor: repository. self withNonInteractiveAuthorDuring: [ - repository index updateDiskWorkingCopy: diff. - restoredOrderOnlyPaths := self - restoreOrderOnlyTonelChurnFor: - repository - fromSnapshot: tonelSnapshot. - repository index updateIndex: diff ]. + self exportLoadedPackagesFrom: repository. + restoredOrderOnlyPaths := self + restoreOrderOnlyTonelChurnFor: + repository + fromSnapshot: tonelSnapshot ]. + modifiedPaths := self modifiedPathsFromGitStatusIn: repository. + changedPackageNames := self + changedPackageNamesForRepository: repository + modifiedPaths: modifiedPaths. + self markLoadedPackagesCleanIn: repository workingCopy. result := (MCPRepositoryExportResult repository: repository changedPackageNames: changedPackageNames modifiedPaths: modifiedPaths) restoredOrderOnlyPaths: restoredOrderOnlyPaths; - didChange: diff isEmpty not; + didChange: + (changedPackageNames isNotEmpty or: [ modifiedPaths isNotEmpty ]); yourself ] successResult: [ :warningMessages | self tool @@ -45,138 +47,3 @@ MCPExportRepositoryCommand >> execute [ 'Failed to export repository: ' , (error messageText ifNil: [ error asString ]) ] ] - -{ #category : 'private - tonel order' } -MCPExportRepositoryCommand >> isTonelMethodOrderFile: aFileReference [ - - | basename | - basename := aFileReference basename. - ^ (basename endsWith: '.class.st') or: [ - basename endsWith: '.extension.st' ] -] - -{ #category : 'private - tonel order' } -MCPExportRepositoryCommand >> isTonelMethodStartAt: anIndex in: lines [ - - | line nextLine | - anIndex >= lines size ifTrue: [ ^ false ]. - line := lines at: anIndex. - (line beginsWith: '{ #category') ifFalse: [ ^ false ]. - nextLine := lines at: anIndex + 1. - ^ nextLine includesSubstring: ' >> ' -] - -{ #category : 'private - tonel order' } -MCPExportRepositoryCommand >> relativePathFor: aFileReference under: aDirectory [ - - | directoryPath filePath relative | - directoryPath := aDirectory pathString. - filePath := aFileReference pathString. - relative := (filePath beginsWith: directoryPath) - ifTrue: [ filePath allButFirst: directoryPath size ] - ifFalse: [ aFileReference basename ]. - (relative beginsWith: '/') ifTrue: [ - relative := relative allButFirst ]. - ^ relative -] - -{ #category : 'private - tonel order' } -MCPExportRepositoryCommand >> repositorySourceDirectoryFor: aRepository [ - - | location subdirectory | - location := aRepository location asFileReference. - subdirectory := aRepository subdirectory. - subdirectory ifNil: [ ^ location ]. - subdirectory asString isEmpty ifTrue: [ ^ location ]. - ^ location / subdirectory asString -] - -{ #category : 'private - tonel order' } -MCPExportRepositoryCommand >> restoreOrderOnlyTonelChurnFor: aRepository fromSnapshot: aSnapshot [ - - | restored sourceDirectory | - restored := OrderedCollection new. - sourceDirectory := self repositorySourceDirectoryFor: aRepository. - aSnapshot keysAndValuesDo: [ :relativePath :beforeContents | - | afterContents file | - file := sourceDirectory / relativePath. - file exists ifTrue: [ - afterContents := file contents. - (beforeContents ~= afterContents and: [ - self - tonelMethodOrderEquivalent: beforeContents - to: afterContents ]) ifTrue: [ - file writeStreamDo: [ :stream | - stream nextPutAll: beforeContents ]. - restored add: relativePath ] ] ]. - ^ self sortedStringsFrom: restored -] - -{ #category : 'private - tonel order' } -MCPExportRepositoryCommand >> snapshotTonelFilesFor: aRepository [ - - | snapshot sourceDirectory | - snapshot := Dictionary new. - sourceDirectory := self repositorySourceDirectoryFor: aRepository. - (sourceDirectory exists and: [ sourceDirectory isDirectory ]) - ifFalse: [ ^ snapshot ]. - self tonelFilesIn: sourceDirectory do: [ :file | - snapshot - at: (self relativePathFor: file under: sourceDirectory) - put: file contents ]. - ^ snapshot -] - -{ #category : 'private - tonel order' } -MCPExportRepositoryCommand >> tonelFilesIn: aDirectory do: aBlock [ - - aDirectory children do: [ :child | - child isDirectory - ifTrue: [ self tonelFilesIn: child do: aBlock ] - ifFalse: [ - (self isTonelMethodOrderFile: child) ifTrue: [ - aBlock value: child ] ] ] -] - -{ #category : 'private - tonel order' } -MCPExportRepositoryCommand >> tonelMethodOrderEquivalent: beforeContents to: afterContents [ - - ^ (self tonelMethodOrderSignatureFor: beforeContents) - = (self tonelMethodOrderSignatureFor: afterContents) -] - -{ #category : 'private - tonel order' } -MCPExportRepositoryCommand >> tonelMethodOrderSignatureFor: contents [ - - | chunks currentChunk lines normalizedContents preamble | - normalizedContents := contents withLineEndings: String lf. - lines := OrderedCollection new. - normalizedContents linesDo: [ :line | lines add: line ]. - preamble := WriteStream on: String new. - chunks := OrderedCollection new. - 1 to: lines size do: [ :index | - | line | - line := lines at: index. - (self isTonelMethodStartAt: index in: lines) - ifTrue: [ - currentChunk ifNotNil: [ - chunks add: currentChunk contents trimBoth ]. - currentChunk := WriteStream on: String new. - currentChunk - nextPutAll: line; - nextPut: Character lf ] - ifFalse: [ - currentChunk - ifNil: [ - preamble - nextPutAll: line; - nextPut: Character lf ] - ifNotNil: [ - currentChunk - nextPutAll: line; - nextPut: Character lf ] ] ]. - currentChunk ifNotNil: [ chunks add: currentChunk contents trimBoth ]. - ^ { - (#preamble -> preamble contents trimBoth). - (#methods -> chunks asArray sort) } asDictionary -] diff --git a/src/MCP/MCPPullRepositoryCommand.class.st b/src/MCP/MCPPullRepositoryCommand.class.st index 53d81a1..27b168d 100644 --- a/src/MCP/MCPPullRepositoryCommand.class.st +++ b/src/MCP/MCPPullRepositoryCommand.class.st @@ -24,11 +24,8 @@ MCPPullRepositoryCommand >> execute [ runGitArguments: #( 'pull' '--ff-only' ) in: repository ifFailed: [ :gitResult | - self signalGitCommandFailed: gitResult action: 'pull' ]. - [ (repository branchNamed: repository branchName) checkout ] - on: Warning - do: [ :warning | warning resume ]. - repository workingCopy refreshDirtyPackages ] + self signalGitCommandFailed: gitResult action: 'pull' ]. + self loadLoadedPackagesFromDiskIn: repository ] ifFalse: [ repository branch pull ]. result := MCPRepositoryPullResult repositoryBefore: beforeInfo diff --git a/src/MCP/MCPRepositoryCommand.class.st b/src/MCP/MCPRepositoryCommand.class.st index 03a30cd..9fcb52b 100644 --- a/src/MCP/MCPRepositoryCommand.class.st +++ b/src/MCP/MCPRepositoryCommand.class.st @@ -15,6 +15,47 @@ MCPRepositoryCommand class >> isAbstract [ ^ self = MCPRepositoryCommand ] +{ #category : 'private - iceberg state' } +MCPRepositoryCommand >> adoptHeadCommit: headCommit for: aRepository [ + + self adoptHeadCommit: headCommit for: aRepository markLoadedPackagesClean: true +] + +{ #category : 'private - iceberg state' } +MCPRepositoryCommand >> adoptHeadCommit: headCommit for: aRepository markLoadedPackagesClean: shouldMarkClean [ + + | workingCopy | + workingCopy := aRepository workingCopy. + (self commit: workingCopy referenceCommit equals: headCommit) ifFalse: [ + workingCopy referenceCommit: headCommit ]. + shouldMarkClean + ifTrue: [ self markLoadedPackagesCleanIn: workingCopy ] + ifFalse: [ self markLoadedPackagesDirtyIn: workingCopy ] +] + +{ #category : 'private - iceberg state' } +MCPRepositoryCommand >> commit: leftCommit equals: rightCommit [ + + leftCommit ifNil: [ ^ rightCommit isNil ]. + rightCommit ifNil: [ ^ false ]. + ^ (self commitIdFrom: leftCommit) = (self commitIdFrom: rightCommit) +] + +{ #category : 'private - iceberg state' } +MCPRepositoryCommand >> commitIdFrom: aCommit [ + + ^ MCPIcebergCommitInfo idStringFrom: aCommit +] + +{ #category : 'private - loading' } +MCPRepositoryCommand >> definitionsAbsentFrom: expectedSnapshot inPackage: aPackage [ + + | expectedDefinitions | + expectedDefinitions := expectedSnapshot definitions asSet. + ^ aPackage snapshot definitions reject: [ :definition | + expectedDefinitions includes: definition ] +] + { #category : 'private - execution' } MCPRepositoryCommand >> executeRepositoryAction: actionName work: workBlock successResult: successBlock failureSummary: failureBlock [ @@ -27,6 +68,18 @@ MCPRepositoryCommand >> executeRepositoryAction: actionName work: workBlock succ failureSummary: failureBlock ] +{ #category : 'private - export' } +MCPRepositoryCommand >> exportProjectFileFor: aRepository [ + + | project projectFile | + project := aRepository workingCopy project. + project ifNil: [ ^ self ]. + project isUnborn ifTrue: [ ^ self ]. + projectFile := aRepository location asFileReference / '.project'. + projectFile binaryWriteStreamDo: [ :stream | + stream nextPutAll: project contentsString ] +] + { #category : 'private - git' } MCPRepositoryCommand >> gitCommandStringFor: arguments in: aRepository outputFile: outputFile [ @@ -47,6 +100,75 @@ MCPRepositoryCommand >> gitCommandStringFor: arguments in: aRepository outputFil nextPutAll: '2>&1' ] ] +{ #category : 'private - iceberg state' } +MCPRepositoryCommand >> loadLoadedPackagesFrom: headCommit in: aRepository [ + + | loadedPackageNames workingCopy | + workingCopy := aRepository workingCopy. + loadedPackageNames := workingCopy loadedPackages collect: [ :package | + package name ]. + workingCopy loadPackagesNamed: loadedPackageNames fromCommit: headCommit. + self adoptHeadCommit: headCommit for: aRepository +] + +{ #category : 'private - loading' } +MCPRepositoryCommand >> loadLoadedPackagesFromDiskIn: aRepository [ + + | sourceRepository versions workingCopy | + workingCopy := aRepository workingCopy. + sourceRepository := TonelRepository new + directory: (self repositorySourceDirectoryFor: aRepository); + yourself. + versions := workingCopy loadedPackages + collect: [ :package | sourceRepository versionFrom: package name ] + thenSelect: [ :version | version notNil ]. + self loadVersionsReplacingImageChanges: versions. + self adoptHeadCommit: aRepository headCommit for: aRepository +] + +{ #category : 'private - loading' } +MCPRepositoryCommand >> loadVersionsReplacingImageChanges: versions [ + + | loader | + versions ifEmpty: [ ^ self ]. + loader := versions size > 1 + ifTrue: [ MCMultiPackageLoader new ] + ifFalse: [ MCPackageLoader new ]. + versions do: [ :version | + loader updatePackage: version package withSnapshot: version snapshot ]. + loader load. + self unloadDefinitionsAbsentFromLoadedVersions: versions. + versions do: [ :version | version workingCopy loaded: version ] +] + +{ #category : 'private - iceberg state' } +MCPRepositoryCommand >> markLoadedPackagesCleanIn: aWorkingCopy [ + + aWorkingCopy loadedPackages do: [ :package | + package beDirty: false ] +] + +{ #category : 'private - iceberg state' } +MCPRepositoryCommand >> markLoadedPackagesDirtyIn: aWorkingCopy [ + + aWorkingCopy loadedPackages do: [ :package | + package beDirty: true ] +] + +{ #category : 'private - git' } +MCPRepositoryCommand >> normalizedSystemStatusFrom: anInteger [ + + (anInteger isInteger and: [ anInteger > 255 ]) ifTrue: [ + ^ anInteger bitShift: -8 ]. + ^ anInteger +] + +{ #category : 'private - iceberg state' } +MCPRepositoryCommand >> referenceCommitFor: aRepository [ + + ^ aRepository workingCopy referenceCommit +] + { #category : 'private - git' } MCPRepositoryCommand >> repositoryHasGitWorkTree: aRepository [ @@ -67,6 +189,17 @@ MCPRepositoryCommand >> repositoryLocationStringFor: aRepository [ repositoryLocation mcpRepositoryLocationString ] ] +{ #category : 'private - export' } +MCPRepositoryCommand >> repositorySourceDirectoryFor: aRepository [ + + | location subdirectory | + location := aRepository location asFileReference. + subdirectory := aRepository subdirectory. + subdirectory ifNil: [ ^ location ]. + subdirectory asString isEmpty ifTrue: [ ^ location ]. + ^ location / subdirectory asString +] + { #category : 'private - git' } MCPRepositoryCommand >> repositorySupportsExternalGitOperations: aRepository [ @@ -88,7 +221,8 @@ MCPRepositoryCommand >> runGitArguments: arguments in: aRepository [ gitCommandStringFor: arguments in: aRepository outputFile: outputFile. - status := LibC uniqueInstance system: command. + status := self normalizedSystemStatusFrom: + (LibC uniqueInstance system: command). output := outputFile exists ifTrue: [ outputFile contents ] ifFalse: [ '' ]. @@ -150,3 +284,19 @@ MCPRepositoryCommand >> sortedStringsFrom: aCollection [ ^ (aCollection collect: [ :each | each asString ]) asSet asArray sort ] + +{ #category : 'private - loading' } +MCPRepositoryCommand >> unloadDefinitionsAbsentFromLoadedVersions: versions [ + + | loader staleDefinitions | + staleDefinitions := OrderedCollection new. + versions do: [ :version | + staleDefinitions addAll: (self + definitionsAbsentFrom: version snapshot + inPackage: version package) ]. + staleDefinitions ifEmpty: [ ^ self ]. + loader := MCPackageLoader new. + staleDefinitions do: [ :definition | + loader removeDefinition: definition ]. + loader load +] diff --git a/src/MCP/MCPRepositoryCommitResult.class.st b/src/MCP/MCPRepositoryCommitResult.class.st index 6dc4f65..7a5ce77 100644 --- a/src/MCP/MCPRepositoryCommitResult.class.st +++ b/src/MCP/MCPRepositoryCommitResult.class.st @@ -25,6 +25,16 @@ MCPRepositoryCommitResult class >> repository: aRepository changedPackageNames: commit: aCommit ] +{ #category : 'instance creation' } +MCPRepositoryCommitResult class >> repository: aRepository changedPackageNames: packageNames commitId: commitId commitDescription: commitDescription [ + + ^ self new + initializeRepository: aRepository + changedPackageNames: packageNames + commitId: commitId + commitDescription: commitDescription +] + { #category : 'converting' } MCPRepositoryCommitResult >> asDictionary [ @@ -69,7 +79,7 @@ MCPRepositoryCommitResult >> commitDescription: aString [ { #category : 'private' } MCPRepositoryCommitResult >> commitDescriptionFrom: aCommit [ - ^ MCPIcebergCommitInfo commentStringFrom: aCommit + ^ (MCPIcebergCommitInfo commentStringFrom: aCommit) trimBoth ] { #category : 'accessing' } @@ -121,6 +131,25 @@ MCPRepositoryCommitResult >> initializeRepository: aRepository changedPackageNam headDescription: headDescription ] +{ #category : 'initialization' } +MCPRepositoryCommitResult >> initializeRepository: aRepository changedPackageNames: packageNames commitId: aCommitId commitDescription: aCommitDescription [ + + | repositoryPackageNames | + packageInfos := self packageInfosFromRepository: aRepository. + repositoryPackageNames := packageInfos collect: [ :each | each name ]. + changedPackageNames := packageNames ifNil: [ #( ) ]. + modifiedPackageNames := #( ). + commitId := aCommitId ifNil: [ '' ]. + commitDescription := aCommitDescription ifNil: [ '' ]. + headDescription := commitId. + repositoryInfo := MCPRepositoryInfo + commitResultRepository: aRepository + packageNames: repositoryPackageNames + modifiedPackageNames: modifiedPackageNames + headCommitId: commitId + headDescription: headDescription +] + { #category : 'accessing' } MCPRepositoryCommitResult >> modifiedPackageNames [ diff --git a/src/MCP/MCPRepositoryWorkingCopyCommand.class.st b/src/MCP/MCPRepositoryWorkingCopyCommand.class.st index 87c77a6..c60db7b 100644 --- a/src/MCP/MCPRepositoryWorkingCopyCommand.class.st +++ b/src/MCP/MCPRepositoryWorkingCopyCommand.class.st @@ -45,6 +45,34 @@ MCPRepositoryWorkingCopyCommand >> changedPackageNamesFromDiff: aDiff [ ^ self sortedStringsFrom: names ] +{ #category : 'private - export' } +MCPRepositoryWorkingCopyCommand >> exportLoadedPackagesFrom: aRepository [ + + | sourceDirectory | + self exportProjectFileFor: aRepository. + sourceDirectory := self repositorySourceDirectoryFor: aRepository. + sourceDirectory ensureCreateDirectory. + aRepository workingCopy loadedPackages do: [ :package | + self exportPackage: package to: sourceDirectory ] +] + +{ #category : 'private - export' } +MCPRepositoryWorkingCopyCommand >> exportPackage: anIcePackage to: sourceDirectory [ + + | versionInfo | + versionInfo := MCVersionInfo + name: anIcePackage packageName + id: UUID new + message: '' + date: Date today + time: Time now + ancestors: #( ) + stepChildren: #( ). + TonelWriter + fileOut: (MCVersion package: anIcePackage mcPackage info: versionInfo) + on: sourceDirectory +] + { #category : 'private - diff' } MCPRepositoryWorkingCopyCommand >> gitStatusLinesFor: aRepository [ @@ -65,6 +93,26 @@ MCPRepositoryWorkingCopyCommand >> imageModifiedPackageNamesFor: aRepository [ (aRepository modifiedPackages collect: [ :each | each name ]) ] +{ #category : 'private - tonel order' } +MCPRepositoryWorkingCopyCommand >> isTonelMethodOrderFile: aFileReference [ + + | basename | + basename := aFileReference basename. + ^ (basename endsWith: '.class.st') or: [ + basename endsWith: '.extension.st' ] +] + +{ #category : 'private - tonel order' } +MCPRepositoryWorkingCopyCommand >> isTonelMethodStartAt: anIndex in: lines [ + + | line nextLine | + anIndex >= lines size ifTrue: [ ^ false ]. + line := lines at: anIndex. + (line beginsWith: '{ #category') ifFalse: [ ^ false ]. + nextLine := lines at: anIndex + 1. + ^ nextLine includesSubstring: ' >> ' +] + { #category : 'private - diff' } MCPRepositoryWorkingCopyCommand >> modifiedPathsFromDiff: aDiff [ @@ -95,17 +143,21 @@ MCPRepositoryWorkingCopyCommand >> packageNameFromDiffPackageNode: aNode [ { #category : 'private - diff' } MCPRepositoryWorkingCopyCommand >> packageNameFromPath: pathString in: aRepository [ - | segments subdirectory subdirectorySegments | + | packageName segments subdirectory subdirectorySegments | pathString isEmpty ifTrue: [ ^ '' ]. segments := pathString substrings: '/'. segments isEmpty ifTrue: [ ^ '' ]. subdirectory := aRepository subdirectory ifNil: [ '' ]. - subdirectory isEmpty ifTrue: [ ^ segments first ]. - subdirectorySegments := subdirectory substrings: '/'. - segments size <= subdirectorySegments size ifTrue: [ ^ '' ]. - (segments first: subdirectorySegments size) = subdirectorySegments - ifFalse: [ ^ '' ]. - ^ segments at: subdirectorySegments size + 1 + packageName := subdirectory isEmpty + ifTrue: [ segments first ] + ifFalse: [ + subdirectorySegments := subdirectory substrings: '/'. + segments size <= subdirectorySegments size ifTrue: [ ^ '' ]. + (segments first: subdirectorySegments size) + = subdirectorySegments ifFalse: [ ^ '' ]. + segments at: subdirectorySegments size + 1 ]. + (packageName beginsWith: '.') ifTrue: [ ^ '' ]. + ^ packageName ] { #category : 'private - diff' } @@ -127,6 +179,109 @@ MCPRepositoryWorkingCopyCommand >> pathFromGitStatusLine: aLine [ ^ path ] +{ #category : 'private - tonel order' } +MCPRepositoryWorkingCopyCommand >> relativePathFor: aFileReference under: aDirectory [ + + | directoryPath filePath relative | + directoryPath := aDirectory pathString. + filePath := aFileReference pathString. + relative := (filePath beginsWith: directoryPath) + ifTrue: [ filePath allButFirst: directoryPath size ] + ifFalse: [ aFileReference basename ]. + (relative beginsWith: '/') ifTrue: [ relative := relative allButFirst ]. + ^ relative +] + +{ #category : 'private - tonel order' } +MCPRepositoryWorkingCopyCommand >> restoreOrderOnlyTonelChurnFor: aRepository fromSnapshot: aSnapshot [ + + | restored sourceDirectory | + restored := OrderedCollection new. + sourceDirectory := self repositorySourceDirectoryFor: aRepository. + aSnapshot keysAndValuesDo: [ :relativePath :beforeContents | + | afterContents file | + file := sourceDirectory / relativePath. + file exists ifTrue: [ + afterContents := file contents. + (beforeContents ~= afterContents and: [ + self + tonelMethodOrderEquivalent: beforeContents + to: afterContents ]) ifTrue: [ + file writeStreamDo: [ :stream | + stream nextPutAll: beforeContents ]. + restored add: relativePath ] ] ]. + ^ self sortedStringsFrom: restored +] + +{ #category : 'private - tonel order' } +MCPRepositoryWorkingCopyCommand >> snapshotTonelFilesFor: aRepository [ + + | snapshot sourceDirectory | + snapshot := Dictionary new. + sourceDirectory := self repositorySourceDirectoryFor: aRepository. + (sourceDirectory exists and: [ sourceDirectory isDirectory ]) + ifFalse: [ ^ snapshot ]. + self tonelFilesIn: sourceDirectory do: [ :file | + snapshot + at: (self relativePathFor: file under: sourceDirectory) + put: file contents ]. + ^ snapshot +] + +{ #category : 'private - tonel order' } +MCPRepositoryWorkingCopyCommand >> tonelFilesIn: aDirectory do: aBlock [ + + aDirectory children do: [ :child | + child isDirectory + ifTrue: [ self tonelFilesIn: child do: aBlock ] + ifFalse: [ + (self isTonelMethodOrderFile: child) ifTrue: [ + aBlock value: child ] ] ] +] + +{ #category : 'private - tonel order' } +MCPRepositoryWorkingCopyCommand >> tonelMethodOrderEquivalent: beforeContents to: afterContents [ + + ^ (self tonelMethodOrderSignatureFor: beforeContents) + = (self tonelMethodOrderSignatureFor: afterContents) +] + +{ #category : 'private - tonel order' } +MCPRepositoryWorkingCopyCommand >> tonelMethodOrderSignatureFor: contents [ + + | chunks currentChunk lines normalizedContents preamble | + normalizedContents := contents withLineEndings: String lf. + lines := OrderedCollection new. + normalizedContents linesDo: [ :line | lines add: line ]. + preamble := WriteStream on: String new. + chunks := OrderedCollection new. + 1 to: lines size do: [ :index | + | line | + line := lines at: index. + (self isTonelMethodStartAt: index in: lines) + ifTrue: [ + currentChunk ifNotNil: [ + chunks add: currentChunk contents trimBoth ]. + currentChunk := WriteStream on: String new. + currentChunk + nextPutAll: line; + nextPut: Character lf ] + ifFalse: [ + currentChunk + ifNil: [ + preamble + nextPutAll: line; + nextPut: Character lf ] + ifNotNil: [ + currentChunk + nextPutAll: line; + nextPut: Character lf ] ] ]. + currentChunk ifNotNil: [ chunks add: currentChunk contents trimBoth ]. + ^ { + (#preamble -> preamble contents trimBoth). + (#methods -> chunks asArray sort) } asDictionary +] + { #category : 'private - execution' } MCPRepositoryWorkingCopyCommand >> withNonInteractiveAuthorDuring: aBlock [ diff --git a/src/MCP/MCPSwitchRepositoryBranchCommand.class.st b/src/MCP/MCPSwitchRepositoryBranchCommand.class.st index e6f8276..51fa84a 100644 --- a/src/MCP/MCPSwitchRepositoryBranchCommand.class.st +++ b/src/MCP/MCPSwitchRepositoryBranchCommand.class.st @@ -12,24 +12,36 @@ Class { { #category : 'executing' } MCPSwitchRepositoryBranchCommand >> execute [ - | beforeInfo repository result | + | beforeInfo headCommit repository result | ^ self - executeRepositoryAction: 'switchBranch' - work: [ - repository := self request repository. - beforeInfo := MCPRepositoryInfo fromRepository: repository. - repository switchToBranchNamed: self request branchName. - result := MCPRepositoryBranchResult - repositoryBefore: beforeInfo - after: repository ] - successResult: [ :warningMessages | - self tool - successResultText: - 'Switched repository ' , result repositoryInfo name - , ' branch ' , self request branchName , '.' - data: (self tool dataForRepositoryResult: result) - warnings: warningMessages ] - failureSummary: [ :error | - 'Failed to switch repository branch: ' - , (error messageText ifNil: [ error asString ]) ] + executeRepositoryAction: 'switchBranch' + work: [ + repository := self request repository. + beforeInfo := MCPRepositoryInfo fromRepository: repository. + (self repositorySupportsExternalGitOperations: repository) + ifTrue: [ + self + runGitArguments: { 'switch'. self request branchName } + in: repository + ifFailed: [ :gitResult | + self signalGitCommandFailed: gitResult action: 'switch' ]. + headCommit := repository headCommit. + self + adoptHeadCommit: headCommit + for: repository + markLoadedPackagesClean: false ] + ifFalse: [ repository switchToBranchNamed: self request branchName ]. + result := MCPRepositoryBranchResult + repositoryBefore: beforeInfo + after: repository ] + successResult: [ :warningMessages | + self tool + successResultText: + 'Switched repository ' , result repositoryInfo name + , ' branch ' , self request branchName , '.' + data: (self tool dataForRepositoryResult: result) + warnings: warningMessages ] + failureSummary: [ :error | + 'Failed to switch repository branch: ' + , (error messageText ifNil: [ error asString ]) ] ]