Repository: nhat-phan/merge-request-integration Branch: master Commit: 4649c1e49849 Files: 590 Total size: 1.1 MB Directory structure: gitextract_0m11ptlg/ ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── contracts/ │ ├── build.gradle.kts │ ├── settings.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ └── net/ │ └── ntworld/ │ └── mergeRequest/ │ ├── Approval.kt │ ├── Change.kt │ ├── Comment.kt │ ├── CommentPosition.kt │ ├── CommentPositionChangeType.kt │ ├── CommentPositionSource.kt │ ├── Commit.kt │ ├── DateTime.kt │ ├── DateTimeSerializer.kt │ ├── DiffReference.kt │ ├── MergeRequest.kt │ ├── MergeRequestInfo.kt │ ├── MergeRequestState.kt │ ├── Pipeline.kt │ ├── PipelineStatus.kt │ ├── Project.kt │ ├── ProjectVisibility.kt │ ├── ProviderData.kt │ ├── ProviderInfo.kt │ ├── ProviderStatus.kt │ ├── User.kt │ ├── UserInfo.kt │ ├── UserStatus.kt │ ├── api/ │ │ ├── ApiConnection.kt │ │ ├── ApiCredentials.kt │ │ ├── ApiOptions.kt │ │ ├── ApiProvider.kt │ │ ├── Cache.kt │ │ ├── CacheNotFoundException.kt │ │ ├── CommentApi.kt │ │ ├── CommitApi.kt │ │ ├── DraftCommentStorage.kt │ │ ├── MergeRequestApi.kt │ │ ├── MergeRequestOrdering.kt │ │ ├── ProjectApi.kt │ │ └── UserApi.kt │ ├── command/ │ │ ├── ApproveMergeRequestCommand.kt │ │ ├── DeleteCommentCommand.kt │ │ ├── ResolveCommentCommand.kt │ │ ├── UnapproveMergeRequestCommand.kt │ │ └── UnresolveCommentCommand.kt │ ├── query/ │ │ ├── FindApprovalQuery.kt │ │ ├── FindApprovalQueryResult.kt │ │ ├── FindMergeRequestQuery.kt │ │ ├── FindMergeRequestQueryResult.kt │ │ ├── GetCommentsQuery.kt │ │ ├── GetCommentsQueryResult.kt │ │ ├── GetCommitsQuery.kt │ │ ├── GetCommitsQueryResult.kt │ │ ├── GetMergeRequestFilter.kt │ │ ├── GetMergeRequestsQuery.kt │ │ ├── GetMergeRequestsQueryResult.kt │ │ ├── GetPipelinesQuery.kt │ │ ├── GetPipelinesQueryResult.kt │ │ ├── GetProjectMembersQuery.kt │ │ ├── GetProjectMembersQueryResult.kt │ │ └── QueryBase.kt │ ├── request/ │ │ ├── CreateCommentRequest.kt │ │ ├── PublishAllCommentsRequest.kt │ │ ├── PublishCommentsRequest.kt │ │ ├── ReplyCommentRequest.kt │ │ └── UpdateCommentRequest.kt │ └── response/ │ ├── CreateCommentResponse.kt │ ├── PublishAllCommentsResponse.kt │ ├── PublishCommentsResponse.kt │ ├── ReplyCommentResponse.kt │ └── UpdateCommentResponse.kt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── merge-request-integration/ │ ├── build.gradle.kts │ ├── settings.gradle.kts │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── net/ │ │ └── ntworld/ │ │ └── mergeRequestIntegration/ │ │ ├── DefaultProviderStorage.kt │ │ ├── MergeRequestIntegrationInfrastructure.kt │ │ ├── ProviderStorage.kt │ │ ├── _const.kt │ │ ├── commandHandler/ │ │ │ ├── ApproveMergeRequestCommandHandler.kt │ │ │ ├── DeleteCommentCommandHandler.kt │ │ │ ├── ResolveCommentCommandHandler.kt │ │ │ ├── UnapproveMergeRequestCommandHandler.kt │ │ │ └── UnresolveCommentCommandHandler.kt │ │ ├── exception/ │ │ │ ├── InvalidCacheKeyException.kt │ │ │ ├── InvalidTTLException.kt │ │ │ └── ProviderNotFoundException.kt │ │ ├── internal/ │ │ │ ├── ApiOptionsImpl.kt │ │ │ ├── ApprovalImpl.kt │ │ │ ├── ChangeImpl.kt │ │ │ ├── CommentImpl.kt │ │ │ ├── CommentPositionImpl.kt │ │ │ ├── CommitImpl.kt │ │ │ ├── DiffReferenceImpl.kt │ │ │ ├── MergeRequestImpl.kt │ │ │ ├── MergeRequestInfoImpl.kt │ │ │ ├── MergeRequestSearchResultImpl.kt │ │ │ ├── PipelineImpl.kt │ │ │ ├── ProjectImpl.kt │ │ │ ├── ProviderDataImpl.kt │ │ │ ├── UserImpl.kt │ │ │ └── UserInfoImpl.kt │ │ ├── provider/ │ │ │ ├── DraftCommentApi.kt │ │ │ ├── FuelClient.kt │ │ │ ├── MemoryCache.kt │ │ │ ├── MemoryDraftCommentStorage.kt │ │ │ ├── MergeRequestApiDecorator.kt │ │ │ ├── ProviderException.kt │ │ │ ├── Transformer.kt │ │ │ ├── github/ │ │ │ │ ├── Github.kt │ │ │ │ ├── GithubApiProvider.kt │ │ │ │ ├── GithubClient.kt │ │ │ │ ├── GithubFailedRequestError.kt │ │ │ │ ├── GithubFuelClient.kt │ │ │ │ ├── GithubMergeRequestApi.kt │ │ │ │ ├── GithubProjectApi.kt │ │ │ │ ├── GithubRequest.kt │ │ │ │ ├── GithubUserApi.kt │ │ │ │ ├── GithubUtil.kt │ │ │ │ ├── _const.kt │ │ │ │ ├── model/ │ │ │ │ │ ├── PullRequestSearchItem.kt │ │ │ │ │ └── SearchPullRequestResult.kt │ │ │ │ ├── request/ │ │ │ │ │ ├── GithubFindCurrentUserRequest.kt │ │ │ │ │ ├── GithubFindRepositoryRequest.kt │ │ │ │ │ ├── GithubSearchPRsRequest.kt │ │ │ │ │ └── GithubSearchRepositoriesRequest.kt │ │ │ │ ├── requestHandler/ │ │ │ │ │ ├── GithubFindCurrentUserRequestHandler.kt │ │ │ │ │ ├── GithubFindRepositoryRequestHandler.kt │ │ │ │ │ ├── GithubSearchPRsRequestHandler.kt │ │ │ │ │ └── GithubSearchRepositoriesRequestHandler.kt │ │ │ │ ├── response/ │ │ │ │ │ ├── GithubFindRepositoryResponse.kt │ │ │ │ │ ├── GithubFindUserResponse.kt │ │ │ │ │ ├── GithubSearchPRsResponse.kt │ │ │ │ │ └── GithubSearchRepositoriesResponse.kt │ │ │ │ ├── transformer/ │ │ │ │ │ ├── GithubRepositoryTransformer.kt │ │ │ │ │ ├── GithubSearchPullRequestItemTransformer.kt │ │ │ │ │ └── GithubUserTransformer.kt │ │ │ │ └── vo/ │ │ │ │ ├── GithubMergeRequestId.kt │ │ │ │ ├── GithubProjectId.kt │ │ │ │ └── GithubUserId.kt │ │ │ └── gitlab/ │ │ │ ├── Gitlab.kt │ │ │ ├── GitlabApiProvider.kt │ │ │ ├── GitlabClient.kt │ │ │ ├── GitlabCommentApi.kt │ │ │ ├── GitlabCommitApi.kt │ │ │ ├── GitlabCredentials.kt │ │ │ ├── GitlabFailedRequestError.kt │ │ │ ├── GitlabFailedRequestException.kt │ │ │ ├── GitlabFuelClient.kt │ │ │ ├── GitlabMergeRequestApi.kt │ │ │ ├── GitlabMergeRequestApiCache.kt │ │ │ ├── GitlabProjectApi.kt │ │ │ ├── GitlabRequest.kt │ │ │ ├── GitlabUserApi.kt │ │ │ ├── GitlabUtil.kt │ │ │ ├── _const.kt │ │ │ ├── command/ │ │ │ │ ├── GitlabApproveMRCommand.kt │ │ │ │ ├── GitlabCreateDiffNoteCommand.kt │ │ │ │ ├── GitlabDeleteNoteCommand.kt │ │ │ │ ├── GitlabResolveNoteCommand.kt │ │ │ │ ├── GitlabUnapproveMRCommand.kt │ │ │ │ └── GitlabUpdateDiffNoteCommand.kt │ │ │ ├── commandHandler/ │ │ │ │ ├── GitlabApproveMRCommandHandler.kt │ │ │ │ ├── GitlabCreateDiffNoteCommandHandler.kt │ │ │ │ ├── GitlabDeleteNoteCommandHandler.kt │ │ │ │ ├── GitlabResolveNoteCommandHandler.kt │ │ │ │ ├── GitlabUnapproveMRCommandHandler.kt │ │ │ │ └── GitlabUpdateDiffNoteCommandHandler.kt │ │ │ ├── model/ │ │ │ │ ├── ApprovalModel.kt │ │ │ │ ├── ApproverModel.kt │ │ │ │ ├── GetCommentsPayload.kt │ │ │ │ ├── GraphqlRequest.kt │ │ │ │ ├── PipelineModel.kt │ │ │ │ ├── ReplyCommentPayload.kt │ │ │ │ └── UserInfoModel.kt │ │ │ ├── request/ │ │ │ │ ├── GitlabCreateNoteRequest.kt │ │ │ │ ├── GitlabFindCurrentUserRequest.kt │ │ │ │ ├── GitlabFindMRApprovalRequest.kt │ │ │ │ ├── GitlabFindMRRequest.kt │ │ │ │ ├── GitlabFindProjectRequest.kt │ │ │ │ ├── GitlabFindUserRequest.kt │ │ │ │ ├── GitlabGetCommitChangesRequest.kt │ │ │ │ ├── GitlabGetMRChangesRequest.kt │ │ │ │ ├── GitlabGetMRCommentsRequest.kt │ │ │ │ ├── GitlabGetMRCommitsRequest.kt │ │ │ │ ├── GitlabGetMRDiscussionsRequest.kt │ │ │ │ ├── GitlabGetMRPipelinesRequest.kt │ │ │ │ ├── GitlabGetProjectMembersRequest.kt │ │ │ │ ├── GitlabReplyNoteRequest.kt │ │ │ │ ├── GitlabSearchMRsRequest.kt │ │ │ │ └── GitlabSearchProjectsRequest.kt │ │ │ ├── requestHandler/ │ │ │ │ ├── GitlabCreateNoteRequestHandler.kt │ │ │ │ ├── GitlabFindCurrentUserRequestHandler.kt │ │ │ │ ├── GitlabFindMRApprovalRequestHandler.kt │ │ │ │ ├── GitlabFindMRRequestHandler.kt │ │ │ │ ├── GitlabFindProjectRequestHandler.kt │ │ │ │ ├── GitlabFindUserRequestHandler.kt │ │ │ │ ├── GitlabGetCommitChangesRequestHandler.kt │ │ │ │ ├── GitlabGetMRChangesRequestHandler.kt │ │ │ │ ├── GitlabGetMRCommentsRequestHandler.kt │ │ │ │ ├── GitlabGetMRCommitsRequestHandler.kt │ │ │ │ ├── GitlabGetMRDiscussionsRequestHandler.kt │ │ │ │ ├── GitlabGetMRPipelinesRequestHandler.kt │ │ │ │ ├── GitlabGetProjectMembersRequestHandler.kt │ │ │ │ ├── GitlabReplyNoteRequestHandler.kt │ │ │ │ ├── GitlabSearchMRsRequestHandler.kt │ │ │ │ └── GitlabSearchProjectsRequestHandler.kt │ │ │ ├── response/ │ │ │ │ ├── GitlabCreateNoteResponse.kt │ │ │ │ ├── GitlabFindMRApprovalResponse.kt │ │ │ │ ├── GitlabFindMRResponse.kt │ │ │ │ ├── GitlabFindProjectResponse.kt │ │ │ │ ├── GitlabFindUserResponse.kt │ │ │ │ ├── GitlabGetCommitChangesResponse.kt │ │ │ │ ├── GitlabGetMRChangesResponse.kt │ │ │ │ ├── GitlabGetMRCommentsResponse.kt │ │ │ │ ├── GitlabGetMRCommitsResponse.kt │ │ │ │ ├── GitlabGetMRDiscussionsResponse.kt │ │ │ │ ├── GitlabGetMRPipelinesResponse.kt │ │ │ │ ├── GitlabGetProjectMembersResponse.kt │ │ │ │ ├── GitlabReplyNoteResponse.kt │ │ │ │ ├── GitlabSearchMRsResponse.kt │ │ │ │ └── GitlabSearchProjectsResponse.kt │ │ │ └── transformer/ │ │ │ ├── GitlabApprovalTransformer.kt │ │ │ ├── GitlabCommentTransformer.kt │ │ │ ├── GitlabCommitTransformer.kt │ │ │ ├── GitlabDiffRefTransformer.kt │ │ │ ├── GitlabDiffTransformer.kt │ │ │ ├── GitlabDiscussionTransformer.kt │ │ │ ├── GitlabMRSimpleTransformer.kt │ │ │ ├── GitlabMRTransformer.kt │ │ │ ├── GitlabMemberTransformer.kt │ │ │ ├── GitlabPipelineTransformer.kt │ │ │ ├── GitlabProjectTransformer.kt │ │ │ ├── GitlabUserInfoTransformer.kt │ │ │ └── GitlabUserTransformer.kt │ │ ├── queryHandler/ │ │ │ ├── FindApprovalQueryHandler.kt │ │ │ ├── FindMergeRequestQueryHandler.kt │ │ │ ├── GetCommentsQueryHandler.kt │ │ │ ├── GetCommitsQueryHandler.kt │ │ │ ├── GetMergeRequestsQueryHandler.kt │ │ │ ├── GetPipelinesQueryHandler.kt │ │ │ └── GetProjectMembersQueryHandler.kt │ │ ├── requestHandler/ │ │ │ ├── CreateCommentRequestHandler.kt │ │ │ ├── PublishAllCommentsRequestHandler.kt │ │ │ ├── PublishCommentsRequestHandler.kt │ │ │ ├── ReplyCommentRequestHandler.kt │ │ │ └── UpdateCommentRequestHandler.kt │ │ ├── update/ │ │ │ ├── UpdateManager.kt │ │ │ └── UpdateMetadata.kt │ │ └── util/ │ │ ├── DateTimeUtil.kt │ │ └── SavedFiltersUtil.kt │ └── test/ │ ├── kotlin/ │ │ └── net/ │ │ └── ntworld/ │ │ └── mergeRequestIntegration/ │ │ └── provider/ │ │ └── MemoryCacheTests.kt │ └── resources/ │ └── update-metadata.json ├── merge-request-integration-ce/ │ ├── build.gradle.kts │ ├── doc/ │ │ ├── description.html │ │ ├── release-notes.2019.3.0.html │ │ ├── release-notes.2019.3.1.html │ │ ├── release-notes.2019.3.2.html │ │ ├── release-notes.2019.3.3.html │ │ ├── release-notes.2019.3.4.html │ │ ├── release-notes.2019.3.5.html │ │ ├── release-notes.2020.1.0.html │ │ ├── release-notes.2020.1.1.html │ │ ├── release-notes.2020.1.2.html │ │ ├── release-notes.2020.1.3.html │ │ ├── release-notes.2020.1.4.html │ │ ├── release-notes.2020.1.5.html │ │ ├── release-notes.2020.2.0.html │ │ └── release-notes.2020.3.0.html │ ├── settings.gradle.kts │ └── src/ │ └── main/ │ ├── kotlin/ │ │ └── net/ │ │ └── ntworld/ │ │ └── mergeRequestIntegrationIdeCE/ │ │ ├── CommunityApplicationServiceProvider.kt │ │ ├── CommunityProjectServiceProvider.kt │ │ ├── Configuration.kt │ │ ├── DiffExtension.kt │ │ ├── DiffViewAddCommentAction.kt │ │ ├── DiffViewToggleCommentsAction.kt │ │ ├── GithubConnectionsConfigurable.kt │ │ ├── GitlabConnectionsConfigurable.kt │ │ ├── MainToolWindowFactory.kt │ │ └── SingleMRToolWindowFactory.kt │ └── resources/ │ └── META-INF/ │ └── plugin.xml ├── merge-request-integration-core/ │ ├── build.gradle.kts │ ├── settings.gradle.kts │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── net/ │ │ │ └── ntworld/ │ │ │ └── mergeRequestIntegrationIde/ │ │ │ ├── AbstractModel.kt │ │ │ ├── AbstractPresenter.kt │ │ │ ├── AbstractSimpleModel.kt │ │ │ ├── AbstractSimplePresenter.kt │ │ │ ├── AbstractSimpleView.kt │ │ │ ├── AbstractView.kt │ │ │ ├── Component.kt │ │ │ ├── ComponentFactory.kt │ │ │ ├── DataChangedSource.kt │ │ │ ├── DefaultComponentFactory.kt │ │ │ ├── IdeInfrastructure.kt │ │ │ ├── Model.kt │ │ │ ├── Presenter.kt │ │ │ ├── SimpleModel.kt │ │ │ ├── SimplePresenter.kt │ │ │ ├── SimpleView.kt │ │ │ ├── View.kt │ │ │ ├── _const.kt │ │ │ ├── compatibility/ │ │ │ │ ├── IntellijIdeApi.kt │ │ │ │ ├── Version193Adapter.kt │ │ │ │ ├── Version201Adapter.kt │ │ │ │ └── Version203Adapter.kt │ │ │ ├── component/ │ │ │ │ ├── Icons.kt │ │ │ │ ├── PaginationToolbar.kt │ │ │ │ ├── PaginationToolbarImpl.kt │ │ │ │ ├── comment/ │ │ │ │ │ ├── CommentComponent.kt │ │ │ │ │ ├── CommentComponentFactory.kt │ │ │ │ │ ├── CommentComponentFactoryImpl.kt │ │ │ │ │ ├── CommentComponentImpl.kt │ │ │ │ │ ├── CommentEvent.kt │ │ │ │ │ ├── CommentEventPropagator.kt │ │ │ │ │ ├── EditorComponent.kt │ │ │ │ │ ├── EditorComponentImpl.kt │ │ │ │ │ ├── GroupComponent.kt │ │ │ │ │ ├── GroupComponentImpl.kt │ │ │ │ │ └── Options.kt │ │ │ │ ├── dialog/ │ │ │ │ │ ├── LegalWarningDialog.form │ │ │ │ │ └── LegalWarningDialog.kt │ │ │ │ ├── gutter/ │ │ │ │ │ ├── GutterActionType.kt │ │ │ │ │ ├── GutterIconRenderer.kt │ │ │ │ │ ├── GutterIconRendererActionListener.kt │ │ │ │ │ ├── GutterIconRendererFactory.kt │ │ │ │ │ ├── GutterIconRendererImpl.kt │ │ │ │ │ ├── GutterPosition.kt │ │ │ │ │ └── GutterState.kt │ │ │ │ └── thread/ │ │ │ │ ├── ThreadFactory.kt │ │ │ │ ├── ThreadModel.kt │ │ │ │ ├── ThreadModelImpl.kt │ │ │ │ ├── ThreadPresenter.kt │ │ │ │ ├── ThreadPresenterImpl.kt │ │ │ │ ├── ThreadView.kt │ │ │ │ └── ThreadViewImpl.kt │ │ │ ├── configuration/ │ │ │ │ ├── README.md │ │ │ │ └── vos/ │ │ │ │ └── GitRemotePathInfo.kt │ │ │ ├── diff/ │ │ │ │ ├── AbstractDiffView.kt │ │ │ │ ├── CommentPoint.kt │ │ │ │ ├── DiffExtensionBase.kt │ │ │ │ ├── DiffFactory.kt │ │ │ │ ├── DiffModel.kt │ │ │ │ ├── DiffModelImpl.kt │ │ │ │ ├── DiffPresenter.kt │ │ │ │ ├── DiffPresenterImpl.kt │ │ │ │ ├── DiffView.kt │ │ │ │ ├── DiffViewAddCommentActionBase.kt │ │ │ │ ├── DiffViewToggleCommentsActionBase.kt │ │ │ │ ├── SimpleOneSideDiffView.kt │ │ │ │ ├── TwoSideTextDiffView.kt │ │ │ │ └── UnifiedDiffView.kt │ │ │ ├── exception/ │ │ │ │ └── InvalidConnectionException.kt │ │ │ ├── infrastructure/ │ │ │ │ ├── AbstractApplicationServiceProvider.kt │ │ │ │ ├── AbstractProjectServiceProvider.kt │ │ │ │ ├── ApplicationServiceProvider.kt │ │ │ │ ├── ProjectServiceProvider.kt │ │ │ │ ├── ProviderSettings.kt │ │ │ │ ├── ReviewContext.kt │ │ │ │ ├── ReviewContextManager.kt │ │ │ │ ├── internal/ │ │ │ │ │ ├── ApiCredentialsImpl.kt │ │ │ │ │ ├── DiffPreviewProviderImpl.kt │ │ │ │ │ ├── DiffRequestProcessorImpl.kt │ │ │ │ │ ├── ProviderSettingsImpl.kt │ │ │ │ │ ├── ReviewContextImpl.kt │ │ │ │ │ ├── ReviewContextManagerImpl.kt │ │ │ │ │ └── ServiceBase.kt │ │ │ │ ├── notifier/ │ │ │ │ │ ├── DiffNotifier.kt │ │ │ │ │ ├── MergeRequestDataNotifier.kt │ │ │ │ │ ├── ProjectNotifier.kt │ │ │ │ │ ├── ProjectNotifierAdapter.kt │ │ │ │ │ ├── ReworkEditorNotifier.kt │ │ │ │ │ ├── ReworkWatcherNotifier.kt │ │ │ │ │ ├── SingleMRToolWindowNotifier.kt │ │ │ │ │ └── provider/ │ │ │ │ │ └── MergeRequestDataProvider.kt │ │ │ │ ├── service/ │ │ │ │ │ ├── FiltersStorageService.kt │ │ │ │ │ ├── RepositoryFileService.kt │ │ │ │ │ ├── internal/ │ │ │ │ │ │ └── FiltersStorageServiceImpl.kt │ │ │ │ │ └── repositoryFile/ │ │ │ │ │ ├── CachedRepositoryFile.kt │ │ │ │ │ ├── LocalRepositoryFileService.kt │ │ │ │ │ └── RepositoryFileDecorator.kt │ │ │ │ └── setting/ │ │ │ │ ├── ApplicationSettings.kt │ │ │ │ ├── ApplicationSettingsImpl.kt │ │ │ │ ├── ApplicationSettingsManager.kt │ │ │ │ ├── ApplicationSettingsManagerImpl.kt │ │ │ │ └── option/ │ │ │ │ ├── BooleanOption.kt │ │ │ │ ├── CheckoutTargetBranchOption.kt │ │ │ │ ├── DisplayCommentsInDiffViewOption.kt │ │ │ │ ├── DisplayMergeRequestStateOption.kt │ │ │ │ ├── DisplayUpVotesAndDownVotesOption.kt │ │ │ │ ├── EnableRequestCacheOption.kt │ │ │ │ ├── EnableReworkProcessOption.kt │ │ │ │ ├── MaxDiffChangesOpenedAutomaticallyOption.kt │ │ │ │ ├── SaveMRFilterStateOption.kt │ │ │ │ ├── SettingOption.kt │ │ │ │ └── ShowAddCommentIconsInDiffViewGutterOption.kt │ │ │ ├── mergeRequest/ │ │ │ │ ├── comments/ │ │ │ │ │ ├── CommentsTabFactory.kt │ │ │ │ │ ├── CommentsTabModel.kt │ │ │ │ │ ├── CommentsTabModelImpl.kt │ │ │ │ │ ├── CommentsTabPresenter.kt │ │ │ │ │ ├── CommentsTabPresenterImpl.kt │ │ │ │ │ ├── CommentsTabView.kt │ │ │ │ │ ├── CommentsTabViewImpl.kt │ │ │ │ │ └── tree/ │ │ │ │ │ ├── CommentTreeFactory.kt │ │ │ │ │ ├── CommentTreeModel.kt │ │ │ │ │ ├── CommentTreeModelImpl.kt │ │ │ │ │ ├── CommentTreePresenter.kt │ │ │ │ │ ├── CommentTreePresenterImpl.kt │ │ │ │ │ ├── CommentTreeView.kt │ │ │ │ │ ├── CommentTreeViewImpl.kt │ │ │ │ │ ├── CommentTreeViewToolbar.kt │ │ │ │ │ └── node/ │ │ │ │ │ ├── AbstractNode.kt │ │ │ │ │ ├── CommentNode.kt │ │ │ │ │ ├── FileLineNode.kt │ │ │ │ │ ├── FileNode.kt │ │ │ │ │ ├── GeneralCommentsNode.kt │ │ │ │ │ ├── Node.kt │ │ │ │ │ ├── NodeDescriptorService.kt │ │ │ │ │ ├── NodeDescriptorServiceImpl.kt │ │ │ │ │ ├── NodeFactory.kt │ │ │ │ │ ├── NodeSyncManager.kt │ │ │ │ │ ├── NodeSyncManagerImpl.kt │ │ │ │ │ ├── RootNode.kt │ │ │ │ │ ├── RootNodeBuilder.kt │ │ │ │ │ ├── SyncedTree.kt │ │ │ │ │ ├── ThreadNode.kt │ │ │ │ │ └── _fn.kt │ │ │ │ └── fn.kt │ │ │ ├── rework/ │ │ │ │ ├── BranchWatcher.kt │ │ │ │ ├── ReworkEditorController.kt │ │ │ │ ├── ReworkEditorManager.kt │ │ │ │ ├── ReworkManager.kt │ │ │ │ ├── ReworkWatcher.kt │ │ │ │ └── internal/ │ │ │ │ ├── BranchWatcherImpl.kt │ │ │ │ ├── ReworkEditorControllerImpl.kt │ │ │ │ ├── ReworkEditorManagerImpl.kt │ │ │ │ ├── ReworkGeneralCommentsView.kt │ │ │ │ ├── ReworkManagerImpl.kt │ │ │ │ └── ReworkWatcherImpl.kt │ │ │ ├── task/ │ │ │ │ ├── FetchProjectMembersTask.kt │ │ │ │ ├── FindApprovalTask.kt │ │ │ │ ├── FindMergeRequestTask.kt │ │ │ │ ├── GetAvailableUpdatesTask.kt │ │ │ │ ├── GetCommentsTask.kt │ │ │ │ ├── GetCommitsTask.kt │ │ │ │ ├── GetPipelinesTask.kt │ │ │ │ ├── RegisterProviderTask.kt │ │ │ │ ├── RepositoryFetchAllRemotesTask.kt │ │ │ │ └── SearchMergeRequestTask.kt │ │ │ ├── toolWindow/ │ │ │ │ ├── CommentsToolWindowTab.kt │ │ │ │ ├── FilesToolWindowTab.kt │ │ │ │ ├── ReworkToolWindowTab.kt │ │ │ │ ├── SingleMRToolWindowFactoryBase.kt │ │ │ │ ├── SingleMRToolWindowManager.kt │ │ │ │ └── internal/ │ │ │ │ ├── CommentsToolWindowTabImpl.kt │ │ │ │ └── FilesToolWindowTabImpl.kt │ │ │ ├── ui/ │ │ │ │ ├── Component.kt │ │ │ │ ├── MainToolWindowFactoryBase.kt │ │ │ │ ├── README.md │ │ │ │ ├── configuration/ │ │ │ │ │ ├── AbstractConnectionsConfigurable.kt │ │ │ │ │ ├── ConfigurationBase.kt │ │ │ │ │ ├── ConnectionUI.kt │ │ │ │ │ ├── GithubConnection.form │ │ │ │ │ ├── GithubConnection.kt │ │ │ │ │ ├── GithubConnectionsConfigurableBase.kt │ │ │ │ │ ├── GithubProjectFinder.kt │ │ │ │ │ ├── GitlabConnection.form │ │ │ │ │ ├── GitlabConnection.kt │ │ │ │ │ ├── GitlabConnectionsConfigurableBase.kt │ │ │ │ │ ├── GitlabProjectFinder.kt │ │ │ │ │ ├── ProjectFinderUI.kt │ │ │ │ │ ├── SettingsConfiguration.form │ │ │ │ │ ├── SettingsConfiguration.kt │ │ │ │ │ └── SettingsUI.kt │ │ │ │ ├── mergeRequest/ │ │ │ │ │ ├── AbstractMergeRequestCollection.kt │ │ │ │ │ ├── MergeRequestCollection.kt │ │ │ │ │ ├── MergeRequestCollectionEventListener.kt │ │ │ │ │ ├── MergeRequestCollectionFilter.kt │ │ │ │ │ ├── MergeRequestCollectionFilterEventListener.kt │ │ │ │ │ ├── MergeRequestCollectionFilterUI.kt │ │ │ │ │ ├── MergeRequestCollectionTree.kt │ │ │ │ │ ├── MergeRequestCollectionTreeNode.kt │ │ │ │ │ ├── MergeRequestCollectionUI.kt │ │ │ │ │ ├── MergeRequestDetails.kt │ │ │ │ │ ├── MergeRequestDetailsToolbar.kt │ │ │ │ │ ├── MergeRequestDetailsToolbarUI.kt │ │ │ │ │ ├── MergeRequestDetailsUI.kt │ │ │ │ │ └── tab/ │ │ │ │ │ ├── MergeRequestCommitsTab.kt │ │ │ │ │ ├── MergeRequestCommitsTabUI.kt │ │ │ │ │ ├── MergeRequestDescriptionTab.kt │ │ │ │ │ ├── MergeRequestDescriptionTabUI.kt │ │ │ │ │ ├── MergeRequestInfoTab.kt │ │ │ │ │ ├── MergeRequestInfoTabUI.kt │ │ │ │ │ └── commit/ │ │ │ │ │ ├── CommitChanges.kt │ │ │ │ │ ├── CommitChangesUI.kt │ │ │ │ │ ├── CommitCollection.kt │ │ │ │ │ ├── CommitCollectionUI.kt │ │ │ │ │ └── CommitSelectUtil.kt │ │ │ │ ├── panel/ │ │ │ │ │ ├── ApprovalPanel.form │ │ │ │ │ ├── ApprovalPanel.kt │ │ │ │ │ ├── CommitItemPanel.form │ │ │ │ │ ├── CommitItemPanel.kt │ │ │ │ │ ├── MergeRequestFilterPropertiesPanel.form │ │ │ │ │ ├── MergeRequestFilterPropertiesPanel.kt │ │ │ │ │ ├── MergeRequestInfoPanel.form │ │ │ │ │ ├── MergeRequestInfoPanel.kt │ │ │ │ │ ├── MergeRequestItemPanel.form │ │ │ │ │ ├── MergeRequestItemPanel.kt │ │ │ │ │ ├── ProjectPanel.form │ │ │ │ │ ├── ProjectPanel.kt │ │ │ │ │ ├── ProviderInformationPanel.form │ │ │ │ │ ├── ProviderInformationPanel.kt │ │ │ │ │ ├── ProviderItemPanel.form │ │ │ │ │ ├── ProviderItemPanel.kt │ │ │ │ │ ├── UserInfoItemPanel.form │ │ │ │ │ └── UserInfoItemPanel.kt │ │ │ │ ├── provider/ │ │ │ │ │ ├── ProviderCollection.kt │ │ │ │ │ ├── ProviderCollectionList.kt │ │ │ │ │ ├── ProviderCollectionListEventListener.kt │ │ │ │ │ ├── ProviderCollectionListUI.kt │ │ │ │ │ ├── ProviderCollectionToolbar.kt │ │ │ │ │ ├── ProviderCollectionToolbarEventListener.kt │ │ │ │ │ ├── ProviderCollectionToolbarUI.kt │ │ │ │ │ ├── ProviderDetails.kt │ │ │ │ │ ├── ProviderDetailsMRList.kt │ │ │ │ │ └── ProviderDetailsUI.kt │ │ │ │ ├── service/ │ │ │ │ │ ├── CheckoutService.kt │ │ │ │ │ ├── CodeReviewService.kt │ │ │ │ │ ├── DisplayChangesService.kt │ │ │ │ │ ├── EditorStateService.kt │ │ │ │ │ └── FetchService.kt │ │ │ │ ├── toolWindowTab/ │ │ │ │ │ ├── HomeToolWindowTab.kt │ │ │ │ │ ├── MergeRequestToolWindowTab.kt │ │ │ │ │ └── UpdateInfoTab.kt │ │ │ │ └── util/ │ │ │ │ ├── CustomSimpleToolWindowPanel.kt │ │ │ │ ├── ImageUtil.kt │ │ │ │ ├── Tabs.kt │ │ │ │ ├── TabsUI.kt │ │ │ │ ├── ToolbarUtil.kt │ │ │ │ └── fn.kt │ │ │ ├── util/ │ │ │ │ ├── CommentUtil.kt │ │ │ │ ├── FileTypeUtil.kt │ │ │ │ ├── HtmlHelper.kt │ │ │ │ ├── RepositoryUtil.kt │ │ │ │ └── TextChoiceUtil.kt │ │ │ └── watcher/ │ │ │ ├── Watcher.kt │ │ │ ├── WatcherManager.kt │ │ │ └── WatcherManagerImpl.kt │ │ └── resources/ │ │ └── templates/ │ │ ├── mr.comment.html │ │ ├── mr.description.html │ │ └── update.html │ └── test/ │ └── kotlin/ │ └── net/ │ └── ntworld/ │ └── mergeRequestIntegrationIde/ │ ├── configuration/ │ │ └── vos/ │ │ └── GitRemotePathInfoTest.kt │ ├── infrastructure/ │ │ └── DummyProjectServiceProvider.kt │ ├── internal/ │ │ └── CodeReviewServiceImplTest.kt │ ├── mergeRequest/ │ │ └── comments/ │ │ └── tree/ │ │ └── node/ │ │ └── NodeSyncManagerImplTest.kt │ └── watcher/ │ └── WatcherManagerImplTest.kt ├── merge-request-integration-ee/ │ ├── LICENSE │ ├── build.gradle.kts │ ├── doc/ │ │ ├── description.html │ │ ├── release-notes.2019.3.1.html │ │ ├── release-notes.2019.3.2.html │ │ ├── release-notes.2019.3.3.html │ │ ├── release-notes.2019.3.4.html │ │ ├── release-notes.2019.3.5.html │ │ ├── release-notes.2020.1.0.html │ │ ├── release-notes.2020.1.1.html │ │ ├── release-notes.2020.1.2.html │ │ ├── release-notes.2020.1.3.html │ │ ├── release-notes.2020.1.4.html │ │ ├── release-notes.2020.1.5.html │ │ ├── release-notes.2020.2.0.html │ │ └── release-notes.2020.3.0.html │ ├── settings.gradle.kts │ └── src/ │ └── main/ │ ├── kotlin/ │ │ └── net/ │ │ └── ntworld/ │ │ └── mergeRequestIntegrationIdeEE/ │ │ ├── CheckLicense.kt │ │ ├── Configuration.kt │ │ ├── DiffExtension.kt │ │ ├── DiffViewAddCommentAction.kt │ │ ├── DiffViewToggleCommentsAction.kt │ │ ├── EnterpriseApplicationServiceProvider.kt │ │ ├── EnterpriseProjectServiceProvider.kt │ │ ├── GithubConnectionsConfigurable.kt │ │ ├── GitlabConnectionsConfigurable.kt │ │ ├── MainToolWindowFactory.kt │ │ └── SingleMRToolWindowFactory.kt │ └── resources/ │ └── META-INF/ │ └── plugin.xml └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Ignore Gradle project-specific cache directory .gradle # Ignore Gradle build output directory build out .kotlintest .idea *.iml *.ipr *.iws internal-test/src ================================================ FILE: LICENSE ================================================ Merge Request Integration Community Edition (CE) (c) 2019-present Nhat Phan (https://github.com/nhat-phan) Merge Request Integration Community Edition (CE) is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License. You should have received a copy of the license along with this work. If not, see . Attribution-NonCommercial-NoDerivatives 4.0 International ======================================================================= Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC- licensed material, or material used under an exception or limitation to copyright. More considerations for licensors: wiki.creativecommons.org/Considerations_for_licensors Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason--for example, because of any applicable exception or limitation to copyright--then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More_considerations for the public: wiki.creativecommons.org/Considerations_for_licensees ======================================================================= Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 -- Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. c. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. d. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. e. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. f. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. g. Licensor means the individual(s) or entity(ies) granting rights under this Public License. h. NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 -- Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: a. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and b. produce and reproduce, but not Share, Adapted Material for NonCommercial purposes only. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. 5. Downstream recipients. a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. b. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. Section 3 -- License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material, You must: a. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. For the avoidance of doubt, You do not have permission under this Public License to Share Adapted Material. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. Section 4 -- Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only and provided You do not Share Adapted Material; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 -- Disclaimer of Warranties and Limitation of Liability. a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 -- Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 -- Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 -- Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ======================================================================= Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. ================================================ FILE: README.md ================================================ ## Merge Request Integration Merge Request Integration is a plugin which helps you to do Code Review right in your IDE. Merge Request Integration CE What you can do: - Filter Merge Requests which are assigned to you, waiting for your approval, etc - Check pipeline status and approval status. - Select and review 1 or all commits - Do code review, navigate code with Diff View right in your IDE. - Add, reply, resolve or delete comments - Approve/revoke your approval - More and more features will be coming soon :) Currently the plugin supports GitLab only (gitlab cloud and self-hosted). You can download the plugin on intellij plugins repository: [Community Edition](https://plugins.jetbrains.com/plugin/13607-merge-request-integration-ce--code-review-for-gitlab/), [Enterprise Edition](https://plugins.jetbrains.com/plugin/13615-merge-request-integration-ee--code-review-for-gitlab/) ### How to set up Gitlab connection #### Create Gitlab Personal Access Tokens To get the your Personal Access Token please follow these steps: Step 1: On top right corner > Settings Step 2: Click to Access Tokens menu Step 3: Create a Personal Access Tokens with api scope #### Config connection in your IDE Settings After creating the Personal Access Tokens: - Go to your IDE preferences (macOS: `⌘,` Windows: `Ctrl+Alt+S`) - Merge Request Integration > Gitlab - Fill data, then save. *Tip: you can use Starred/Membership/Own option to search your project quicker.* - Click refresh button of Merge Request Integration CE window if you don't see the connection. If your project has more than 1 repository, just setup multiple connections. ### License The plugin is an open source released under Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International license. It's totally free if you are using it for public repositories. For private repositories, this plugin is a trial. How long is the trial period? Equal to WINRAR's trial period 🙈 [Community Edition (CE)](https://plugins.jetbrains.com/plugin/13607-merge-request-integration-ce--code-review-for-gitlab/) is exactly the same as [Enterprise Edition (EE)](https://plugins.jetbrains.com/plugin/13615-merge-request-integration-ee--code-review-for-gitlab/). You don't need to hack or find a cracked version. Cracking software invites virus to your computer. ### About me My name is Nhat, I'm a software developer at [Personio](https://personio.com) (yes, [we are hiring](https://www.personio.com/about-personio/jobs/) all around the world, relocation to Munich is of course possible). ### Sponsor If you love this plugin, please support me by: - Buy an [Enterprise Edition](https://plugins.jetbrains.com/plugin/13615-merge-request-integration-ee--code-review-for-gitlab/), only 1$/month - Buy me a beer via [Paypal](https://paypal.me/phanhoangnhat) or [Patreon](https://www.patreon.com/nhat/creators). Thanks in advance! ### Attribution - Icons by [Font Awesome](https://fontawesome.com/) are licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) ================================================ FILE: build.gradle.kts ================================================ plugins { // "org.jetbrains.kotlin.jvm" kotlin("jvm") version "1.3.50" apply false // "org.jetbrains.kotlin.kapt" kotlin("kapt") version "1.3.50" apply false // "kotlinx-serialization" id("kotlinx-serialization") version "1.3.50" apply false id("org.jetbrains.intellij") version "0.4.12" apply false } subprojects { if (name == "contracts") { apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "org.jetbrains.kotlin.kapt") apply(plugin = "kotlinx-serialization") } if (name == "merge-request-integration") { apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "org.jetbrains.kotlin.kapt") apply(plugin = "kotlinx-serialization") } if (name == "merge-request-integration-core") { apply(plugin = "org.jetbrains.intellij") apply(plugin = "org.jetbrains.kotlin.jvm") } if (name == "merge-request-integration-ce") { apply(plugin = "org.jetbrains.intellij") apply(plugin = "org.jetbrains.kotlin.jvm") } if (name == "merge-request-integration-ee") { apply(plugin = "org.jetbrains.intellij") apply(plugin = "org.jetbrains.kotlin.jvm") } } ================================================ FILE: contracts/build.gradle.kts ================================================ val artifactGroup: String by project val artifactVersion: String by project val jvmTarget: String by project val foundationVersion: String by project val foundationProcessorVersion: String by project val jodaTimeVersion: String by project val kotlinxSerializationRuntimeVersion: String by project group = artifactGroup version = artifactVersion repositories { jcenter() mavenCentral() mavenLocal() maven("https://jitpack.io") } dependencies { implementation(kotlin("stdlib")) implementation("com.github.nhat-phan.foundation:foundation-jvm:$foundationVersion") implementation("joda-time:joda-time:$jodaTimeVersion") compile("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$kotlinxSerializationRuntimeVersion") kapt("com.github.nhat-phan.foundation:foundation-processor:$foundationProcessorVersion") kaptTest("com.github.nhat-phan.foundation:foundation-processor:$foundationProcessorVersion") testImplementation(kotlin("test")) testImplementation(kotlin("test-junit")) } kapt { arguments { arg("foundation.processor.mode", "contractOnly") arg("foundation.processor.settingsClass", "net.ntworld.mergeRequestIntegration.ContractData") } } tasks { named("compileKotlin") { kotlinOptions { jvmTarget = jvmTarget } } } ================================================ FILE: contracts/settings.gradle.kts ================================================ rootProject.name = "contracts" ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/Approval.kt ================================================ package net.ntworld.mergeRequest interface Approval { val approved: Boolean val approvalsRequired: Int val approvalsLeft: Int val approvers: List val suggestedApprovers: List val approvedBy: List val hasApproved: Boolean val canApprove: Boolean companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/Change.kt ================================================ package net.ntworld.mergeRequest interface Change { val oldPath: String val newPath: String val aMode: String val bMode: String val newFile: Boolean val renamedFile: Boolean val deletedFile: Boolean companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/Comment.kt ================================================ package net.ntworld.mergeRequest interface Comment { val id: String val parentId: String val replyId: String val body: String val author: UserInfo val position: CommentPosition? val createdAt: DateTime val updatedAt: DateTime val resolvable: Boolean val resolved: Boolean val resolvedBy: UserInfo? val isDraft: Boolean companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/CommentPosition.kt ================================================ package net.ntworld.mergeRequest interface CommentPosition { val baseHash: String val startHash: String val headHash: String val oldPath: String? val newPath: String? val oldLine: Int? val newLine: Int? val source: CommentPositionSource val changeType: CommentPositionChangeType companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/CommentPositionChangeType.kt ================================================ package net.ntworld.mergeRequest enum class CommentPositionChangeType{ UNKNOWN, INSERTED, DELETED, MODIFIED } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/CommentPositionSource.kt ================================================ package net.ntworld.mergeRequest enum class CommentPositionSource { UNKNOWN, SERVER, SINGLE_SIDE, SIDE_BY_SIDE_LEFT, SIDE_BY_SIDE_RIGHT, UNIFIED } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/Commit.kt ================================================ package net.ntworld.mergeRequest interface Commit { val id: String val message: String val authorName: String val authorEmail: String val createdAt: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/DateTime.kt ================================================ package net.ntworld.mergeRequest typealias DateTime = String ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/DateTimeSerializer.kt ================================================ @file:UseSerializers(DateTimeSerializer::class) package net.ntworld.mergeRequest import kotlinx.serialization.* import kotlinx.serialization.internal.SerialClassDescImpl import org.joda.time.DateTime /** * TODO: Cannot use until foundation support @SerializerFor(...) */ @Serializer(forClass = DateTime::class) object DateTimeSerializer : KSerializer { override val descriptor: SerialDescriptor = SerialClassDescImpl("DateTime") override fun deserialize(decoder: Decoder): DateTime { return DateTime(decoder.decodeString()) } override fun serialize(encoder: Encoder, obj: DateTime) { encoder.encodeString(obj.toString()) } } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/DiffReference.kt ================================================ package net.ntworld.mergeRequest interface DiffReference { val baseHash: String val headHash: String val startHash: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/MergeRequest.kt ================================================ package net.ntworld.mergeRequest interface MergeRequest: MergeRequestInfo { val assignee: UserInfo? val author: UserInfo val diffReference: DiffReference? val sourceBranch: String val targetBranch: String val upVotes: Int val downVotes: Int val commentsCount: Int val isWorkInProgress: Boolean val canMerged: Boolean val mergedBy: UserInfo? val closedBy: UserInfo? val mergedAt: DateTime? val closedAt: DateTime? companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/MergeRequestInfo.kt ================================================ package net.ntworld.mergeRequest interface MergeRequestInfo { val id: String val provider: String val projectId: String val title: String val description: String val url: String val state: MergeRequestState val createdAt: DateTime val updatedAt: DateTime companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/MergeRequestState.kt ================================================ package net.ntworld.mergeRequest enum class MergeRequestState { ALL, OPENED, CLOSED, MERGED } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/Pipeline.kt ================================================ package net.ntworld.mergeRequest interface Pipeline { val id: String val hash: String val ref: String val status: PipelineStatus val url: String val createdAt: DateTime? val updatedAt: DateTime? companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/PipelineStatus.kt ================================================ package net.ntworld.mergeRequest enum class PipelineStatus { FAILED, RUNNING, PARTIAL_FAILED, SUCCESS, UNKNOWN, } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/Project.kt ================================================ package net.ntworld.mergeRequest interface Project { val id: String val provider: ProviderInfo val name: String val path: String val url: String val avatarUrl: String val visibility: ProjectVisibility val repositoryHttpUrl: String val repositorySshUrl: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/ProjectVisibility.kt ================================================ package net.ntworld.mergeRequest enum class ProjectVisibility { PUBLIC, PRIVATE } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/ProviderData.kt ================================================ package net.ntworld.mergeRequest import net.ntworld.mergeRequest.api.ApiCredentials interface ProviderData { val id: String val key: String val name: String val info: ProviderInfo val credentials: ApiCredentials val project: Project val currentUser: User val repository: String val errorMessage: String? val status: ProviderStatus val hasApprovalFeature: Boolean get() = false val hasAssigneeFeature: Boolean get() = false companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/ProviderInfo.kt ================================================ package net.ntworld.mergeRequest interface ProviderInfo { val id: String val name: String val iconPath: String val icon2xPath: String val icon3xPath: String val icon4xPath: String fun createCommentUrl(mergeRequestUrl: String, comment: Comment): String fun formatMergeRequestId(mergeRequestId: String): String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/ProviderStatus.kt ================================================ package net.ntworld.mergeRequest enum class ProviderStatus { ACTIVE, ERROR } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/User.kt ================================================ package net.ntworld.mergeRequest interface User : UserInfo { val email: String val createdAt: DateTime companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/UserInfo.kt ================================================ package net.ntworld.mergeRequest interface UserInfo { val id: String val name: String val username: String val avatarUrl: String? val url: String val status: UserStatus companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/UserStatus.kt ================================================ package net.ntworld.mergeRequest enum class UserStatus { ACTIVE, INACTIVE } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/api/ApiConnection.kt ================================================ package net.ntworld.mergeRequest.api interface ApiConnection { val url: String val ignoreSSLCertificateErrors: Boolean val login: String val token: String } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/api/ApiCredentials.kt ================================================ package net.ntworld.mergeRequest.api interface ApiCredentials : ApiConnection { val projectId: String val version: String val info: String } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/api/ApiOptions.kt ================================================ package net.ntworld.mergeRequest.api interface ApiOptions { val enableRequestCache: Boolean } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/api/ApiProvider.kt ================================================ package net.ntworld.mergeRequest.api import net.ntworld.mergeRequest.ProviderInfo import net.ntworld.mergeRequest.User import net.ntworld.mergeRequest.UserInfo interface ApiProvider { val info: ProviderInfo val credentials: ApiCredentials val cache: Cache val user: UserApi val mergeRequest: MergeRequestApi val project: ProjectApi val comment: CommentApi val commit: CommitApi fun setOptions(options: ApiOptions) fun initialize(currentUser: UserInfo) } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/api/Cache.kt ================================================ package net.ntworld.mergeRequest.api import org.joda.time.DateTime interface Cache { val defaultTTL: Int fun get(key: String): T fun has(key: String): Boolean fun remove(key: String) fun put(key: String, value: Any, ttl: Int) fun isExpiredAfter(key: String, datetime: DateTime): Boolean fun put(key: String, value: Any) = put(key, value, defaultTTL) fun set(key: String, value: Any) = put(key, value, defaultTTL) fun set(key: String, value: Any, ttl: Int) = put(key, value, ttl) @Suppress("UNCHECKED_CAST") fun getOrRun(key: String, run: (() -> T)): T { try { if (!this.has(key)) { return run() } return this.get(key) } catch (exception: CacheNotFoundException) { return run() } } fun removeIfExpiredAfter(key: String, datetime: DateTime) { if (isExpiredAfter(key, datetime)) { remove(key) } } } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/api/CacheNotFoundException.kt ================================================ package net.ntworld.mergeRequest.api class CacheNotFoundException : Throwable() ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/api/CommentApi.kt ================================================ package net.ntworld.mergeRequest.api import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequest.Project interface CommentApi { fun getAll(project: Project, mergeRequestId: String): List fun create( project: Project, mergeRequestId: String, body: String, position: CommentPosition?, isDraft: Boolean ): String? fun reply(project: Project, mergeRequestId: String, repliedComment: Comment, body: String): String? fun delete(project: Project, mergeRequestId: String, comment: Comment) fun resolve(project: Project, mergeRequestId: String, comment: Comment) fun unresolve(project: Project, mergeRequestId: String, comment: Comment) fun update(project: Project, mergeRequestId: String, comment: Comment, body: String) fun hasDraft(project: Project, mergeRequestId: String): Boolean { return this.getDraftCount(project, mergeRequestId) > 0 } fun getDraftCount(project: Project, mergeRequestId: String): Int fun publishAllDraftComments(project: Project, mergeRequestId: String) fun publishDraftComments(project: Project, mergeRequestId: String, commentIds: List) } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/api/CommitApi.kt ================================================ package net.ntworld.mergeRequest.api import net.ntworld.mergeRequest.Change interface CommitApi { fun getChanges(projectId: String, commitId: String): List } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/api/DraftCommentStorage.kt ================================================ package net.ntworld.mergeRequest.api import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequest.Project interface DraftCommentStorage { fun getAll(project: Project, mergeRequestId: String): List fun findById(project: Project, mergeRequestId: String, commentId: String): Comment? fun create(project: Project, mergeRequestId: String, body: String, position: CommentPosition?): String? fun update(project: Project, mergeRequestId: String, comment: Comment, body: String) fun delete(project: Project, mergeRequestId: String, comment: Comment) } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/api/MergeRequestApi.kt ================================================ package net.ntworld.mergeRequest.api import net.ntworld.mergeRequest.* import net.ntworld.mergeRequest.query.GetMergeRequestFilter interface MergeRequestApi { fun find(projectId: String, mergeRequestId: String): MergeRequest? fun approve(projectId: String, mergeRequestId: String, sha: String) fun unapprove(projectId: String, mergeRequestId: String) fun findApproval(projectId: String, mergeRequestId: String): Approval fun getPipelines(projectId: String, mergeRequestId: String): List fun getCommits(projectId: String, mergeRequestId: String): List fun getChanges(projectId: String, mergeRequestId: String): List fun search( projectId: String, currentUserId: String, filterBy: GetMergeRequestFilter, orderBy: MergeRequestOrdering, page: Int, itemsPerPage: Int ): SearchResult fun findOrFail(projectId: String, mergeRequestId: String): MergeRequest interface SearchResult { val data: List val totalPages: Int val totalItems: Int val currentPage: Int } } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/api/MergeRequestOrdering.kt ================================================ package net.ntworld.mergeRequest.api enum class MergeRequestOrdering { RECENTLY_UPDATED, NEWEST, OLDEST } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/api/ProjectApi.kt ================================================ package net.ntworld.mergeRequest.api import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.UserInfo interface ProjectApi { fun find(projectId: String): Project? fun getMembers(projectId: String): List fun findOrFail(projectId: String): Project { val project = find(projectId) if (null === project) { throw Exception("Project $projectId not found.") } return project } } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/api/UserApi.kt ================================================ package net.ntworld.mergeRequest.api import net.ntworld.mergeRequest.User interface UserApi { // @Deprecated("Not used, will be removed", ReplaceWith("Nothing"), DeprecationLevel.HIDDEN) // fun find(id: String): UserInfo fun me(): User } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/command/ApproveMergeRequestCommand.kt ================================================ package net.ntworld.mergeRequest.command import net.ntworld.foundation.cqrs.Command interface ApproveMergeRequestCommand : Command { val providerId: String val mergeRequestId: String val sha: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/command/DeleteCommentCommand.kt ================================================ package net.ntworld.mergeRequest.command import net.ntworld.foundation.cqrs.Command import net.ntworld.mergeRequest.Comment interface DeleteCommentCommand : Command { val providerId: String val mergeRequestId: String val comment: Comment companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/command/ResolveCommentCommand.kt ================================================ package net.ntworld.mergeRequest.command import net.ntworld.foundation.cqrs.Command import net.ntworld.mergeRequest.Comment interface ResolveCommentCommand : Command { val providerId: String val mergeRequestId: String val comment: Comment companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/command/UnapproveMergeRequestCommand.kt ================================================ package net.ntworld.mergeRequest.command import net.ntworld.foundation.cqrs.Command interface UnapproveMergeRequestCommand : Command { val providerId: String val mergeRequestId: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/command/UnresolveCommentCommand.kt ================================================ package net.ntworld.mergeRequest.command import net.ntworld.foundation.cqrs.Command import net.ntworld.mergeRequest.Comment interface UnresolveCommentCommand : Command { val providerId: String val mergeRequestId: String val comment: Comment companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/FindApprovalQuery.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.Query interface FindApprovalQuery : QueryBase, Query { val mergeRequestId: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/FindApprovalQueryResult.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.QueryResult import net.ntworld.mergeRequest.Approval interface FindApprovalQueryResult : QueryResult { val approval: Approval companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/FindMergeRequestQuery.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.Query interface FindMergeRequestQuery: QueryBase, Query { val mergeRequestId: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/FindMergeRequestQueryResult.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.QueryResult import net.ntworld.mergeRequest.MergeRequest interface FindMergeRequestQueryResult : QueryResult { val mergeRequest: MergeRequest companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetCommentsQuery.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.Query interface GetCommentsQuery : QueryBase, Query { val mergeRequestId: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetCommentsQueryResult.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.QueryResult import net.ntworld.mergeRequest.Comment interface GetCommentsQueryResult : QueryResult { val comments: List companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetCommitsQuery.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.Query interface GetCommitsQuery : QueryBase, Query { val mergeRequestId: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetCommitsQueryResult.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.QueryResult import net.ntworld.mergeRequest.Commit interface GetCommitsQueryResult : QueryResult { val commits: List companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetMergeRequestFilter.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.mergeRequest.MergeRequestState interface GetMergeRequestFilter { val id: Int? val state: MergeRequestState val search: String val authorId: String val assigneeId: String val approverIds: List val sourceBranch: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetMergeRequestsQuery.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.Query import net.ntworld.mergeRequest.api.MergeRequestOrdering interface GetMergeRequestsQuery : QueryBase, Query { val filterBy: GetMergeRequestFilter val orderBy: MergeRequestOrdering val page: Int val itemsPerPage: Int companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetMergeRequestsQueryResult.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.QueryResult import net.ntworld.mergeRequest.MergeRequestInfo interface GetMergeRequestsQueryResult : QueryResult { val mergeRequests: List val totalPages: Int val totalItems: Int val currentPage: Int companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetPipelinesQuery.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.Query interface GetPipelinesQuery : QueryBase, Query { val mergeRequestId: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetPipelinesQueryResult.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.QueryResult import net.ntworld.mergeRequest.Pipeline interface GetPipelinesQueryResult : QueryResult { val pipelines: List companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetProjectMembersQuery.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.Query interface GetProjectMembersQuery : QueryBase, Query { companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetProjectMembersQueryResult.kt ================================================ package net.ntworld.mergeRequest.query import net.ntworld.foundation.cqrs.QueryResult import net.ntworld.mergeRequest.UserInfo interface GetProjectMembersQueryResult : QueryResult { val members: List companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/query/QueryBase.kt ================================================ package net.ntworld.mergeRequest.query interface QueryBase { val providerId: String } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/request/CreateCommentRequest.kt ================================================ package net.ntworld.mergeRequest.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequest.response.CreateCommentResponse interface CreateCommentRequest : Request { val providerId: String val mergeRequestId: String val body: String val position: CommentPosition? val isDraft: Boolean companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/request/PublishAllCommentsRequest.kt ================================================ package net.ntworld.mergeRequest.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.response.PublishAllCommentsResponse interface PublishAllCommentsRequest : Request { val providerId: String val mergeRequestId: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/request/PublishCommentsRequest.kt ================================================ package net.ntworld.mergeRequest.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.response.PublishCommentsResponse interface PublishCommentsRequest : Request { val providerId: String val mergeRequestId: String val draftCommentIds: List companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/request/ReplyCommentRequest.kt ================================================ package net.ntworld.mergeRequest.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.response.ReplyCommentResponse interface ReplyCommentRequest : Request { val providerId: String val mergeRequestId: String val repliedComment: Comment val body: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/request/UpdateCommentRequest.kt ================================================ package net.ntworld.mergeRequest.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.response.UpdateCommentResponse interface UpdateCommentRequest : Request { val providerId: String val mergeRequestId: String val comment: Comment val body: String companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/response/CreateCommentResponse.kt ================================================ package net.ntworld.mergeRequest.response import net.ntworld.foundation.Response interface CreateCommentResponse : Response { val createdCommentId: String? companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/response/PublishAllCommentsResponse.kt ================================================ package net.ntworld.mergeRequest.response import net.ntworld.foundation.Response interface PublishAllCommentsResponse : Response { val success: Boolean companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/response/PublishCommentsResponse.kt ================================================ package net.ntworld.mergeRequest.response import net.ntworld.foundation.Response interface PublishCommentsResponse : Response { val success: Boolean companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/response/ReplyCommentResponse.kt ================================================ package net.ntworld.mergeRequest.response import net.ntworld.foundation.Response interface ReplyCommentResponse : Response { val createdCommentId: String? companion object } ================================================ FILE: contracts/src/main/kotlin/net/ntworld/mergeRequest/response/UpdateCommentResponse.kt ================================================ package net.ntworld.mergeRequest.response import net.ntworld.foundation.Response interface UpdateCommentResponse : Response { val commentId: String? companion object } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Thu Oct 10 17:57:14 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-5.3.1-all.zip ================================================ FILE: gradle.properties ================================================ artifactGroup=net.ntworld.nhat-phan.merge-request-integration # Please also change version number in # net.ntworld.mergeRequestIntegration.update.UpdateManager eapRelease=false artifactVersion=2020.3.0 targetIDEVersion=2020.3.x communityEditionVersion=2020.3.0 enterpriseEditionVersion=2020.3.0 # intellijVersion=LATEST-EAP-SNAPSHOT intellijVersion=2020.3 intellijSinceBuild=203 intellijUntilBuild=203.* jvmTarget=1.8 foundationVersion=0.6 foundationProcessorVersion=0.6.1 kotlinxSerializationRuntimeVersion=0.11.1 javaFakerVersion=1.0.0 jodaTimeVersion=2.10.5 fuelVersion=2.2.1 gitlab4jVersion=4.14.5 githubApiVersion=1.101 prettyTimeVersion=4.0.1.Final commonmarkVersion=0.13.0 mockkVersion=1.9.3 ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem http://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: merge-request-integration/build.gradle.kts ================================================ val artifactGroup: String by project val artifactVersion: String by project val jvmTarget: String by project val foundationVersion: String by project val foundationProcessorVersion: String by project val kotlinxSerializationRuntimeVersion: String by project val javaFakerVersion: String by project val jodaTimeVersion: String by project val fuelVersion: String by project val gitlab4jVersion: String by project val githubApiVersion: String by project val prettyTimeVersion: String by project val commonmarkVersion: String by project group = artifactGroup version = artifactVersion repositories { jcenter() mavenCentral() mavenLocal() maven("https://jitpack.io") } dependencies { implementation(kotlin("stdlib")) implementation("com.github.nhat-phan.foundation:foundation-jvm:$foundationVersion") implementation(project(":contracts")) implementation("joda-time:joda-time:$jodaTimeVersion") implementation("com.github.kittinunf.fuel:fuel:$fuelVersion") implementation("org.gitlab4j:gitlab4j-api:$gitlab4jVersion") implementation("org.kohsuke:github-api:$githubApiVersion") implementation("org.ocpsoft.prettytime:prettytime:$prettyTimeVersion") compile("com.atlassian.commonmark:commonmark:$commonmarkVersion") compile("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$kotlinxSerializationRuntimeVersion") compile("com.github.javafaker:javafaker:$javaFakerVersion") kapt("com.github.nhat-phan.foundation:foundation-processor:$foundationProcessorVersion") kaptTest("com.github.nhat-phan.foundation:foundation-processor:$foundationProcessorVersion") testImplementation(kotlin("test")) testImplementation(kotlin("test-junit")) testImplementation("io.mockk:mockk:1.9") testImplementation("io.kotlintest:kotlintest-runner-junit5:3.3.0") } tasks.withType { useJUnitPlatform() } kapt { arguments { arg("foundation.processor.globalNamespace", "net.ntworld.mergeRequestIntegration") } } tasks { named("compileKotlin") { kotlinOptions { jvmTarget = jvmTarget } } } ================================================ FILE: merge-request-integration/settings.gradle.kts ================================================ rootProject.name = "merge-request-integration" ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/DefaultProviderStorage.kt ================================================ package net.ntworld.mergeRequestIntegration import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.* import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequest.api.ApiOptions import net.ntworld.mergeRequest.api.ApiProvider import net.ntworld.mergeRequestIntegration.exception.ProviderNotFoundException import net.ntworld.mergeRequestIntegration.internal.ProjectImpl import net.ntworld.mergeRequestIntegration.internal.ProviderDataImpl import net.ntworld.mergeRequestIntegration.internal.UserImpl import net.ntworld.mergeRequestIntegration.provider.MemoryCache import net.ntworld.mergeRequestIntegration.provider.gitlab.Gitlab import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabApiProvider import java.util.* class DefaultProviderStorage : ProviderStorage { private val data = Collections.synchronizedMap(mutableMapOf()) private val api = Collections.synchronizedMap(mutableMapOf()) override val registeredProviders get() = data.values.toList() override fun updateApiOptions(options: ApiOptions) { api.forEach { it.value.setOptions(options) } } override fun register( infrastructure: Infrastructure, id: String, key: String, name: String, info: ProviderInfo, credentials: ApiCredentials, repository: String ): Pair { val api = createApiProvider(infrastructure = infrastructure, id = id, info = info, credentials = credentials) var throwable: Throwable? = null var message: String? = null try { val user = api.user.me() api.initialize(user) val project = api.project.find(credentials.projectId) if (null !== project) { val providerData = ProviderDataImpl( id = id, key = key, name = name, info = info, credentials = credentials, project = project, currentUser = user, repository = repository, errorMessage = null, status = ProviderStatus.ACTIVE ) data[id] = providerData return Pair(providerData, null) } } catch (exception: Throwable) { message = exception.message throwable = exception } val invalid = ProviderDataImpl( id = id, key = key, name = name, info = info, credentials = credentials, project = ProjectImpl("", info, "", "", "", "", ProjectVisibility.PUBLIC, "", ""), currentUser = UserImpl("", "[Error]", "", "", "", UserStatus.INACTIVE, "", ""), repository = repository, errorMessage = message, status = ProviderStatus.ERROR ) data[id] = invalid return Pair(invalid, throwable) } override fun clear() { data.clear() api.clear() } private fun createApiProvider( infrastructure: Infrastructure, id: String, info: ProviderInfo, credentials: ApiCredentials ): ApiProvider { val created = when (info.id) { Gitlab.id -> GitlabApiProvider( infrastructure = infrastructure, credentials = credentials, cache = MemoryCache() ) // Github.id -> GithubApiProvider( // infrastructure = infrastructure, // credentials = credentials, // cache = MemoryCache() // ) else -> throw Exception("Cannot create ApiProvider ${info.id}") } api[id] = created return created } override fun findOrFail(id: String): Pair { return Pair(findDataOrFail(id), findProviderOrFail(id)) } private fun findDataOrFail(id: String): ProviderData { val item = data[id] return if (null !== item) { item } else { throw ProviderNotFoundException() } } private fun findProviderOrFail(id: String): ApiProvider { val provider = api[id] return if (null !== provider) { provider } else { throw ProviderNotFoundException() } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/MergeRequestIntegrationInfrastructure.kt ================================================ package net.ntworld.mergeRequestIntegration import net.ntworld.foundation.InfrastructureProvider class MergeRequestIntegrationInfrastructure( private val providerStorage: ProviderStorage ) : InfrastructureProvider() { private val included = listOf( AutoGeneratedInfrastructureProvider(providerStorage) ) init { wire(this.root, this.included) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/ProviderStorage.kt ================================================ package net.ntworld.mergeRequestIntegration import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.ProviderInfo import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequest.api.ApiOptions import net.ntworld.mergeRequest.api.ApiProvider interface ProviderStorage { val registeredProviders: List fun updateApiOptions(options: ApiOptions) fun register( infrastructure: Infrastructure, id: String, key: String, name: String, info: ProviderInfo, credentials: ApiCredentials, repository: String ): Pair fun clear() fun findOrFail(id: String): Pair } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/_const.kt ================================================ package net.ntworld.mergeRequestIntegration const val DEFAULT_DATETIME = "1970-01-01T00:00:00Z" ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/commandHandler/ApproveMergeRequestCommandHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.commandHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.CommandHandler import net.ntworld.mergeRequest.command.ApproveMergeRequestCommand import net.ntworld.mergeRequestIntegration.ProviderStorage @Handler class ApproveMergeRequestCommandHandler( private val providerStorage: ProviderStorage ) : CommandHandler { override fun handle(command: ApproveMergeRequestCommand) { val (data, api) = providerStorage.findOrFail(command.providerId) api.mergeRequest.approve( projectId = data.project.id, mergeRequestId = command.mergeRequestId, sha = command.sha ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/commandHandler/DeleteCommentCommandHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.commandHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.CommandHandler import net.ntworld.mergeRequest.command.DeleteCommentCommand import net.ntworld.mergeRequestIntegration.ProviderStorage @Handler class DeleteCommentCommandHandler( private val providerStorage: ProviderStorage ) : CommandHandler { override fun handle(command: DeleteCommentCommand) { val (data, api) = providerStorage.findOrFail(command.providerId) api.comment.delete(data.project, command.mergeRequestId, command.comment) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/commandHandler/ResolveCommentCommandHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.commandHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.CommandHandler import net.ntworld.mergeRequest.command.ResolveCommentCommand import net.ntworld.mergeRequestIntegration.ProviderStorage @Handler class ResolveCommentCommandHandler( private val providerStorage: ProviderStorage ) : CommandHandler { override fun handle(command: ResolveCommentCommand) { val (data, api) = providerStorage.findOrFail(command.providerId) api.comment.resolve(data.project, command.mergeRequestId, command.comment) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/commandHandler/UnapproveMergeRequestCommandHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.commandHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.CommandHandler import net.ntworld.mergeRequest.command.UnapproveMergeRequestCommand import net.ntworld.mergeRequestIntegration.ProviderStorage @Handler class UnapproveMergeRequestCommandHandler( private val providerStorage: ProviderStorage ) : CommandHandler { override fun handle(command: UnapproveMergeRequestCommand) { val (data, api) = providerStorage.findOrFail(command.providerId) api.mergeRequest.unapprove( projectId = data.project.id, mergeRequestId = command.mergeRequestId ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/commandHandler/UnresolveCommentCommandHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.commandHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.CommandHandler import net.ntworld.mergeRequest.command.UnresolveCommentCommand import net.ntworld.mergeRequestIntegration.ProviderStorage @Handler class UnresolveCommentCommandHandler( private val providerStorage: ProviderStorage ) : CommandHandler { override fun handle(command: UnresolveCommentCommand) { val (data, api) = providerStorage.findOrFail(command.providerId) api.comment.unresolve(data.project, command.mergeRequestId, command.comment) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/exception/InvalidCacheKeyException.kt ================================================ package net.ntworld.mergeRequestIntegration.exception import java.lang.Exception class InvalidCacheKeyException(message: String): Exception(message) ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/exception/InvalidTTLException.kt ================================================ package net.ntworld.mergeRequestIntegration.exception class InvalidTTLException: Exception() ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/exception/ProviderNotFoundException.kt ================================================ package net.ntworld.mergeRequestIntegration.exception import java.lang.Exception class ProviderNotFoundException: Exception() ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/ApiOptionsImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.api.ApiOptions data class ApiOptionsImpl(override val enableRequestCache: Boolean) : ApiOptions { companion object { val DEFAULT = ApiOptionsImpl( enableRequestCache = true ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/ApprovalImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.Approval import net.ntworld.mergeRequest.UserInfo data class ApprovalImpl( override val approved: Boolean, override val approvalsRequired: Int, override val approvalsLeft: Int, override val suggestedApprovers: List, override val approvers: List, override val approvedBy: List, override val hasApproved: Boolean, override val canApprove: Boolean ) : Approval ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/ChangeImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.Change data class ChangeImpl( override val oldPath: String, override val newPath: String, override val aMode: String, override val bMode: String, override val newFile: Boolean, override val renamedFile: Boolean, override val deletedFile: Boolean ) : Change ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/CommentImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequest.DateTime import net.ntworld.mergeRequest.UserInfo data class CommentImpl( override val id: String, override val parentId: String, override val replyId: String, override val body: String, override val author: UserInfo, override val position: CommentPosition?, override val createdAt: DateTime, override val updatedAt: DateTime, override val resolvable: Boolean, override val resolved: Boolean, override val resolvedBy: UserInfo?, override val isDraft: Boolean ) : Comment { } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/CommentPositionImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequest.CommentPositionChangeType import net.ntworld.mergeRequest.CommentPositionSource data class CommentPositionImpl( override val baseHash: String, override val startHash: String, override val headHash: String, override val oldPath: String?, override val newPath: String?, override val oldLine: Int?, override val newLine: Int?, override val source: CommentPositionSource, override val changeType: CommentPositionChangeType ): CommentPosition ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/CommitImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.Commit data class CommitImpl( override val id: String, override val message: String, override val authorName: String, override val authorEmail: String, override val createdAt: String ) : Commit ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/DiffReferenceImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.DiffReference data class DiffReferenceImpl( override val baseHash: String, override val headHash: String, override val startHash: String ): DiffReference ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/MergeRequestImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.* class MergeRequestImpl( override val id: String, override val provider: String, override val projectId: String, override val title: String, override val description: String, override val url: String, override val state: MergeRequestState, override val createdAt: DateTime, override val updatedAt: DateTime, override val assignee: UserInfo?, override val author: UserInfo, override val diffReference: DiffReference?, override val sourceBranch: String, override val targetBranch: String, override val upVotes: Int, override val downVotes: Int, override val commentsCount: Int, override val isWorkInProgress: Boolean, override val canMerged: Boolean, override val mergedBy: UserInfo?, override val closedBy: UserInfo?, override val mergedAt: DateTime?, override val closedAt: DateTime? ) : MergeRequest ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/MergeRequestInfoImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.DateTime import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.MergeRequestState data class MergeRequestInfoImpl( override val id: String, override val provider: String, override val projectId: String, override val title: String, override val description: String, override val url: String, override val state: MergeRequestState, override val createdAt: DateTime, override val updatedAt: DateTime ) : MergeRequestInfo ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/MergeRequestSearchResultImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.api.MergeRequestApi data class MergeRequestSearchResultImpl( override val data: List, override val totalPages: Int, override val totalItems: Int, override val currentPage: Int ): MergeRequestApi.SearchResult ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/PipelineImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.DateTime import net.ntworld.mergeRequest.Pipeline import net.ntworld.mergeRequest.PipelineStatus data class PipelineImpl( override val id: String, override val hash: String, override val ref: String, override val status: PipelineStatus, override val url: String, override val createdAt: DateTime?, override val updatedAt: DateTime? ) : Pipeline ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/ProjectImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.ProjectVisibility import net.ntworld.mergeRequest.ProviderInfo data class ProjectImpl( override val id: String, override val provider: ProviderInfo, override val name: String, override val path: String, override val url: String, override val avatarUrl: String, override val visibility: ProjectVisibility, override val repositoryHttpUrl: String, override val repositorySshUrl: String ): Project ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/ProviderDataImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.* import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.Gitlab import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil data class ProviderDataImpl( override val id: String, override val key: String, override val name: String, override val info: ProviderInfo, override val credentials: ApiCredentials, override val project: Project, override val currentUser: User, override val repository: String, override val errorMessage: String?, override val status: ProviderStatus ) : ProviderData { override val hasApprovalFeature: Boolean get() { if (this.info.id != Gitlab.id) { return true } if (GitlabUtil.hasMergeApprovalFeature(credentials)) { return true } return false } override val hasAssigneeFeature: Boolean get() { if (this.info.id == Gitlab.id) { return true } return false } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/UserImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.DateTime import net.ntworld.mergeRequest.User import net.ntworld.mergeRequest.UserStatus data class UserImpl( override val id: String, override val name: String, override val username: String, override val avatarUrl: String?, override val url: String, override val status: UserStatus, override val email: String, override val createdAt: DateTime ) : User ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/UserInfoImpl.kt ================================================ package net.ntworld.mergeRequestIntegration.internal import net.ntworld.mergeRequest.UserInfo import net.ntworld.mergeRequest.UserStatus data class UserInfoImpl( override val id: String, override val name: String, override val username: String, override val avatarUrl: String?, override val url: String, override val status: UserStatus ) : UserInfo { companion object { val None = UserInfoImpl( id = "", name = "", username = "", avatarUrl = "", url = "", status = UserStatus.ACTIVE ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/DraftCommentApi.kt ================================================ package net.ntworld.mergeRequestIntegration.provider import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.api.CommentApi import net.ntworld.mergeRequest.api.DraftCommentStorage class DraftCommentApi(private val api: CommentApi, private val storage: DraftCommentStorage): CommentApi { override fun getAll(project: Project, mergeRequestId: String): List { val result = mutableListOf() result.addAll(api.getAll(project, mergeRequestId)) result.addAll(storage.getAll(project, mergeRequestId)) return result } override fun create( project: Project, mergeRequestId: String, body: String, position: CommentPosition?, isDraft: Boolean ): String? { if (isDraft) { return storage.create(project, mergeRequestId, body, position) } return api.create(project, mergeRequestId, body, position, false) } override fun reply(project: Project, mergeRequestId: String, repliedComment: Comment, body: String): String? { return api.reply(project, mergeRequestId, repliedComment, body) } override fun delete(project: Project, mergeRequestId: String, comment: Comment) { if (comment.isDraft) { return storage.delete(project, mergeRequestId, comment) } return api.delete(project, mergeRequestId, comment) } override fun resolve(project: Project, mergeRequestId: String, comment: Comment) { if (!comment.isDraft) { return api.resolve(project, mergeRequestId, comment) } } override fun unresolve(project: Project, mergeRequestId: String, comment: Comment) { if (!comment.isDraft) { return api.unresolve(project, mergeRequestId, comment) } } override fun update(project: Project, mergeRequestId: String, comment: Comment, body: String) { if (!comment.isDraft) { return api.update(project, mergeRequestId, comment, body) } return storage.update(project, mergeRequestId, comment, body) } override fun getDraftCount(project: Project, mergeRequestId: String): Int { return storage.getAll(project, mergeRequestId).count() } override fun publishAllDraftComments(project: Project, mergeRequestId: String) { val comments = storage.getAll(project, mergeRequestId) for (comment in comments) { publishDraftComment(project, mergeRequestId, comment) } } override fun publishDraftComments(project: Project, mergeRequestId: String, commentIds: List) { for (commentId in commentIds) { val comment = storage.findById(project, mergeRequestId, commentId) if (null !== comment) { publishDraftComment(project, mergeRequestId, comment) } } } private fun publishDraftComment(project: Project, mergeRequestId: String, comment: Comment) { if (comment.isDraft) { api.create(project, mergeRequestId, comment.body, comment.position, false) storage.delete(project, mergeRequestId, comment) } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/FuelClient.kt ================================================ package net.ntworld.mergeRequestIntegration.provider import com.github.kittinunf.fuel.core.* import com.github.kittinunf.result.Result import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonConfiguration import net.ntworld.mergeRequest.api.ApiCredentials import java.security.cert.X509Certificate import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager abstract class FuelClient ( private val credentials: ApiCredentials ) { val json = Json(JsonConfiguration.Stable.copy(strictMode = false)) protected abstract fun injectAuthentication(httpRequest: Request): Request protected fun makeRequestFactory(): RequestFactory.Convenience { if (credentials.ignoreSSLCertificateErrors) { return FuelManager().apply { val trustAllCerts = arrayOf(object : X509TrustManager { override fun getAcceptedIssuers(): Array? = null override fun checkClientTrusted(chain: Array, authType: String) = Unit override fun checkServerTrusted(chain: Array, authType: String) = Unit }) socketFactory = SSLContext.getInstance("SSL").apply { init(null, trustAllCerts, java.security.SecureRandom()) }.socketFactory hostnameVerifier = HostnameVerifier { _, _ -> true } } } return FuelManager() } fun postJson(url: String, parameters: Parameters? = null): String { return executeRequest(makeRequestFactory().post(url, parameters)) } fun deleteJson(url: String, parameters: Parameters? = null): String { return executeRequest(makeRequestFactory().delete(url, parameters)) } fun putJson(url: String, parameters: Parameters? = null): String { return executeRequest(makeRequestFactory().put(url, parameters)) } fun getJson(url: String, parameters: Parameters? = null): String { return executeRequest(makeRequestFactory().get(url, parameters)) } protected fun executeRequest(request: Request) : String { val httpRequest = injectAuthentication(request) val (_, response, result) = httpRequest.responseString() return when (result) { is Result.Success -> { result.value } is Result.Failure -> { throw HttpException(response.statusCode, result.error.message ?: "Unknown") } } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/MemoryCache.kt ================================================ package net.ntworld.mergeRequestIntegration.provider import net.ntworld.mergeRequest.api.Cache import net.ntworld.mergeRequest.api.CacheNotFoundException import net.ntworld.mergeRequestIntegration.exception.InvalidCacheKeyException import net.ntworld.mergeRequestIntegration.exception.InvalidTTLException import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.joda.time.LocalDateTime class MemoryCache(ttl: Int? = null) : Cache { private val data = mutableMapOf() override var defaultTTL: Int = 0 private set init { defaultTTL = if (null === ttl) { 60000 } else { if (ttl <= 0) { throw InvalidTTLException() } ttl } } @Suppress("UNCHECKED_CAST") override fun get(key: String): T { assertKeyIsValid(key) if (!data.containsKey(key)) { throw CacheNotFoundException() } val cachedItem = data[key] if (null === cachedItem || isExpired(cachedItem.expired)) { throw CacheNotFoundException() } return cachedItem.value as T } override fun has(key: String): Boolean { assertKeyIsValid(key) if (!data.containsKey(key)) { return false } val cachedItem = data[key] return null !== cachedItem && !isExpired(cachedItem.expired) } override fun remove(key: String) { assertKeyIsValid(key) data.remove(key) } override fun put(key: String, value: Any, ttl: Int) { data[key] = CachedData(value, now() + ttl) } override fun isExpiredAfter(key: String, datetime: DateTime): Boolean { assertKeyIsValid(key) val cachedItem = data[key] if (null === cachedItem) { throw CacheNotFoundException() } return cachedItem.expired < toUtc(datetime).millis } private fun assertKeyIsValid(key: String) { if (key.trim().isEmpty()) { throw InvalidCacheKeyException("Key is invalid") } } internal fun isExpired(expired: Long): Boolean { return expired < now() } internal fun now(): Long { return toUtc(DateTime.now()).millis } private fun toUtc(datetime: DateTime): DateTime { return LocalDateTime(datetime).toDateTime(DateTimeZone.UTC) } private data class CachedData( val value: Any, val expired: Long ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/MemoryDraftCommentStorage.kt ================================================ package net.ntworld.mergeRequestIntegration.provider import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.UserInfo import net.ntworld.mergeRequest.api.DraftCommentStorage import net.ntworld.mergeRequestIntegration.internal.CommentImpl import net.ntworld.mergeRequestIntegration.util.DateTimeUtil import org.joda.time.DateTime import java.util.* class MemoryDraftCommentStorage(private val currentUser: UserInfo) : DraftCommentStorage { private val data = mutableMapOf>() private fun getMutableMap(project: Project, mergeRequestId: String): MutableMap { val key = project.id + "-" + mergeRequestId if (!data.contains(key)) { data[key] = mutableMapOf() } return data[key] as MutableMap } override fun getAll(project: Project, mergeRequestId: String): List { return getMutableMap(project, mergeRequestId).values.toList() } override fun findById(project: Project, mergeRequestId: String, commentId: String): Comment? { val mutableMap = getMutableMap(project, mergeRequestId) return mutableMap[commentId] } override fun create(project: Project, mergeRequestId: String, body: String, position: CommentPosition?): String? { val mutableMap = getMutableMap(project, mergeRequestId) val id = "tmp:${UUID.randomUUID()}" mutableMap[id] = CommentImpl( id = id, parentId = UUID.randomUUID().toString(), replyId = "", body = body, position = position, createdAt = DateTimeUtil.fromDate(DateTime.now().toDate()), updatedAt = DateTimeUtil.fromDate(DateTime.now().toDate()), resolvable = false, resolved = false, resolvedBy = null, author = currentUser, isDraft = true ) return id } override fun update(project: Project, mergeRequestId: String, comment: Comment, body: String) { if (!comment.isDraft) { return } val mutableMap = getMutableMap(project, mergeRequestId) if (mutableMap.contains(comment.id)) { mutableMap[comment.id] = CommentImpl( id = comment.id, parentId = comment.parentId, replyId = "", body = body, position = comment.position, createdAt = comment.createdAt, updatedAt = DateTimeUtil.fromDate(DateTime.now().toDate()), resolvable = false, resolved = false, resolvedBy = null, author = currentUser, isDraft = true ) } } override fun delete(project: Project, mergeRequestId: String, comment: Comment) { if (!comment.isDraft) { return } val mutableMap = getMutableMap(project, mergeRequestId) mutableMap.remove(comment.id) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/MergeRequestApiDecorator.kt ================================================ package net.ntworld.mergeRequestIntegration.provider import net.ntworld.mergeRequest.api.MergeRequestApi open class MergeRequestApiDecorator( private val wrappee: MergeRequestApi ): MergeRequestApi by wrappee ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/ProviderException.kt ================================================ package net.ntworld.mergeRequestIntegration.provider import net.ntworld.foundation.Error class ProviderException(val error: Error) : Exception(error.message) ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/Transformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider interface Transformer { fun transform(input: T): R } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/Github.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.ProviderInfo object Github : ProviderInfo { override val id: String = "github" override val name: String = "Github" override val iconPath: String = "/icons/github.svg" override val icon2xPath: String = "/icons/github@2x.svg" override val icon3xPath: String = "/icons/github@3x.svg" override val icon4xPath: String = "/icons/github@4x.svg" override fun createCommentUrl(mergeRequestUrl: String, comment: Comment): String { return "$mergeRequestUrl#comment_${comment.id}" } override fun formatMergeRequestId(mergeRequestId: String): String { return mergeRequestId } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubApiProvider.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.ProviderInfo import net.ntworld.mergeRequest.UserInfo import net.ntworld.mergeRequest.api.* class GithubApiProvider( private val infrastructure: Infrastructure, override val credentials: ApiCredentials, override val cache: Cache ) : ApiProvider { private val myMergeRequestApi = GithubMergeRequestApi(infrastructure, credentials) override val info: ProviderInfo = Github override val user: UserApi = GithubUserApi(infrastructure, credentials) override val mergeRequest: MergeRequestApi = myMergeRequestApi override val project: ProjectApi = GithubProjectApi(infrastructure, credentials) override val comment: CommentApi get() = TODO("not implemented") override val commit: CommitApi get() = TODO("Not yet implemented") override fun setOptions(options: ApiOptions) { } override fun initialize(currentUser: UserInfo) { } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubClient.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github import net.ntworld.foundation.Error import net.ntworld.foundation.Request import net.ntworld.foundation.Response import net.ntworld.mergeRequest.api.ApiCredentials import org.kohsuke.github.GitHub import org.kohsuke.github.GitHubBuilder import java.io.IOException object GithubClient { private fun makeGitHub(credentials: ApiCredentials) : GitHub { return GitHubBuilder() .withEndpoint(credentials.url) .withPassword(credentials.login, credentials.token) .build() } operator fun invoke( request: T, execute: (GitHub.(T) -> R), failed: ((Error) -> R) ) : R where T : Request, T : GithubRequest { return try { execute.invoke( makeGitHub(request.credentials), request ) } catch (exception: IOException) { failed.invoke( GithubFailedRequestError( exception.message ?: "Failed request", 400 ) ) } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubFailedRequestError.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github import net.ntworld.foundation.Error data class GithubFailedRequestError( override val message: String, override val code: Int ): Error { override val type: String = "net.ntworld.mergeRequestIntegration.provider.github.GithubFailedRequestError" } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubFuelClient.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github import com.github.kittinunf.fuel.core.HttpException import com.github.kittinunf.fuel.core.Request import com.github.kittinunf.fuel.core.extensions.authentication import net.ntworld.foundation.Error import net.ntworld.foundation.Response import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.FuelClient class GithubFuelClient private constructor( private val credentials: ApiCredentials ) : FuelClient(credentials) { val searchIssuesUrl = "${credentials.url}/search/issues" override fun injectAuthentication(httpRequest: Request): Request { return httpRequest.authentication().basic(credentials.login, credentials.token) } companion object { operator fun invoke( request: T, execute: (GithubFuelClient.(T) -> R), failed: ((Error) -> R) ): R where T : net.ntworld.foundation.Request, T : GithubRequest { return try { val client = GithubFuelClient(request.credentials) execute.invoke(client, request) } catch (exception: HttpException) { failed.invoke( GithubFailedRequestError( exception.message ?: "Failed request", 500 ) ) } } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubMergeRequestApi.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.* import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequest.api.MergeRequestApi import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequestIntegration.internal.MergeRequestSearchResultImpl import net.ntworld.mergeRequestIntegration.provider.github.request.GithubSearchPRsRequest import net.ntworld.mergeRequestIntegration.provider.github.transformer.GithubSearchPullRequestItemTransformer import net.ntworld.mergeRequestIntegration.provider.github.vo.GithubProjectId import net.ntworld.mergeRequestIntegration.provider.github.vo.GithubUserId import kotlin.math.ceil class GithubMergeRequestApi( private val infrastructure: Infrastructure, private val credentials: ApiCredentials ) : MergeRequestApi { override fun find(projectId: String, mergeRequestId: String): MergeRequest? { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } override fun approve(projectId: String, mergeRequestId: String, sha: String) { } override fun unapprove(projectId: String, mergeRequestId: String) { } override fun findApproval(projectId: String, mergeRequestId: String): Approval { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } override fun getPipelines(projectId: String, mergeRequestId: String): List { // TODO: get pipelines return listOf() } override fun getCommits(projectId: String, mergeRequestId: String): List { // TODO: get commits return listOf() } override fun getChanges(projectId: String, mergeRequestId: String): List { TODO("Not yet implemented") } override fun search( projectId: String, currentUserId: String, filterBy: GetMergeRequestFilter, orderBy: MergeRequestOrdering, page: Int, itemsPerPage: Int ): MergeRequestApi.SearchResult { val (sort, order) = resolveOrderAndSort(orderBy) val reviewer = if (filterBy.approverIds.isNotEmpty() && filterBy.approverIds[0].isNotEmpty()) { GithubUserId.parseLogin(filterBy.approverIds[0]) } else "" val out = infrastructure.serviceBus() process GithubSearchPRsRequest( credentials = credentials, repo = GithubProjectId.parseFullName(credentials.projectId), state = when (filterBy.state) { MergeRequestState.ALL -> "" MergeRequestState.OPENED -> PULL_REQUEST_STATE_OPEN MergeRequestState.CLOSED -> PULL_REQUEST_STATE_CLOSED MergeRequestState.MERGED -> PULL_REQUEST_STATE_MERGED }, author = if (filterBy.authorId.isNotEmpty()) GithubUserId.parseLogin(filterBy.authorId) else "", assignee = if (filterBy.assigneeId.isNotEmpty()) GithubUserId.parseLogin(filterBy.assigneeId) else "", reviewer = reviewer, term = filterBy.search, sort = sort, order = order, page = page, perPage = itemsPerPage ) return if (out.hasError()) { MergeRequestSearchResultImpl(listOf(), 0, 0, 0) } else { val response = out.getResponse() val transformer = GithubSearchPullRequestItemTransformer(projectId) MergeRequestSearchResultImpl( data = response.result.items.map { transformer.transform(it) }, totalItems = response.result.totalCount, totalPages = ceil(response.result.totalCount.toDouble() / itemsPerPage).toInt(), currentPage = page ) } } private fun resolveState(state: MergeRequestState): String = when(state) { MergeRequestState.ALL -> "all" MergeRequestState.OPENED -> "open" MergeRequestState.CLOSED -> "closed" MergeRequestState.MERGED -> "merged" } private fun resolveOrderAndSort(orderBy: MergeRequestOrdering): Pair { return when (orderBy) { MergeRequestOrdering.RECENTLY_UPDATED -> Pair("updated", "desc") MergeRequestOrdering.NEWEST -> Pair("created", "desc") MergeRequestOrdering.OLDEST -> Pair("created", "asc") } } override fun findOrFail(projectId: String, mergeRequestId: String): MergeRequest { val mergeRequest = find(projectId, mergeRequestId) if (null === mergeRequest) { throw Exception("MergeRequest $mergeRequestId not found.") } return mergeRequest } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubProjectApi.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.UserInfo import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequest.api.ProjectApi import net.ntworld.mergeRequestIntegration.provider.github.request.GithubFindRepositoryRequest import net.ntworld.mergeRequestIntegration.provider.github.transformer.GithubRepositoryTransformer import net.ntworld.mergeRequestIntegration.provider.github.vo.GithubProjectId class GithubProjectApi( private val infrastructure: Infrastructure, private val credentials: ApiCredentials ) : ProjectApi { override fun find(projectId: String): Project? { val out = infrastructure.serviceBus() process GithubFindRepositoryRequest( credentials, repositoryId = GithubProjectId.parseId(projectId).toString() ) return if (out.hasError()) { null } else { GithubRepositoryTransformer.transform(out.getResponse().repository) } } override fun getMembers(projectId: String): List { // TODO: Get members of project return listOf() } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github import net.ntworld.mergeRequest.api.ApiCredentials interface GithubRequest { val credentials: ApiCredentials } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubUserApi.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.User import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequest.api.UserApi import net.ntworld.mergeRequestIntegration.provider.github.request.GithubFindCurrentUserRequest import net.ntworld.mergeRequestIntegration.provider.github.transformer.GithubUserTransformer class GithubUserApi( private val infrastructure: Infrastructure, private val credentials: ApiCredentials ) : UserApi { override fun me(): User { val response = infrastructure.serviceBus() process GithubFindCurrentUserRequest(credentials) ifError { throw Exception("Cannot find info of current user.") } return GithubUserTransformer.transform(response.user) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubUtil.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github object GithubUtil ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/_const.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github const val PULL_REQUEST_STATE_OPEN = "open" const val PULL_REQUEST_STATE_CLOSED = "closed" const val PULL_REQUEST_STATE_MERGED = "merged" ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/model/PullRequestSearchItem.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class PullRequestSearchItem( val id: Long, val number: Int, @SerialName("node_id") val nodeId: String, val title: String, val body: String, @SerialName("html_url") val htmlUrl: String, val state: String, @SerialName("created_at") val createdAt: String, @SerialName("updated_at") val updatedAt: String ) ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/model/SearchPullRequestResult.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class SearchPullRequestResult( @SerialName("total_count") val totalCount: Int, @SerialName("incomplete_results") val incompleteResults: Boolean, val items: List ) ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/request/GithubFindCurrentUserRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.github.GithubRequest import net.ntworld.mergeRequestIntegration.provider.github.response.GithubFindUserResponse data class GithubFindCurrentUserRequest( override val credentials: ApiCredentials ): GithubRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/request/GithubFindRepositoryRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.github.GithubRequest import net.ntworld.mergeRequestIntegration.provider.github.response.GithubFindRepositoryResponse data class GithubFindRepositoryRequest( override val credentials: ApiCredentials, val repositoryId: String ): GithubRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/request/GithubSearchPRsRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.github.GithubRequest import net.ntworld.mergeRequestIntegration.provider.github.response.GithubSearchPRsResponse data class GithubSearchPRsRequest( override val credentials: ApiCredentials, val repo: String, val state: String, val author: String, val reviewer: String, val assignee: String, val term: String, val sort: String, val order: String, val page: Int = 0, val perPage: Int = 10 ) : GithubRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/request/GithubSearchRepositoriesRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.github.GithubRequest import net.ntworld.mergeRequestIntegration.provider.github.response.GithubSearchRepositoriesResponse data class GithubSearchRepositoriesRequest( override val credentials: ApiCredentials, val term: String ) : GithubRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/requestHandler/GithubFindCurrentUserRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.github.GithubClient import net.ntworld.mergeRequestIntegration.provider.github.request.GithubFindCurrentUserRequest import net.ntworld.mergeRequestIntegration.provider.github.response.GithubFindUserResponse import org.kohsuke.github.GHUser @Handler class GithubFindCurrentUserRequestHandler : RequestHandler { override fun handle(request: GithubFindCurrentUserRequest): GithubFindUserResponse = GithubClient( request = request, execute = { GithubFindUserResponse(error = null, user = myself) }, failed = { GithubFindUserResponse(error = it, user = GHUser()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/requestHandler/GithubFindRepositoryRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.github.GithubClient import net.ntworld.mergeRequestIntegration.provider.github.GithubUtil import net.ntworld.mergeRequestIntegration.provider.github.request.GithubFindRepositoryRequest import net.ntworld.mergeRequestIntegration.provider.github.response.GithubFindRepositoryResponse import org.kohsuke.github.GHRepository @Handler class GithubFindRepositoryRequestHandler : RequestHandler { override fun handle(request: GithubFindRepositoryRequest): GithubFindRepositoryResponse = GithubClient( request = request, execute = { val repository = this.getRepositoryById(request.repositoryId) GithubFindRepositoryResponse(error = null, repository = repository) }, failed = { GithubFindRepositoryResponse(error = it, repository = GHRepository()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/requestHandler/GithubSearchPRsRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.requestHandler import kotlinx.serialization.serializer import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.github.GithubFuelClient import net.ntworld.mergeRequestIntegration.provider.github.model.SearchPullRequestResult import net.ntworld.mergeRequestIntegration.provider.github.request.GithubSearchPRsRequest import net.ntworld.mergeRequestIntegration.provider.github.response.GithubSearchPRsResponse @Handler class GithubSearchPRsRequestHandler : RequestHandler { /** * Because there is no way to do pagination manually with the library https://github.com/github-api/github-api/ * then I have to change the contract & use a custom client. */ override fun handle(request: GithubSearchPRsRequest): GithubSearchPRsResponse = GithubFuelClient( request = request, execute = { val q = mutableListOf() q.add("repo:${request.repo}") q.add("is:pr") q.add("state:${request.state}") if (request.author.isNotEmpty()) { q.add("author:${request.author}") } if (request.reviewer.isNotEmpty()) { q.add("reviewed-by:${request.reviewer}") } if (request.assignee.isNotEmpty()) { q.add("assignee:${request.assignee}") } if (request.term.trim().isNotEmpty()) { q.add(request.term.trim()) } val params: MutableList> = mutableListOf( Pair("q", q.joinToString(" ")), Pair("sort", request.sort), Pair("order", request.order), Pair("page", request.page), Pair("per_page", request.perPage) ) val input = this.getJson(searchIssuesUrl, params) GithubSearchPRsResponse(error = null, result = json.parse( SearchPullRequestResult.serializer(), input )) }, failed = { GithubSearchPRsResponse(error = it, result = SearchPullRequestResult( totalCount = 0, incompleteResults = false, items = listOf() )) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/requestHandler/GithubSearchRepositoriesRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.github.GithubClient import net.ntworld.mergeRequestIntegration.provider.github.request.GithubSearchRepositoriesRequest import net.ntworld.mergeRequestIntegration.provider.github.response.GithubSearchRepositoriesResponse import org.kohsuke.github.GitHubBuilder @Handler class GithubSearchRepositoriesRequestHandler : RequestHandler { override fun handle(request: GithubSearchRepositoriesRequest): GithubSearchRepositoriesResponse = GithubClient( request = request, execute = { val search = this.searchRepositories().q(request.term) val pager = search.list().withPageSize(10) val data = pager.iterator().nextPage() GithubSearchRepositoriesResponse(error = null, repositories = data) }, failed = { GithubSearchRepositoriesResponse(error = it, repositories = listOf()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/response/GithubFindRepositoryResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import org.kohsuke.github.GHRepository data class GithubFindRepositoryResponse( override val error: Error?, val repository: GHRepository ) : Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/response/GithubFindUserResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import org.kohsuke.github.GHUser data class GithubFindUserResponse( override val error: Error?, val user: GHUser ): Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/response/GithubSearchPRsResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import net.ntworld.mergeRequestIntegration.provider.github.model.PullRequestSearchItem import net.ntworld.mergeRequestIntegration.provider.github.model.SearchPullRequestResult data class GithubSearchPRsResponse( override val error: Error?, val result: SearchPullRequestResult ): Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/response/GithubSearchRepositoriesResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import org.kohsuke.github.GHRepository data class GithubSearchRepositoriesResponse( override val error: Error?, val repositories: List ): Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/transformer/GithubRepositoryTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.transformer import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.ProjectVisibility import net.ntworld.mergeRequestIntegration.internal.ProjectImpl import net.ntworld.mergeRequestIntegration.provider.github.Github import net.ntworld.mergeRequestIntegration.provider.Transformer import net.ntworld.mergeRequestIntegration.provider.github.vo.GithubProjectId import org.kohsuke.github.GHRepository object GithubRepositoryTransformer : Transformer { override fun transform(input: GHRepository): Project = ProjectImpl( // Unlike gitlab, github works based on :owner/:repo rather than id // So to keep everything works as expected, we have to add :owner/:repo information // Please use GithubProjectId value-object to work with generate/parsing user id id = GithubProjectId(id = input.id, owner = input.ownerName, repo = input.name).getValue(), provider = Github, name = input.name, path = input.fullName, url = input.url.toString(), visibility = if (input.isPrivate) ProjectVisibility.PRIVATE else ProjectVisibility.PUBLIC, avatarUrl = "", repositoryHttpUrl = input.htmlUrl.toString(), repositorySshUrl = input.sshUrl ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/transformer/GithubSearchPullRequestItemTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.transformer import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.MergeRequestState import net.ntworld.mergeRequestIntegration.internal.MergeRequestInfoImpl import net.ntworld.mergeRequestIntegration.provider.Transformer import net.ntworld.mergeRequestIntegration.provider.github.Github import net.ntworld.mergeRequestIntegration.provider.github.PULL_REQUEST_STATE_CLOSED import net.ntworld.mergeRequestIntegration.provider.github.PULL_REQUEST_STATE_MERGED import net.ntworld.mergeRequestIntegration.provider.github.PULL_REQUEST_STATE_OPEN import net.ntworld.mergeRequestIntegration.provider.github.model.PullRequestSearchItem import net.ntworld.mergeRequestIntegration.provider.github.vo.GithubMergeRequestId class GithubSearchPullRequestItemTransformer( private val projectId: String ) : Transformer { override fun transform(input: PullRequestSearchItem): MergeRequestInfo = MergeRequestInfoImpl( id = GithubMergeRequestId(input.id, input.number, input.nodeId).getValue(), provider = Github.id, projectId = projectId, title = input.title, description = input.body, url = input.htmlUrl, state = when (input.state) { PULL_REQUEST_STATE_OPEN -> MergeRequestState.OPENED PULL_REQUEST_STATE_MERGED -> MergeRequestState.MERGED PULL_REQUEST_STATE_CLOSED -> MergeRequestState.CLOSED else -> MergeRequestState.CLOSED }, createdAt = input.createdAt, updatedAt = input.updatedAt ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/transformer/GithubUserTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.transformer import net.ntworld.mergeRequest.User import net.ntworld.mergeRequest.UserStatus import net.ntworld.mergeRequestIntegration.DEFAULT_DATETIME import net.ntworld.mergeRequestIntegration.internal.UserImpl import net.ntworld.mergeRequestIntegration.provider.Transformer import net.ntworld.mergeRequestIntegration.provider.github.vo.GithubUserId import net.ntworld.mergeRequestIntegration.util.DateTimeUtil import org.kohsuke.github.GHUser object GithubUserTransformer : Transformer { override fun transform(input: GHUser): User = UserImpl( // Unlike gitlab, github works based on login rather than id // So to keep everything works as expected, we have to add username into id information // Please use GithubUserId value-object to work with generate/parsing user id id = GithubUserId(input.id, input.login).getValue(), name = if (input.name.isNullOrBlank()) input.login else input.name, username = input.login, avatarUrl = input.avatarUrl, url = input.htmlUrl.toString(), status = UserStatus.ACTIVE, email = if (input.email.isNullOrBlank()) "" else input.email, createdAt = if (null === input.createdAt) DEFAULT_DATETIME else DateTimeUtil.fromDate(input.createdAt) ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/vo/GithubMergeRequestId.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.vo data class GithubMergeRequestId( val id: Long, val number: Int, val nodeId: String ) { fun getValue() : String { return "$id:$number:$nodeId" } companion object { fun parse(input: String): GithubMergeRequestId { val parts = input.split(":") if (parts.size != 3) { throw Exception("Invalid Github mergeRequestId") } return GithubMergeRequestId( id = parts[0].toLong(), number = parts[1].toInt(), nodeId = parts[2] ) } fun parseId(mergeRequestId: String) : Long { return parse(mergeRequestId).id } fun parseNumber(mergeRequestId: String) : Int { return parse(mergeRequestId).number } fun parseNodeId(mergeRequestId: String) : String { return parse(mergeRequestId).nodeId } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/vo/GithubProjectId.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.vo data class GithubProjectId( val id: Long, val owner: String, val repo: String ) { fun getValue() : String { return "$id:$owner/$repo" } companion object { fun parse(input: String): GithubProjectId { val parts = input.split(":") if (parts.size != 2) { throw Exception("Invalid Github projectId") } val info = parts[1].split("/") if (info.size != 2) { throw Exception("Invalid Github projectId") } return GithubProjectId( id = parts[0].toLong(), owner = info[0], repo = info[1] ) } fun parseId(projectId: String) : Long { return parse(projectId).id } fun parseFullName(projectId: String): String { val data = parse(projectId) return "${data.owner}/${data.repo}" } fun parseOwner(projectId: String) : String { return parse(projectId).owner } fun parseRepo(projectId: String): String { return parse(projectId).repo } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/vo/GithubUserId.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.github.vo data class GithubUserId( val id: Long, val login: String ) { fun getValue() : String { return "$id:$login" } companion object { fun parse(input: String): GithubUserId { val parts = input.split(":") if (parts.size != 2) { throw Exception("Invalid Github userId") } return GithubUserId( id = parts[0].toLong(), login = parts[1] ) } fun parseId(input: String): Long { return parse(input).id } fun parseLogin(input: String): String { return parse(input).login } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/Gitlab.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.ProviderInfo object Gitlab : ProviderInfo { override val id: String = "gitlab" override val name: String = "GitLab" override val iconPath: String = "/icons/gitlab.svg" override val icon2xPath: String = "/icons/gitlab@2x.svg" override val icon3xPath: String = "/icons/gitlab@3x.svg" override val icon4xPath: String = "/icons/gitlab@4x.svg" @Synchronized override fun createCommentUrl(mergeRequestUrl: String, comment: Comment): String { return "$mergeRequestUrl#note_${comment.id}" } @Synchronized override fun formatMergeRequestId(mergeRequestId: String): String { return "!$mergeRequestId" } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabApiProvider.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.ProviderInfo import net.ntworld.mergeRequest.UserInfo import net.ntworld.mergeRequest.api.* import net.ntworld.mergeRequestIntegration.provider.DraftCommentApi import net.ntworld.mergeRequestIntegration.provider.MemoryDraftCommentStorage class GitlabApiProvider( private val infrastructure: Infrastructure, override val credentials: ApiCredentials, override val cache: Cache ) : ApiProvider { private var myCommentApi: CommentApi? = null private val myMergeRequestApi = GitlabMergeRequestApiCache( GitlabMergeRequestApi(infrastructure, credentials), cache ) override val info: ProviderInfo = Gitlab override val user: UserApi = GitlabUserApi(infrastructure, credentials) override val mergeRequest: MergeRequestApi = myMergeRequestApi override val project: ProjectApi = GitlabProjectApi(infrastructure, credentials) override val comment: CommentApi get() = myCommentApi!! override val commit: CommitApi = GitlabCommitApi(infrastructure, credentials) override fun setOptions(options: ApiOptions) { myMergeRequestApi.options = options } override fun initialize(currentUser: UserInfo) { this.myCommentApi = DraftCommentApi( GitlabCommentApi(infrastructure, credentials), MemoryDraftCommentStorage(currentUser) ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabClient.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.foundation.Error import net.ntworld.foundation.Request import net.ntworld.foundation.Response import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabSearchProjectsRequest import org.gitlab4j.api.Constants import org.gitlab4j.api.GitLabApi import org.gitlab4j.api.GitLabApiException import org.glassfish.jersey.client.ClientProperties.CONNECT_TIMEOUT import org.glassfish.jersey.client.ClientProperties.READ_TIMEOUT class GitlabClient { companion object { private fun makeGitLabApi(credentials: ApiCredentials): GitLabApi { val config: HashMap = HashMap() config.put(READ_TIMEOUT, 10000) config.put(CONNECT_TIMEOUT, 10000) val api = GitLabApi( credentials.url, Constants.TokenType.PRIVATE, credentials.token, null, config ) if (credentials.ignoreSSLCertificateErrors) { api.ignoreCertificateErrors = true } return api } operator fun invoke( request: T, execute: (GitLabApi.(T) -> R), failed: ((Error) -> R) ): R where T : Request, T : GitlabRequest { return try { execute.invoke( makeGitLabApi(request.credentials), request ) } catch (exception: GitLabApiException) { failed.invoke( GitlabFailedRequestError( exception.message ?: "Failed request", exception.httpStatus ) ) } } operator fun invoke( credentials: ApiCredentials, execute: (GitLabApi.() -> T), failed: ((Error) -> T) ): T { return try { execute.invoke( makeGitLabApi(credentials) ) } catch (exception: GitLabApiException) { failed.invoke( GitlabFailedRequestError( exception.message ?: "Failed request", exception.httpStatus ) ) } } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabCommentApi.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.* import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequest.api.CommentApi import net.ntworld.mergeRequestIntegration.provider.ProviderException import net.ntworld.mergeRequestIntegration.provider.gitlab.command.* import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabCreateNoteRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRCommentsRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRDiscussionsRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabReplyNoteRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabCommentTransformer import net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabDiscussionTransformer import org.gitlab4j.api.models.Position import java.util.logging.Level import java.util.logging.Logger class GitlabCommentApi( private val infrastructure: Infrastructure, private val credentials: ApiCredentials ) : CommentApi { companion object { private val log: Logger = Logger.getLogger("GitlabCommentApi") } override fun getAll(project: Project, mergeRequestId: String): List { val request = GitlabGetMRDiscussionsRequest( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt() ) val response = infrastructure.serviceBus() process request ifError { throw Exception(it.message) } val comments = mutableListOf() response.discussions.forEach { comments.addAll(GitlabDiscussionTransformer.transform(it)) } return comments } private fun getAllGraphQL(project: Project, mergeRequestId: String): List { val fullPath = findProjectFullPath(project) val comments = mutableListOf() var endCursor = "" do { val request = GitlabGetMRCommentsRequest( credentials = credentials, projectFullPath = fullPath, mergeRequestInternalId = mergeRequestId.toInt(), endCursor = endCursor ) val response = infrastructure.serviceBus() process request ifError { throw Exception(it.message) } val payload = response.payload if (null !== payload) { endCursor = payload.data.project.mergeRequest.notes.pageInfo.endCursor payload.data.project.mergeRequest.notes.nodes.forEach { if (!it.system) { comments.add(GitlabCommentTransformer.transform(it)) } } } } while (null === payload || payload.data.project.mergeRequest.notes.pageInfo.hasNextPage) return comments } override fun create( project: Project, mergeRequestId: String, body: String, position: CommentPosition?, isDraft: Boolean ): String? { if (isDraft) { println(body) println(position) // TODO: do something return "" } val createdCommentId = if (null === position) { createGeneralComment(mergeRequestId, body) } else { createPositionComment(mergeRequestId, body, position) } return if (createdCommentId == 0) null else createdCommentId.toString() } private fun createGeneralComment(mergeRequestId: String, body: String): Int { val request = GitlabCreateNoteRequest( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt(), body = body, position = null ) val response = infrastructure.serviceBus() process request ifError { throw ProviderException(it) } return response.createdCommentId } private fun createPositionComment(mergeRequestId: String, body: String, commentPosition: CommentPosition) : Int { val position = makePosition(commentPosition) if (commentPosition.changeType != CommentPositionChangeType.UNKNOWN) { if (commentPosition.source == CommentPositionSource.SIDE_BY_SIDE_LEFT) { position.newLine = null position.newPath = null } if (commentPosition.source == CommentPositionSource.SIDE_BY_SIDE_RIGHT) { position.oldLine = null position.oldPath = null } } val request = GitlabCreateNoteRequest( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt(), body = body, position = position ) val out = infrastructure.serviceBus() process request if (out.hasError()) { throw ProviderException(out.getResponse().error!!) } return out.getResponse().createdCommentId } private fun makePosition(position: CommentPosition): Position { val model = Position() model.baseSha = position.baseHash model.headSha = position.headHash model.startSha = position.startHash model.oldPath = if (null === position.oldLine || position.oldLine!! < 0) null else position.oldPath model.newPath = if (null === position.newLine || position.newLine!! < 0) null else position.newPath model.oldLine = if (null === position.oldLine || position.oldLine!! < 0) null else position.oldLine model.newLine = if (null === position.newLine || position.newLine!! < 0) null else position.newLine model.positionType = Position.PositionType.TEXT return model } override fun reply(project: Project, mergeRequestId: String, repliedComment: Comment, body: String): String? { val request = GitlabReplyNoteRequest( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt(), discussionId = repliedComment.parentId, noteId = repliedComment.id.toInt(), body = body ) val response = infrastructure.serviceBus() process request ifError { throw ProviderException(it) } return response.createdCommentId.toString() } override fun delete(project: Project, mergeRequestId: String, comment: Comment) { val command = GitlabDeleteNoteCommand( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt(), discussionId = comment.parentId, noteId = comment.id.toInt() ) infrastructure.commandBus() process command } override fun resolve(project: Project, mergeRequestId: String, comment: Comment) { infrastructure.commandBus() process GitlabResolveNoteCommand( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt(), discussionId = comment.parentId, resolve = true ) } override fun unresolve(project: Project, mergeRequestId: String, comment: Comment) { infrastructure.commandBus() process GitlabResolveNoteCommand( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt(), discussionId = comment.parentId, resolve = false ) } override fun update(project: Project, mergeRequestId: String, comment: Comment, body: String) { infrastructure.commandBus() process GitlabUpdateDiffNoteCommand( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt(), discussionId = comment.parentId, body = body, noteId = comment.id.toInt() ) } override fun getDraftCount(project: Project, mergeRequestId: String): Int { throw Exception("Not implemented in GitlabCommentApi, see DraftCommentApi") } override fun publishAllDraftComments(project: Project, mergeRequestId: String) { throw Exception("Not implemented in GitlabCommentApi, see DraftCommentApi") } override fun publishDraftComments(project: Project, mergeRequestId: String, commentIds: List) { throw Exception("Not implemented in GitlabCommentApi, see DraftCommentApi") } private fun findProjectFullPath(project: Project): String { val url = project.url.replace(credentials.url, "") return if (url.startsWith("/")) url.substring(1) else url } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabCommitApi.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.Change import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequest.api.CommitApi import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetCommitChangesRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabDiffTransformer class GitlabCommitApi( private val infrastructure: Infrastructure, private val credentials: ApiCredentials ) : CommitApi { override fun getChanges(projectId: String, commitId: String): List { val out = infrastructure.serviceBus() process GitlabGetCommitChangesRequest( credentials = credentials, commitSha = commitId ) return if (out.hasError()) { listOf() } else { out.getResponse().changes.map { GitlabDiffTransformer.transform(it) } } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabCredentials.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.mergeRequest.api.ApiCredentials data class GitlabCredentials( override val url: String, override val token: String, override val projectId: String, override val login: String = "", override val version: String = "v4", override val info: String = "", override val ignoreSSLCertificateErrors: Boolean = false ): ApiCredentials { companion object { val Empty = GitlabCredentials("", "", "", "") } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabFailedRequestError.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.foundation.Error data class GitlabFailedRequestError( override val message: String, override val code: Int ): Error { override val type: String = "net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFailedRequestError" } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabFailedRequestException.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.foundation.Error class GitlabFailedRequestException(error: Error) : Throwable() ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabFuelClient.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import com.github.kittinunf.fuel.core.* import com.github.kittinunf.result.Result import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonConfiguration import net.ntworld.foundation.Error import net.ntworld.foundation.Response import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.FuelClient import net.ntworld.mergeRequestIntegration.provider.gitlab.model.GraphqlRequest import java.security.cert.X509Certificate import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager class GitlabFuelClient private constructor( private val credentials: ApiCredentials ) : FuelClient(credentials) { override fun injectAuthentication(httpRequest: Request): Request { return httpRequest.header("PRIVATE-TOKEN", credentials.token) } val baseUrl: String = when (credentials.version) { "v4" -> "${credentials.url}/api/v4" else -> throw Exception("Not supported") } val baseProjectUrl: String = when (credentials.version) { "v4" -> "${credentials.url}/api/v4/projects/${credentials.projectId}" else -> throw Exception("Not supported") } fun callGraphQL(graphqlRequest: String): String { val httpRequest = makeRequestFactory().post("${credentials.url}/api/graphql") httpRequest.header("Authorization", "Bearer ${credentials.token}") httpRequest.header("Content-Type", "application/json") httpRequest.header("Accept", "application/json") httpRequest.body(graphqlRequest) val (_, response, result) = httpRequest.responseString() return when (result) { is Result.Success -> { result.value } is Result.Failure -> { throw HttpException(response.statusCode, result.error.message ?: "Unknown") } } } fun callGraphQL(query: String, variables: Map): String { val graphqlRequest = GraphqlRequest( query = query, variables = variables ) return this.callGraphQL(json.stringify(GraphqlRequest.serializer(), graphqlRequest)) } companion object { operator fun invoke( request: T, execute: (GitlabFuelClient.(T) -> R), failed: ((Error) -> R) ): R where T : net.ntworld.foundation.Request, T : GitlabRequest { return try { val client = GitlabFuelClient(request.credentials) execute.invoke(client, request) } catch (exception: HttpException) { failed.invoke( GitlabFailedRequestError( exception.message ?: "Failed request", 500 ) ) } } operator fun invoke( credentials: ApiCredentials, execute: (GitlabFuelClient.() -> T), failed: ((Error) -> T) ) : T { return try { val client = GitlabFuelClient(credentials) execute.invoke(client) } catch (exception: HttpException) { failed.invoke( GitlabFailedRequestError( exception.message ?: "Failed request", 500 ) ) } } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabMergeRequestApi.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.* import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequest.api.MergeRequestApi import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequestIntegration.internal.MergeRequestSearchResultImpl import net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabApproveMRCommand import net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabUnapproveMRCommand import net.ntworld.mergeRequestIntegration.provider.gitlab.request.* import net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.* import org.gitlab4j.api.Constants class GitlabMergeRequestApi( private val infrastructure: Infrastructure, private val credentials: ApiCredentials ) : MergeRequestApi { override fun find(projectId: String, mergeRequestId: String): MergeRequest? { val out = infrastructure.serviceBus() process GitlabFindMRRequest( credentials = credentials, mergeRequestInternalId = mergeRequestId ) return if (out.hasError()) { null } else { GitlabMRTransformer.transform(out.getResponse().mergeRequest) } } override fun approve(projectId: String, mergeRequestId: String, sha: String) { infrastructure.commandBus() process GitlabApproveMRCommand( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt(), sha = sha ) } override fun unapprove(projectId: String, mergeRequestId: String) { infrastructure.commandBus() process GitlabUnapproveMRCommand( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt() ) } override fun findApproval(projectId: String, mergeRequestId: String): Approval { val out = infrastructure.serviceBus() process GitlabFindMRApprovalRequest( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt() ) return GitlabApprovalTransformer.transform(out.getResponse().approval) } override fun getPipelines(projectId: String, mergeRequestId: String): List { val out = infrastructure.serviceBus() process GitlabGetMRPipelinesRequest( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt() ) return if (out.hasError()) { listOf() } else { out.getResponse().pipelines.map { GitlabPipelineTransformer.transform(it) } } } override fun getCommits(projectId: String, mergeRequestId: String): List { val out = infrastructure.serviceBus() process GitlabGetMRCommitsRequest( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt() ) return if (out.hasError()) { listOf() } else { out.getResponse().commits.map { GitlabCommitTransformer.transform(it) } } } override fun getChanges(projectId: String, mergeRequestId: String): List { val out = infrastructure.serviceBus() process GitlabGetMRChangesRequest( credentials = credentials, mergeRequestInternalId = mergeRequestId.toInt() ) return if (out.hasError()) { listOf() } else { out.getResponse().changes.map { GitlabDiffTransformer.transform(it) } } } override fun search( projectId: String, currentUserId: String, filterBy: GetMergeRequestFilter, orderBy: MergeRequestOrdering, page: Int, itemsPerPage: Int ): MergeRequestApi.SearchResult { val (order, sort) = resolveOrderAndSort(orderBy) val out = infrastructure.serviceBus() process GitlabSearchMRsRequest( credentials = credentials, state = resolveState(filterBy.state), filterById = filterBy.id, search = filterBy.search, authorId = filterBy.authorId, assigneeId = filterBy.assigneeId, approverIds = filterBy.approverIds, sourceBranch = filterBy.sourceBranch, orderBy = order, sort = sort, page = page, perPage = itemsPerPage ) return if (out.hasError()) { MergeRequestSearchResultImpl(listOf(), 0, 0, 0) } else { val response = out.getResponse() MergeRequestSearchResultImpl( data = response.mergeRequests.map { GitlabMRSimpleTransformer.transform(it) }, totalItems = response.totalItems, totalPages = response.totalPages, currentPage = response.currentPage ) } } override fun findOrFail(projectId: String, mergeRequestId: String): MergeRequest { val mergeRequest = find(projectId, mergeRequestId) if (null === mergeRequest) { throw Exception("MergeRequest $mergeRequestId not found.") } return mergeRequest } private fun resolveState(state: MergeRequestState): Constants.MergeRequestState = when (state) { MergeRequestState.ALL -> Constants.MergeRequestState.ALL MergeRequestState.OPENED -> Constants.MergeRequestState.OPENED MergeRequestState.CLOSED -> Constants.MergeRequestState.CLOSED MergeRequestState.MERGED -> Constants.MergeRequestState.MERGED } private fun resolveOrderAndSort( orderBy: MergeRequestOrdering ): Pair { return when (orderBy) { MergeRequestOrdering.RECENTLY_UPDATED -> Pair( Constants.MergeRequestOrderBy.UPDATED_AT, Constants.SortOrder.DESC ) MergeRequestOrdering.NEWEST -> Pair(Constants.MergeRequestOrderBy.CREATED_AT, Constants.SortOrder.DESC) MergeRequestOrdering.OLDEST -> Pair(Constants.MergeRequestOrderBy.CREATED_AT, Constants.SortOrder.ASC) } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabMergeRequestApiCache.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.mergeRequest.MergeRequest import net.ntworld.mergeRequest.api.* import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequestIntegration.internal.ApiOptionsImpl import net.ntworld.mergeRequestIntegration.provider.MergeRequestApiDecorator import org.joda.time.DateTime class GitlabMergeRequestApiCache( private val api: MergeRequestApi, private val cache: Cache ) : MergeRequestApiDecorator(api) { var options: ApiOptions = ApiOptionsImpl.DEFAULT override fun findOrFail(projectId: String, mergeRequestId: String): MergeRequest { if (!options.enableRequestCache) { return super.findOrFail(projectId, mergeRequestId) } val key = makeFindCacheKey(mergeRequestId) return cache.getOrRun(key) { val mergeRequest = super.findOrFail(projectId, mergeRequestId) cache.set(key, mergeRequest) mergeRequest } } override fun search( projectId: String, currentUserId: String, filterBy: GetMergeRequestFilter, orderBy: MergeRequestOrdering, page: Int, itemsPerPage: Int ): MergeRequestApi.SearchResult { val result = super.search(projectId, currentUserId, filterBy, orderBy, page, itemsPerPage) if (options.enableRequestCache) { result.data.forEach { try { val key = makeFindCacheKey(it.id) if (cache.isExpiredAfter(key, DateTime(it.updatedAt))) { cache.remove(key) } } catch (cacheNotFound: CacheNotFoundException) { } } } return result } private fun makeFindCacheKey(mergeRequestId: String) = "MR:find:$mergeRequestId" } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabProjectApi.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.UserInfo import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequest.api.ProjectApi import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindProjectRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetProjectMembersRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabMemberTransformer import net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabProjectTransformer class GitlabProjectApi( private val infrastructure: Infrastructure, private val credentials: ApiCredentials ) : ProjectApi { override fun find(projectId: String): Project? { val out = infrastructure.serviceBus() process GitlabFindProjectRequest(credentials, projectId.toInt()) return if (out.hasError()) { null } else { GitlabProjectTransformer.transform(out.getResponse().project) } } override fun getMembers(projectId: String): List { val out = infrastructure.serviceBus() process GitlabGetProjectMembersRequest(credentials) return if (out.hasError()) { listOf() } else { out.getResponse().members.map { GitlabMemberTransformer.transform(it) } } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.mergeRequest.api.ApiCredentials interface GitlabRequest { val credentials: ApiCredentials } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabUserApi.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.User import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequest.api.UserApi import net.ntworld.mergeRequestIntegration.provider.ProviderException import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindCurrentUserRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabUserTransformer class GitlabUserApi( private val infrastructure: Infrastructure, private val credentials: ApiCredentials ) : UserApi { override fun me(): User { val response = infrastructure.serviceBus() process GitlabFindCurrentUserRequest(credentials) ifError { throw ProviderException(it) } return GitlabUserTransformer.transform(response.user) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabUtil.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab import net.ntworld.mergeRequest.UserStatus import net.ntworld.mergeRequest.api.ApiCredentials object GitlabUtil { fun findUserStatus(state: String): UserStatus { return when (state) { USER_STATE_ACTIVE -> UserStatus.ACTIVE USER_STATE_INACTIVE -> UserStatus.INACTIVE else -> UserStatus.INACTIVE } } fun hasMergeApprovalFeature(credentials: ApiCredentials): Boolean { return credentials.info.contains(GITLAB_HAS_MERGE_APPROVAL_FEATURE) } fun getMergeApprovalFeatureInfo() = GITLAB_HAS_MERGE_APPROVAL_FEATURE } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/_const.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab const val GITLAB_HAS_MERGE_APPROVAL_FEATURE = "mergeApprovalFeature:true" const val MERGE_REQUEST_STATE_OPENED = "opened" const val MERGE_REQUEST_STATE_CLOSED = "closed" const val MERGE_REQUEST_STATE_MERGED = "merged" const val USER_STATE_ACTIVE = "active" const val USER_STATE_INACTIVE = "inactive" const val MERGE_STATUS_CAN_BE_MERGED = "can_be_merged" const val PIPELINE_STATUS_RUNNING = "running" const val PIPELINE_STATUS_FAILED = "failed" const val PIPELINE_STATUS_SUCCESS = "success" const val PIPELINE_STATUS_PARTIAL_FAILED = "partial_failed" ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/command/GitlabApproveMRCommand.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.command import net.ntworld.foundation.cqrs.Command import net.ntworld.mergeRequest.api.ApiCredentials data class GitlabApproveMRCommand( val credentials: ApiCredentials, val mergeRequestInternalId: Int, val sha: String ) : Command ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/command/GitlabCreateDiffNoteCommand.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.command import net.ntworld.foundation.cqrs.Command import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest data class GitlabCreateDiffNoteCommand( val credentials: ApiCredentials, val mergeRequestInternalId: Int, val body: String, val position: CommentPosition ) : Command ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/command/GitlabDeleteNoteCommand.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.command import net.ntworld.foundation.cqrs.Command import net.ntworld.mergeRequest.api.ApiCredentials data class GitlabDeleteNoteCommand( val credentials: ApiCredentials, val mergeRequestInternalId: Int, val discussionId: String, val noteId: Int ) : Command ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/command/GitlabResolveNoteCommand.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.command import net.ntworld.foundation.cqrs.Command import net.ntworld.mergeRequest.api.ApiCredentials data class GitlabResolveNoteCommand( val credentials: ApiCredentials, val mergeRequestInternalId: Int, val discussionId: String, val resolve: Boolean ) : Command ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/command/GitlabUnapproveMRCommand.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.command import net.ntworld.foundation.cqrs.Command import net.ntworld.mergeRequest.api.ApiCredentials data class GitlabUnapproveMRCommand( val credentials: ApiCredentials, val mergeRequestInternalId: Int ) : Command ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/command/GitlabUpdateDiffNoteCommand.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.command import net.ntworld.foundation.cqrs.Command import net.ntworld.mergeRequest.api.ApiCredentials class GitlabUpdateDiffNoteCommand( val credentials: ApiCredentials, val mergeRequestInternalId: Int, val discussionId: String, val noteId: Int, val body: String ): Command ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/commandHandler/GitlabApproveMRCommandHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.commandHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.CommandHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabApproveMRCommand import org.gitlab4j.api.Constants import org.gitlab4j.api.GitLabApi @Handler class GitlabApproveMRCommandHandler : CommandHandler { override fun handle(command: GitlabApproveMRCommand) { val api = GitLabApi(command.credentials.url, Constants.TokenType.PRIVATE, command.credentials.token) api.mergeRequestApi.approveMergeRequest( command.credentials.projectId.toInt(), command.mergeRequestInternalId, command.sha ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/commandHandler/GitlabCreateDiffNoteCommandHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.commandHandler import kotlinx.serialization.Serializable import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.CommandHandler import net.ntworld.foundation.util.UUIDGenerator import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient import net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabCreateDiffNoteCommand @Handler class GitlabCreateDiffNoteCommandHandler : CommandHandler { override fun handle(command: GitlabCreateDiffNoteCommand) = GitlabFuelClient( credentials = command.credentials, execute = { val clientMutationId = generateClientMutationId() val graphqlRequest = CustomGraphqlRequest( query = mutation, variables = CustomVariables( clientMutationId = clientMutationId, body = attachFooterToBody(command.body, clientMutationId), mrIid = "gid://gitlab/MergeRequest/${command.mergeRequestInternalId}", headSha = command.position.headHash, baseSha = command.position.baseHash, startSha = command.position.startHash, oldPath = command.position.oldPath, oldLine = command.position.oldLine, newPath = command.position.newPath, newLine = command.position.newLine ) ) this.callGraphQL(json.stringify(CustomGraphqlRequest.serializer(), graphqlRequest)) Unit }, failed = { println(it) Unit } ) private fun generateClientMutationId(): String { return "MRI:${UUIDGenerator.generate()}" } private fun attachFooterToBody(body: String, clientMutationId: String): String { return body } @Serializable data class CustomGraphqlRequest( val query: String, val variables: CustomVariables ) @Serializable data class CustomVariables( val clientMutationId: String, val body: String, val mrIid: String, val headSha: String, val baseSha: String, val startSha: String, val oldPath: String?, val oldLine: Int?, val newPath: String?, val newLine: Int? ) private val mutation = """ mutation createComment( $${"clientMutationId"}: String, $${"body"}: String!, $${"mrIid"}: ID!, $${"headSha"}: String!, $${"baseSha"}: String, $${"startSha"}: String!, $${"oldPath"}: String, $${"oldLine"}: Int, $${"newPath"}: String, $${"newLine"}: Int! ) { createDiffNote(input: { body: $${"body"} noteableId: $${"mrIid"}, clientMutationId: $${"clientMutationId"}, position: { headSha: $${"headSha"}, baseSha: $${"baseSha"}, startSha: $${"startSha"}, paths: { oldPath: $${"oldPath"}, newPath: $${"newPath"} }, oldLine: $${"oldLine"}, newLine: $${"newLine"} } }) { clientMutationId } } """ } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/commandHandler/GitlabDeleteNoteCommandHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.commandHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.CommandHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient import net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabDeleteNoteCommand @Handler class GitlabDeleteNoteCommandHandler : CommandHandler { override fun handle(command: GitlabDeleteNoteCommand) = GitlabFuelClient( credentials = command.credentials, execute = { this.deleteJson("$baseProjectUrl/merge_requests/${command.mergeRequestInternalId}/discussions/${command.discussionId}/notes/${command.noteId}") Unit }, failed = {} ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/commandHandler/GitlabResolveNoteCommandHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.commandHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.CommandHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient import net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabResolveNoteCommand @Handler class GitlabResolveNoteCommandHandler : CommandHandler { override fun handle(command: GitlabResolveNoteCommand) = GitlabFuelClient( credentials = command.credentials, execute = { val params = listOf( Pair("resolved", command.resolve) ) this.putJson( "$baseProjectUrl/merge_requests/${command.mergeRequestInternalId}/discussions/${command.discussionId}", params ) Unit }, failed = { println(it) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/commandHandler/GitlabUnapproveMRCommandHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.commandHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.CommandHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabUnapproveMRCommand import org.gitlab4j.api.Constants import org.gitlab4j.api.GitLabApi @Handler class GitlabUnapproveMRCommandHandler: CommandHandler { override fun handle(command: GitlabUnapproveMRCommand) { val api = GitLabApi(command.credentials.url, Constants.TokenType.PRIVATE, command.credentials.token) api.mergeRequestApi.unapproveMergeRequest( command.credentials.projectId.toInt(), command.mergeRequestInternalId ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/commandHandler/GitlabUpdateDiffNoteCommandHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.commandHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.CommandHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient import net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabUpdateDiffNoteCommand @Handler class GitlabUpdateDiffNoteCommandHandler : CommandHandler { override fun handle(command: GitlabUpdateDiffNoteCommand) = GitlabFuelClient( credentials = command.credentials, execute = { val params = listOf( Pair("body", command.body) ) this.putJson( "$baseProjectUrl/merge_requests/${command.mergeRequestInternalId}/discussions/${command.discussionId}/notes/${command.noteId}", params ) Unit }, failed = { println(it) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/ApprovalModel.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class ApprovalModel( val approved: Boolean, @SerialName("approvals_required") val approvalsRequired: Int, @SerialName("approvals_left") val approvalsLeft: Int, @SerialName("suggested_approvers") val suggestedApprovers: List, val approvers: List, @SerialName("approved_by") val approvedBy: List, @SerialName("user_has_approved") val hasApproved: Boolean, @SerialName("user_can_approve") val canApprove: Boolean ) { companion object { val Empty = ApprovalModel( approved = false, approvalsRequired = 0, approvalsLeft = 0, suggestedApprovers = listOf(), approvers = listOf(), approvedBy = listOf(), hasApproved = false, canApprove = false ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/ApproverModel.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.model import kotlinx.serialization.Serializable @Serializable data class ApproverModel( val user: UserInfoModel ) ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/GetCommentsPayload.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.model import kotlinx.serialization.Serializable @Serializable data class GetCommentsPayload( val data: Data ) { @Serializable data class Data( val project: Project ) @Serializable data class Project( val id: String, val name: String, val mergeRequest: MergeRequest ) @Serializable data class MergeRequest( val id: String, val iid: String, val notes: NoteCollection ) @Serializable data class NoteCollection( val pageInfo: NotesPageInfo, val nodes: List ) @Serializable data class NotesPageInfo( val endCursor: String, val startCursor: String, val hasNextPage: Boolean, val hasPreviousPage: Boolean ) @Serializable data class Note( val id: String, val body: String, val bodyHtml: String, val author: User, val resolvable: Boolean, val resolvedAt: String?, val resolvedBy: User?, val system: Boolean, val createdAt: String, val updatedAt: String, val position: NotePosition? ) @Serializable data class User( val name: String, val username: String, val webUrl: String, val avatarUrl: String ) @Serializable data class NotePosition( val diffRefs: DiffRef, val filePath: String, val newLine: Int?, val oldLine: Int?, val newPath: String?, val oldPath: String?, val positionType: String ) @Serializable data class DiffRef( val baseSha: String, val startSha: String, val headSha: String ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/GraphqlRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.model import kotlinx.serialization.Serializable @Serializable data class GraphqlRequest( val query: String, val variables: Map ) ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/PipelineModel.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class PipelineModel( val id: Int, val sha: String, val ref: String, val status: String, @SerialName("created_at") val createdAt: String? = null, @SerialName("updated_at") val updatedAt: String? = null, @SerialName("web_url") val webUrl: String ) ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/ReplyCommentPayload.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.model import kotlinx.serialization.Serializable @Serializable data class ReplyCommentPayload( val id: Int ) ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/UserInfoModel.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class UserInfoModel( val id: Int, val name: String, val username: String, @SerialName("avatar_url") val avatarUrl: String, @SerialName("web_url") val webUrl: String, val state: String ) ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabCreateNoteRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabCreateNoteResponse import org.gitlab4j.api.models.Position data class GitlabCreateNoteRequest( override val credentials: ApiCredentials, val mergeRequestInternalId: Int, val position: Position?, val body: String ) : GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabFindCurrentUserRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindUserResponse data class GitlabFindCurrentUserRequest( override val credentials: ApiCredentials ): GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabFindMRApprovalRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindMRApprovalResponse data class GitlabFindMRApprovalRequest( override val credentials: ApiCredentials, val mergeRequestInternalId: Int ) : GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabFindMRRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindMRResponse // FIXME: projectId, mergeRequestInternalId in this package belong to Gitlab, then it should be an integer not String data class GitlabFindMRRequest( override val credentials: ApiCredentials, val mergeRequestInternalId: String ): GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabFindProjectRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindProjectResponse data class GitlabFindProjectRequest( override val credentials: ApiCredentials, val projectId: Int, val projectPath: String = "" ) : GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabFindUserRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindUserResponse // FIXME: userId in this package belong to Gitlab, then it should be an integer not String data class GitlabFindUserRequest( override val credentials: ApiCredentials, val userId: String ) : GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetCommitChangesRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetCommitChangesResponse data class GitlabGetCommitChangesRequest( override val credentials: ApiCredentials, val commitSha: String ): GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetMRChangesRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRChangesResponse data class GitlabGetMRChangesRequest( override val credentials: ApiCredentials, val mergeRequestInternalId: Int ): GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetMRCommentsRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRCommentsResponse data class GitlabGetMRCommentsRequest( override val credentials: ApiCredentials, val projectFullPath: String, val endCursor: String, val mergeRequestInternalId: Int ) : GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetMRCommitsRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRCommitsResponse data class GitlabGetMRCommitsRequest( override val credentials: ApiCredentials, val mergeRequestInternalId: Int ): GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetMRDiscussionsRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRDiscussionsResponse data class GitlabGetMRDiscussionsRequest( override val credentials: ApiCredentials, val mergeRequestInternalId: Int ) : GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetMRPipelinesRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRPipelinesResponse data class GitlabGetMRPipelinesRequest( override val credentials: ApiCredentials, val mergeRequestInternalId: Int ): GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetProjectMembersRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetProjectMembersResponse data class GitlabGetProjectMembersRequest( override val credentials: ApiCredentials ): GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabReplyNoteRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabReplyNoteResponse data class GitlabReplyNoteRequest( override val credentials: ApiCredentials, val mergeRequestInternalId: Int, val discussionId: String, val noteId: Int, val body: String ) : GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabSearchMRsRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabSearchMRsResponse import org.gitlab4j.api.Constants // FIXME: authorId, assigneeId... in this package belong to Gitlab, then it should be an integer not String data class GitlabSearchMRsRequest( override val credentials: ApiCredentials, val state: Constants.MergeRequestState, val filterById: Int?, val search: String, val authorId: String, val assigneeId: String, val approverIds: List, val sourceBranch: String, val orderBy: Constants.MergeRequestOrderBy = Constants.MergeRequestOrderBy.UPDATED_AT, val sort: Constants.SortOrder, val page: Int = 0, val perPage: Int = 100 ): GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabSearchProjectsRequest.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.request import net.ntworld.foundation.Request import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabSearchProjectsResponse data class GitlabSearchProjectsRequest( override val credentials: ApiCredentials, val term: String, val owner: Boolean = false, val starred: Boolean = false, val membership: Boolean = false ) : GitlabRequest, Request ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabCreateNoteRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabCreateNoteRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabCreateNoteResponse import java.util.* @Handler class GitlabCreateNoteRequestHandler : RequestHandler { override fun handle(request: GitlabCreateNoteRequest): GitlabCreateNoteResponse = GitlabClient( request = request, execute = { val result = this.discussionsApi.createMergeRequestDiscussion( request.credentials.projectId.toInt(), request.mergeRequestInternalId, request.body, Date(), null, // This is a bug from the API client, null is okay request.position ) if (result.notes.isNotEmpty()) { GitlabCreateNoteResponse(error = null, createdCommentId = result.notes.first().id) } else { GitlabCreateNoteResponse(error = null, createdCommentId = 0) } }, failed = { GitlabCreateNoteResponse(error = it, createdCommentId = 0) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabFindCurrentUserRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindCurrentUserRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindUserResponse import org.gitlab4j.api.models.User @Handler class GitlabFindCurrentUserRequestHandler : RequestHandler { override fun handle(request: GitlabFindCurrentUserRequest): GitlabFindUserResponse = GitlabClient( request = request, execute = { GitlabFindUserResponse(error = null, user = this.userApi.currentUser) }, failed = { GitlabFindUserResponse(error = it, user = User()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabFindMRApprovalRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient import net.ntworld.mergeRequestIntegration.provider.gitlab.model.ApprovalModel import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindMRApprovalRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindMRApprovalResponse @Handler class GitlabFindMRApprovalRequestHandler : RequestHandler { override fun handle(request: GitlabFindMRApprovalRequest): GitlabFindMRApprovalResponse = GitlabFuelClient( request = request, execute = { val response = this.getJson( "${this.baseProjectUrl}/merge_requests/${request.mergeRequestInternalId}/approvals" ) GitlabFindMRApprovalResponse( error = null, approval = this.json.parse(ApprovalModel.serializer(), response) ) }, failed = { GitlabFindMRApprovalResponse(it, ApprovalModel.Empty) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabFindMRRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindMRRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindMRResponse import org.gitlab4j.api.models.MergeRequest @Handler class GitlabFindMRRequestHandler : RequestHandler { override fun handle(request: GitlabFindMRRequest): GitlabFindMRResponse = GitlabClient( request = request, execute = { val mr = this.mergeRequestApi.getMergeRequest( request.credentials.projectId.toInt(), request.mergeRequestInternalId.toInt() ) GitlabFindMRResponse(null, mr) }, failed = { GitlabFindMRResponse(it, MergeRequest()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabFindProjectRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindProjectRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindProjectResponse import org.gitlab4j.api.models.Project @Handler class GitlabFindProjectRequestHandler : RequestHandler { override fun handle(request: GitlabFindProjectRequest): GitlabFindProjectResponse = GitlabClient( request = request, execute = { val project = if (request.projectPath.isNotBlank()) { this.projectApi.getProject(request.projectPath) } else { this.projectApi.getProject(request.projectId) } GitlabFindProjectResponse(error = null, project = project) }, failed = { GitlabFindProjectResponse(error = it, project = Project()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabFindUserRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Error import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFailedRequestError import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindUserRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindUserResponse import org.gitlab4j.api.models.User @Handler class GitlabFindUserRequestHandler : RequestHandler { override fun handle(request: GitlabFindUserRequest): GitlabFindUserResponse = GitlabClient( request = request, execute = { val user = this.userApi.getUser(request.userId.toInt()) if (null == user) { GitlabFindUserResponse( error = GitlabFailedRequestError("User not found", 404), user = User() ) } else { GitlabFindUserResponse(error = null, user = user) } }, failed = { GitlabFindUserResponse(error = it, user = User()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetCommitChangesRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetCommitChangesRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetCommitChangesResponse @Handler class GitlabGetCommitChangesRequestHandler : RequestHandler { override fun handle(request: GitlabGetCommitChangesRequest): GitlabGetCommitChangesResponse = GitlabClient( request = request, execute = { val diff = this.commitsApi.getDiff( request.credentials.projectId.toInt(), request.commitSha ) GitlabGetCommitChangesResponse(error = null, changes = diff) }, failed = { GitlabGetCommitChangesResponse(error = it, changes = listOf()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetMRChangesRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRChangesRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRChangesResponse @Handler class GitlabGetMRChangesRequestHandler : RequestHandler { override fun handle(request: GitlabGetMRChangesRequest): GitlabGetMRChangesResponse = GitlabClient( request = request, execute = { val mr = this.mergeRequestApi.getMergeRequestChanges( request.credentials.projectId.toInt(), request.mergeRequestInternalId ) GitlabGetMRChangesResponse(error = null, changes = mr.changes) }, failed = { GitlabGetMRChangesResponse(error = it, changes = listOf()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetMRCommentsRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient import net.ntworld.mergeRequestIntegration.provider.gitlab.model.GetCommentsPayload import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRCommentsRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRCommentsResponse @Handler class GitlabGetMRCommentsRequestHandler : RequestHandler { override fun handle(request: GitlabGetMRCommentsRequest): GitlabGetMRCommentsResponse = GitlabFuelClient( request = request, execute = { val response = this.callGraphQL(query = query, variables = mapOf( "projectPath" to request.projectFullPath, "mrIid" to request.mergeRequestInternalId.toString(), "endCursor" to request.endCursor )) GitlabGetMRCommentsResponse( error = null, payload = this.json.parse(GetCommentsPayload.serializer(), response) ) }, failed = { GitlabGetMRCommentsResponse(error = it, payload = null) } ) private val query = """ query GetNotes($${"projectPath"}: ID!, $${"mrIid"}: String, $${"endCursor"}: String){ project(fullPath: $${"projectPath"}) { id, name, mergeRequest(iid: $${"mrIid"}) { id iid notes(after: $${"endCursor"}) { pageInfo { endCursor startCursor hasNextPage hasPreviousPage } nodes { id body bodyHtml author { name username webUrl avatarUrl } resolvable resolvedAt resolvedBy { name username webUrl avatarUrl } system createdAt updatedAt position { diffRefs { baseSha, startSha, headSha } filePath newLine oldLine newPath oldPath positionType } } } } } } """ } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetMRCommitsRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRCommitsRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRCommitsResponse @Handler class GitlabGetMRCommitsRequestHandler : RequestHandler { override fun handle(request: GitlabGetMRCommitsRequest): GitlabGetMRCommitsResponse = GitlabClient( request = request, execute = { val commits = this.mergeRequestApi.getCommits( request.credentials.projectId.toInt(), request.mergeRequestInternalId ) GitlabGetMRCommitsResponse(error = null, commits = commits) }, failed = { GitlabGetMRCommitsResponse(error = it, commits = listOf()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetMRDiscussionsRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRDiscussionsRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRDiscussionsResponse @Handler class GitlabGetMRDiscussionsRequestHandler : RequestHandler { override fun handle(request: GitlabGetMRDiscussionsRequest): GitlabGetMRDiscussionsResponse = GitlabClient( request = request, execute = { val discussions = this.discussionsApi.getMergeRequestDiscussions( request.credentials.projectId.toInt(), request.mergeRequestInternalId ) GitlabGetMRDiscussionsResponse(error = null, discussions = discussions) }, failed = { GitlabGetMRDiscussionsResponse(error = it, discussions = listOf()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetMRPipelinesRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import kotlinx.serialization.list import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient import net.ntworld.mergeRequestIntegration.provider.gitlab.model.PipelineModel import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRPipelinesRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRPipelinesResponse @Handler class GitlabGetMRPipelinesRequestHandler : RequestHandler { override fun handle(request: GitlabGetMRPipelinesRequest): GitlabGetMRPipelinesResponse = GitlabFuelClient( request = request, execute = { val response = this.getJson( "${this.baseProjectUrl}/merge_requests/${request.mergeRequestInternalId}/pipelines" ) GitlabGetMRPipelinesResponse( error = null, pipelines = this.json.parse(PipelineModel.serializer().list, response) ) }, failed = { GitlabGetMRPipelinesResponse(error = it, pipelines = listOf()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetProjectMembersRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetProjectMembersRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetProjectMembersResponse @Handler class GitlabGetProjectMembersRequestHandler : RequestHandler { override fun handle(request: GitlabGetProjectMembersRequest): GitlabGetProjectMembersResponse = GitlabClient( request = request, execute = { val members = this.projectApi.getAllMembers(request.credentials.projectId.toInt()) GitlabGetProjectMembersResponse(error = null, members = members) }, failed = { GitlabGetProjectMembersResponse(error = it, members = listOf()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabReplyNoteRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient import net.ntworld.mergeRequestIntegration.provider.gitlab.model.ReplyCommentPayload import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabReplyNoteRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabReplyNoteResponse @Handler class GitlabReplyNoteRequestHandler : RequestHandler { override fun handle(request: GitlabReplyNoteRequest) = GitlabFuelClient( request = request, execute = { val url = "${baseProjectUrl}/merge_requests/${request.mergeRequestInternalId}/discussions/${request.discussionId}/notes" val parameters = listOf( Pair("body", request.body) ) val result = this.postJson(url = url, parameters = parameters) val payload = json.parse(ReplyCommentPayload.serializer(), result) GitlabReplyNoteResponse(error = null, createdCommentId = payload.id) }, failed = { GitlabReplyNoteResponse(error = it, createdCommentId = 0) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabSearchMRsRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabSearchMRsRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabSearchMRsResponse import org.gitlab4j.api.GitLabApiForm import org.gitlab4j.api.models.MergeRequestFilter @Handler class GitlabSearchMRsRequestHandler : RequestHandler { override fun handle(request: GitlabSearchMRsRequest): GitlabSearchMRsResponse = GitlabClient( request = request, execute = { val filter = buildMergeRequestFilter(request) val pager = this.mergeRequestApi.getMergeRequests(filter, request.perPage) val result = pager.page(if (request.page <= pager.totalPages) request.page else pager.totalPages) GitlabSearchMRsResponse( error = null, mergeRequests = result, totalPages = pager.totalPages, totalItems = pager.totalItems, currentPage = pager.currentPage ) }, failed = { GitlabSearchMRsResponse( error = it, mergeRequests = listOf(), totalPages = 0, totalItems = 0, currentPage = 0 ) } ) internal fun buildMergeRequestFilter(request: GitlabSearchMRsRequest): MyMergeRequestFilter { val filter = MyMergeRequestFilter() if (request.sourceBranch.isNotEmpty()) { filter.sourceBranch = request.sourceBranch } filter.state = request.state filter.projectId = request.credentials.projectId.toInt() setFilterByIdIfNotEmpty(filter, request) setSearchFilterParamIfNotEmpty(filter, request) setAuthorFilterParamIfNotEmpty(filter, request) setAssigneeFilterParamIfNotEmpty(filter, request) setApproverIdsParamIfNotEmpty(filter, request) filter.orderBy = request.orderBy filter.sort = request.sort return filter } private fun setFilterByIdIfNotEmpty(filter: MyMergeRequestFilter, request: GitlabSearchMRsRequest) { if (null !== request.filterById && request.filterById > 0) { filter.withIids(listOf(request.filterById)) } } private fun setSearchFilterParamIfNotEmpty(filter: MyMergeRequestFilter, request: GitlabSearchMRsRequest) { if (request.search.isNotEmpty()) { filter.search = request.search } } private fun setAuthorFilterParamIfNotEmpty(filter: MyMergeRequestFilter, request: GitlabSearchMRsRequest) { if (request.authorId.isNotEmpty()) { filter.authorId = request.authorId.toInt() } } private fun setAssigneeFilterParamIfNotEmpty(filter: MyMergeRequestFilter, request: GitlabSearchMRsRequest) { if (request.assigneeId.isNotEmpty()) { filter.assigneeId = request.assigneeId.toInt() } } private fun setApproverIdsParamIfNotEmpty(filter: MyMergeRequestFilter, request: GitlabSearchMRsRequest) { if (GitlabUtil.hasMergeApprovalFeature(request.credentials)) { val approverIds = request.approverIds .filter { it.isNotEmpty() } .map { it.toInt() } if (approverIds.isNotEmpty()) { filter.approverIds = approverIds } } } internal class MyMergeRequestFilter : MergeRequestFilter() { var approverIds: List = listOf() override fun getQueryParams(): GitLabApiForm { val form = super.getQueryParams() form.withParam("author_id", this.authorId) val approvers = approverIds.filter { it > 0 } if (approvers.isNotEmpty()) { form.withParam("approver_ids", approvers) } return form } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabSearchProjectsRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabSearchProjectsRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabSearchProjectsResponse import org.gitlab4j.api.Constants @Handler class GitlabSearchProjectsRequestHandler : RequestHandler { override fun handle(request: GitlabSearchProjectsRequest): GitlabSearchProjectsResponse = GitlabClient( request = request, execute = { val projects = if (it.term.isEmpty()) { this.projectApi.getProjects(10).first() } else { this.projectApi.getProjects( false, null, Constants.ProjectOrderBy.CREATED_AT, Constants.SortOrder.ASC, request.term, false, request.owner, request.membership, request.starred, false, 10 ).first() } GitlabSearchProjectsResponse(error = null, projects = projects) }, failed = { GitlabSearchProjectsResponse(error = it, projects = listOf()) } ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabCreateNoteResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response data class GitlabCreateNoteResponse( override val error: Error?, val createdCommentId: Int ) : Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabFindMRApprovalResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import net.ntworld.mergeRequestIntegration.provider.gitlab.model.ApprovalModel data class GitlabFindMRApprovalResponse( override val error: Error?, val approval: ApprovalModel ) : Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabFindMRResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import org.gitlab4j.api.models.MergeRequest data class GitlabFindMRResponse( override val error: Error?, val mergeRequest: MergeRequest ) :Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabFindProjectResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import org.gitlab4j.api.models.Project as GitlabProject data class GitlabFindProjectResponse( override val error: Error?, val project: GitlabProject ) : Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabFindUserResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import org.gitlab4j.api.models.User data class GitlabFindUserResponse( override val error: Error?, val user: User ): Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetCommitChangesResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import org.gitlab4j.api.models.Diff data class GitlabGetCommitChangesResponse( override val error: Error?, val changes: List ) : Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetMRChangesResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import org.gitlab4j.api.models.Diff data class GitlabGetMRChangesResponse( override val error: Error?, val changes: List ) : Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetMRCommentsResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import net.ntworld.mergeRequestIntegration.provider.gitlab.model.GetCommentsPayload data class GitlabGetMRCommentsResponse( override val error: Error?, val payload: GetCommentsPayload? ) : Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetMRCommitsResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import org.gitlab4j.api.models.Commit data class GitlabGetMRCommitsResponse( override val error: Error?, val commits: List ) : Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetMRDiscussionsResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import org.gitlab4j.api.models.Discussion data class GitlabGetMRDiscussionsResponse( override val error: Error?, val discussions: List ) : Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetMRPipelinesResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import net.ntworld.mergeRequestIntegration.provider.gitlab.model.PipelineModel data class GitlabGetMRPipelinesResponse( override val error: Error?, val pipelines: List ) : Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetProjectMembersResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import org.gitlab4j.api.models.Member data class GitlabGetProjectMembersResponse( override val error: Error?, val members: List ): Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabReplyNoteResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response data class GitlabReplyNoteResponse( override val error: Error?, val createdCommentId: Int ) : Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabSearchMRsResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import org.gitlab4j.api.models.MergeRequest data class GitlabSearchMRsResponse( override val error: Error?, val mergeRequests: List, val totalPages: Int, val totalItems: Int, val currentPage: Int ): Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabSearchProjectsResponse.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.response import net.ntworld.foundation.Error import net.ntworld.foundation.Response import org.gitlab4j.api.models.Project as GitlabProject data class GitlabSearchProjectsResponse( override val error: Error?, val projects: List ) : Response ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabApprovalTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer import net.ntworld.mergeRequest.Approval import net.ntworld.mergeRequest.UserInfo import net.ntworld.mergeRequestIntegration.internal.ApprovalImpl import net.ntworld.mergeRequestIntegration.internal.UserInfoImpl import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil import net.ntworld.mergeRequestIntegration.provider.Transformer import net.ntworld.mergeRequestIntegration.provider.gitlab.model.ApprovalModel import net.ntworld.mergeRequestIntegration.provider.gitlab.model.ApproverModel import net.ntworld.mergeRequestIntegration.provider.gitlab.model.UserInfoModel object GitlabApprovalTransformer : Transformer { override fun transform(input: ApprovalModel): Approval = ApprovalImpl( approved = input.approved, approvalsRequired = input.approvalsRequired, approvalsLeft = input.approvalsLeft, suggestedApprovers = input.suggestedApprovers.map { transformUserInfo(it) }, approvers = input.approvers.map { transformApprover(it) }, approvedBy = input.approvedBy.map { transformApprover(it) }, hasApproved = input.hasApproved, canApprove = input.canApprove ) private fun transformApprover(input: ApproverModel): UserInfo = transformUserInfo(input.user) private fun transformUserInfo(input: UserInfoModel): UserInfo = UserInfoImpl( id = input.id.toString(), name = input.name, username = input.username, avatarUrl = input.avatarUrl, url = input.webUrl, status = GitlabUtil.findUserStatus(input.state) ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabCommentTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPositionChangeType import net.ntworld.mergeRequest.CommentPositionSource import net.ntworld.mergeRequest.UserStatus import net.ntworld.mergeRequestIntegration.internal.CommentImpl import net.ntworld.mergeRequestIntegration.internal.CommentPositionImpl import net.ntworld.mergeRequestIntegration.internal.UserInfoImpl import net.ntworld.mergeRequestIntegration.provider.Transformer import net.ntworld.mergeRequestIntegration.provider.gitlab.model.GetCommentsPayload object GitlabCommentTransformer : Transformer { override fun transform(input: GetCommentsPayload.Note): Comment = CommentImpl( id = input.id, parentId = "", replyId = input.id, body = input.body, author = UserInfoImpl( id = "", name = input.author.name, username = input.author.username, url = input.author.webUrl, avatarUrl = input.author.avatarUrl, status = UserStatus.ACTIVE ), createdAt = input.createdAt, updatedAt = input.updatedAt, resolvedBy = if (null !== input.resolvedBy) { UserInfoImpl( id = "", name = input.author.name, username = input.author.username, url = input.author.webUrl, avatarUrl = input.author.avatarUrl, status = UserStatus.ACTIVE ) } else null, resolved = null !== input.resolvedBy, resolvable = input.resolvable, position = if (null !== input.position) { CommentPositionImpl( startHash = input.position.diffRefs.startSha, baseHash = input.position.diffRefs.baseSha, headHash = input.position.diffRefs.headSha, oldPath = input.position.oldPath, newPath = input.position.newPath, oldLine = input.position.oldLine, newLine = input.position.newLine, source = CommentPositionSource.SERVER, changeType = CommentPositionChangeType.UNKNOWN ) } else null, isDraft = false ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabCommitTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer import net.ntworld.mergeRequest.Commit import net.ntworld.mergeRequestIntegration.internal.CommitImpl import net.ntworld.mergeRequestIntegration.provider.Transformer import net.ntworld.mergeRequestIntegration.util.DateTimeUtil import org.gitlab4j.api.models.Commit as CommitModel object GitlabCommitTransformer : Transformer { override fun transform(input: CommitModel): Commit = CommitImpl( id = input.id, message = input.message, authorEmail = input.authorEmail, authorName = input.authorName, createdAt = DateTimeUtil.fromDate(input.createdAt) ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabDiffRefTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer import net.ntworld.mergeRequest.DiffReference import net.ntworld.mergeRequestIntegration.internal.DiffReferenceImpl import net.ntworld.mergeRequestIntegration.provider.Transformer import org.gitlab4j.api.models.DiffRef object GitlabDiffRefTransformer: Transformer { override fun transform(input: DiffRef): DiffReference = DiffReferenceImpl( baseHash = input.baseSha, headHash = input.headSha, startHash = input.startSha ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabDiffTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer import net.ntworld.mergeRequest.Change import net.ntworld.mergeRequestIntegration.internal.ChangeImpl import net.ntworld.mergeRequestIntegration.provider.Transformer import org.gitlab4j.api.models.Diff object GitlabDiffTransformer : Transformer { override fun transform(input: Diff): Change = ChangeImpl( oldPath = input.oldPath, newPath = input.newPath, aMode = input.aMode, bMode = input.bMode, newFile = input.newFile, renamedFile = input.renamedFile, deletedFile = input.deletedFile ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabDiscussionTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPositionChangeType import net.ntworld.mergeRequest.CommentPositionSource import net.ntworld.mergeRequest.UserStatus import net.ntworld.mergeRequestIntegration.internal.CommentImpl import net.ntworld.mergeRequestIntegration.internal.CommentPositionImpl import net.ntworld.mergeRequestIntegration.internal.UserInfoImpl import net.ntworld.mergeRequestIntegration.provider.Transformer import net.ntworld.mergeRequestIntegration.util.DateTimeUtil import org.gitlab4j.api.models.Discussion import org.gitlab4j.api.models.Note object GitlabDiscussionTransformer : Transformer> { override fun transform(input: Discussion): List { return input.notes.filter { !it.system }.map { transformItem(input, it) } } private fun transformItem(discussion: Discussion, input: Note) = CommentImpl( id = input.id.toString(), parentId = discussion.id, replyId = input.id.toString(), body = input.body, author = UserInfoImpl( id = input.author.id.toString(), name = input.author.name, username = input.author.username, url = input.author.webUrl, avatarUrl = input.author.avatarUrl, status = UserStatus.ACTIVE ), createdAt = DateTimeUtil.fromDate(input.createdAt), updatedAt = DateTimeUtil.fromDate(input.updatedAt), resolvedBy = if (null !== input.resolvedBy) { UserInfoImpl( id = input.resolvedBy.id.toString(), name = input.resolvedBy.name, username = input.resolvedBy.username, url = input.resolvedBy.webUrl, avatarUrl = input.resolvedBy.avatarUrl, status = UserStatus.ACTIVE ) } else null, resolved = if (null === input.resolved) false else input.resolved, resolvable = input.resolvable, position = if (null !== input.position) { CommentPositionImpl( startHash = input.position.startSha, baseHash = input.position.baseSha, headHash = input.position.headSha, oldPath = input.position.oldPath, newPath = input.position.newPath, oldLine = input.position.oldLine, newLine = input.position.newLine, source = CommentPositionSource.SERVER, changeType = CommentPositionChangeType.UNKNOWN ) } else null, isDraft = false ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabMRSimpleTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.MergeRequestState import net.ntworld.mergeRequestIntegration.internal.MergeRequestInfoImpl import net.ntworld.mergeRequestIntegration.provider.Transformer import net.ntworld.mergeRequestIntegration.provider.gitlab.* import net.ntworld.mergeRequestIntegration.util.DateTimeUtil import org.gitlab4j.api.models.MergeRequest object GitlabMRSimpleTransformer : Transformer { override fun transform(input: MergeRequest): MergeRequestInfo = MergeRequestInfoImpl( id = input.iid.toString(), provider = Gitlab.id, projectId = input.projectId.toString(), title = input.title, description = input.description, url = input.webUrl, state = when (input.state) { MERGE_REQUEST_STATE_OPENED -> MergeRequestState.OPENED MERGE_REQUEST_STATE_MERGED -> MergeRequestState.MERGED MERGE_REQUEST_STATE_CLOSED -> MergeRequestState.CLOSED else -> MergeRequestState.CLOSED }, createdAt = DateTimeUtil.fromDate(input.createdAt), updatedAt = DateTimeUtil.fromDate(input.updatedAt) ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabMRTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer import net.ntworld.mergeRequest.MergeRequest import net.ntworld.mergeRequest.MergeRequestState import net.ntworld.mergeRequestIntegration.internal.MergeRequestImpl import net.ntworld.mergeRequestIntegration.provider.Transformer import net.ntworld.mergeRequestIntegration.provider.gitlab.* import net.ntworld.mergeRequestIntegration.util.DateTimeUtil import org.gitlab4j.api.models.MergeRequest as MergeRequestModel object GitlabMRTransformer : Transformer { override fun transform(input: MergeRequestModel): MergeRequest = MergeRequestImpl( id = input.iid.toString(), provider = Gitlab.id, projectId = input.projectId.toString(), title = input.title, description = input.description, url = input.webUrl, state = when (input.state) { MERGE_REQUEST_STATE_OPENED -> MergeRequestState.OPENED MERGE_REQUEST_STATE_MERGED -> MergeRequestState.MERGED MERGE_REQUEST_STATE_CLOSED -> MergeRequestState.CLOSED else -> MergeRequestState.CLOSED }, createdAt = DateTimeUtil.fromDate(input.createdAt), updatedAt = DateTimeUtil.fromDate(input.updatedAt), assignee = if (null !== input.assignee) GitlabMemberTransformer.transform(input.assignee) else null, author = GitlabMemberTransformer.transform(input.author), diffReference = GitlabDiffRefTransformer.transform(input.diffRefs), sourceBranch = input.sourceBranch, targetBranch = input.targetBranch, upVotes = input.upvotes, downVotes = input.downvotes, commentsCount = input.userNotesCount, isWorkInProgress = input.workInProgress, canMerged = input.mergeStatus == MERGE_STATUS_CAN_BE_MERGED, mergedBy = if (null !== input.mergedBy) GitlabMemberTransformer.transform(input.mergedBy) else null, closedBy = if (null !== input.closedBy) GitlabMemberTransformer.transform(input.closedBy) else null, mergedAt = if (null !== input.mergedAt) DateTimeUtil.fromDate(input.mergedAt) else null, closedAt = if (null !== input.closedAt) DateTimeUtil.fromDate(input.closedAt) else null ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabMemberTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer import net.ntworld.mergeRequest.UserInfo import net.ntworld.mergeRequestIntegration.internal.UserInfoImpl import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil import net.ntworld.mergeRequestIntegration.provider.Transformer import org.gitlab4j.api.models.AbstractUser object GitlabMemberTransformer : Transformer, UserInfo> { override fun transform(input: AbstractUser<*>): UserInfo = UserInfoImpl( id = input.getId().toString(), name = input.getName(), username = input.getUsername(), avatarUrl = input.getAvatarUrl(), url = input.getWebUrl(), status = GitlabUtil.findUserStatus(input.getState()) ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabPipelineTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer import net.ntworld.mergeRequest.Pipeline import net.ntworld.mergeRequest.PipelineStatus import net.ntworld.mergeRequestIntegration.internal.PipelineImpl import net.ntworld.mergeRequestIntegration.provider.Transformer import net.ntworld.mergeRequestIntegration.provider.gitlab.* import net.ntworld.mergeRequestIntegration.provider.gitlab.model.PipelineModel object GitlabPipelineTransformer : Transformer { override fun transform(input: PipelineModel): Pipeline = PipelineImpl( id = input.id.toString(), hash = input.sha, ref = input.ref, status = findStatus(input.status), url = input.webUrl, createdAt = input.createdAt, updatedAt = input.updatedAt ) private fun findStatus(status: String): PipelineStatus { if (status == PIPELINE_STATUS_FAILED) { return PipelineStatus.FAILED } if (status == PIPELINE_STATUS_PARTIAL_FAILED) { return PipelineStatus.PARTIAL_FAILED } if (status == PIPELINE_STATUS_SUCCESS) { return PipelineStatus.SUCCESS } if (status == PIPELINE_STATUS_RUNNING) { return PipelineStatus.RUNNING } return PipelineStatus.UNKNOWN } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabProjectTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.ProjectVisibility import net.ntworld.mergeRequestIntegration.internal.ProjectImpl import net.ntworld.mergeRequestIntegration.provider.gitlab.Gitlab import net.ntworld.mergeRequestIntegration.provider.Transformer import org.gitlab4j.api.models.Visibility import org.gitlab4j.api.models.Project as GitlabProject object GitlabProjectTransformer : Transformer { override fun transform(input: GitlabProject): Project = ProjectImpl( id = input.id.toString(), provider = Gitlab, name = input.name, path = input.path, url = input.webUrl, visibility = if (input.visibility == Visibility.PUBLIC) ProjectVisibility.PUBLIC else ProjectVisibility.PRIVATE, avatarUrl = input.avatarUrl ?: "", repositoryHttpUrl = input.httpUrlToRepo, repositorySshUrl = input.sshUrlToRepo ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabUserInfoTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer import net.ntworld.mergeRequest.UserInfo import net.ntworld.mergeRequestIntegration.internal.UserInfoImpl import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil import net.ntworld.mergeRequestIntegration.provider.Transformer import org.gitlab4j.api.models.User as UserModel object GitlabUserInfoTransformer: Transformer { override fun transform(input: UserModel): UserInfo = UserInfoImpl( id = input.id.toString(), name = input.name, username = input.username, avatarUrl = input.avatarUrl, url = input.webUrl, status = GitlabUtil.findUserStatus(input.state) ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabUserTransformer.kt ================================================ package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer import net.ntworld.mergeRequest.User import net.ntworld.mergeRequestIntegration.internal.UserImpl import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil import net.ntworld.mergeRequestIntegration.provider.Transformer import org.gitlab4j.api.models.User as UserModel object GitlabUserTransformer: Transformer { override fun transform(input: UserModel): User = UserImpl( id = input.id.toString(), name = input.name, username = input.username, avatarUrl = input.avatarUrl, url = input.webUrl, status = GitlabUtil.findUserStatus(input.state), email = input.email, createdAt = input.createdAt.toString() ) } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/FindApprovalQueryHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.queryHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.QueryHandler import net.ntworld.mergeRequest.query.FindApprovalQuery import net.ntworld.mergeRequest.query.FindApprovalQueryResult import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegration.make @Handler class FindApprovalQueryHandler( private val providerStorage: ProviderStorage ) : QueryHandler { override fun handle(query: FindApprovalQuery): FindApprovalQueryResult { val (data, api) = providerStorage.findOrFail(query.providerId) return FindApprovalQueryResult.make( approval = api.mergeRequest.findApproval(data.project.id, query.mergeRequestId) ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/FindMergeRequestQueryHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.queryHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.QueryHandler import net.ntworld.mergeRequest.query.FindMergeRequestQuery import net.ntworld.mergeRequest.query.FindMergeRequestQueryResult import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegration.make @Handler class FindMergeRequestQueryHandler( private val providerStorage: ProviderStorage ): QueryHandler { override fun handle(query: FindMergeRequestQuery): FindMergeRequestQueryResult { val (data, api) = providerStorage.findOrFail(query.providerId) return FindMergeRequestQueryResult.make( mergeRequest = api.mergeRequest.findOrFail(data.project.id, query.mergeRequestId) ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/GetCommentsQueryHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.queryHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.QueryHandler import net.ntworld.mergeRequest.query.GetCommentsQuery import net.ntworld.mergeRequest.query.GetCommentsQueryResult import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegration.make @Handler class GetCommentsQueryHandler( private val providerStorage: ProviderStorage ) : QueryHandler { override fun handle(query: GetCommentsQuery): GetCommentsQueryResult { val (data, api) = providerStorage.findOrFail(query.providerId) return GetCommentsQueryResult.make( comments = api.comment.getAll(data.project, query.mergeRequestId) ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/GetCommitsQueryHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.queryHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.QueryHandler import net.ntworld.mergeRequest.query.GetCommitsQuery import net.ntworld.mergeRequest.query.GetCommitsQueryResult import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegration.make @Handler class GetCommitsQueryHandler( private val providerStorage: ProviderStorage ) : QueryHandler { override fun handle(query: GetCommitsQuery): GetCommitsQueryResult { val (data, api) = providerStorage.findOrFail(query.providerId) return GetCommitsQueryResult.make( commits = api.mergeRequest.getCommits(data.project.id, query.mergeRequestId) ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/GetMergeRequestsQueryHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.queryHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.QueryHandler import net.ntworld.mergeRequest.query.GetMergeRequestsQuery import net.ntworld.mergeRequest.query.GetMergeRequestsQueryResult import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegration.make @Handler class GetMergeRequestsQueryHandler( private val providerStorage: ProviderStorage ) : QueryHandler { override fun handle(query: GetMergeRequestsQuery): GetMergeRequestsQueryResult { val (data, api) = providerStorage.findOrFail(query.providerId) val result = api.mergeRequest.search( projectId = data.project.id, currentUserId = data.currentUser.id, filterBy = query.filterBy, orderBy = query.orderBy, page = query.page, itemsPerPage = query.itemsPerPage ) return GetMergeRequestsQueryResult.make( mergeRequests = result.data, totalPages = result.totalPages, totalItems = result.totalItems, currentPage = result.currentPage ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/GetPipelinesQueryHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.queryHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.QueryHandler import net.ntworld.mergeRequest.query.GetPipelinesQuery import net.ntworld.mergeRequest.query.GetPipelinesQueryResult import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegration.make @Handler class GetPipelinesQueryHandler( private val providerStorage: ProviderStorage ) : QueryHandler { override fun handle(query: GetPipelinesQuery): GetPipelinesQueryResult { val (data, api) = providerStorage.findOrFail(query.providerId) return GetPipelinesQueryResult.make( pipelines = api.mergeRequest.getPipelines(data.project.id, query.mergeRequestId) ) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/GetProjectMembersQueryHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.queryHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.cqrs.QueryHandler import net.ntworld.mergeRequest.query.GetProjectMembersQuery import net.ntworld.mergeRequest.query.GetProjectMembersQueryResult import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegration.make @Handler class GetProjectMembersQueryHandler( private val providerStorage: ProviderStorage ) : QueryHandler { override fun handle(query: GetProjectMembersQuery): GetProjectMembersQueryResult { val (data, api) = providerStorage.findOrFail(query.providerId) return GetProjectMembersQueryResult.make(api.project.getMembers(data.project.id)) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/requestHandler/CreateCommentRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequest.request.CreateCommentRequest import net.ntworld.mergeRequest.response.CreateCommentResponse import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegration.provider.ProviderException @Handler class CreateCommentRequestHandler( private val providerStorage: ProviderStorage ) : RequestHandler { override fun handle(request: CreateCommentRequest): CreateCommentResponse { val (data, api) = providerStorage.findOrFail(request.providerId) return try { val createdCommentId = api.comment.create( project = data.project, mergeRequestId = request.mergeRequestId, body = request.body, position = request.position, isDraft = request.isDraft ) CreateCommentResponse.make(error = null, createdCommentId = createdCommentId) } catch (exception: ProviderException) { CreateCommentResponse.make(error = exception.error, createdCommentId = null) } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/requestHandler/PublishAllCommentsRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequest.request.PublishAllCommentsRequest import net.ntworld.mergeRequest.response.PublishAllCommentsResponse import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegration.provider.ProviderException @Handler class PublishAllCommentsRequestHandler( private val providerStorage: ProviderStorage ): RequestHandler { override fun handle(request: PublishAllCommentsRequest): PublishAllCommentsResponse { val (data, api) = providerStorage.findOrFail(request.providerId) return try { api.comment.publishAllDraftComments( project = data.project, mergeRequestId = request.mergeRequestId ) PublishAllCommentsResponse.make(error = null, success = true) } catch (exception: ProviderException) { PublishAllCommentsResponse.make(error = exception.error, success = false) } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/requestHandler/PublishCommentsRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequest.request.PublishCommentsRequest import net.ntworld.mergeRequest.response.PublishCommentsResponse import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegration.provider.ProviderException @Handler class PublishCommentsRequestHandler( private val providerStorage: ProviderStorage ): RequestHandler { override fun handle(request: PublishCommentsRequest): PublishCommentsResponse { val (data, api) = providerStorage.findOrFail(request.providerId) return try { api.comment.publishDraftComments( project = data.project, mergeRequestId = request.mergeRequestId, commentIds = request.draftCommentIds ) PublishCommentsResponse.make(error = null, success = true) } catch (exception: ProviderException) { PublishCommentsResponse.make(error = exception.error, success = false) } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/requestHandler/ReplyCommentRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequest.request.ReplyCommentRequest import net.ntworld.mergeRequest.response.ReplyCommentResponse import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegration.provider.ProviderException @Handler class ReplyCommentRequestHandler( private val providerStorage: ProviderStorage ) : RequestHandler { override fun handle(request: ReplyCommentRequest): ReplyCommentResponse { val (data, api) = providerStorage.findOrFail(request.providerId) return try { val createdCommentId = api.comment.reply( project = data.project, mergeRequestId = request.mergeRequestId, repliedComment = request.repliedComment, body = request.body ) ReplyCommentResponse.make(error = null, createdCommentId = createdCommentId) } catch (exception: ProviderException) { ReplyCommentResponse.make(error = exception.error, createdCommentId = null) } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/requestHandler/UpdateCommentRequestHandler.kt ================================================ package net.ntworld.mergeRequestIntegration.requestHandler import net.ntworld.foundation.Handler import net.ntworld.foundation.RequestHandler import net.ntworld.mergeRequest.request.UpdateCommentRequest import net.ntworld.mergeRequest.response.UpdateCommentResponse import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegration.provider.ProviderException @Handler class UpdateCommentRequestHandler( private val providerStorage: ProviderStorage ): RequestHandler { override fun handle(request: UpdateCommentRequest): UpdateCommentResponse { val (data, api) = providerStorage.findOrFail(request.providerId) return try { api.comment.update( project = data.project, mergeRequestId = request.mergeRequestId, comment = request.comment, body = request.body ) UpdateCommentResponse.make(error = null, commentId = request.comment.id) } catch (exception: ProviderException) { UpdateCommentResponse.make(error = exception.error, commentId = null) } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/update/UpdateManager.kt ================================================ package net.ntworld.mergeRequestIntegration.update import com.github.kittinunf.fuel.core.FuelManager import com.github.kittinunf.fuel.core.ResponseResultOf import com.github.kittinunf.result.Result import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.list import java.util.* object UpdateManager { private const val CURRENT_VERSION = "2020.1.2" private const val METADATA_URL = "https://nhat-phan.github.io/updates/merge-request-integration/metadata.json" private const val CHECK_INTERVAL = 3600000 // Every 1 hour private val myJson: Json = Json(JsonConfiguration.Stable.copy(strictMode = false)) private var myLastCheckDate : Date? = null fun shouldGetAvailableUpdates(): Boolean { val lastCheck = myLastCheckDate if (null === lastCheck) { return true } val difference = Date().time - lastCheck.time return difference > CHECK_INTERVAL } private fun makeGetRequest(url: String): ResponseResultOf { return FuelManager().get(url).responseString() } fun getAvailableUpdates(): List { try { val (_, _, result) = makeGetRequest(METADATA_URL) return when (result) { is Result.Success -> { myLastCheckDate = Date() buildAvailableUpdates(result.value) } is Result.Failure -> { myLastCheckDate = Date() listOf() } } } catch (exception: Exception) { myLastCheckDate = Date() return listOf() } } private fun buildAvailableUpdates(input: String): List { val metadata = myJson.parse(UpdateMetadata.serializer().list, input).sortedBy { it.id } val currentVersion = metadata.firstOrNull { it.version == CURRENT_VERSION } if (null === currentVersion) { return listOf() } val updates = metadata.filter { it.id > currentVersion.id && it.active } return updates.map { try { val (_, _, result) = makeGetRequest(it.changesUrl) when (result) { is Result.Success -> { result.value .replace("", "

${it.version}

") .replace("", "") } is Result.Failure -> { "" } } } catch (exception: Exception) { "" } } } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/update/UpdateMetadata.kt ================================================ package net.ntworld.mergeRequestIntegration.update import kotlinx.serialization.Serializable @Serializable data class UpdateMetadata( val id: Int, val version: String, val changesUrl: String, val active: Boolean ) ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/util/DateTimeUtil.kt ================================================ package net.ntworld.mergeRequestIntegration.util import net.ntworld.mergeRequest.DateTime import org.joda.time.DateTimeZone import org.joda.time.format.ISODateTimeFormat import org.ocpsoft.prettytime.PrettyTime import java.text.SimpleDateFormat import java.util.* object DateTimeUtil { private val timezone = TimeZone.getTimeZone("UTC") private val toStringDateFormat by lazy { val df = SimpleDateFormat("yyyy-MM-dd HH:mm") df.timeZone = timezone df } private val convertDateFormat by lazy { val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") df.timeZone = timezone df } private val parser = ISODateTimeFormat.dateTimeParser() private val localTimeZone = DateTimeZone.getDefault() private val prettyTime = PrettyTime() @Synchronized fun fromDate(date: Date): DateTime = convertDateFormat.format(date) @Synchronized fun toDate(datetime: DateTime): Date { return parser.parseDateTime(datetime).withZone(localTimeZone).toDate() } @Synchronized fun formatDate(date: Date): String = toStringDateFormat.format(date) ?: "" @Synchronized fun toPretty(date: Date): String { return prettyTime.format(date) } @Synchronized fun toPretty(datetime: DateTime): String { return prettyTime.format(toDate(datetime)) } } ================================================ FILE: merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/util/SavedFiltersUtil.kt ================================================ package net.ntworld.mergeRequestIntegration.util import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonConfiguration import net.ntworld.mergeRequest.MergeRequestState import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequest.query.generated.GetMergeRequestFilterImpl class SavedFiltersUtil { @Serializable private class SavedFilters constructor( val state: String, val search: String, val authorId: String, val assigneeId: String, val approverIds: List, val ordering: String ) { fun toPair(): Pair { return Pair( GetMergeRequestFilterImpl( id = null, state = when (state) { "all" -> MergeRequestState.ALL "opened" -> MergeRequestState.OPENED "closed" -> MergeRequestState.CLOSED "merged" -> MergeRequestState.MERGED else -> MergeRequestState.OPENED }, search = search, authorId = authorId, assigneeId = assigneeId, approverIds = approverIds, sourceBranch = "" ), when (ordering) { "recently-updated" -> MergeRequestOrdering.RECENTLY_UPDATED "newest" -> MergeRequestOrdering.NEWEST "oldest" -> MergeRequestOrdering.OLDEST else -> MergeRequestOrdering.RECENTLY_UPDATED } ) } } companion object { @JvmStatic private val json = Json(JsonConfiguration.Stable.copy(strictMode = false)) @JvmStatic fun stringify(filter: GetMergeRequestFilter, ordering: MergeRequestOrdering): String { val data = SavedFilters( state = when (filter.state) { MergeRequestState.ALL -> "all" MergeRequestState.OPENED -> "opened" MergeRequestState.CLOSED -> "closed" MergeRequestState.MERGED -> "merged" }, search = filter.search, authorId = filter.authorId, assigneeId = filter.assigneeId, approverIds = filter.approverIds, ordering = when (ordering) { MergeRequestOrdering.RECENTLY_UPDATED -> "recently-updated" MergeRequestOrdering.NEWEST -> "newest" MergeRequestOrdering.OLDEST -> "oldest" } ) return json.stringify(SavedFilters.serializer(), data) } @JvmStatic fun parse(input: String) : Pair? { return try { val data = json.parse(SavedFilters.serializer(), input) data.toPair() } catch (exception: Exception) { null } } } } ================================================ FILE: merge-request-integration/src/test/kotlin/net/ntworld/mergeRequestIntegration/provider/MemoryCacheTests.kt ================================================ package net.ntworld.mergeRequestIntegration.provider import io.kotlintest.specs.DescribeSpec import io.mockk.every import io.mockk.spyk import net.ntworld.mergeRequest.api.Cache import net.ntworld.mergeRequest.api.CacheNotFoundException import net.ntworld.mergeRequestIntegration.exception.InvalidCacheKeyException import net.ntworld.mergeRequestIntegration.exception.InvalidTTLException import org.joda.time.DateTime import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue class MemoryCacheTests : DescribeSpec({ fun makeMemoryCache(defaultTTL: Int? = null) = MemoryCache(ttl = defaultTTL) fun makeMemoryCacheSpy() = spyk(MemoryCache()) describe(".defaultTTL") { context("use default value") { it("return 60 seconds") { val cache = makeMemoryCache() val defaultTimeToLive = cache.defaultTTL assertEquals(60000, defaultTimeToLive) } } context("use constructable valid value") { it("return given value") { val cache = makeMemoryCache(1) val defaultTimeToLive = cache.defaultTTL assertEquals(1, defaultTimeToLive) } } context("use constructable invalid value") { it("throws InvalidTTLException") { assertFailsWith { makeMemoryCache(0) } } } } describe(".get()") { context("invalid key") { it("throws InvalidCacheKeyException with empty key") { val cache = makeMemoryCache() assertFailsWith { cache.get("") } } it("throws InvalidCacheKeyException with key contains all space characters") { val cache = makeMemoryCache() assertFailsWith { cache.get(" ") } } } context("does not exists") { it("throws CacheNotFoundException") { val cache = makeMemoryCache() assertFailsWith { cache.get("key-not-found") } } } context("exists, still alive") { it("returns set value") { val cache = makeMemoryCache() cache.set("abcd", "value") val result: String = cache.get("abcd") assertEquals("value", result) } } context("exists, expired") { it("throws CacheNotFoundException") { val cache = makeMemoryCacheSpy() every { cache.isExpired(any()) } returns true cache.set("abc", "") assertFailsWith { cache.get("abc") } } } } describe(".has()") { context("invalid key") { it("throws InvalidCacheKeyException with empty key") { val cache = makeMemoryCache() assertFailsWith { cache.has("") } } it("throws InvalidCacheKeyException with key contains all space characters") { val cache = makeMemoryCache() assertFailsWith { cache.has(" ") } } } context("does not exists") { it("returns false") { val cache = makeMemoryCache() val result = cache.has("key-not-found") assertFalse(result) } } context("key exists, still alive") { it("returns true") { val cache = makeMemoryCache() cache.set("abc", "") val result = cache.has("abc") assertTrue(result) } } context("key exists, expired") { it("returns false") { val cache = makeMemoryCacheSpy() every { cache.isExpired(any()) } returns true cache.set("abc", "") val result = cache.has("abc") assertFalse(result) } } } describe(".remove()") { context("invalid key") { it("throws InvalidCacheKeyException with empty key") { val cache = makeMemoryCache() assertFailsWith { cache.remove("") } } it("throws InvalidCacheKeyException with key contains all space characters") { val cache = makeMemoryCache() assertFailsWith { cache.remove(" ") } } } context("valid key") { it("removes given key") { val cache = makeMemoryCache() cache.set("abc", "") val beforeRemoving = cache.has("abc") cache.remove("abc") val afterRemoving = cache.has("abc") assertTrue(beforeRemoving) assertFalse(afterRemoving) } } } describe(".isExpiredAfter()") { context("invalid key") { it("throws InvalidCacheKeyException with empty key") { val cache = makeMemoryCache() assertFailsWith { cache.isExpiredAfter("", DateTime.now()) } } it("throws InvalidCacheKeyException with key contains all space characters") { val cache = makeMemoryCache() assertFailsWith { cache.isExpiredAfter(" ", DateTime.now()) } } } context("key not found") { it("returns true") { val cache = makeMemoryCache() assertFailsWith { cache.isExpiredAfter("abc", DateTime.now()) } } } context("key found") { it("returns false if not expired yet") { val cache = makeMemoryCache() cache.set("abc", "value") val result = cache.isExpiredAfter("abc", DateTime.now()) assertFalse(result) } it("returns true if the key already expired") { val cache = makeMemoryCache() cache.set("abc", "value") val result = cache.isExpiredAfter("abc", DateTime.now().plus(86400)) assertTrue(result) } } } }) ================================================ FILE: merge-request-integration/src/test/resources/update-metadata.json ================================================ [ { "id": 1, "version": "2019.3.0", "changesUrl": "https://nhat-phan.github.io/updates/merge-request-integration/changes/2019.3.0.html", "active": true }, { "id": 2, "version": "2019.3.1", "changesUrl": "https://nhat-phan.github.io/updates/merge-request-integration/changes/2019.3.1.html", "active": false } ] ================================================ FILE: merge-request-integration-ce/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile val artifactGroup: String by project val targetIDEVersion: String by project val communityEditionVersion: String by project val intellijVersion: String by project val jvmTarget: String by project val foundationVersion: String by project val gitlab4jVersion: String by project val githubApiVersion: String by project val prettyTimeVersion: String by project val commonmarkVersion: String by project val intellijSinceBuild: String by project val intellijUntilBuild: String by project val eapRelease: String by project group = artifactGroup version = if (eapRelease == "false") { "$communityEditionVersion-built-for-ide-$targetIDEVersion" } else { "$communityEditionVersion-eap-$eapRelease-for-ide-$targetIDEVersion" } repositories { jcenter() mavenCentral() maven("https://jitpack.io") } dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib") implementation("com.github.nhat-phan.foundation:foundation-jvm:$foundationVersion") implementation("org.gitlab4j:gitlab4j-api:$gitlab4jVersion") implementation("org.kohsuke:github-api:$githubApiVersion") implementation("org.ocpsoft.prettytime:prettytime:$prettyTimeVersion") compile("com.atlassian.commonmark:commonmark:$commonmarkVersion") implementation(project(":contracts")) implementation(project(":merge-request-integration-core")) implementation(project(":merge-request-integration")) } // See https://github.com/JetBrains/gradle-intellij-plugin/ intellij { version = intellijVersion updateSinceUntilBuild = true setPlugins("git4idea") } val compileKotlin: KotlinCompile by tasks val compileTestKotlin: KotlinCompile by tasks compileKotlin.kotlinOptions { jvmTarget = jvmTarget } compileTestKotlin.kotlinOptions { jvmTarget = jvmTarget } tasks { named("compileKotlin") { kotlinOptions { jvmTarget = jvmTarget } } named("patchPluginXml") { val version = if (!communityEditionVersion.endsWith("eap")) communityEditionVersion else communityEditionVersion.substring(0, communityEditionVersion.length - 3) changeNotes(htmlFixer("./merge-request-integration-ce/doc/release-notes.$version.html")) pluginDescription(htmlFixer("./merge-request-integration-ce/doc/description.html")) sinceBuild(intellijSinceBuild) untilBuild(intellijUntilBuild) } } fun htmlFixer(filename: String): String { if (!File(filename).exists()) { throw Exception("File $filename not found.") } return File(filename).readText().replace("", "").replace("", "") } ================================================ FILE: merge-request-integration-ce/doc/description.html ================================================

Merge Request Integration CE is an open-source plugin for JetBrains IDEs which helps you

  • Do code review right in your IDE.
  • Address review comments from your colleagues.

What you can do:

  • Check approval statuses of merge requests which are waiting for your approval.
  • Filter Merge Requests which are assigned to you, belongs to your colleagues, etc
  • Check pipeline status and approval status.
  • Do code review, navigate code with Diff View right in your IDE.
  • Address review comments, navigate comments in editors.
  • Add and reply a comment
  • Approve/revoke your approval
  • More and more features will be coming soon :)

Currently the plugin supports GitLab only (gitlab cloud and self-hosted).

How to setup Gitlab connection

You need a personal api token. To get the token please follow these steps:

  • Log in to your Gitlab site
  • Go to Settings > Access Token and create a personal access token
  • Go to your IDE preferences, Merge Request Integration > Gitlab
  • Fill data, then save and click refresh button of Merge Request Integration CE window

License

The plugin is an open source released under Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International license.


It's totally free if you are using it for public repositories on gitlab.com.


For private repositories, this plugin is a trial. How long is the trial period? Equal to WINRAR's trial period 🙈


Community Edition (CE) is exactly the same as Enterprise Edition (EE). You don't need to hack or find a cracked version. Cracking software invites virus to your computer.

About me

My name is Nhat, I'm a software developer at Personio (yes, we are hiring all around the world, relocation to Munich is of course possible).

Sponsor

If you love this plugin, please support me by:


Thanks in advance!

Attribution

================================================ FILE: merge-request-integration-ce/doc/release-notes.2019.3.0.html ================================================
  • Initial release :)
================================================ FILE: merge-request-integration-ce/doc/release-notes.2019.3.1.html ================================================
  • Update Enterprise Edition url
================================================ FILE: merge-request-integration-ce/doc/release-notes.2019.3.2.html ================================================
  • Introduce commit list and changes tree
  • Resolve and delete comments
  • Display comments on editors
  • Bug fixes
================================================ FILE: merge-request-integration-ce/doc/release-notes.2019.3.3.html ================================================

Notable changes

  • Improved style of list
  • Improved approval panel which only opens if there is information to be displayed
  • Replaced deprecated api
  • Fixed bug #4: Cannot leave a comment in unified view or on Windows
  • Fixed bug #5: Cannot open comment editor if IDE does not support Markdown
  • Fixed bug: Token on Windows cannot be saved
  • Fixed grammatical error on GitLab configuration panel

Acknowledgement

  • Thank @Savak, @SToto98 for reporting bug #4
  • Thank @smallcreep for reporting bug #5
  • Thank Artem Kuchyn for reporting "Token on Windows cannot be saved" bug.
  • Thank Jay for correcting grammar.
================================================ FILE: merge-request-integration-ce/doc/release-notes.2019.3.4.html ================================================

Notable changes

  • Supported self-signed certificate
  • Supported cache option for finding merge request to improve performance
  • New implementation of configuration panel
  • Fixed #1: Support multi repositories project
  • Fixed #9: Connection list cannot be saved
  • Fixed #16: Pipeline is not displayed on Gitlab v12.2.4
  • Fixed bug: Empty description string on some actions
  • Fixed bug: UpdateManager requires weird format of metadata

Acknowledgement

================================================ FILE: merge-request-integration-ce/doc/release-notes.2019.3.5.html ================================================

Notable changes

  • Hot-fix #21: Gitlab configuration page is empty for new users who have no connections data

Acknowledgement

================================================ FILE: merge-request-integration-ce/doc/release-notes.2020.1.0.html ================================================

Notable changes

    • Feature: introduce create-general-comment button
    • Option: allow doing code review without checking out the target branch
    • Option: open diff changes if the number of changes is less than configurable value
    • Option: keep selected values of filters and order state in Merge Request list
    • UX: display a comment after creating/replying comment
    • UX: select project automatically based on remote url in configuration
    • UX: remove left border of Tabs
    • Bug fix: cannot start an IDE if CE and EE are installed simultaneously

Acknowledgement

================================================ FILE: merge-request-integration-ce/doc/release-notes.2020.1.1.html ================================================

Notable changes

  • Hotfix: Crashes on Intellij IDEs v2020.1 EAP

Acknowledgement

================================================ FILE: merge-request-integration-ce/doc/release-notes.2020.1.2.html ================================================

Notable changes

  • Feature: Display, resolve, delete, reply, write comments in diff view
  • Feature: Open diff view when selecting a group in comments tree
  • Bug fix: Duplicated/wrong comment position

Acknowledgement

================================================ FILE: merge-request-integration-ce/doc/release-notes.2020.1.3.html ================================================

Notable changes

  • Branch project based on IDE versions, deploy plugin to EAP channel for 2020.1 EAP IDEs
  • Display approval status in merge request list
  • Improve UI response when deleting and adding a new comment
  • Sync comment collection when a comment gets changed in diff view
  • Fix #44: error when comments get destroyed in the event thread

Acknowledgement

================================================ FILE: merge-request-integration-ce/doc/release-notes.2020.1.4.html ================================================

Notable changes

  • Bug fixed: comments are not shown in diff view on Windows
  • Bug fixed: comments are filtered out when the MR has too many commits
================================================ FILE: merge-request-integration-ce/doc/release-notes.2020.1.5.html ================================================

Notable changes

  • Support IDEA 2020.1, Android Studio 4.0.0
  • Introduce new Comments tab which
    • Has all current functionalities
    • Can open diff view when you select a file
    • Can jump to code when you travel the Comments tree
  • Open diff view when you click to Changes tree in Commits tab
  • Display merge request id in Home tab, Merge Request tree and Info tab
  • Fix #17: Cannot do Code Review on Android Studio
  • Fix #54: Merge Request tree should be invoked in application thread when get reloaded
  • Bug fixes

Acknowledgement

================================================ FILE: merge-request-integration-ce/doc/release-notes.2020.2.0.html ================================================

Notable changes

  • Support IDEA 2020.2
  • Introduce new Rework flow which helps
    • Display comments and file changes if current branch matches your MR
    • Jump and address comments more easily
    Note: because of technical difficulty Reply button only works when open a dialog
  • Introduce options to hide UpVote, DownVote or State buttons in toolbar.
  • Improve configuration area.
  • Throw all exceptions which may happen inside plugin.
  • Fix #65: loading data and refreshing not work properly
  • Fix #88: fetching merge requests error for some timezones

Acknowledgement

================================================ FILE: merge-request-integration-ce/doc/release-notes.2020.3.0.html ================================================

Notable changes

  • Support IDEA 2020.3.x
  • Support search by ID when ID is typed in the search box
  • Introduce edit comment functionality
  • Introduce draft comment feature which can temporarily save and push when Code Review is stopped
  • Fix #93: Crash with empty avatar
  • Fix #98: Crash when project gets closed

Acknowledgement

================================================ FILE: merge-request-integration-ce/settings.gradle.kts ================================================ rootProject.name = "merge-request-integration-ce" ================================================ FILE: merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/CommunityApplicationServiceProvider.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeCE import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.openapi.project.Project import net.ntworld.mergeRequestIntegrationIde.infrastructure.AbstractApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider @State(name = "MergeRequestIntegrationApplicationLevel", storages = [(Storage("merge-request-integration.xml"))]) class CommunityApplicationServiceProvider: AbstractApplicationServiceProvider() { override fun findProjectServiceProvider(project: Project): ProjectServiceProvider { val service = ServiceManager.getService(project, CommunityProjectServiceProvider::class.java) registerProjectServiceProvider(service) return service } override val singleMRToolWindowName: String = "Merge Request CE" } ================================================ FILE: merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/CommunityProjectServiceProvider.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeCE import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.openapi.project.Project import net.ntworld.mergeRequestIntegrationIde.infrastructure.AbstractProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider @State(name = "MergeRequestIntegrationProjectLevel", storages = [(Storage("merge-request-integration-ce.xml"))]) class CommunityProjectServiceProvider(project: Project) : AbstractProjectServiceProvider(project) { override val applicationServiceProvider: ApplicationServiceProvider = ServiceManager.getService( CommunityApplicationServiceProvider::class.java ) init { initWithApplicationServiceProvider(applicationServiceProvider) } } ================================================ FILE: merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/Configuration.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeCE import com.intellij.openapi.components.ServiceManager import net.ntworld.mergeRequestIntegrationIde.ui.configuration.ConfigurationBase class Configuration: ConfigurationBase(ServiceManager.getService(CommunityApplicationServiceProvider::class.java)) { override fun getId(): String { return "merge-request-integration-ce" } override fun getDisplayName(): String { return "Merge Request Integration CE" } } ================================================ FILE: merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/DiffExtension.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeCE import com.intellij.openapi.components.ServiceManager import net.ntworld.mergeRequestIntegrationIde.diff.DiffExtensionBase class DiffExtension : DiffExtensionBase( ServiceManager.getService(CommunityApplicationServiceProvider::class.java) ) ================================================ FILE: merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/DiffViewAddCommentAction.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeCE import net.ntworld.mergeRequestIntegrationIde.diff.DiffViewAddCommentActionBase class DiffViewAddCommentAction : DiffViewAddCommentActionBase() ================================================ FILE: merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/DiffViewToggleCommentsAction.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeCE import net.ntworld.mergeRequestIntegrationIde.diff.DiffViewToggleCommentsActionBase class DiffViewToggleCommentsAction : DiffViewToggleCommentsActionBase() ================================================ FILE: merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/GithubConnectionsConfigurable.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeCE import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.project.Project import net.ntworld.mergeRequestIntegrationIde.ui.configuration.GithubConnectionsConfigurableBase class GithubConnectionsConfigurable(project: Project): GithubConnectionsConfigurableBase( ServiceManager.getService(project, CommunityProjectServiceProvider::class.java) ) { override fun getId(): String = "MRI:github-ce" override fun getDisplayName(): String = "Github" } ================================================ FILE: merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/GitlabConnectionsConfigurable.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeCE import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.project.Project import net.ntworld.mergeRequestIntegrationIde.ui.configuration.GitlabConnectionsConfigurableBase class GitlabConnectionsConfigurable(project: Project) : GitlabConnectionsConfigurableBase( ServiceManager.getService(project, CommunityProjectServiceProvider::class.java) ) { override fun getId(): String = "MRI:gitlab-ce" override fun getDisplayName(): String = "Gitlab" } ================================================ FILE: merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/MainToolWindowFactory.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeCE import com.intellij.openapi.components.ServiceManager import net.ntworld.mergeRequestIntegrationIde.ui.MainToolWindowFactoryBase class MainToolWindowFactory : MainToolWindowFactoryBase( ServiceManager.getService(CommunityApplicationServiceProvider::class.java) ) ================================================ FILE: merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/SingleMRToolWindowFactory.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeCE import com.intellij.openapi.components.ServiceManager import net.ntworld.mergeRequestIntegrationIde.toolWindow.SingleMRToolWindowFactoryBase class SingleMRToolWindowFactory : SingleMRToolWindowFactoryBase( ServiceManager.getService(CommunityApplicationServiceProvider::class.java) ) ================================================ FILE: merge-request-integration-ce/src/main/resources/META-INF/plugin.xml ================================================ net.ntworld.nhat-phan.merge-request-integration-ce Merge Request Integration CE - Code Review for GitLab ntworld com.intellij.modules.platform com.intellij.modules.lang com.intellij.modules.vcs Git4Idea ================================================ FILE: merge-request-integration-core/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile val artifactGroup: String by project val artifactVersion: String by project val intellijVersion: String by project val jvmTarget: String by project val foundationVersion: String by project val gitlab4jVersion: String by project val githubApiVersion: String by project val prettyTimeVersion: String by project val commonmarkVersion: String by project val mockkVersion: String by project group = artifactGroup version = artifactVersion repositories { jcenter() mavenCentral() maven("https://jitpack.io") } dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib") implementation("com.github.nhat-phan.foundation:foundation-jvm:$foundationVersion") implementation("org.gitlab4j:gitlab4j-api:$gitlab4jVersion") implementation("org.kohsuke:github-api:$githubApiVersion") implementation("org.ocpsoft.prettytime:prettytime:$prettyTimeVersion") compile("com.atlassian.commonmark:commonmark:$commonmarkVersion") implementation(project(":contracts")) implementation(project(":merge-request-integration")) testImplementation(kotlin("test")) testImplementation(kotlin("test-junit")) testImplementation("io.mockk:mockk:$mockkVersion") } intellij { version = intellijVersion setPlugins("git4idea") } val compileKotlin: KotlinCompile by tasks val compileTestKotlin: KotlinCompile by tasks compileKotlin.kotlinOptions { jvmTarget = jvmTarget } compileTestKotlin.kotlinOptions { jvmTarget = jvmTarget } tasks { named("compileKotlin") { kotlinOptions { jvmTarget = jvmTarget } } } ================================================ FILE: merge-request-integration-core/settings.gradle.kts ================================================ rootProject.name = "merge-request-integration-core" ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/AbstractModel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import com.intellij.util.EventDispatcher import java.util.* abstract class AbstractModel: Model { protected abstract val dispatcher: EventDispatcher override fun addDataListener(listener: DataListener) = dispatcher.addListener(listener) override fun removeDataListener(listener: DataListener) = dispatcher.removeListener(listener) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/AbstractPresenter.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import com.intellij.util.EventDispatcher import java.util.* abstract class AbstractPresenter : Presenter { protected abstract val dispatcher: EventDispatcher override fun addListener(listener: T) = dispatcher.addListener(listener) override fun removeListener(listener: T) = dispatcher.removeListener(listener) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/AbstractSimpleModel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import com.intellij.util.EventDispatcher import java.util.* abstract class AbstractSimpleModel: SimpleModel { protected val dispatcher = EventDispatcher.create(EventListener::class.java) override fun addDataListener(listener: EventListener) = dispatcher.addListener(listener) override fun removeDataListener(listener: EventListener) = dispatcher.removeListener(listener) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/AbstractSimplePresenter.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import com.intellij.util.EventDispatcher import java.util.* abstract class AbstractSimplePresenter : SimplePresenter { protected val dispatcher = EventDispatcher.create(EventListener::class.java) override fun addListener(listener: EventListener) = dispatcher.addListener(listener) override fun removeListener(listener: EventListener) = dispatcher.removeListener(listener) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/AbstractSimpleView.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import com.intellij.util.EventDispatcher import java.util.* abstract class AbstractSimpleView: SimpleView { protected val dispatcher = EventDispatcher.create(EventListener::class.java) override fun addActionListener(listener: EventListener) = dispatcher.addListener(listener) override fun removeActionListener(listener: EventListener) = dispatcher.removeListener(listener) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/AbstractView.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import com.intellij.util.EventDispatcher import java.util.* abstract class AbstractView: View { protected abstract val dispatcher: EventDispatcher override fun addActionListener(listener: ActionListener) = dispatcher.addListener(listener) override fun removeActionListener(listener: ActionListener) = dispatcher.removeListener(listener) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/Component.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import javax.swing.JComponent interface Component { val component: JComponent } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ComponentFactory.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.component.PaginationToolbar import net.ntworld.mergeRequestIntegrationIde.component.comment.CommentComponentFactory import net.ntworld.mergeRequestIntegrationIde.toolWindow.CommentsToolWindowTab import net.ntworld.mergeRequestIntegrationIde.toolWindow.FilesToolWindowTab interface ComponentFactory { val toolWindowTabs: ToolWindowTabFactory val commentComponents: CommentComponentFactory fun makePaginationToolbar(displayRefreshButton: Boolean = false): PaginationToolbar interface ToolWindowTabFactory { fun makeFilesToolWindowTab(providerData: ProviderData, isCodeReviewChanges: Boolean): FilesToolWindowTab fun makeCommentsToolWindowTab( providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List ): CommentsToolWindowTab } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/DataChangedSource.kt ================================================ package net.ntworld.mergeRequestIntegrationIde enum class DataChangedSource { UI, NOTIFIER } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/DefaultComponentFactory.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.component.PaginationToolbar import net.ntworld.mergeRequestIntegrationIde.component.PaginationToolbarImpl import net.ntworld.mergeRequestIntegrationIde.component.comment.CommentComponentFactory import net.ntworld.mergeRequestIntegrationIde.component.comment.CommentComponentFactoryImpl import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.toolWindow.CommentsToolWindowTab import net.ntworld.mergeRequestIntegrationIde.toolWindow.FilesToolWindowTab import net.ntworld.mergeRequestIntegrationIde.toolWindow.internal.CommentsToolWindowTabImpl import net.ntworld.mergeRequestIntegrationIde.toolWindow.internal.FilesToolWindowTabImpl class DefaultComponentFactory( private val projectServiceProvider: ProjectServiceProvider ) : ComponentFactory { override val toolWindowTabs: ComponentFactory.ToolWindowTabFactory = MyToolWindowTabFactory(projectServiceProvider) override val commentComponents: CommentComponentFactory = CommentComponentFactoryImpl(projectServiceProvider) override fun makePaginationToolbar(displayRefreshButton: Boolean): PaginationToolbar { return PaginationToolbarImpl(displayRefreshButton) } private class MyToolWindowTabFactory( private val projectServiceProvider: ProjectServiceProvider ) : ComponentFactory.ToolWindowTabFactory { override fun makeFilesToolWindowTab( providerData: ProviderData, isCodeReviewChanges: Boolean ): FilesToolWindowTab { return FilesToolWindowTabImpl(projectServiceProvider, providerData, isCodeReviewChanges) } override fun makeCommentsToolWindowTab( providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List ): CommentsToolWindowTab { return CommentsToolWindowTabImpl(projectServiceProvider, providerData, mergeRequestInfo, comments) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/IdeInfrastructure.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import net.ntworld.foundation.InfrastructureProvider import net.ntworld.mergeRequestIntegration.MergeRequestIntegrationInfrastructure import net.ntworld.mergeRequestIntegration.ProviderStorage class IdeInfrastructure( private val providerStorage: ProviderStorage ) : InfrastructureProvider() { private val included = listOf( MergeRequestIntegrationInfrastructure(providerStorage) ) init { wire(this.root, included) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/Model.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import java.util.* interface Model { fun addDataListener(listener: DataListener) fun removeDataListener(listener: DataListener) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/Presenter.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import java.util.* interface Presenter { fun addListener(listener: T) fun removeListener(listener: T) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/SimpleModel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import java.util.* interface SimpleModel : Model ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/SimplePresenter.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import java.util.* interface SimplePresenter : Presenter ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/SimpleView.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import java.util.* interface SimpleView : View ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/View.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import java.util.* interface View { fun addActionListener(listener: ActionListener) fun removeActionListener(listener: ActionListener) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/_const.kt ================================================ package net.ntworld.mergeRequestIntegrationIde import com.intellij.openapi.diagnostic.Logger const val ENTERPRISE_EDITION_URL = "https://plugins.jetbrains.com/plugin/13615-merge-request-integration-ee--code-review-for-gitlab/" const val SEARCH_MERGE_REQUEST_ITEMS_PER_PAGE = 10 const val SINGLE_MR_CHANGES_WHEN_DOING_CODE_REVIEW_NAME = "Changed Files" const val SINGLE_MR_REWORK_CHANGES_PREFIX = "Files" const val SINGLE_MR_REWORK_COMMENTS_PREFIX = "Comments" private const val DEBUG = false val logger = Logger.getInstance("MRI") fun debug(message: String) { if (DEBUG) { println(message) logger.debug(message) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/compatibility/IntellijIdeApi.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.compatibility import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.impl.EditorEmbeddedComponentManager import com.intellij.openapi.project.Project import com.intellij.vcs.log.impl.VcsLogManager import gnu.trove.TIntFunction interface IntellijIdeApi { fun findLeftLineNumberConverter(editor: EditorEx): TIntFunction fun findRightLineNumberConverter(editor: EditorEx): TIntFunction fun makeEditorEmbeddedComponentManagerProperties(offset: Int): EditorEmbeddedComponentManager.Properties fun getVcsLogManager(ideaProject: Project, action: ((VcsLogManager) -> Unit)) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/compatibility/Version193Adapter.kt ================================================ //package net.ntworld.mergeRequestIntegrationIde.compatibility // //import com.intellij.openapi.editor.ex.EditorEx //import com.intellij.openapi.editor.impl.EditorEmbeddedComponentManager //import com.intellij.vcs.log.impl.VcsLogContentUtil //import com.intellij.vcs.log.impl.VcsLogManager //import gnu.trove.TIntFunction // //class Version193Adapter : IntellijIdeApi { // private val resizePolicy by lazy { // val constructors = EditorEmbeddedComponentManager.ResizePolicy::class.java.declaredConstructors // for (ctor in constructors) { // ctor.isAccessible = true // return@lazy ctor.newInstance(0) as EditorEmbeddedComponentManager.ResizePolicy // } // return@lazy EditorEmbeddedComponentManager.ResizePolicy.any() // } // // override fun findLeftLineNumberConverter(editor: EditorEx): TIntFunction { // val myLineNumberConvertor = editor.gutter.javaClass.getDeclaredField("myLineNumberConvertor") // myLineNumberConvertor.isAccessible = true // return myLineNumberConvertor.get(editor.gutter) as TIntFunction // } // // override fun findRightLineNumberConverter(editor: EditorEx): TIntFunction { // val myAdditionalLineNumberConvertor = editor.gutter.javaClass.getDeclaredField( // "myAdditionalLineNumberConvertor" // ) // myAdditionalLineNumberConvertor.isAccessible = true // return myAdditionalLineNumberConvertor.get(editor.gutter) as TIntFunction // } // // override fun makeEditorEmbeddedComponentManagerProperties(offset: Int): EditorEmbeddedComponentManager.Properties { // return EditorEmbeddedComponentManager.Properties( // resizePolicy, // true, // false, // 0, // offset // ) // } // // override fun getVcsLogManager(ideaProject: Project, action: (VcsLogManager) -> Unit) { // val log = VcsLogContentUtil.getOrCreateLog(ideaProject) // if (null === log) { // return // } // action.invoke(log) // } //} ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/compatibility/Version201Adapter.kt ================================================ //package net.ntworld.mergeRequestIntegrationIde.compatibility // //import com.intellij.openapi.editor.ex.EditorEx //import com.intellij.openapi.editor.impl.EditorEmbeddedComponentManager //import com.intellij.openapi.editor.impl.LineNumberConverterAdapter //import com.intellij.openapi.project.Project //import com.intellij.vcs.log.impl.VcsLogContentUtil //import com.intellij.vcs.log.impl.VcsLogManager //import gnu.trove.TIntFunction // //class Version201Adapter : IntellijIdeApi { // override fun findLeftLineNumberConverter(editor: EditorEx): TIntFunction { // val myLineNumberConverter = editor.gutter.javaClass.getDeclaredField("myLineNumberConverter") // myLineNumberConverter.isAccessible = true // val adapter = myLineNumberConverter.get(editor.gutter) as LineNumberConverterAdapter // return TIntFunction { p0 -> adapter.convert(editor, p0) ?: -1 } // } // // override fun findRightLineNumberConverter(editor: EditorEx): TIntFunction { // val myAdditionalLineNumberConverter = editor.gutter.javaClass.getDeclaredField( // "myAdditionalLineNumberConverter" // ) // myAdditionalLineNumberConverter.isAccessible = true // val adapter = myAdditionalLineNumberConverter.get(editor.gutter) as LineNumberConverterAdapter // return TIntFunction { p0 -> adapter.convert(editor, p0) ?: -1 } // } // // override fun makeEditorEmbeddedComponentManagerProperties(offset: Int): EditorEmbeddedComponentManager.Properties { // return EditorEmbeddedComponentManager.Properties( // EditorEmbeddedComponentManager.ResizePolicy.none(), // null, // true, // false, // 0, // offset // ) // } // // override fun getVcsLogManager(ideaProject: Project, action: (VcsLogManager) -> Unit) { // val log = VcsLogContentUtil.getOrCreateLog(ideaProject) // if (null === log) { // return // } // action.invoke(log) // } //} ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/compatibility/Version203Adapter.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.compatibility import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.impl.EditorEmbeddedComponentManager import com.intellij.openapi.editor.impl.LineNumberConverterAdapter import com.intellij.openapi.project.Project import com.intellij.vcs.log.impl.VcsLogManager import com.intellij.vcs.log.impl.VcsProjectLog import gnu.trove.TIntFunction class Version203Adapter: IntellijIdeApi { override fun findLeftLineNumberConverter(editor: EditorEx): TIntFunction { val myLineNumberConverter = editor.gutter.javaClass.getDeclaredField("myLineNumberConverter") myLineNumberConverter.isAccessible = true val adapter = myLineNumberConverter.get(editor.gutter) as LineNumberConverterAdapter return TIntFunction { p0 -> adapter.convert(editor, p0) ?: -1 } } override fun findRightLineNumberConverter(editor: EditorEx): TIntFunction { val myAdditionalLineNumberConverter = editor.gutter.javaClass.getDeclaredField( "myAdditionalLineNumberConverter" ) myAdditionalLineNumberConverter.isAccessible = true val adapter = myAdditionalLineNumberConverter.get(editor.gutter) as LineNumberConverterAdapter return TIntFunction { p0 -> adapter.convert(editor, p0) ?: -1 } } override fun makeEditorEmbeddedComponentManagerProperties(offset: Int): EditorEmbeddedComponentManager.Properties { return EditorEmbeddedComponentManager.Properties( EditorEmbeddedComponentManager.ResizePolicy.none(), null, true, false, 0, offset ) } override fun getVcsLogManager(ideaProject: Project, action: (VcsLogManager) -> Unit) { VcsProjectLog.runWhenLogIsReady(ideaProject) { action.invoke(it) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/Icons.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component import com.intellij.openapi.util.IconLoader object Icons { val Comments = IconLoader.getIcon("/icons/comments.svg", this::class.java) val ThumbsUp = IconLoader.getIcon("/icons/thumbs-up.svg", this::class.java) val ThumbsDown = IconLoader.getIcon("/icons/thumbs-down.svg", this::class.java) val PipelineRunning = IconLoader.getIcon("/icons/clock.svg", this::class.java) val PipelineFailed = IconLoader.getIcon("/icons/exclamation-circle.svg", this::class.java) val PipelineSuccess = IconLoader.getIcon("/icons/check-circle.svg", this::class.java) val NoApproval = IconLoader.getIcon("/icons/square-gray.svg", this::class.java) val RequiredApproval = IconLoader.getIcon("/icons/square-yellow.svg", this::class.java) val Approved = IconLoader.getIcon("/icons/check-square.svg", this::class.java) val ExternalLink = IconLoader.getIcon("/icons/external-link-alt.svg", this::class.java) val Description = IconLoader.getIcon("/icons/file-alt.svg", this::class.java) val StateMerged = IconLoader.getIcon("/icons/door-closed-green.svg", this::class.java) val StateClosed = IconLoader.getIcon("/icons/door-closed-red.svg", this::class.java) val StateOpened = IconLoader.getIcon("/icons/door-open.svg", this::class.java) val HasComment = IconLoader.getIcon("/icons/sticky-note.svg", this::class.java) val ReplyComment = IconLoader.getIcon("/icons/reply.svg", this::class.java) val Trash = IconLoader.getIcon("/icons/trash.svg", this::class.java) val Edit = IconLoader.getIcon("/icons/edit.svg", this::class.java) val Resolve = IconLoader.getIcon("/icons/check-circle-gray.svg", this::class.java) val Resolved = IconLoader.getIcon("/icons/check-circle.svg", this::class.java) val CaretDown = IconLoader.getIcon("/icons/chevron-down.svg", this::class.java) val CaretRight = IconLoader.getIcon("/icons/chevron-right.svg", this::class.java) val LegalWarning = IconLoader.getIcon("/icons/exclamation-triangle.svg", this::class.java) object Gutter { val Empty = IconLoader.getIcon("/icons/1px.svg", this::class.java) val Comment = IconLoader.getIcon("/icons/gutter-comment.svg", this::class.java) val Comments = IconLoader.getIcon("/icons/gutter-comments.svg", this::class.java) val AddComment = IconLoader.getIcon("/icons/gutter-plus-small.svg", this::class.java) val WritingComment = IconLoader.getIcon("/icons/gutter-writing-comment.svg", this::class.java) val HasDraft = IconLoader.getIcon("/icons/edit.svg", this::class.java) } object TreeNode { val ResolvedComment = IconLoader.getIcon("/icons/check-square.svg", this::class.java) val UnresolvedComment = IconLoader.getIcon("/icons/square-yellow.svg", this::class.java) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/PaginationToolbar.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component import net.ntworld.mergeRequestIntegrationIde.Component import java.util.* interface PaginationToolbar : Component { fun enable() fun disable() fun getCurrentPage(): Int fun setData(page: Int, totalPages: Int, totalItems: Int) fun addListener(listener: Listener) fun removeListener(listener: Listener) interface Listener : EventListener { fun changePage(page: Int) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/PaginationToolbarImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DefaultActionGroup import net.miginfocom.swing.MigLayout import com.intellij.util.EventDispatcher import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JPanel internal class PaginationToolbarImpl(private val displayRefreshButton: Boolean = false) : PaginationToolbar { private val dispatcher = EventDispatcher.create(PaginationToolbar.Listener::class.java) private var myEnabled: Boolean = true private var myIsValid: Boolean = true private var myPage: Int = 0 private var myTotalPages: Int = 0 private var myTotalItems: Int = 0 private val myRefreshButtonAction = MyRefreshButtonAction(this) private val myFirstAction = MyFirstAction(this) private val myPrevAction = MyPrevAction(this) private val myNextAction = MyNextAction(this) private val myLastAction = MyLastAction(this) private val myInfo = JLabel() private val myPanel by lazy { val panel = JPanel(MigLayout("ins 0, fill", "[left, fill]push[right]", "center")) val actionGroup = DefaultActionGroup() if (displayRefreshButton) { actionGroup.add(myRefreshButtonAction) actionGroup.addSeparator() } actionGroup.add(myFirstAction) actionGroup.add(myPrevAction) actionGroup.add(myNextAction) actionGroup.add(myLastAction) val toolbar = ActionManager.getInstance().createActionToolbar( "${this::class.java.canonicalName}/toolbar-right", actionGroup, true ) panel.add(myInfo) panel.add(toolbar.component) panel } override val component: JComponent = myPanel override fun enable() { myEnabled = true updateInfo() } override fun disable() { myEnabled = false updateInfo() } override fun getCurrentPage(): Int { return if (myPage <= 0) 1 else myPage } override fun setData(page: Int, totalPages: Int, totalItems: Int) { myIsValid = true if (page <= 0 || totalPages <= 0 || totalItems <= 0) { myIsValid = false } if (page > totalPages) { myIsValid = false } myPage = page myTotalPages = totalPages myTotalItems = totalItems updateInfo() } override fun addListener(listener: PaginationToolbar.Listener) { dispatcher.addListener(listener) } override fun removeListener(listener: PaginationToolbar.Listener) { dispatcher.removeListener(listener) } private fun updateInfo() { if (myEnabled) { myInfo.text = " Displaying page $myPage/$myTotalPages. Total items: $myTotalItems" } else { myInfo.text = " Loading data..." } } private class MyRefreshButtonAction(private val self: PaginationToolbarImpl) : AnAction("Refresh", "Refresh", AllIcons.Actions.Refresh) { override fun actionPerformed(e: AnActionEvent) { self.dispatcher.multicaster.changePage(self.myPage) } override fun update(e: AnActionEvent) { e.presentation.isEnabled = self.myEnabled && self.myIsValid } } private class MyFirstAction(private val self: PaginationToolbarImpl) : AnAction("First", "Go to the first page", AllIcons.Actions.Play_first) { override fun actionPerformed(e: AnActionEvent) { self.setData(1, self.myTotalPages, self.myTotalItems) self.dispatcher.multicaster.changePage(self.myPage) } override fun update(e: AnActionEvent) { e.presentation.isEnabled = self.myEnabled && self.myIsValid && self.myPage != 1 } } private class MyPrevAction(private val self: PaginationToolbarImpl) : AnAction("Previous", "Previous page", AllIcons.Actions.Play_back) { override fun actionPerformed(e: AnActionEvent) { self.setData(self.myPage - 1, self.myTotalPages, self.myTotalItems) self.dispatcher.multicaster.changePage(self.myPage) } override fun update(e: AnActionEvent) { e.presentation.isEnabled = self.myEnabled && self.myIsValid && self.myPage > 1 && self.myTotalPages > 1 } } private class MyNextAction(private val self: PaginationToolbarImpl) : AnAction("Next", "Next page", AllIcons.Actions.Play_forward) { override fun actionPerformed(e: AnActionEvent) { self.setData(self.myPage + 1, self.myTotalPages, self.myTotalItems) self.dispatcher.multicaster.changePage(self.myPage) } override fun update(e: AnActionEvent) { e.presentation.isEnabled = self.myEnabled && self.myIsValid && self.myPage < self.myTotalPages } } private class MyLastAction(private val self: PaginationToolbarImpl) : AnAction("Last", "Go to the last page", AllIcons.Actions.Play_last) { override fun actionPerformed(e: AnActionEvent) { self.setData(self.myTotalPages, self.myTotalPages, self.myTotalItems) self.dispatcher.multicaster.changePage(self.myPage) } override fun update(e: AnActionEvent) { e.presentation.isEnabled = self.myEnabled && self.myIsValid && self.myPage < self.myTotalPages } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/CommentComponent.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.comment import net.ntworld.mergeRequestIntegrationIde.Component interface CommentComponent : Component { fun hideMoveToDialogButtons() fun showMoveToDialogButtons() } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/CommentComponentFactory.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.comment import com.intellij.openapi.project.Project import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData interface CommentComponentFactory { fun makeGroup( providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, ideaProject: Project, borderTop: Boolean, groupId: String, comments: List, options: Options ) : GroupComponent fun makeComment( groupComponent: GroupComponent, providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comment: Comment, indent: Int, options: Options ): CommentComponent fun makeEditor( ideaProject: Project, type: EditorComponent.Type, indent: Int, borderLeftRight: Int, showCancelAction: Boolean, isDoingCodeReview: Boolean ): EditorComponent } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/CommentComponentFactoryImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.comment import com.intellij.openapi.project.Project as IdeaProject import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider internal class CommentComponentFactoryImpl( private val projectServiceProvider: ProjectServiceProvider ) : CommentComponentFactory { override fun makeGroup( providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, ideaProject: IdeaProject, borderTop: Boolean, groupId: String, comments: List, options: Options ) : GroupComponent { return GroupComponentImpl( this, borderTop, providerData, mergeRequestInfo, ideaProject, groupId, comments, options ) } override fun makeComment( groupComponent: GroupComponent, providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comment: Comment, indent: Int, options: Options ): CommentComponent { return CommentComponentImpl( this, projectServiceProvider, groupComponent, providerData, mergeRequestInfo, comment, indent, options ) } override fun makeEditor( ideaProject: IdeaProject, type: EditorComponent.Type, indent: Int, borderLeftRight: Int, showCancelAction: Boolean, isDoingCodeReview: Boolean ): EditorComponent { return EditorComponentImpl( ideaProject, type, indent, borderLeftRight, showCancelAction, isDoingCodeReview ) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/CommentComponentImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.comment import com.intellij.icons.AllIcons import com.intellij.ide.BrowserUtil import com.intellij.ide.util.TipUIUtil import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.Messages import com.intellij.openapi.ui.SimpleToolWindowPanel import com.intellij.ui.JBColor import com.intellij.ui.components.Label import net.miginfocom.swing.MigLayout import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegration.util.DateTimeUtil import net.ntworld.mergeRequestIntegrationIde.ENTERPRISE_EDITION_URL import net.ntworld.mergeRequestIntegrationIde.component.Icons import net.ntworld.mergeRequestIntegrationIde.component.dialog.LegalWarningDialog import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.util.HtmlHelper import java.awt.Color import java.awt.Cursor import java.awt.event.MouseEvent import java.awt.event.MouseListener import javax.swing.BorderFactory import javax.swing.JComponent import javax.swing.JPanel class CommentComponentImpl( private val factory: CommentComponentFactory, private val projectServiceProvider: ProjectServiceProvider, private val groupComponent: GroupComponent, private val providerData: ProviderData, private val mergeRequestInfo: MergeRequestInfo, private val comment: Comment, private val indent: Int, private val options: Options ) : CommentComponent { private var displayMoveToDialog: Boolean = options.showMoveToDialog private val myPanel = SimpleToolWindowPanel(true, false) private val myNameLabel = Label(comment.author.name) private val myUsernameLabel = Label("@${comment.author.username}") private val myNameSeparatorLabel = Label("·") private var myUsePrettyTime: Boolean = true private var isPublishingDraft: Boolean = false private var myEditEditor: EditorComponent? = null private val myWebView = TipUIUtil.createBrowser() as TipUIUtil.Browser private val myHtmlTemplate = CommentComponentImpl::class.java.getResource( "/templates/mr.comment.html" ).readText() private val myEditEditorListener = object: EditorComponent.EventListener { override fun onEditorFocused(editor: EditorComponent) { } override fun onEditorResized(editor: EditorComponent) { } override fun onCancelClicked(editor: EditorComponent) { myWebView.component.isVisible = true myPanel.setContent(myWebView.component) groupComponent.editEditorDestroyed(comment, editor) myEditEditor = null } override fun onSubmitClicked(editor: EditorComponent, isDraft: Boolean) { val text = editor.text.trim() if (text.isNotEmpty() && text !== comment.body) { groupComponent.requestEditComment(comment, text) } } } private val myTimeAction = MyTimeAction(this) private val myDraftStatusAction = MyDraftStatusAction(this) private val myOpenInBrowserAction = MyOpenInBrowserAction(this) private val myReplyAction = MyReplyAction(this) private val myDeleteAction = MyDeleteAction(this) private val myEditAction = MyEditAction(this) private val myMoveToDialogAction = MyMoveToDialogAction(this) private val myLegalWarningAction = MyLegalWarningAction(this) private class MyResolveAction(private val self: CommentComponentImpl) : AnAction() { override fun actionPerformed(e: AnActionEvent) { self.groupComponent.requestToggleResolvedStateOfComment(self.comment) } override fun update(e: AnActionEvent) { super.update(e) if (self.comment.resolved) { e.presentation.icon = Icons.Resolved e.presentation.description = "Unresolve thread" val resolvedBy = self.comment.resolvedBy if (null !== resolvedBy) { e.presentation.text = "Resolved by ${resolvedBy.name}" } } else { e.presentation.icon = Icons.Resolve e.presentation.text = "Resolve thead" e.presentation.description = "Mark thread as resolved" } } } private val myResolveAction = MyResolveAction(this) private val myNameMouseListener = object : MouseListener { override fun mouseReleased(e: MouseEvent?) {} override fun mouseEntered(e: MouseEvent?) {} override fun mousePressed(e: MouseEvent?) {} override fun mouseExited(e: MouseEvent?) {} override fun mouseClicked(e: MouseEvent?) { groupComponent.collapse = !groupComponent.collapse myNameLabel.icon = if (groupComponent.collapse) Icons.CaretRight else Icons.CaretDown } } override val component: JComponent = myPanel init { if (indent == 0 && groupComponent.comments.size > 1) { myNameLabel.icon = if (groupComponent.collapse) Icons.CaretRight else Icons.CaretDown myNameLabel.addMouseListener(myNameMouseListener) myNameLabel.cursor = Cursor.getDefaultCursor() } myUsernameLabel.foreground = Color(153, 153, 153) myWebView.text = buildHtml(providerData, comment) myPanel.toolbar = createToolbar() myPanel.setContent(myWebView.component) myPanel.border = BorderFactory.createMatteBorder( 0, indent * 40 + options.borderLeftRight, 1, options.borderLeftRight, JBColor.border() ) } private fun hideWebViewAndShowEditEditor() { myWebView.component.isVisible = false if (null === this.myEditEditor) { val createdEditor = factory.makeEditor( projectServiceProvider.project, EditorComponent.Type.EDIT, 0, borderLeftRight = 0, showCancelAction = true, isDoingCodeReview = projectServiceProvider.isDoingCodeReview() ) groupComponent.editEditorCreated(comment, createdEditor) createdEditor.addListener(myEditEditorListener) createdEditor.text = comment.body + "\n" myEditEditor = createdEditor } myPanel.setContent(this.myEditEditor!!.component) myEditEditor!!.focus() } override fun hideMoveToDialogButtons() { displayMoveToDialog = false } override fun showMoveToDialogButtons() { displayMoveToDialog = true } private fun createToolbar(): JComponent { val panel = JPanel(MigLayout("ins 0, fill", "5[left]5[left]5[left]0[left, fill]push[right]", "center")) val leftActionGroupTwo = DefaultActionGroup() leftActionGroupTwo.add(myTimeAction) leftActionGroupTwo.add(myDraftStatusAction) val leftToolbarTwo = ActionManager.getInstance().createActionToolbar( "${CommentComponentImpl::class.java.canonicalName}/toolbar-left-two", leftActionGroupTwo, true ) val rightActionGroup = DefaultActionGroup() if (options.showMoveToDialog) { rightActionGroup.add(myMoveToDialogAction) rightActionGroup.addSeparator() } if (providerData.currentUser.id == comment.author.id) { rightActionGroup.add(myEditAction) rightActionGroup.add(myDeleteAction) if (!comment.isDraft) { rightActionGroup.addSeparator() } } if (!comment.isDraft) { rightActionGroup.add(myOpenInBrowserAction) rightActionGroup.addSeparator() if (comment.resolvable) { rightActionGroup.add(myResolveAction) } rightActionGroup.add(myReplyAction) } if (!projectServiceProvider.applicationServiceProvider.isLegal(providerData)) { rightActionGroup.addSeparator() rightActionGroup.add(myLegalWarningAction) } val rightToolbar = ActionManager.getInstance().createActionToolbar( "${CommentComponentImpl::class.java.canonicalName}/toolbar-right", rightActionGroup, true ) panel.add(myNameLabel) panel.add(myUsernameLabel) panel.add(myNameSeparatorLabel) panel.add(leftToolbarTwo.component) panel.add(rightToolbar.component) return panel } private fun buildHtml(providerData: ProviderData, comment: Comment): String { val output = myHtmlTemplate .replace("{{content}}", HtmlHelper.convertFromMarkdown(comment.body)) return HtmlHelper.resolveRelativePath(providerData, output) } private class MyMoveToDialogAction(private val self: CommentComponentImpl): AnAction( "Open in a Dialog", "", AllIcons.Actions.MoveToWindow ) { override fun actionPerformed(e: AnActionEvent) { self.groupComponent.requestOpenDialog() } override fun update(e: AnActionEvent) { super.update(e) e.presentation.isVisible = self.displayMoveToDialog } } private class MyTimeAction(private val self: CommentComponentImpl) : AnAction(null, null, null) { override fun actionPerformed(e: AnActionEvent) { self.myUsePrettyTime = !self.myUsePrettyTime } override fun update(e: AnActionEvent) { e.presentation.text = if (self.myUsePrettyTime) { DateTimeUtil.toPretty(DateTimeUtil.toDate(self.comment.updatedAt)) } else { DateTimeUtil.formatDate(DateTimeUtil.toDate(self.comment.updatedAt)) } e.presentation.isVisible = !self.comment.isDraft } override fun useSmallerFontForTextInToolbar(): Boolean = false override fun displayTextInToolbar() = true } private class MyDraftStatusAction(private val self: CommentComponentImpl) : AnAction( "Draft, click to publish this comment", "This is a draft comment, click to publish", null ) { override fun actionPerformed(e: AnActionEvent) { if (self.isPublishingDraft) { return } self.isPublishingDraft = true self.groupComponent.publishDraftComment(self.comment) } override fun update(e: AnActionEvent) { e.presentation.isVisible = self.comment.isDraft if (self.comment.isDraft && self.isPublishingDraft) { e.presentation.text = "Publishing..." } else { e.presentation.text = "Draft, click to publish this comment" } } override fun useSmallerFontForTextInToolbar(): Boolean = false override fun displayTextInToolbar() = true } private class MyOpenInBrowserAction(private val self: CommentComponentImpl): AnAction( "View in browser", "Open and view the comment in browser", Icons.ExternalLink ) { override fun actionPerformed(e: AnActionEvent) { BrowserUtil.open(self.providerData.info.createCommentUrl(self.mergeRequestInfo.url, self.comment)) } } private class MyReplyAction(private val self: CommentComponentImpl): AnAction( "Reply", "Reply this comment", Icons.ReplyComment ) { override fun actionPerformed(e: AnActionEvent) { self.groupComponent.showReplyEditor() if (self.options.openEditorInDialog) { self.groupComponent.requestOpenDialog() } } } private class MyDeleteAction(private val self: CommentComponentImpl): AnAction( "Delete comment", "Delete comment", Icons.Trash ) { override fun actionPerformed(e: AnActionEvent) { val result = Messages.showYesNoDialog( "Do you want to delete the comment?", "Are you sure", Messages.getQuestionIcon() ) if (result == Messages.YES) { self.groupComponent.requestDeleteComment(self.comment) } } } private class MyEditAction(private val self: CommentComponentImpl): AnAction( "Edit comment", "Edit comment", AllIcons.Actions.Edit ) { override fun actionPerformed(e: AnActionEvent) { self.hideWebViewAndShowEditEditor() if (self.options.openEditorInDialog) { self.groupComponent.requestOpenDialog() } } override fun update(e: AnActionEvent) { e.presentation.isEnabled = null === self.myEditEditor } } private class MyLegalWarningAction(private val self: CommentComponentImpl): AnAction( "Illegal", "You cannot use CE for private repositories, please buy Enterprise Edition, only 1\$/month.", Icons.LegalWarning ) { override fun actionPerformed(e: AnActionEvent) { val builder = DialogBuilder() builder.title("Please buy Enterprise Edition") builder.setCenterPanel(LegalWarningDialog().component) builder.addOkAction() builder.okAction.setText("Buy Enterprise Edition") val code = builder.show() // This line run after the dialog is closed if (code == DialogWrapper.OK_EXIT_CODE) { BrowserUtil.open(ENTERPRISE_EDITION_URL) } } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/CommentEvent.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.comment import net.ntworld.mergeRequest.Comment import java.util.* interface CommentEvent: EventListener { fun onDeleteCommentRequested(comment: Comment) fun onResolveCommentRequested(comment: Comment) fun onUnresolveCommentRequested(comment: Comment) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/CommentEventPropagator.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.comment import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.Comment class CommentEventPropagator( private val dispatcher: EventDispatcher ) : CommentEvent { override fun onDeleteCommentRequested(comment: Comment) { dispatcher.multicaster.onDeleteCommentRequested(comment) } override fun onResolveCommentRequested(comment: Comment) { dispatcher.multicaster.onResolveCommentRequested(comment) } override fun onUnresolveCommentRequested(comment: Comment) { dispatcher.multicaster.onUnresolveCommentRequested(comment) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/EditorComponent.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.comment import com.intellij.openapi.Disposable import net.ntworld.mergeRequestIntegrationIde.Component interface EditorComponent : Component, Disposable { var text: String var addCommentNowButtonText: String var addCommentNowButtonDesc: String var startAReviewButtonText: String var startAReviewButtonDesc: String var isVisible: Boolean fun focus() fun drawBorderTop(display: Boolean) fun addListener(listener: EventListener) interface EventListener: java.util.EventListener { fun onEditorFocused(editor: EditorComponent) fun onEditorResized(editor: EditorComponent) fun onCancelClicked(editor: EditorComponent) fun onSubmitClicked(editor: EditorComponent, isDraft: Boolean) } enum class Type { NEW_DISCUSSION, EDIT, REPLY } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/EditorComponentImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.comment import com.intellij.openapi.actionSystem.* import com.intellij.openapi.editor.impl.DocumentImpl import com.intellij.openapi.project.Project as IdeaProject import com.intellij.ui.EditorSettingsProvider import com.intellij.ui.EditorTextField import com.intellij.ui.JBColor import com.intellij.util.EventDispatcher import net.miginfocom.swing.MigLayout import net.ntworld.mergeRequestIntegrationIde.ui.util.CustomSimpleToolWindowPanel import net.ntworld.mergeRequestIntegrationIde.util.FileTypeUtil import java.awt.event.ComponentAdapter import java.awt.event.ComponentEvent import java.awt.event.FocusEvent import java.awt.event.FocusListener import javax.swing.BorderFactory import javax.swing.JComponent import javax.swing.JPanel class EditorComponentImpl( private val ideaProject: IdeaProject, private val type: EditorComponent.Type, val indent: Int, private val borderLeftRight: Int = 1, private val showCancelAction: Boolean = true, private val isDoingCodeReview: Boolean = false ) : EditorComponent { private val dispatcher = EventDispatcher.create(EditorComponent.EventListener::class.java) private val myPanel = CustomSimpleToolWindowPanel(vertical = true, borderless = false) private val myDocument = DocumentImpl("") private val myEditorSettingsProvider = EditorSettingsProvider { editor -> if (null !== editor) { editor.settings.isLineNumbersShown = true editor.settings.isFoldingOutlineShown = true editor.settings.isUseSoftWraps = true editor.setHorizontalScrollbarVisible(false) editor.setVerticalScrollbarVisible(true) } } private val myEditorTextField by lazy { val textField = EditorTextField(myDocument, ideaProject, FileTypeUtil.markdownFileType) textField.setOneLineMode(false) textField.addSettingsProvider(myEditorSettingsProvider) textField } private val myComponentListener = object: ComponentAdapter() { override fun componentResized(e: ComponentEvent?) { dispatcher.multicaster.onEditorResized(this@EditorComponentImpl) } } private class MyCancelAction(private val self: EditorComponentImpl) : AnAction( "Cancel", "Cancel and delete this comment", null ) { override fun actionPerformed(e: AnActionEvent) { self.dispatcher.multicaster.onCancelClicked(self) } override fun displayTextInToolbar() = true } private val myCancelAction = MyCancelAction(this) private class MyAddCommentNowAction(private val self: EditorComponentImpl) : AnAction( "Add comment now", "Add comment to the current position", null ) { override fun actionPerformed(e: AnActionEvent) { if (self.myEditorTextField.text.trim().isNotBlank()) { self.dispatcher.multicaster.onSubmitClicked(self, false) } } override fun update(e: AnActionEvent) { when (self.type) { EditorComponent.Type.NEW_DISCUSSION -> { e.presentation.text = self.addCommentNowButtonText e.presentation.description = self.addCommentNowButtonDesc } EditorComponent.Type.REPLY -> { e.presentation.text = "Reply" e.presentation.description = "Reply to current discussion thread" } EditorComponent.Type.EDIT -> { e.presentation.text = "Save" e.presentation.description = "Update current comment" } } } override fun useSmallerFontForTextInToolbar(): Boolean = false override fun displayTextInToolbar() = true } private class MyStartAReviewAction(private val self: EditorComponentImpl) : AnAction( "Save as Draft", "Save a comment for now then publish (all) comments later", null ) { override fun actionPerformed(e: AnActionEvent) { if (self.myEditorTextField.text.trim().isNotBlank()) { self.dispatcher.multicaster.onSubmitClicked(self, true) } } override fun update(e: AnActionEvent) { when (self.type) { EditorComponent.Type.NEW_DISCUSSION -> { e.presentation.text = self.startAReviewButtonText e.presentation.description = self.startAReviewButtonDesc e.presentation.isVisible = true } EditorComponent.Type.REPLY, EditorComponent.Type.EDIT -> { e.presentation.isVisible = false } } if (self.isDoingCodeReview) { e.presentation.text = "Start a review" } else { e.presentation.text = "Save as Draft" } } override fun useSmallerFontForTextInToolbar(): Boolean = false override fun displayTextInToolbar() = true } private val myAddCommentNowAction = MyAddCommentNowAction(this) private val myStartAReviewAction = MyStartAReviewAction(this) private val myEditorFocusListener = object: FocusListener { override fun focusLost(e: FocusEvent?) { val editor = myEditorTextField.editor if (null === editor) { return } if (myEditorTextField.text.isBlank() || myEditorTextField.text == EMPTY_TEXT_REPLACED) { myEditorTextField.text = "" myPanel.toolbar!!.isVisible = false dispatcher.multicaster.onEditorResized(this@EditorComponentImpl) } } override fun focusGained(e: FocusEvent?) { if (myEditorTextField.text.isBlank()) { myEditorTextField.text = EMPTY_TEXT_REPLACED } myPanel.toolbar!!.isVisible = true dispatcher.multicaster.onEditorResized(this@EditorComponentImpl) dispatcher.multicaster.onEditorFocused(this@EditorComponentImpl) } } init { when(type) { EditorComponent.Type.NEW_DISCUSSION -> myEditorTextField.setPlaceholder("Start a new discussion...") EditorComponent.Type.REPLY -> myEditorTextField.setPlaceholder("Reply...") } myPanel.setContent(myEditorTextField) myPanel.toolbar = createToolbar() myPanel.toolbar!!.isVisible = false drawBorderTop(false) myEditorTextField.addComponentListener(myComponentListener) myEditorTextField.addFocusListener(myEditorFocusListener) } override val component: JComponent = myPanel override var text: String get() = myEditorTextField.text set(value) { myEditorTextField.text = value } override var addCommentNowButtonText: String = "Add comment now" override var addCommentNowButtonDesc: String = "Add comment to the current position" override var startAReviewButtonText: String = "Start a review" override var startAReviewButtonDesc: String = "Save a comment for now then publish all comments later" override var isVisible: Boolean get() = myPanel.isVisible set(value) { myPanel.isVisible = value } override fun focus() { myEditorTextField.grabFocus() } override fun drawBorderTop(display: Boolean) { myPanel.border = BorderFactory.createMatteBorder( if (display) 1 else 0, indent * 40 + borderLeftRight, 1, borderLeftRight, JBColor.border() ) } override fun addListener(listener: EditorComponent.EventListener) = dispatcher.addListener(listener) override fun dispose() { dispatcher.listeners.clear() } private fun createToolbar(): JComponent { val panel = JPanel(MigLayout("ins 0, fill", "15[left]push[right]5", "center")) val leftActionGroup = DefaultActionGroup() leftActionGroup.add(myStartAReviewAction) leftActionGroup.addSeparator() leftActionGroup.add(myAddCommentNowAction) if (showCancelAction) { leftActionGroup.addSeparator() leftActionGroup.add(myCancelAction) } val leftToolbar = ActionManager.getInstance().createActionToolbar( "${CommentComponentImpl::class.java.canonicalName}/toolbar-left", leftActionGroup, true ) val rightActionGroup = DefaultActionGroup() val rightToolbar = ActionManager.getInstance().createActionToolbar( "${CommentComponentImpl::class.java.canonicalName}/toolbar-left", rightActionGroup, true ) panel.add(leftToolbar.component) panel.add(rightToolbar.component) return panel } companion object { const val EMPTY_TEXT_REPLACED = "\n" } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/GroupComponent.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.comment import com.intellij.openapi.Disposable import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequestIntegrationIde.Component interface GroupComponent : Component, Disposable { val id: String var comments: List var collapse: Boolean fun requestOpenDialog() fun requestDeleteComment(comment: Comment) fun requestEditComment(comment: Comment, content: String) fun requestToggleResolvedStateOfComment(comment: Comment) fun resetReplyEditor() fun showReplyEditor() fun destroyReplyEditor() fun editEditorCreated(comment: Comment, editor: EditorComponent) fun editEditorDestroyed(comment: Comment, editor: EditorComponent) fun publishDraftComment(comment: Comment) fun addListener(listener: EventListener) fun hideMoveToDialogButtons() fun showMoveToDialogButtons() interface EventListener : java.util.EventListener, CommentEvent { fun onResized() fun onOpenDialogClicked() fun onEditorCreated(groupId: String, editor: EditorComponent) fun onEditorDestroyed(groupId: String, editor: EditorComponent) fun onReplyCommentRequested(comment: Comment, content: String) fun onEditCommentRequested(comment: Comment, content: String) fun onPublishDraftCommentRequested(comment: Comment) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/GroupComponentImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.comment import com.intellij.openapi.ui.Messages import com.intellij.openapi.project.Project as IdeaProject import com.intellij.ui.JBColor import com.intellij.ui.components.Panel import com.intellij.util.EventDispatcher import com.intellij.util.ui.JBUI import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import javax.swing.BorderFactory import javax.swing.BoxLayout import javax.swing.JComponent class GroupComponentImpl( private val factory: CommentComponentFactory, private val borderTop: Boolean, private val providerData: ProviderData, private val mergeRequestInfo: MergeRequestInfo, private val project: IdeaProject, override val id: String, comments: List, private val options: Options ) : GroupComponent { private val dispatcher = EventDispatcher.create(GroupComponent.EventListener::class.java) private val myBoxLayoutPanel = JBUI.Panels.simplePanel() private val myPanel = Panel() private val myCommentComponents = mutableListOf() private var myEditor: EditorComponent? = null private var myEditorEventListener = object : EditorComponent.EventListener { override fun onEditorFocused(editor: EditorComponent) { } override fun onEditorResized(editor: EditorComponent) { dispatcher.multicaster.onResized() } override fun onCancelClicked(editor: EditorComponent) { val shouldDestroy = if (editor.text.isNotBlank()) { val result = Messages.showYesNoDialog( "Do you want to delete the whole content?", "Are you sure", Messages.getQuestionIcon() ) result == Messages.YES } else true if (shouldDestroy) { destroyReplyEditor() dispatcher.multicaster.onResized() } else { editor.focus() } } override fun onSubmitClicked(editor: EditorComponent, isDraft: Boolean) { dispatcher.multicaster.onReplyCommentRequested(comments.first(), editor.text) } } override var comments: List = comments set(value) { if (updateComments(field, value)) { field = value } } init { myPanel.layout = BoxLayout(myPanel, BoxLayout.Y_AXIS) if (borderTop) { myPanel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, JBColor.border()) } myBoxLayoutPanel.addToCenter(myPanel) rerenderComments(comments) } override var collapse: Boolean = false set(value) { field = value if (value) { myPanel.components.forEachIndexed { index, component -> if (index != 0) { component.isVisible = false } } val editor = myEditor if (null !== editor) { editor.isVisible = false } } else { myPanel.components.forEachIndexed { index, component -> if (index != 0) { component.isVisible = true } } val editor = myEditor if (null !== editor) { editor.isVisible = true } } dispatcher.multicaster.onResized() } override fun requestOpenDialog() { dispatcher.multicaster.onOpenDialogClicked() } override fun requestDeleteComment(comment: Comment) { dispatcher.multicaster.onDeleteCommentRequested(comment) } override fun requestEditComment(comment: Comment, content: String) { dispatcher.multicaster.onEditCommentRequested(comment, content) } override fun requestToggleResolvedStateOfComment(comment: Comment) { if (comment.resolved) { dispatcher.multicaster.onUnresolveCommentRequested(comment) } else { dispatcher.multicaster.onResolveCommentRequested(comment) } } override val component: JComponent = myBoxLayoutPanel override fun resetReplyEditor() { destroyReplyEditor() dispatcher.multicaster.onResized() } override fun showReplyEditor() { val editor = myEditor if (null === editor) { val createdEditor = factory.makeEditor( project, EditorComponent.Type.REPLY, 1, options.borderLeftRight, showCancelAction = true, isDoingCodeReview = false ) dispatcher.multicaster.onEditorCreated(this.id, createdEditor) // createdEditor.drawBorderTop(true) createdEditor.addListener(myEditorEventListener) myBoxLayoutPanel.addToBottom(createdEditor.component) createdEditor.focus() myEditor = createdEditor } else { editor.focus() } } override fun destroyReplyEditor() { val editor = myEditor if (null !== editor) { editor.dispose() myBoxLayoutPanel.remove(editor.component) dispatcher.multicaster.onEditorDestroyed(this.id, editor) myEditor = null } } override fun editEditorCreated(comment: Comment, editor: EditorComponent) { dispatcher.multicaster.onEditorCreated(this.id, editor) } override fun editEditorDestroyed(comment: Comment, editor: EditorComponent) { editor.dispose() dispatcher.multicaster.onEditorDestroyed(this.id, editor) dispatcher.multicaster.onResized() } override fun publishDraftComment(comment: Comment) { dispatcher.multicaster.onPublishDraftCommentRequested(comment) } override fun addListener(listener: GroupComponent.EventListener) = dispatcher.addListener(listener) override fun hideMoveToDialogButtons() { myCommentComponents.forEach { it.hideMoveToDialogButtons() } } override fun showMoveToDialogButtons() { myCommentComponents.forEach { it.showMoveToDialogButtons() } } override fun dispose() { dispatcher.listeners.clear() } private fun updateComments(old: List, new: List): Boolean { rerenderComments(new) return true // TODO: Find the way to not update comments if it doesn't change // if (old.size != new.size) { // rerenderComments(new) // return true // } // for (i in 0 until old.lastIndex) { // val oldItem = old[i] // val newItem = new[i] // if (oldItem != newItem) { // rerenderComments(new) // println("Change, rerender") // return true // } // } // println("Not change, do nothing") // return false } private fun rerenderComments(items: List) { myPanel.removeAll() myCommentComponents.clear() items.forEachIndexed { index, comment -> val commentComponent = factory.makeComment( this, providerData, mergeRequestInfo, comment, if (index == 0) 0 else 1, options ) myPanel.add(commentComponent.component) myCommentComponents.add(commentComponent) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/Options.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.comment data class Options( val borderLeftRight: Int = 1, val showMoveToDialog: Boolean = true, val openEditorInDialog: Boolean = false ) ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/dialog/LegalWarningDialog.form ================================================
================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/dialog/LegalWarningDialog.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.dialog; import net.ntworld.mergeRequestIntegrationIde.Component import javax.swing.*; class LegalWarningDialog: Component { var myWrapper: JPanel? = null var myLegalWarning: JLabel? = null var myWords1: JLabel? = null var myWords2: JLabel? = null var myWords3: JLabel? = null override val component: JComponent = myWrapper!! } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterActionType.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.gutter enum class GutterActionType { ADD, TOGGLE } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterIconRenderer.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.gutter import com.intellij.diff.util.Side interface GutterIconRenderer { val visibleLineLeft: Int? val visibleLineRight: Int? val logicalLine: Int val side: Side fun setState(state: GutterState) fun triggerAddAction() fun triggerToggleAction() } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterIconRendererActionListener.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.gutter interface GutterIconRendererActionListener { fun performGutterIconRendererAction(gutterIconRenderer: GutterIconRenderer, type: GutterActionType) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterIconRendererFactory.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.gutter import com.intellij.diff.util.Side import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.markup.RangeHighlighter object GutterIconRendererFactory { fun makeGutterIconRenderer( highlighter: RangeHighlighter, showAddIcon: Boolean, logicalLine: Int, visibleLineLeft: Int?, visibleLineRight: Int?, side: Side, actionListener: GutterIconRendererActionListener ): GutterIconRenderer { val gutterIconRenderer = GutterIconRendererImpl( showAddIcon, visibleLineLeft, visibleLineRight, logicalLine, side, actionListener ) highlighter.gutterIconRenderer = gutterIconRenderer return gutterIconRenderer } fun findGutterIconRenderer(editor: Editor): GutterIconRenderer? { val logicalLine = editor.caretModel.logicalPosition.line for (highlighter in editor.markupModel.allHighlighters) { val gutterRenderer = highlighter.gutterIconRenderer if (gutterRenderer !is GutterIconRenderer || gutterRenderer.logicalLine != logicalLine) { continue } return gutterRenderer } return null } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterIconRendererImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.gutter import com.intellij.diff.util.Side import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import net.ntworld.mergeRequestIntegrationIde.component.Icons import com.intellij.openapi.editor.markup.GutterIconRenderer as GutterIconRendererClass class GutterIconRendererImpl( private val showAddIcon: Boolean, override val visibleLineLeft: Int?, override val visibleLineRight: Int?, override val logicalLine: Int, override val side: Side, private val actionListener: GutterIconRendererActionListener ) : GutterIconRenderer, GutterIconRendererClass() { private var icon = if (showAddIcon) Icons.Gutter.AddComment else Icons.Gutter.Empty private var desc = "" private class MyClickAction(private val self: GutterIconRendererImpl) : AnAction() { override fun actionPerformed(e: AnActionEvent) { if (self.icon == Icons.Gutter.AddComment || self.icon == Icons.Gutter.Empty) { self.actionListener.performGutterIconRendererAction(self, GutterActionType.ADD) } else { self.actionListener.performGutterIconRendererAction(self, GutterActionType.TOGGLE) } } } private val clickAction = MyClickAction(this) override fun setState(state: GutterState) { when (state) { GutterState.NO_COMMENT -> { icon = if (showAddIcon) Icons.Gutter.AddComment else Icons.Gutter.Empty desc = if (showAddIcon) "Add new comment" else "" } GutterState.THREAD_HAS_SINGLE_COMMENT -> { icon = Icons.Gutter.Comment desc = "Toggle comment thread" } GutterState.THREAD_HAS_MULTI_COMMENTS -> { icon = Icons.Gutter.Comments desc = "Toggle comment thread" } GutterState.WRITING -> { icon = Icons.Gutter.WritingComment desc = "Toggle comment thread & continue writing your comment" } GutterState.HAS_DRAFT -> { icon = Icons.Gutter.HasDraft desc = "Toggle comment thread to see draft comment" } } } override fun triggerAddAction() { actionListener.performGutterIconRendererAction(this, GutterActionType.ADD) } override fun triggerToggleAction() { actionListener.performGutterIconRendererAction(this, GutterActionType.TOGGLE) } override fun getClickAction(): AnAction = clickAction override fun getIcon() = icon override fun getTooltipText(): String? = desc override fun isNavigateAction() = icon != Icons.Gutter.Empty override fun hashCode(): Int = System.identityHashCode(this) override fun equals(other: Any?): Boolean = other == this } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterPosition.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.gutter import net.ntworld.mergeRequestIntegrationIde.diff.DiffView data class GutterPosition( val editorType: DiffView.EditorType, val changeType: DiffView.ChangeType, val oldLine: Int?, val newLine: Int?, val oldPath: String?, val newPath: String?, val baseHash: String? = null, val startHash: String? = null, val headHash: String? = null ) ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterState.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.gutter enum class GutterState { NO_COMMENT, THREAD_HAS_SINGLE_COMMENT, THREAD_HAS_MULTI_COMMENTS, WRITING, HAS_DRAFT } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadFactory.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.thread import com.intellij.diff.util.Side import com.intellij.openapi.editor.ex.EditorEx import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider object ThreadFactory { fun makeModel(comments: List): ThreadModel { return ThreadModelImpl(comments, false) } fun makeView( projectServiceProvider: ProjectServiceProvider, editor: EditorEx, providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, logicalLine: Int, side: Side, position: GutterPosition, replyInDialog: Boolean = false ): ThreadView { return ThreadViewImpl( projectServiceProvider, editor, providerData, mergeRequestInfo, logicalLine, side, position, replyInDialog ) } fun makePresenter(model: ThreadModel, view: ThreadView): ThreadPresenter { return ThreadPresenterImpl(model, view) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadModel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.thread import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequestIntegrationIde.Model import java.util.* interface ThreadModel : Model { var comments: List var visible: Boolean var showEditor: Boolean fun resetEditor(comment: Comment?) interface DataListener : EventListener { fun onCommentsChanged(comments: List) fun onVisibilityChanged(visibility: Boolean) fun onEditorVisibilityChanged(visibility: Boolean) fun onEditorReset(comment: Comment?) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadModelImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.thread import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequestIntegrationIde.AbstractModel class ThreadModelImpl( comments: List, visibility: Boolean ) : AbstractModel(), ThreadModel { override val dispatcher = EventDispatcher.create(ThreadModel.DataListener::class.java) override var comments: List = comments set(value) { field = value dispatcher.multicaster.onCommentsChanged(value) } override var visible: Boolean = visibility set(value) { if (field == value) { return } field = value dispatcher.multicaster.onVisibilityChanged(value) } override var showEditor: Boolean = false set(value) { field = value dispatcher.multicaster.onEditorVisibilityChanged(value) } override fun resetEditor(comment: Comment?) { dispatcher.multicaster.onEditorReset(comment) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadPresenter.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.thread import com.intellij.diff.util.Side import com.intellij.openapi.Disposable import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequestIntegrationIde.Presenter import net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEvent import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition interface ThreadPresenter : Presenter, Disposable { val model: ThreadModel val view: ThreadView interface EventListener: CommentEvent { fun onMainEditorClosed(threadPresenter: ThreadPresenter) fun onEditCommentRequested(comment: Comment, content: String) fun onReplyCommentRequested(content: String, repliedComment: Comment, logicalLine: Int, side: Side) fun onCreateCommentRequested(content: String, position: GutterPosition, logicalLine: Int, side: Side, isDraft: Boolean) fun onPublishDraftCommentRequested(comment: Comment) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadPresenterImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.thread import com.intellij.diff.util.Side import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequestIntegrationIde.AbstractPresenter import net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEvent import net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEventPropagator import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition import net.ntworld.mergeRequestIntegrationIde.util.CommentUtil class ThreadPresenterImpl( override val model: ThreadModel, override val view: ThreadView ) : AbstractPresenter(), ThreadPresenter, ThreadModel.DataListener { override val dispatcher = EventDispatcher.create(ThreadPresenter.EventListener::class.java) private val myCommentEventPropagator = CommentEventPropagator(dispatcher) private val myThreadViewActionListener = object : ThreadView.ActionListener, CommentEvent by myCommentEventPropagator { override fun onMainEditorClosed() { dispatcher.multicaster.onMainEditorClosed(this@ThreadPresenterImpl) } override fun onEditCommentRequested(comment: Comment, content: String) { dispatcher.multicaster.onEditCommentRequested(comment, content) } override fun onPublishDraftCommentRequested(comment: Comment) { dispatcher.multicaster.onPublishDraftCommentRequested(comment) } override fun onCreateCommentRequested( content: String, logicalLine: Int, side: Side, repliedComment: Comment?, position: GutterPosition?, isDraft: Boolean ) { if (null !== repliedComment) { dispatcher.multicaster.onReplyCommentRequested(content, repliedComment, logicalLine, side) } if (null !== position) { dispatcher.multicaster.onCreateCommentRequested(content, position, logicalLine, side, isDraft) } } } init { model.addDataListener(this) view.addActionListener(myThreadViewActionListener) view.initialize() onCommentsChanged(model.comments) } override fun dispose() { view.dispose() } override fun onCommentsChanged(comments: List) { val groups = CommentUtil.groupCommentsByThreadId(model.comments) val currentGroups = view.getAllGroupOfCommentsIds().toMutableSet() groups.forEach { (id, items) -> if (view.hasGroupOfComments(id)) { view.updateGroupOfComments(id, items) } else { view.addGroupOfComments(id, items) } currentGroups.remove(id) } currentGroups.forEach { id -> view.deleteGroupOfComments(id) } if (model.visible) { view.show() } else { view.hide() } } override fun onVisibilityChanged(visibility: Boolean) { if (model.visible) { view.show() if (model.showEditor) { view.showEditor() } } else { view.hide() } } override fun onEditorVisibilityChanged(visibility: Boolean) { if (visibility) { view.show() view.showEditor() } } override fun onEditorReset(comment: Comment?) { if (null === comment) { model.showEditor = false view.resetMainEditor() } else { view.resetEditorOfGroup(comment.parentId) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadView.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.thread import com.intellij.diff.util.Side import com.intellij.openapi.Disposable import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequestIntegrationIde.View import net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEvent import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition interface ThreadView : View, Disposable { val logicalLine: Int val side: Side val position: GutterPosition fun initialize() fun getAllGroupOfCommentsIds(): Set fun hasGroupOfComments(groupId: String): Boolean fun addGroupOfComments(groupId: String, comments: List) fun updateGroupOfComments(groupId: String, comments: List) fun deleteGroupOfComments(groupId: String) fun resetMainEditor() fun resetEditorOfGroup(groupId: String) fun showEditor() fun show() fun hide() interface ActionListener : CommentEvent { fun onMainEditorClosed() fun onEditCommentRequested(comment: Comment, content: String) fun onPublishDraftCommentRequested(comment: Comment) fun onCreateCommentRequested( content: String, logicalLine: Int, side: Side, repliedComment: Comment?, position: GutterPosition?, isDraft: Boolean ) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadViewImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.component.thread import com.intellij.diff.util.Side import com.intellij.openapi.Disposable import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.editor.impl.EditorEmbeddedComponentManager import com.intellij.openapi.editor.impl.EditorImpl import com.intellij.openapi.editor.impl.view.FontLayoutService import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.Disposer import com.intellij.ui.components.JBScrollPane import com.intellij.ui.components.Panel import com.intellij.util.EventDispatcher import com.intellij.util.ui.JBUI import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.AbstractView import net.ntworld.mergeRequestIntegrationIde.component.comment.* import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import java.awt.Cursor import java.awt.Dimension import java.awt.Font import java.awt.GridBagLayout import java.awt.event.ComponentAdapter import java.awt.event.ComponentEvent import javax.swing.BoxLayout import javax.swing.JComponent import javax.swing.JPanel import javax.swing.ScrollPaneConstants import kotlin.math.ceil import kotlin.math.max import kotlin.math.min class ThreadViewImpl( private val projectServiceProvider: ProjectServiceProvider, private val editor: EditorEx, private val providerData: ProviderData, private val mergeRequestInfo: MergeRequestInfo, override val logicalLine: Int, override val side: Side, override val position: GutterPosition, private val replyInDialog: Boolean ) : AbstractView(), ThreadView { override val dispatcher = EventDispatcher.create(ThreadView.ActionListener::class.java) private val myThreadPanel = Panel() private val myBoxLayoutPanel = JBUI.Panels.simplePanel() private val myComponent = MyComponent(myBoxLayoutPanel) private val myWrapper = JPanel() private val myEditorWidthWatcher = EditorTextWidthWatcher() private val myCreatedEditors = mutableMapOf() private val myGroups = mutableMapOf() private val myEditor by lazy { val editorComponent = projectServiceProvider.componentFactory.commentComponents.makeEditor( editor.project!!, EditorComponent.Type.NEW_DISCUSSION, 0, borderLeftRight = 1, showCancelAction = true, isDoingCodeReview = projectServiceProvider.isDoingCodeReview() ) editorComponent.isVisible = false editorComponent.addListener(myEditorComponentEventListener) Disposer.register(this@ThreadViewImpl, editorComponent) myCreatedEditors[""] = editorComponent myBoxLayoutPanel.addToBottom(editorComponent.component) editorComponent } private val myEditorComponentEventListener = object : EditorComponent.EventListener { override fun onEditorFocused(editor: EditorComponent) {} override fun onEditorResized(editor: EditorComponent) { myEditorWidthWatcher.updateWidthForAllInlays() } override fun onCancelClicked(editor: EditorComponent) { val mainEditor = myCreatedEditors[""] if (null !== mainEditor && editor === mainEditor) { val shouldHide = if (mainEditor.text.isNotBlank()) { val result = Messages.showYesNoDialog( "Do you want to delete the whole content?", "Are you sure", Messages.getQuestionIcon() ) result == Messages.YES } else true if (shouldHide) { mainEditor.text = "" mainEditor.isVisible = false myEditorWidthWatcher.updateWidthForAllInlays() dispatcher.multicaster.onMainEditorClosed() } else { mainEditor.focus() } } } override fun onSubmitClicked(editor: EditorComponent, isDraft: Boolean) { val mainEditor = myCreatedEditors[""] if (null !== mainEditor && editor === mainEditor) { dispatcher.multicaster.onCreateCommentRequested( mainEditor.text, logicalLine, side, repliedComment = null, position = position, isDraft = isDraft ) } } } private val myCommentEventPropagator = CommentEventPropagator(dispatcher) private val myGroupComponentEventListener = object : GroupComponent.EventListener, CommentEvent by myCommentEventPropagator { override fun onResized() { myEditorWidthWatcher.updateWidthForAllInlays() } override fun onOpenDialogClicked() { val builder = DialogBuilder() builder.setCenterPanel(myComponent) myEditorWidthWatcher.updateWidthForAllInlays() myGroups.forEach { it.value.hideMoveToDialogButtons() } builder.removeAllActions() builder.addOkAction() builder.resizable(true) builder.show() // This line run after the dialog is closed myGroups.forEach { it.value.showMoveToDialogButtons() } myWrapper.add(myComponent) } override fun onEditorCreated(groupId: String, editor: EditorComponent) { editor.addListener(myEditorComponentEventListener) Disposer.register(this@ThreadViewImpl, editor) myCreatedEditors[groupId] = editor } override fun onEditorDestroyed(groupId: String, editor: EditorComponent) { myCreatedEditors.remove(groupId) } override fun onReplyCommentRequested(comment: Comment, content: String) { dispatcher.multicaster.onCreateCommentRequested( content, logicalLine, side, repliedComment = comment, position = null, isDraft = false ) } override fun onEditCommentRequested(comment: Comment, content: String) { dispatcher.multicaster.onEditCommentRequested(comment, content) } override fun onPublishDraftCommentRequested(comment: Comment) { dispatcher.multicaster.onPublishDraftCommentRequested(comment) } } init { myBoxLayoutPanel.addToCenter(myThreadPanel) myThreadPanel.layout = BoxLayout(myThreadPanel, BoxLayout.Y_AXIS) myComponent.isVisible = false myComponent.cursor = Cursor.getDefaultCursor() myWrapper.layout = GridBagLayout() myWrapper.add(myComponent) } override fun dispose() { editor.scrollPane.viewport.removeComponentListener(myEditorWidthWatcher) myCreatedEditors.values.forEach { it.dispose() } } override fun initialize() { editor.scrollPane.viewport.addComponentListener(myEditorWidthWatcher) Disposer.register(this, Disposable { editor.scrollPane.viewport.removeComponentListener(myEditorWidthWatcher) }) val editorEmbeddedComponentManager = EditorEmbeddedComponentManager.getInstance() val offset = editor.document.getLineEndOffset(logicalLine) editorEmbeddedComponentManager.addComponent( editor, myWrapper, projectServiceProvider.intellijIdeApi.makeEditorEmbeddedComponentManagerProperties(offset) ) EditorUtil.disposeWithEditor(editor, this) } override fun getAllGroupOfCommentsIds(): Set { return myGroups.keys.toSet() } override fun hasGroupOfComments(groupId: String): Boolean = myGroups.containsKey(groupId) override fun addGroupOfComments(groupId: String, comments: List) { val group = projectServiceProvider.componentFactory.commentComponents.makeGroup( providerData, mergeRequestInfo, editor.project!!, myGroups.isEmpty(), groupId, comments, Options(borderLeftRight = 1, showMoveToDialog = true, openEditorInDialog = replyInDialog) ) group.addListener(myGroupComponentEventListener) Disposer.register(this, group) myGroups[groupId] = group myThreadPanel.add(group.component) } override fun updateGroupOfComments(groupId: String, comments: List) { val group = myGroups[groupId] if (null !== group) { group.comments = comments } } override fun deleteGroupOfComments(groupId: String) { val group = myGroups[groupId] if (null !== group) { myThreadPanel.remove(group.component) group.dispose() myGroups.remove(groupId) } } override fun resetMainEditor() { myEditor.text = "" myEditor.isVisible = false myEditorWidthWatcher.updateWidthForAllInlays() } override fun resetEditorOfGroup(groupId: String) { val group = myGroups[groupId] if (null !== group) { group.resetReplyEditor() } } override fun showEditor() { myEditor.isVisible = true myEditor.focus() myEditor.drawBorderTop(myGroups.isEmpty()) myEditorWidthWatcher.updateWidthForAllInlays() } override fun show() { myComponent.isVisible = true myEditorWidthWatcher.updateWidthForAllInlays() } override fun hide() { myComponent.isVisible = false myEditorWidthWatcher.updateWidthForAllInlays() } private inner class MyComponent(private val component: JComponent) : JBScrollPane(component) { init { isOpaque = false viewport.isOpaque = false border = JBUI.Borders.empty() viewportBorder = JBUI.Borders.empty() horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER verticalScrollBar.preferredSize = Dimension(0, 0) setViewportView(component) component.addComponentListener(object : ComponentAdapter() { override fun componentResized(e: ComponentEvent) { return dispatchEvent(ComponentEvent(component, ComponentEvent.COMPONENT_RESIZED)) } }) } override fun getPreferredSize(): Dimension { return Dimension( myEditorWidthWatcher.editorTextWidth, if (component.isVisible) component.preferredSize.height else 0 ) } } private inner class EditorTextWidthWatcher : ComponentAdapter() { var editorTextWidth: Int = 0 private val maximumEditorTextWidth: Int private val verticalScrollbarFlipped: Boolean init { val metrics = (editor as EditorImpl).getFontMetrics(Font.PLAIN) val spaceWidth = FontLayoutService.getInstance().charWidth2D(metrics, ' '.toInt()) maximumEditorTextWidth = ceil(spaceWidth * (editor.settings.getRightMargin(editor.project)) - 1).toInt() val scrollbarFlip = editor.scrollPane.getClientProperty(JBScrollPane.Flip::class.java) verticalScrollbarFlipped = scrollbarFlip == JBScrollPane.Flip.HORIZONTAL || scrollbarFlip == JBScrollPane.Flip.BOTH } override fun componentResized(e: ComponentEvent) = updateWidthForAllInlays() override fun componentHidden(e: ComponentEvent) = updateWidthForAllInlays() override fun componentShown(e: ComponentEvent) = updateWidthForAllInlays() fun updateWidthForAllInlays() { val newWidth = calcWidth() editorTextWidth = newWidth myWrapper.dispatchEvent(ComponentEvent(myWrapper, ComponentEvent.COMPONENT_RESIZED)) myWrapper.invalidate() } private fun calcWidth(): Int { val visibleEditorTextWidth = editor.scrollPane.viewport.width - getVerticalScrollbarWidth() - getGutterTextGap() return min(max(visibleEditorTextWidth, 0), maximumEditorTextWidth) } private fun getVerticalScrollbarWidth(): Int { val width = editor.scrollPane.verticalScrollBar.width return if (!verticalScrollbarFlipped) width * 2 else width } private fun getGutterTextGap(): Int { return if (verticalScrollbarFlipped) { val gutter = editor.gutterComponentEx gutter.width - gutter.whitespaceSeparatorOffset } else 0 } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/configuration/README.md ================================================ ## Notes I'm going to refactor everything to MVP architecture and DDD style. Each module is a separated domain (DDD), each domain can have a sub-domain. For now these are the domain: - configuration: For configuration views - mergeRequest : For merge request views -> this is big domain - Comment : sub-domain of merge Request - ... - diff : domain for everything related to diff view - home : domain for home tab Each domain has: - model - view - presenter - util (if needed) - internal (if needed) - entity (if needed) - vos (if needed) - ... ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/configuration/vos/GitRemotePathInfo.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.configuration.vos import java.net.URL class GitRemotePathInfo(private val input: String) { var namespace: String = "" private set var project: String = "" private set var isValid: Boolean = false private set init { val inputLowerCase = input.toLowerCase() if (inputLowerCase.startsWith("https://") || inputLowerCase.startsWith("http://")) { parseHttpUrl(input) } if (inputLowerCase.startsWith("git@")) { parseSshUrl(input) } } private fun parseSshUrl(input: String) { val index = input.lastIndexOf(':') if (index < 0) { return } val parts = input.substring(index + 1).split('/') if (parts.size != 2) { return } namespace = parts[0] project = if (parts[1].endsWith(".git")) parts[1].substring(0, parts[1].length - 4) else parts[1] isValid = true } private fun parseHttpUrl(input: String) { val url = URL(input) val parts = url.path.split('/') if (parts.size != 3) { return } namespace = parts[1] project = if (parts[2].endsWith(".git")) parts[2].substring(0, parts[2].length - 4) else parts[2] isValid = true } override fun toString(): String { return "$namespace/$project" } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/AbstractDiffView.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import com.intellij.diff.tools.util.base.DiffViewerBase import com.intellij.diff.tools.util.base.DiffViewerListener import com.intellij.diff.util.Side import com.intellij.diff.util.TextDiffType import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.util.Disposer import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.AbstractView import net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEvent import net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEventPropagator import net.ntworld.mergeRequestIntegrationIde.component.gutter.* import net.ntworld.mergeRequestIntegrationIde.component.thread.ThreadFactory import net.ntworld.mergeRequestIntegrationIde.component.thread.ThreadModel import net.ntworld.mergeRequestIntegrationIde.component.thread.ThreadPresenter import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider abstract class AbstractDiffView( private val projectServiceProvider: ProjectServiceProvider, private val viewerBase: DiffViewerBase ) : AbstractView(), DiffView { final override val dispatcher = EventDispatcher.create(DiffView.ActionListener::class.java) private val diffViewerListener = object : DiffViewerListener() { override fun onInit() = dispatcher.multicaster.onInit() override fun onDispose() = dispatcher.multicaster.onDispose() override fun onBeforeRediff() = dispatcher.multicaster.onBeforeRediff() override fun onAfterRediff() = dispatcher.multicaster.onAfterRediff() override fun onRediffAborted() = dispatcher.multicaster.onRediffAborted() } private val myGutterIconRenderersOfLeft = mutableMapOf() private val myGutterIconRenderersOfRight = mutableMapOf() private val myThreadOfLeft = mutableMapOf() private val myThreadOfRight = mutableMapOf() private val myCommentEventPropagator = CommentEventPropagator(dispatcher) private val myThreadPresenterEventListener = object : ThreadPresenter.EventListener, CommentEvent by myCommentEventPropagator { override fun onMainEditorClosed(threadPresenter: ThreadPresenter) { val renderer = findGutterIconRenderer(threadPresenter.view.logicalLine, threadPresenter.view.side) if (null !== renderer) { if (threadPresenter.model.comments.isEmpty()) { renderer.setState(GutterState.NO_COMMENT) } else { if (threadPresenter.model.comments.filter { it.isDraft }.isNotEmpty()) { renderer.setState(GutterState.HAS_DRAFT) } else { renderer.setState( if (threadPresenter.model.comments.size == 1) GutterState.THREAD_HAS_SINGLE_COMMENT else GutterState.THREAD_HAS_MULTI_COMMENTS ) } } } } override fun onEditCommentRequested(comment: Comment, content: String) { dispatcher.multicaster.onEditCommentRequested(comment, content) } override fun onReplyCommentRequested(content: String, repliedComment: Comment, logicalLine: Int, side: Side) { if (content.trim().isNotEmpty()) { dispatcher.multicaster.onReplyCommentRequested(content, repliedComment, logicalLine, side) } } override fun onCreateCommentRequested(content: String, position: GutterPosition, logicalLine: Int, side: Side, isDraft: Boolean) { if (content.trim().isNotEmpty()) { dispatcher.multicaster.onCreateCommentRequested(content, position, logicalLine, side, isDraft) } } override fun onPublishDraftCommentRequested(comment: Comment) { dispatcher.multicaster.onPublishDraftCommentRequested(comment) } } protected val myGutterIconRendererActionListener = object : GutterIconRendererActionListener { override fun performGutterIconRendererAction(gutterIconRenderer: GutterIconRenderer, type: GutterActionType) { dispatcher.multicaster.onGutterActionPerformed(gutterIconRenderer, type, DiffView.DisplayCommentMode.TOGGLE) } } init { viewerBase.addListener(diffViewerListener) } protected abstract fun convertVisibleLineToLogicalLine(visibleLine: Int, side: Side): Int override fun destroyExistingComments(excludedVisibleLines: Set, side: Side) { val excludedLogicalLines = excludedVisibleLines .map { convertVisibleLineToLogicalLine(it, side) } .filter { it >= 0 } val map = if (side == Side.LEFT) myThreadOfLeft else myThreadOfRight val removedKeys = mutableListOf() for (entry in map) { if (excludedLogicalLines.contains(entry.key)) { continue } entry.value.model.comments = listOf() removedKeys.add(entry.key) } removedKeys.forEach { map.remove(it) } } override fun showAllComments() = changeAllCommentsVisibility(true) override fun hideAllComments() = changeAllCommentsVisibility(false) override fun resetGutterIcons() { for (renderer in myGutterIconRenderersOfLeft.values) { renderer.setState(GutterState.NO_COMMENT) } for (renderer in myGutterIconRenderersOfRight.values) { renderer.setState(GutterState.NO_COMMENT) } } override fun resetEditorOnLine(logicalLine: Int, side: Side, repliedComment: Comment?) { val map = if (side == Side.LEFT) myThreadOfLeft else myThreadOfRight val thread = map[logicalLine] if (null !== thread) { thread.model.resetEditor(repliedComment) } } override fun dispose() { myGutterIconRenderersOfLeft.clear() myGutterIconRenderersOfRight.clear() } protected fun initializeThreadOnLineIfNotAvailable( providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, editor: EditorEx, position: GutterPosition, logicalLine: Int, side: Side, comments: List ) { val map = if (side == Side.LEFT) myThreadOfLeft else myThreadOfRight if (!map.containsKey(logicalLine)) { val model = ThreadFactory.makeModel(comments) val view = ThreadFactory.makeView( projectServiceProvider, editor, providerData, mergeRequestInfo, logicalLine, side, position ) val presenter = ThreadFactory.makePresenter(model, view) presenter.addListener(myThreadPresenterEventListener) Disposer.register(this, presenter) map[logicalLine] = presenter } } protected fun registerGutterIconRenderer(renderer: GutterIconRenderer) { val map = if (renderer.side == Side.LEFT) myGutterIconRenderersOfLeft else myGutterIconRenderersOfRight map[renderer.logicalLine] = renderer } protected fun findGutterIconRenderer(logicalLine: Int, side: Side): GutterIconRenderer? { val map = if (side == Side.LEFT) myGutterIconRenderersOfLeft else myGutterIconRenderersOfRight return map[logicalLine] } override fun displayComments(visibleLine: Int, side: Side, mode: DiffView.DisplayCommentMode) { val logicalLine = convertVisibleLineToLogicalLine(visibleLine, side) if (-1 != logicalLine) { assertThreadAvailable(logicalLine, side) { displayComments(it.model, logicalLine, side, mode) } } } override fun displayComments(renderer: GutterIconRenderer, mode: DiffView.DisplayCommentMode) { assertThreadAvailable(renderer.logicalLine, renderer.side) { displayComments(it.model, renderer.logicalLine, renderer.side, mode) } } protected fun displayComments( model: ThreadModel, logicalLine: Int, side: Side, mode: DiffView.DisplayCommentMode ) { when (mode) { DiffView.DisplayCommentMode.TOGGLE -> model.visible = !model.visible DiffView.DisplayCommentMode.SHOW -> model.visible = true DiffView.DisplayCommentMode.HIDE -> model.visible = false } setWritingStateOfGutterIconRenderer(model, logicalLine, side) } override fun displayEditorOnLine(logicalLine: Int, side: Side) { assertThreadAvailable(logicalLine, side) { it.model.showEditor = true setWritingStateOfGutterIconRenderer(it.model, logicalLine, side) } } protected fun updateComments(renderer: GutterIconRenderer, comments: List) { assertThreadAvailable(renderer.logicalLine, renderer.side) { it.model.comments = comments } updateGutterIcon(renderer, comments) } // TODO: Duplicate EditorManagerImpl.updateGutterIcon maybe move to Util class protected fun updateGutterIcon(renderer: GutterIconRenderer, comments: List) { val state = if (comments.isEmpty()) { GutterState.NO_COMMENT } else { if (comments.filter { it.isDraft }.isNotEmpty()) { GutterState.HAS_DRAFT } else { if (comments.size == 1) GutterState.THREAD_HAS_SINGLE_COMMENT else GutterState.THREAD_HAS_MULTI_COMMENTS } } renderer.setState(state) } private fun changeAllCommentsVisibility(displayed: Boolean) { val mode = if (displayed) DiffView.DisplayCommentMode.SHOW else DiffView.DisplayCommentMode.HIDE for (renderer in myGutterIconRenderersOfLeft.values) { dispatcher.multicaster.onGutterActionPerformed( renderer, GutterActionType.TOGGLE, mode ) } for (renderer in myGutterIconRenderersOfRight.values) { dispatcher.multicaster.onGutterActionPerformed( renderer, GutterActionType.TOGGLE, mode ) } } private fun setWritingStateOfGutterIconRenderer(model: ThreadModel, logicalLine: Int, side: Side) { val renderer = findGutterIconRenderer(logicalLine, side) if (null !== renderer && model.showEditor) { renderer.setState(GutterState.WRITING) } } private fun assertThreadAvailable(logicalLine: Int, side: Side, invoker: ((ThreadPresenter) -> Unit)) { val map = if (side == Side.LEFT) myThreadOfLeft else myThreadOfRight val thread = map[logicalLine] if (null !== thread) { invoker.invoke(thread) } } protected fun findThreadPresenter(logicalLine: Int, side: Side): ThreadPresenter? { val map = if (side == Side.LEFT) myThreadOfLeft else myThreadOfRight return map[logicalLine] } protected fun findChangeType(editor: EditorEx, logicalLine: Int): DiffView.ChangeType { if (editor.isDisposed) { return DiffView.ChangeType.UNKNOWN } val guessChangeTypeByColorFunction = makeGuessChangeTypeByColorFunction(editor) val highlighters = editor.markupModel.allHighlighters var type = DiffView.ChangeType.UNKNOWN for (highlighter in highlighters) { val startLogicalPosition = editor.offsetToLogicalPosition(highlighter.startOffset) val endLogicalPosition = editor.offsetToLogicalPosition(highlighter.endOffset) if (startLogicalPosition.line > logicalLine || logicalLine > endLogicalPosition.line) { continue } val guessType = guessChangeTypeByColorFunction(highlighter.textAttributes) if (guessType != DiffView.ChangeType.UNKNOWN) { type = guessType } } return type } private fun collectThreadIds(comments: List): MutableSet { val result = mutableSetOf() comments.forEach { result.add(it.parentId) } return result } private fun makeGuessChangeTypeByColorFunction(editor: Editor): ((TextAttributes?) -> DiffView.ChangeType) { val insertedColor = TextDiffType.INSERTED.getColor(editor).rgb val insertedIgnoredColor = TextDiffType.INSERTED.getIgnoredColor(editor).rgb val deletedColor = TextDiffType.DELETED.getColor(editor).rgb val deletedIgnoredColor = TextDiffType.DELETED.getIgnoredColor(editor).rgb val modifiedColor = TextDiffType.MODIFIED.getColor(editor).rgb val modifiedIgnoredColor = TextDiffType.MODIFIED.getIgnoredColor(editor).rgb return { if (null === it) { DiffView.ChangeType.UNKNOWN } else { val bgColor = it.backgroundColor if (null === bgColor) { DiffView.ChangeType.UNKNOWN } else { when (bgColor.rgb) { insertedColor, insertedIgnoredColor -> DiffView.ChangeType.INSERTED modifiedColor, modifiedIgnoredColor -> DiffView.ChangeType.MODIFIED deletedColor, deletedIgnoredColor -> DiffView.ChangeType.DELETED else -> DiffView.ChangeType.UNKNOWN } } } } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/CommentPoint.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import net.ntworld.mergeRequest.Comment data class CommentPoint( val line: Int, val comment: Comment ) { val id: String = comment.id } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffExtensionBase.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import com.intellij.diff.DiffContext import com.intellij.diff.DiffExtension import com.intellij.diff.FrameDiffTool import com.intellij.diff.requests.DiffRequest import com.intellij.diff.tools.util.base.DiffViewerBase import com.intellij.diff.util.DiffUserDataKeys import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.ToggleAction import com.intellij.openapi.project.Project as IdeaProject import com.intellij.openapi.vcs.changes.Change import com.intellij.openapi.vcs.changes.actions.diff.ChangeDiffRequestProducer import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.component.Icons import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.util.TextChoiceUtil open class DiffExtensionBase( private val applicationServiceProvider: ApplicationServiceProvider ) : DiffExtension() { override fun onViewerCreated(viewer: FrameDiffTool.DiffViewer, context: DiffContext, request: DiffRequest) { val presenter = createPresenter(viewer, request) if (null === presenter) { return } context.putUserData( DiffUserDataKeys.CONTEXT_ACTIONS, listOf( MyToggleAllCommentsAction( presenter, applicationServiceProvider.settingsManager.displayCommentsInDiffView ), MyToggleResolvedCommentsAction( presenter, false ), MyShowDraftCommentsOnlyAction( presenter, false ), MyPublishDraftCommentsAction( presenter ) ) ) } private fun findReviewContext(projectServiceProvider: ProjectServiceProvider, request: DiffRequest): ReviewContext? { val reviewContext = request.getUserData(ReviewContext.KEY) if (null !== reviewContext) { return reviewContext } return projectServiceProvider.reviewContextManager.findDoingCodeReviewContext() } private fun createPresenter(viewer: FrameDiffTool.DiffViewer, request: DiffRequest): DiffPresenter? { return if (viewer is DiffViewerBase) { assertViewerIsValid(viewer) { project, change -> val projectServiceProvider = applicationServiceProvider.findProjectServiceProvider(project) val reviewContext = findReviewContext(projectServiceProvider, request) if (null === reviewContext) { return@assertViewerIsValid null } val model = DiffFactory.makeDiffModel(projectServiceProvider, reviewContext, change) val view = DiffFactory.makeView(projectServiceProvider, viewer, change) if (null !== view && null !== model) { DiffFactory.makeDiffPresenter( projectServiceProvider = projectServiceProvider, model = model, view = view ) } else { null } } } else null } private fun assertViewerIsValid( viewer: DiffViewerBase, invoker: ((IdeaProject, Change) -> DiffPresenter?) ): DiffPresenter? { val project = viewer.project val change = viewer.request.getUserData(ChangeDiffRequestProducer.CHANGE_KEY) if (null === change || null === project) { return null } return invoker.invoke(project, change) } private class MyToggleAllCommentsAction( private val presenter: DiffPresenter, initializedState: Boolean ) : ToggleAction("Toggle Comments", "Toggle all comments in this diff view", Icons.Comments) { private var myShowAll = initializedState override fun isSelected(e: AnActionEvent): Boolean { return myShowAll } override fun setSelected(e: AnActionEvent, state: Boolean) { myShowAll = state if (myShowAll) { presenter.view.showAllComments() } else { presenter.view.hideAllComments() } } } private class MyPublishDraftCommentsAction( private val presenter: DiffPresenter ) : AnAction("Publish draft comments", "Publish all draft comments in this diff view", null) { override fun actionPerformed(e: AnActionEvent) { presenter.publishAllDraftComments() } override fun update(e: AnActionEvent) { if (presenter.model.draftComments.isNotEmpty()) { e.presentation.isVisible = true e.presentation.text = "Publish " + TextChoiceUtil.draftComment(presenter.model.draftComments.count()) } else { e.presentation.isVisible = false } } override fun displayTextInToolbar() = true } private class MyShowDraftCommentsOnlyAction( private val presenter: DiffPresenter, initializedState: Boolean ) : ToggleAction("Draft Comments Only", "Only show draft comments", Icons.Edit) { private var myOnlyDraft = initializedState override fun isSelected(e: AnActionEvent): Boolean { return myOnlyDraft } override fun setSelected(e: AnActionEvent, state: Boolean) { myOnlyDraft = state presenter.model.rebuildCommentsWhenOnlyShowDraftChanged(myOnlyDraft) } override fun update(e: AnActionEvent) { super.update(e) e.presentation.isVisible = presenter.model.draftComments.isNotEmpty() } } private class MyToggleResolvedCommentsAction( private val presenter: DiffPresenter, initializedState: Boolean ) : ToggleAction("Show Resolved Comments", "Toggle resolved comments in this diff view", Icons.Resolved) { private var myShown = initializedState override fun isSelected(e: AnActionEvent): Boolean { return myShown } override fun setSelected(e: AnActionEvent, state: Boolean) { myShown = state presenter.model.rebuildCommentsWhenShowResolvedChanged(myShown) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffFactory.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import com.intellij.diff.tools.fragmented.UnifiedDiffViewer import com.intellij.diff.tools.simple.SimpleOnesideDiffViewer import com.intellij.diff.tools.util.base.DiffViewerBase import com.intellij.diff.tools.util.side.TwosideTextDiffViewer import com.intellij.diff.util.Side import com.intellij.openapi.vcs.changes.Change import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext object DiffFactory { fun makeDiffPresenter( projectServiceProvider: ProjectServiceProvider, model: DiffModel, view: DiffView<*> ): DiffPresenter { return DiffPresenterImpl(projectServiceProvider, model, view) } fun makeView( projectServiceProvider: ProjectServiceProvider, viewer: DiffViewerBase, change: Change ): DiffView<*>? { return when (viewer) { is SimpleOnesideDiffViewer -> makeSimpleOneSideDiffViewer(projectServiceProvider, viewer, change) is TwosideTextDiffViewer -> makeTwoSideTextDiffViewer(projectServiceProvider, viewer, change) is UnifiedDiffViewer -> makeUnifiedDiffView(projectServiceProvider, viewer, change) else -> null } } private fun makeSimpleOneSideDiffViewer( projectServiceProvider: ProjectServiceProvider, viewer: SimpleOnesideDiffViewer, change: Change ): DiffView? { return SimpleOneSideDiffView(projectServiceProvider, viewer, change, when(change.type) { Change.Type.DELETED -> Side.LEFT Change.Type.NEW -> Side.RIGHT else -> throw Exception("Invalid change type") }) } private fun makeTwoSideTextDiffViewer( projectServiceProvider: ProjectServiceProvider, viewer: TwosideTextDiffViewer, change: Change ): DiffView? { return TwoSideTextDiffView(projectServiceProvider, viewer, change) } private fun makeUnifiedDiffView( projectServiceProvider: ProjectServiceProvider, viewer: UnifiedDiffViewer, change: Change ): DiffView? { return UnifiedDiffView(projectServiceProvider, viewer, change) } fun makeDiffModel(projectServiceProvider: ProjectServiceProvider, reviewContext: ReviewContext, change: Change): DiffModel? { return DiffModelImpl(projectServiceProvider, reviewContext, change, false, onlyShowDraftComments = false) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffModel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import com.intellij.openapi.Disposable import com.intellij.openapi.vcs.changes.Change import com.intellij.util.messages.MessageBusConnection import net.ntworld.mergeRequest.* import net.ntworld.mergeRequestIntegrationIde.DataChangedSource import net.ntworld.mergeRequestIntegrationIde.Model import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext import java.util.* interface DiffModel : Model, Disposable { val reviewContext: ReviewContext val providerData: ProviderData val mergeRequestInfo: MergeRequestInfo val messageBusConnection: MessageBusConnection val diffReference: DiffReference? val commits: List val change: Change val draftComments: List val commentsOnBeforeSide: List val commentsOnAfterSide: List var displayResolvedComments: Boolean var onlyShowDraftComments: Boolean fun rebuildCommentsWhenShowResolvedChanged(showResolved: Boolean) fun rebuildCommentsWhenOnlyShowDraftChanged(onlyShowDraft: Boolean) interface DataListener : EventListener { fun onCommentsUpdated(source: DataChangedSource) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffModelImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import com.intellij.openapi.util.Disposer import com.intellij.openapi.vcs.changes.Change import com.intellij.openapi.vcs.changes.ContentRevision import com.intellij.util.EventDispatcher import com.intellij.util.messages.MessageBusConnection import net.ntworld.mergeRequest.* import net.ntworld.mergeRequestIntegrationIde.AbstractModel import net.ntworld.mergeRequestIntegrationIde.DataChangedSource import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.MergeRequestDataNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil class DiffModelImpl( private val projectServiceProvider: ProjectServiceProvider, override val reviewContext: ReviewContext, override val change: Change, override var displayResolvedComments: Boolean, override var onlyShowDraftComments: Boolean ) : AbstractModel(), DiffModel { override val dispatcher = EventDispatcher.create(DiffModel.DataListener::class.java) private val commentFilterOnBeforeSide: ((ContentRevision, Comment) -> Boolean) = { revision, comment -> val position = comment.position if (null === position || null === position.oldPath) { false } else { RepositoryUtil.findAbsoluteCrossPlatformsPath(reviewContext.repository, position.oldPath!!) == RepositoryUtil.transformToCrossPlatformsPath(revision.file.path) } } private val commentFilterOnAfterSide: ((ContentRevision, Comment) -> Boolean) = { revision, comment -> val position = comment.position if (null === position || null === position.newPath) { false } else { RepositoryUtil.findAbsoluteCrossPlatformsPath(reviewContext.repository, position.newPath!!) == RepositoryUtil.transformToCrossPlatformsPath(revision.file.path) } } private val commentFactoryOnBeforeSide: ((CommentPosition, Comment) -> CommentPoint) = { position, comment -> CommentPoint(position.oldLine!!, comment) } private val commentFactoryOnAfterSide: ((CommentPosition, Comment) -> CommentPoint) = { position, comment -> CommentPoint(position.newLine!!, comment) } override val messageBusConnection: MessageBusConnection = reviewContext.messageBusConnection private val myMergeRequestDataNotifier = object : MergeRequestDataNotifier { override fun fetchCommentsRequested(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) { } override fun onCommentsUpdated( providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List ) { if (providerData.id != reviewContext.providerData.id || mergeRequestInfo.id != reviewContext.mergeRequestInfo.id) { return } buildCommentsOnBeforeSide(comments) buildCommentsOnAfterSide(comments) buildDraftComments(comments) dispatcher.multicaster.onCommentsUpdated(DataChangedSource.NOTIFIER) } } override val providerData = reviewContext.providerData override val mergeRequestInfo = reviewContext.mergeRequestInfo override val diffReference: DiffReference? get() = reviewContext.diffReference override val commits get() = reviewContext.commits override val draftComments = mutableListOf() override var commentsOnBeforeSide = mutableListOf() private set override var commentsOnAfterSide = mutableListOf() private set init { messageBusConnection.subscribe(MergeRequestDataNotifier.TOPIC, myMergeRequestDataNotifier) Disposer.register(messageBusConnection, this) buildCommentsOnBeforeSide(null) buildCommentsOnAfterSide(null) } override fun dispose() { dispatcher.listeners.clear() commentsOnBeforeSide.clear() commentsOnAfterSide.clear() } override fun rebuildCommentsWhenShowResolvedChanged(showResolved: Boolean) { displayResolvedComments = showResolved buildCommentsOnBeforeSide(null) buildCommentsOnAfterSide(null) dispatcher.multicaster.onCommentsUpdated(DataChangedSource.UI) } override fun rebuildCommentsWhenOnlyShowDraftChanged(onlyShowDraft: Boolean) { onlyShowDraftComments = onlyShowDraft buildCommentsOnBeforeSide(null) buildCommentsOnAfterSide(null) dispatcher.multicaster.onCommentsUpdated(DataChangedSource.UI) } private fun buildCommentPoints( result: MutableList, revision: ContentRevision?, comments: List?, filter: ((ContentRevision, Comment) -> Boolean), factory: ((CommentPosition, Comment) -> CommentPoint), matcher: ((CommentPosition, ContentRevision, List) -> Boolean) ) { result.clear() if (null === revision) { return } val commitIds = reviewContext.commits.map { it.id } val list = if (null !== comments) { comments.filter { if (onlyShowDraftComments) { it.isDraft } else { if (!displayResolvedComments && it.resolved) { false } else { filter.invoke(revision, it) } } } } else { reviewContext.getCommentsByPath(revision.file.path).filter { if (onlyShowDraftComments) { it.isDraft } else { displayResolvedComments || !it.resolved } } } for (comment in list) { val position = comment.position if (null === position) { continue } if (matcher.invoke(position, revision, commitIds)) { result.add(factory(position, comment)) continue } } } private fun buildDraftComments(comments: List) { draftComments.clear() val items = comments.filter { it.isDraft } draftComments.addAll(items) } private fun buildCommentsOnBeforeSide(comments: List?) = buildCommentPoints( commentsOnBeforeSide, change.beforeRevision, comments, commentFilterOnBeforeSide, commentFactoryOnBeforeSide ) { position, revision, commits -> val hash = revision.revisionNumber.asString() null !== position.oldLine && (position.startHash == hash || position.baseHash == hash || commits.contains(hash)) } private fun buildCommentsOnAfterSide(comments: List?) = buildCommentPoints( commentsOnAfterSide, change.afterRevision, comments, commentFilterOnAfterSide, commentFactoryOnAfterSide ) { position, revision, commits -> val hash = revision.revisionNumber.asString() null !== position.newLine && (position.headHash == hash || position.baseHash == hash || commits.contains(hash)) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffPresenter.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import com.intellij.openapi.Disposable import net.ntworld.mergeRequestIntegrationIde.SimplePresenter interface DiffPresenter : SimplePresenter, Disposable { val model: DiffModel val view: DiffView<*> fun publishAllDraftComments() } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffPresenterImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import com.intellij.diff.util.Side import com.intellij.notification.NotificationType import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.util.Disposer import com.intellij.openapi.vcs.changes.Change import com.intellij.util.EventDispatcher import git4idea.repo.GitRepository import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequest.CommentPositionChangeType import net.ntworld.mergeRequest.CommentPositionSource import net.ntworld.mergeRequest.command.DeleteCommentCommand import net.ntworld.mergeRequest.command.ResolveCommentCommand import net.ntworld.mergeRequest.command.UnresolveCommentCommand import net.ntworld.mergeRequest.request.CreateCommentRequest import net.ntworld.mergeRequest.request.PublishCommentsRequest import net.ntworld.mergeRequest.request.ReplyCommentRequest import net.ntworld.mergeRequest.request.UpdateCommentRequest import net.ntworld.mergeRequestIntegration.internal.CommentPositionImpl import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegration.provider.ProviderException import net.ntworld.mergeRequestIntegrationIde.AbstractPresenter import net.ntworld.mergeRequestIntegrationIde.DataChangedSource import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterActionType import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRenderer import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.DiffNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.MergeRequestDataNotifier import net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil import java.util.* internal class DiffPresenterImpl( private val projectServiceProvider: ProjectServiceProvider, override val model: DiffModel, override val view: DiffView<*> ) : AbstractPresenter(), DiffPresenter, DiffView.ActionListener, DiffModel.DataListener, DiffNotifier { override val dispatcher = EventDispatcher.create(EventListener::class.java) init { view.addActionListener(this) model.addDataListener(this) model.messageBusConnection.subscribe(DiffNotifier.TOPIC, this) Disposer.register(model, this) } override fun dispose() { view.dispose() } override fun onInit() {} override fun onDispose() {} override fun onBeforeRediff() {} override fun onAfterRediff() { view.createGutterIcons() val before = groupCommentsByLine(model.commentsOnBeforeSide) for (item in before) { view.initializeLine(model.reviewContext, item.key, Side.LEFT, item.value) } val after = groupCommentsByLine(model.commentsOnAfterSide) for (item in after) { view.initializeLine(model.reviewContext, item.key, Side.RIGHT, item.value) } if (projectServiceProvider.applicationSettings.displayCommentsInDiffView) { ApplicationManager.getApplication().invokeLater { view.showAllComments() } } val scrollPosition = model.reviewContext.getChangeData(model.change, DiffNotifier.ScrollPosition) if (null !== scrollPosition) { val showComments = model.reviewContext.getChangeData(model.change, DiffNotifier.ScrollShowComments) scrollToLine(scrollPosition, showComments) clearChangeDataOfScrollToLineInReviewContext() } } override fun onRediffAborted() {} override fun onCommentsUpdated(source: DataChangedSource) { if (source == DataChangedSource.NOTIFIER) { ApplicationManager.getApplication().invokeLater { handleWhenCommentsGetUpdated(source) } } else { handleWhenCommentsGetUpdated(source) } } private fun handleWhenCommentsGetUpdated(source: DataChangedSource) { view.resetGutterIcons() val before = groupCommentsByLine(model.commentsOnBeforeSide) view.destroyExistingComments(before.keys, Side.LEFT) for (item in before) { view.initializeLine(model.reviewContext, item.key, Side.LEFT, item.value) view.updateComments(item.key, Side.LEFT, item.value) } val after = groupCommentsByLine(model.commentsOnAfterSide) view.destroyExistingComments(after.keys, Side.RIGHT) for (item in after) { view.initializeLine(model.reviewContext, item.key, Side.RIGHT, item.value) view.updateComments(item.key, Side.RIGHT, item.value) } } override fun onGutterActionPerformed( renderer: GutterIconRenderer, type: GutterActionType, mode: DiffView.DisplayCommentMode ) { when (type) { GutterActionType.ADD -> { view.prepareLine(model.reviewContext, renderer, collectCommentsOfGutterIconRenderer(renderer)) view.displayEditorOnLine(renderer.logicalLine, renderer.side) } GutterActionType.TOGGLE -> { view.prepareLine(model.reviewContext, renderer, collectCommentsOfGutterIconRenderer(renderer)) view.displayComments(renderer, mode) } } } override fun onEditCommentRequested(comment: Comment, content: String) { projectServiceProvider.infrastructure.serviceBus() process UpdateCommentRequest.make( providerId = model.providerData.id, mergeRequestId = model.mergeRequestInfo.id, comment = comment, body = content ) ifError { projectServiceProvider.notify( "There was an error from server. \n\n ${it.message}", NotificationType.ERROR ) throw ProviderException(it) } fetchAndUpdateComments() } override fun onReplyCommentRequested(content: String, repliedComment: Comment, logicalLine: Int, side: Side) { projectServiceProvider.infrastructure.serviceBus() process ReplyCommentRequest.make( providerId = model.providerData.id, mergeRequestId = model.mergeRequestInfo.id, repliedComment = repliedComment, body = content ) ifError { projectServiceProvider.notify( "There was an error from server. \n\n ${it.message}", NotificationType.ERROR ) throw ProviderException(it) } fetchAndUpdateComments() view.resetEditorOnLine(logicalLine, side, repliedComment) } override fun onCreateCommentRequested( content: String, position: GutterPosition, logicalLine: Int, side: Side, isDraft: Boolean ) { val commentPosition = convertGutterPositionToCommentPosition(position) projectServiceProvider.infrastructure.serviceBus() process CreateCommentRequest.make( providerId = model.providerData.id, mergeRequestId = model.mergeRequestInfo.id, position = commentPosition, body = content, isDraft = isDraft ) ifError { projectServiceProvider.notify( "There was an error from server. \n\n ${it.message}", NotificationType.ERROR ) throw ProviderException(it) } fetchAndUpdateComments() view.resetEditorOnLine(logicalLine, side, null) } override fun publishAllDraftComments() { val draftCommentIds = model.draftComments.filter { it.isDraft } .map { it.id } projectServiceProvider.infrastructure.serviceBus() process PublishCommentsRequest.make( providerId = model.providerData.id, mergeRequestId = model.mergeRequestInfo.id, draftCommentIds = draftCommentIds ) ifError { projectServiceProvider.notify( "There was an error from server. \n\n ${it.message}", NotificationType.ERROR ) throw ProviderException(it) } fetchAndUpdateComments() } override fun onPublishDraftCommentRequested(comment: Comment) { projectServiceProvider.infrastructure.serviceBus() process PublishCommentsRequest.make( providerId = model.providerData.id, mergeRequestId = model.mergeRequestInfo.id, draftCommentIds = listOf(comment.id) ) ifError { projectServiceProvider.notify( "There was an error from server. \n\n ${it.message}", NotificationType.ERROR ) throw ProviderException(it) } fetchAndUpdateComments() } override fun onDeleteCommentRequested(comment: Comment) { projectServiceProvider.infrastructure.commandBus() process DeleteCommentCommand.make( providerId = model.providerData.id, mergeRequestId = model.mergeRequestInfo.id, comment = comment ) fetchAndUpdateComments() } override fun onResolveCommentRequested(comment: Comment) { projectServiceProvider.infrastructure.commandBus() process ResolveCommentCommand.make( providerId = model.providerData.id, mergeRequestId = model.mergeRequestInfo.id, comment = comment ) fetchAndUpdateComments() } override fun onUnresolveCommentRequested(comment: Comment) { projectServiceProvider.infrastructure.commandBus() process UnresolveCommentCommand.make( providerId = model.providerData.id, mergeRequestId = model.mergeRequestInfo.id, comment = comment ) fetchAndUpdateComments() } override fun scrollToPositionRequested( reviewContext: ReviewContext, change: Change, position: CommentPosition, showComments: Boolean? ) { if (model.reviewContext === reviewContext && model.change == change) { scrollToLine(position, showComments) clearChangeDataOfScrollToLineInReviewContext() } } override fun hideAllCommentsRequested(reviewContext: ReviewContext, change: Change) { if (model.reviewContext === reviewContext && model.change == change) { view.hideAllComments() } } private fun fetchAndUpdateComments() { projectServiceProvider.messageBus.syncPublisher(MergeRequestDataNotifier.TOPIC).fetchCommentsRequested( model.providerData, model.mergeRequestInfo ) } private fun collectCommentsOfGutterIconRenderer(renderer: GutterIconRenderer): List { val result = mutableMapOf() model.commentsOnBeforeSide .filter { it.line == renderer.visibleLineLeft } .forEach { result[it.id] = it } model.commentsOnAfterSide .filter { it.line == renderer.visibleLineRight } .forEach { result[it.id] = it } return result.values.map { it.comment } } private fun groupCommentsByLine(commentPoints: List): Map> { val result = mutableMapOf>() for (commentPoint in commentPoints) { if (!result.containsKey(commentPoint.line)) { result[commentPoint.line] = mutableListOf() } val list = result[commentPoint.line]!! list.add(commentPoint.comment) } return result } private fun convertGutterPositionToCommentPosition(input: GutterPosition): CommentPosition { val repository: GitRepository? = RepositoryUtil.findRepository(projectServiceProvider, model.providerData) return CommentPositionImpl( oldLine = input.oldLine, oldPath = if (null === input.oldPath) null else RepositoryUtil.findRelativePath(repository, input.oldPath), newLine = input.newLine, newPath = if (null === input.newPath) null else RepositoryUtil.findRelativePath(repository, input.newPath), baseHash = if (input.baseHash.isNullOrEmpty()) findBaseHash() else input.baseHash, headHash = if (input.headHash.isNullOrEmpty()) findHeadHash() else input.headHash, startHash = if (input.startHash.isNullOrEmpty()) findStartHash() else input.startHash, source = when (input.editorType) { DiffView.EditorType.SINGLE_SIDE -> CommentPositionSource.SINGLE_SIDE DiffView.EditorType.TWO_SIDE_LEFT -> CommentPositionSource.SIDE_BY_SIDE_LEFT DiffView.EditorType.TWO_SIDE_RIGHT -> CommentPositionSource.SIDE_BY_SIDE_RIGHT DiffView.EditorType.UNIFIED -> CommentPositionSource.UNIFIED }, changeType = when (input.changeType) { DiffView.ChangeType.UNKNOWN -> CommentPositionChangeType.UNKNOWN DiffView.ChangeType.INSERTED -> CommentPositionChangeType.INSERTED DiffView.ChangeType.DELETED -> CommentPositionChangeType.DELETED DiffView.ChangeType.MODIFIED -> CommentPositionChangeType.MODIFIED } ) } private fun findBaseHash(): String { if (model.commits.isNotEmpty()) { return model.commits.last().id } val diff = model.diffReference return if (null === diff) "" else diff.baseHash } private fun findStartHash(): String { val diff = model.diffReference return if (null === diff) "" else diff.startHash } private fun findHeadHash(): String { if (model.commits.isNotEmpty()) { return model.commits.first().id } val diff = model.diffReference return if (null === diff) "" else diff.headHash } private fun scrollToLine(position: CommentPosition, showComments: Boolean?) { view.scrollToPosition(position, null !== showComments && showComments) } private fun clearChangeDataOfScrollToLineInReviewContext() { model.reviewContext.putChangeData(model.change, DiffNotifier.ScrollPosition, null) model.reviewContext.putChangeData(model.change, DiffNotifier.ScrollShowComments, null) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffView.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import com.intellij.diff.FrameDiffTool import com.intellij.diff.util.Side import com.intellij.openapi.Disposable import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequestIntegrationIde.View import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterActionType import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRenderer import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition import net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEvent import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext interface DiffView : View, Disposable { val viewer: V fun initializeLine(reviewContext: ReviewContext, visibleLine: Int, side: Side, comments: List) fun prepareLine(reviewContext: ReviewContext, renderer: GutterIconRenderer, comments: List) fun createGutterIcons() fun resetGutterIcons() fun destroyExistingComments(excludedVisibleLines: Set, side: Side) fun showAllComments() fun hideAllComments() fun resetEditorOnLine(logicalLine: Int, side: Side, repliedComment: Comment?) fun updateComments(visibleLine: Int, side: Side, comments: List) fun displayEditorOnLine(logicalLine: Int, side: Side) fun displayComments(visibleLine: Int, side: Side, mode: DisplayCommentMode) fun displayComments(renderer: GutterIconRenderer, mode: DisplayCommentMode) fun scrollToPosition(position: CommentPosition, showComments: Boolean) enum class EditorType { SINGLE_SIDE, TWO_SIDE_LEFT, TWO_SIDE_RIGHT, UNIFIED } enum class ChangeType { UNKNOWN, INSERTED, DELETED, MODIFIED } enum class DisplayCommentMode { TOGGLE, SHOW, HIDE } interface ActionListener : CommentEvent { fun onInit() fun onDispose() fun onBeforeRediff() fun onAfterRediff() fun onRediffAborted() fun onGutterActionPerformed(renderer: GutterIconRenderer, type: GutterActionType, mode: DisplayCommentMode) fun onEditCommentRequested(comment: Comment, content: String) fun onReplyCommentRequested(content: String, repliedComment: Comment, logicalLine: Int, side: Side) fun onCreateCommentRequested(content: String, position: GutterPosition, logicalLine: Int, side: Side, isDraft: Boolean) fun onPublishDraftCommentRequested(comment: Comment) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffViewAddCommentActionBase.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorAction import com.intellij.openapi.editor.actionSystem.EditorActionHandler import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRendererFactory open class DiffViewAddCommentActionBase : EditorAction(MyHandler()) { private class MyHandler : EditorActionHandler() { override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { return null !== GutterIconRendererFactory.findGutterIconRenderer(editor) } override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { super.doExecute(editor, caret, dataContext) val gutterRenderer = GutterIconRendererFactory.findGutterIconRenderer(editor) if (null !== gutterRenderer) { gutterRenderer.triggerAddAction() } } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffViewToggleCommentsActionBase.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorAction import com.intellij.openapi.editor.actionSystem.EditorActionHandler import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRendererFactory open class DiffViewToggleCommentsActionBase : EditorAction(MyHandler()) { private class MyHandler : EditorActionHandler() { override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { return null !== GutterIconRendererFactory.findGutterIconRenderer(editor) } override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { super.doExecute(editor, caret, dataContext) val gutterRenderer = GutterIconRendererFactory.findGutterIconRenderer(editor) if (null !== gutterRenderer) { gutterRenderer.triggerToggleAction() } } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/SimpleOneSideDiffView.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import com.intellij.diff.tools.simple.SimpleOnesideDiffViewer import com.intellij.diff.util.Side import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.editor.markup.HighlighterLayer import com.intellij.openapi.vcs.changes.Change import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRenderer import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRendererFactory import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext class SimpleOneSideDiffView( private val projectServiceProvider: ProjectServiceProvider, override val viewer: SimpleOnesideDiffViewer, private val change: Change, private val side: Side ) : AbstractDiffView(projectServiceProvider, viewer) { private var myGutterIconsCreated: Boolean = false override fun convertVisibleLineToLogicalLine(visibleLine: Int, side: Side): Int { return visibleLine - 1 } private fun initializeByLogicalLine(reviewContext: ReviewContext, line: Int, side: Side, comments: List) { initializeThreadOnLineIfNotAvailable( reviewContext.providerData, reviewContext.mergeRequestInfo, viewer.editor, calcPosition(line), line, side, comments ) } override fun initializeLine(reviewContext: ReviewContext, visibleLine: Int, side: Side, comments: List) { val logicalLine = convertVisibleLineToLogicalLine(visibleLine, side) initializeByLogicalLine(reviewContext, logicalLine, side, comments) val renderer = findGutterIconRenderer(logicalLine, side) if (null !== renderer) { updateGutterIcon(renderer, comments) } } override fun prepareLine(reviewContext: ReviewContext, renderer: GutterIconRenderer, comments: List) { initializeByLogicalLine(reviewContext, renderer.logicalLine, renderer.side, comments) } override fun createGutterIcons() { if (myGutterIconsCreated) { return } for (logicalLine in 0 until viewer.editor.document.lineCount) { registerGutterIconRenderer(GutterIconRendererFactory.makeGutterIconRenderer( viewer.editor.markupModel.addLineHighlighter(logicalLine, HighlighterLayer.LAST, null), projectServiceProvider.applicationSettings.showAddCommentIconsInDiffViewGutter, logicalLine, visibleLineLeft = if (side == Side.LEFT) logicalLine + 1 else null, visibleLineRight = if (side == Side.RIGHT) logicalLine + 1 else null, side = side, actionListener = myGutterIconRendererActionListener )) } myGutterIconsCreated = true } override fun updateComments(visibleLine: Int, side: Side, comments: List) { val logicalLine = convertVisibleLineToLogicalLine(visibleLine, side) val renderer = findGutterIconRenderer(logicalLine, side) if (null !== renderer) { updateComments(renderer, comments) } } override fun scrollToPosition(position: CommentPosition, showComments: Boolean) { if (viewer.editor.isDisposed) { return } val oldLine = position.oldLine if (side == Side.LEFT && null !== oldLine) { val logicalLine = convertVisibleLineToLogicalLine(oldLine, Side.LEFT) viewer.editor.scrollingModel.scrollTo(LogicalPosition(logicalLine, 0), ScrollType.MAKE_VISIBLE) if (showComments) { displayComments(oldLine, Side.LEFT, DiffView.DisplayCommentMode.SHOW) } return } val newLine = position.newLine if (side == Side.RIGHT && null !== newLine) { val logicalLine = convertVisibleLineToLogicalLine(newLine, Side.RIGHT) viewer.editor.scrollingModel.scrollTo(LogicalPosition(logicalLine, 0), ScrollType.MAKE_VISIBLE) if (showComments) { displayComments(newLine, Side.RIGHT, DiffView.DisplayCommentMode.SHOW) } return } } private fun calcPosition(logicalLine: Int): GutterPosition { return if (side == Side.LEFT) { GutterPosition( editorType = DiffView.EditorType.SINGLE_SIDE, changeType = findChangeType(viewer.editor, logicalLine), oldLine = logicalLine + 1, oldPath = change.beforeRevision!!.file.toString(), newLine = null, newPath = null, baseHash = change.beforeRevision!!.revisionNumber.asString() ) } else { GutterPosition( editorType = DiffView.EditorType.SINGLE_SIDE, changeType = findChangeType(viewer.editor, logicalLine), newLine = logicalLine + 1, newPath = change.afterRevision!!.file.toString(), oldLine = null, oldPath = null, headHash = change.afterRevision!!.revisionNumber.asString() ) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/TwoSideTextDiffView.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import com.intellij.diff.tools.util.side.TwosideTextDiffViewer import com.intellij.diff.util.Side import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.editor.markup.HighlighterLayer import com.intellij.openapi.vcs.changes.Change import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRenderer import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRendererFactory import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext class TwoSideTextDiffView( private val projectServiceProvider: ProjectServiceProvider, override val viewer: TwosideTextDiffViewer, private val change: Change ) : AbstractDiffView(projectServiceProvider, viewer) { private var myGutterIconsCreated: Boolean = false override fun convertVisibleLineToLogicalLine(visibleLine: Int, side: Side): Int { return visibleLine - 1 } private fun initializeByLogicalLine(reviewContext: ReviewContext, line: Int, side: Side, comments: List) { if (side == Side.LEFT) { initializeThreadOnLineIfNotAvailable( reviewContext.providerData, reviewContext.mergeRequestInfo, viewer.editor1, calcPositionEditor1(line), line, side, comments ) } else { initializeThreadOnLineIfNotAvailable( reviewContext.providerData, reviewContext.mergeRequestInfo, viewer.editor2, calcPositionEditor2(line), line, side, comments ) } } override fun initializeLine(reviewContext: ReviewContext, visibleLine: Int, side: Side, comments: List) { val logicalLine = convertVisibleLineToLogicalLine(visibleLine, side) initializeByLogicalLine(reviewContext, logicalLine, side, comments) val renderer = findGutterIconRenderer(logicalLine, side) if (null !== renderer) { updateGutterIcon(renderer, comments) } } override fun prepareLine(reviewContext: ReviewContext, renderer: GutterIconRenderer, comments: List) { initializeByLogicalLine(reviewContext, renderer.logicalLine, renderer.side, comments) } override fun createGutterIcons() { if (myGutterIconsCreated) { return } for (logicalLine in 0 until viewer.editor1.document.lineCount) { registerGutterIconRenderer( GutterIconRendererFactory.makeGutterIconRenderer( viewer.editor1.markupModel.addLineHighlighter(logicalLine, HighlighterLayer.LAST, null), projectServiceProvider.applicationSettings.showAddCommentIconsInDiffViewGutter, logicalLine, visibleLineLeft = logicalLine + 1, visibleLineRight = null, side = Side.LEFT, actionListener = myGutterIconRendererActionListener ) ) } for (logicalLine in 0 until viewer.editor2.document.lineCount) { registerGutterIconRenderer( GutterIconRendererFactory.makeGutterIconRenderer( viewer.editor2.markupModel.addLineHighlighter(logicalLine, HighlighterLayer.LAST, null), projectServiceProvider.applicationSettings.showAddCommentIconsInDiffViewGutter, logicalLine, visibleLineLeft = null, visibleLineRight = logicalLine + 1, side = Side.RIGHT, actionListener = myGutterIconRendererActionListener ) ) } myGutterIconsCreated = true } override fun updateComments(visibleLine: Int, side: Side, comments: List) { val renderer = findGutterIconRenderer(visibleLine - 1, side) if (null !== renderer) { updateComments(renderer, comments) } } override fun scrollToPosition(position: CommentPosition, showComments: Boolean) { val oldLine = position.oldLine if (oldLine !== null && !viewer.editor1.isDisposed) { val logicalLine = convertVisibleLineToLogicalLine(oldLine, Side.LEFT) viewer.editor1.scrollingModel.scrollTo(LogicalPosition(logicalLine, 0), ScrollType.MAKE_VISIBLE) if (showComments) { displayComments(oldLine, Side.LEFT, DiffView.DisplayCommentMode.SHOW) } } val newLine = position.newLine if (newLine !== null && !viewer.editor1.isDisposed) { val logicalLine = convertVisibleLineToLogicalLine(newLine, Side.RIGHT) viewer.editor2.scrollingModel.scrollTo(LogicalPosition(logicalLine, 0), ScrollType.MAKE_VISIBLE) if (showComments) { displayComments(newLine, Side.RIGHT, DiffView.DisplayCommentMode.SHOW) } } } private fun calcPositionEditor1(logicalLine: Int): GutterPosition { val newLine = viewer.syncScrollSupport!!.scrollable.transfer(Side.LEFT, logicalLine + 1) return GutterPosition( editorType = DiffView.EditorType.TWO_SIDE_LEFT, changeType = findChangeType(viewer.editor1, logicalLine), oldLine = logicalLine + 1, oldPath = change.beforeRevision!!.file.toString(), newLine = newLine, newPath = change.afterRevision!!.file.toString(), baseHash = change.beforeRevision!!.revisionNumber.asString(), headHash = change.afterRevision!!.revisionNumber.asString() ) } private fun calcPositionEditor2(logicalLine: Int): GutterPosition { val oldLine = viewer.syncScrollSupport!!.scrollable.transfer(Side.RIGHT, logicalLine + 1) return GutterPosition( editorType = DiffView.EditorType.TWO_SIDE_RIGHT, changeType = findChangeType(viewer.editor2, logicalLine), oldLine = oldLine, oldPath = change.beforeRevision!!.file.toString(), newLine = logicalLine + 1, newPath = change.afterRevision!!.file.toString(), baseHash = change.beforeRevision!!.revisionNumber.asString(), headHash = change.afterRevision!!.revisionNumber.asString() ) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/UnifiedDiffView.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.diff import com.intellij.diff.tools.fragmented.UnifiedDiffViewer import com.intellij.diff.util.Side import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.editor.markup.HighlighterLayer import com.intellij.openapi.vcs.changes.Change import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRenderer import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRendererFactory import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext class UnifiedDiffView( private val projectServiceProvider: ProjectServiceProvider, override val viewer: UnifiedDiffViewer, private val change: Change ) : AbstractDiffView(projectServiceProvider, viewer) { private var myGutterIconsCreated: Boolean = false private val myLeftLineNumberConverter by lazy { projectServiceProvider.intellijIdeApi.findLeftLineNumberConverter(viewer.editor) } private val myRightLineNumberConverter by lazy { projectServiceProvider.intellijIdeApi.findRightLineNumberConverter(viewer.editor) } private val myCachedLeftLineNumbers = mutableMapOf() private val myCachedRightLineNumbers = mutableMapOf() override fun convertVisibleLineToLogicalLine(visibleLine: Int, side: Side): Int { val map = if (side == Side.LEFT) myCachedLeftLineNumbers else myCachedRightLineNumbers val logicalLine = map[visibleLine - 1] if (null === logicalLine) { return -1 } return logicalLine } private fun initializeByLogicalLine(reviewContext: ReviewContext, line: Int, side: Side, comments: List) { initializeThreadOnLineIfNotAvailable( reviewContext.providerData, reviewContext.mergeRequestInfo, viewer.editor, calcPosition(line), line, side, comments ) } override fun initializeLine(reviewContext: ReviewContext, visibleLine: Int, side: Side, comments: List) { val logicalLine = convertVisibleLineToLogicalLine(visibleLine, side) if (-1 != logicalLine) { initializeByLogicalLine(reviewContext, logicalLine, side, comments) val renderer = findGutterIconRenderer(logicalLine, Side.LEFT) if (null !== renderer) { updateGutterIcon(renderer, comments) } } } override fun prepareLine(reviewContext: ReviewContext, renderer: GutterIconRenderer, comments: List) { initializeByLogicalLine(reviewContext, renderer.logicalLine, renderer.side, comments) } override fun createGutterIcons() { if (myGutterIconsCreated) { return } for (logicalLine in 0 until viewer.editor.document.lineCount) { val left = myLeftLineNumberConverter.execute(logicalLine) val right = myRightLineNumberConverter.execute(logicalLine) myCachedLeftLineNumbers[left] = logicalLine myCachedRightLineNumbers[right] = logicalLine registerGutterIconRenderer( GutterIconRendererFactory.makeGutterIconRenderer( viewer.editor.markupModel.addLineHighlighter(logicalLine, HighlighterLayer.LAST, null), projectServiceProvider.applicationSettings.showAddCommentIconsInDiffViewGutter && (-1 != left || -1 != right), logicalLine, visibleLineLeft = left + 1, visibleLineRight = right + 1, // Doesn't matter, unified view only have 1 side side = Side.LEFT, actionListener = myGutterIconRendererActionListener ) ) } myGutterIconsCreated = true } private fun findGutterIconRenderer(visibleLine: Int, side: Side, invoker: ((Int, GutterIconRenderer) -> Unit)) { val logicalLine = convertVisibleLineToLogicalLine(visibleLine, side) if (-1 == logicalLine) { return } // Doesn't matter, unified view only have 1 side // see exact comment above val renderer = findGutterIconRenderer(logicalLine, Side.LEFT) if (null !== renderer) { invoker.invoke(logicalLine, renderer) } } override fun updateComments(visibleLine: Int, side: Side, comments: List) { findGutterIconRenderer(visibleLine, side) { _, renderer -> updateComments(renderer, comments) } } override fun displayComments(renderer: GutterIconRenderer, mode: DiffView.DisplayCommentMode) { // for unified view the line distributed either on left/right, so we have to find both of them val thread = findThreadPresenter(renderer.logicalLine, renderer.side) if (null !== thread) { displayComments(thread.model, renderer.logicalLine, renderer.side, mode) } else { val oppositeSide = if (renderer.side == Side.LEFT) Side.RIGHT else Side.LEFT val oppositeThread = findThreadPresenter(renderer.logicalLine, oppositeSide) if (null !== oppositeThread) { displayComments(oppositeThread.model, renderer.logicalLine, oppositeSide, mode) } } } override fun scrollToPosition(position: CommentPosition, showComments: Boolean) { if (viewer.editor.isDisposed) { return } val oldLine = position.oldLine val newLine = position.newLine if (null !== newLine && null === oldLine) { return findGutterIconRenderer(newLine, Side.RIGHT) { logicalLine, renderer -> viewer.editor.scrollingModel.scrollTo(LogicalPosition(logicalLine, 0), ScrollType.MAKE_VISIBLE) if (showComments) { displayComments(renderer, DiffView.DisplayCommentMode.SHOW) } } } return findGutterIconRenderer(oldLine!!, Side.LEFT) { logicalLine, renderer -> viewer.editor.scrollingModel.scrollTo(LogicalPosition(logicalLine, 0), ScrollType.MAKE_VISIBLE) if (showComments) { displayComments(renderer, DiffView.DisplayCommentMode.SHOW) } } } private fun calcPosition(logicalLine: Int): GutterPosition { val left = myLeftLineNumberConverter.execute(logicalLine) + 1 val right = myRightLineNumberConverter.execute(logicalLine) + 1 return GutterPosition( editorType = DiffView.EditorType.UNIFIED, changeType = findChangeType(viewer.editor, logicalLine), oldLine = if (left > 0) left else -1, oldPath = change.beforeRevision!!.file.toString(), newLine = if (right > 0) right else -1, newPath = change.afterRevision!!.file.toString(), baseHash = change.beforeRevision!!.revisionNumber.asString(), headHash = change.afterRevision!!.revisionNumber.asString() ) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/exception/InvalidConnectionException.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.exception class InvalidConnectionException(message: String) : Exception(message) ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/AbstractApplicationServiceProvider.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure import com.intellij.ide.AppLifecycleListener import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project import com.intellij.openapi.project.impl.ProjectLifecycleListener import net.ntworld.mergeRequest.ProjectVisibility import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.ProviderInfo import net.ntworld.mergeRequest.ProviderStatus import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegrationIde.compatibility.IntellijIdeApi import net.ntworld.mergeRequestIntegrationIde.compatibility.* import net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ProviderSettingsImpl import net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ServiceBase import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsManager import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsManagerImpl import net.ntworld.mergeRequestIntegrationIde.watcher.WatcherManager import net.ntworld.mergeRequestIntegrationIde.watcher.WatcherManagerImpl import org.jdom.Element import java.net.URL abstract class AbstractApplicationServiceProvider : ApplicationServiceProvider, ServiceBase() { final override val watcherManager: WatcherManager = WatcherManagerImpl() private val myProjectServiceProviders = mutableSetOf() private val publicLegalGrantedDomains = listOf( "gitlab.com", "www.gitlab.com" ) private val legalGrantedDomains = listOf( "gitlab.personio-internal.de" ) private val myAppLifecycleListener = object : AppLifecycleListener { override fun appStarting(projectFromCommandLine: Project?) { if (null !== projectFromCommandLine) { findProjectServiceProvider(projectFromCommandLine).initialize() } } override fun appClosing() { watcherManager.dispose() } } private val myProjectLifecycleListener = object: ProjectLifecycleListener { override fun projectComponentsInitialized(project: Project) { findProjectServiceProvider(project).initialize() } } init { val connection = ApplicationManager.getApplication().messageBus.connect() connection.subscribe(AppLifecycleListener.TOPIC, myAppLifecycleListener) connection.subscribe(ProjectLifecycleListener.TOPIC, myProjectLifecycleListener) } protected fun registerProjectServiceProvider(projectServiceProvider: ProjectServiceProvider) { myProjectServiceProviders.add(projectServiceProvider) projectServiceProvider.providerStorage.updateApiOptions(settingsManager.toApiOptions()) } override val intellijIdeApi: IntellijIdeApi = Version203Adapter() override val settingsManager: ApplicationSettingsManager = ApplicationSettingsManagerImpl(::onSettingsChanged) override fun getAllProjectServiceProviders(): List { return myProjectServiceProviders.toList() } override fun getState(): Element? { val element = super.getState() if (null === element) { return element } settingsManager.writeTo(element) return element } override fun loadState(state: Element) { super.loadState(state) settingsManager.readFrom(state.children) } override fun addProviderConfiguration(id: String, info: ProviderInfo, credentials: ApiCredentials) { providerSettingsData[id] = ProviderSettingsImpl( id = id, info = info, credentials = encryptCredentials(info, credentials), repository = "" ) } override fun removeAllProviderConfigurations() { providerSettingsData.clear() this.state } override fun getProviderConfigurations(): List { return providerSettingsData.values.map { ProviderSettingsImpl( id = it.id, info = it.info, credentials = decryptCredentials(it.info, it.credentials), repository = "" ) } } override fun isLegal(providerData: ProviderData): Boolean { if (providerData.status == ProviderStatus.ERROR || providerData.project.url.isEmpty()) { return false } val url = URL(providerData.project.url) if (publicLegalGrantedDomains.contains(url.host) && providerData.project.visibility == ProjectVisibility.PUBLIC) { return true } return legalGrantedDomains.contains(url.host) } private fun onSettingsChanged(old: ApplicationSettings, new: ApplicationSettings) { getAllProjectServiceProviders().forEach { it.onApplicationSettingsChanged(old, new) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/AbstractProjectServiceProvider.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure import com.intellij.notification.NotificationDisplayType import com.intellij.notification.NotificationGroup import com.intellij.notification.NotificationType import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileEditor.FileEditorManagerEvent import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.vcs.BranchChangeListener import com.intellij.openapi.wm.ToolWindowManager import com.intellij.util.messages.MessageBus import net.ntworld.foundation.Infrastructure import net.ntworld.foundation.MemorizedInfrastructure import net.ntworld.foundation.util.UUIDGenerator import net.ntworld.mergeRequest.MergeRequest import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.ProviderInfo import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequest.request.PublishAllCommentsRequest import net.ntworld.mergeRequest.request.PublishCommentsRequest import net.ntworld.mergeRequestIntegration.DefaultProviderStorage import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegration.provider.MemoryCache import net.ntworld.mergeRequestIntegration.provider.ProviderException import net.ntworld.mergeRequestIntegration.provider.github.Github import net.ntworld.mergeRequestIntegration.provider.gitlab.Gitlab import net.ntworld.mergeRequestIntegrationIde.ComponentFactory import net.ntworld.mergeRequestIntegrationIde.DefaultComponentFactory import net.ntworld.mergeRequestIntegrationIde.IdeInfrastructure import net.ntworld.mergeRequestIntegrationIde.compatibility.IntellijIdeApi import net.ntworld.mergeRequestIntegrationIde.debug import net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ProviderSettingsImpl import net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ReviewContextManagerImpl import net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ServiceBase import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.MergeRequestDataNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkEditorNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.SingleMRToolWindowNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.provider.MergeRequestDataProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.service.FiltersStorageService import net.ntworld.mergeRequestIntegrationIde.infrastructure.service.RepositoryFileService import net.ntworld.mergeRequestIntegrationIde.infrastructure.service.internal.FiltersStorageServiceImpl import net.ntworld.mergeRequestIntegrationIde.infrastructure.service.repositoryFile.CachedRepositoryFile import net.ntworld.mergeRequestIntegrationIde.infrastructure.service.repositoryFile.LocalRepositoryFileService import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings import net.ntworld.mergeRequestIntegrationIde.rework.ReworkEditorManager import net.ntworld.mergeRequestIntegrationIde.rework.ReworkManager import net.ntworld.mergeRequestIntegrationIde.rework.internal.ReworkEditorManagerImpl import net.ntworld.mergeRequestIntegrationIde.rework.internal.ReworkManagerImpl import net.ntworld.mergeRequestIntegrationIde.task.RegisterProviderTask import net.ntworld.mergeRequestIntegrationIde.ui.configuration.GithubConnectionsConfigurableBase import net.ntworld.mergeRequestIntegrationIde.ui.configuration.GitlabConnectionsConfigurableBase import org.jdom.Element import com.intellij.openapi.project.Project as IdeaProject abstract class AbstractProjectServiceProvider( final override val project: IdeaProject ) : ProjectServiceProvider, ServiceBase() { private val myNotification: NotificationGroup = NotificationGroup( "Merge Request Integration", NotificationDisplayType.BALLOON, true ) private var myIsInitialized = false final override val providerStorage: ProviderStorage = DefaultProviderStorage() override val infrastructure: Infrastructure = MemorizedInfrastructure(IdeInfrastructure(providerStorage)) override val applicationSettings: ApplicationSettings get() = applicationServiceProvider.settingsManager override val intellijIdeApi: IntellijIdeApi get() = applicationServiceProvider.intellijIdeApi final override val messageBus: MessageBus by lazy { project.messageBus } override val repositoryFile: RepositoryFileService by lazy { CachedRepositoryFile( LocalRepositoryFileService(this), MemoryCache() ) } override val filtersStorage: FiltersStorageService = FiltersStorageServiceImpl(this) override val reviewContextManager: ReviewContextManager = ReviewContextManagerImpl(this) override val projectNotifierTopic: ProjectNotifier = messageBus.syncPublisher(ProjectNotifier.TOPIC) override val singleMRToolWindowNotifierTopic = messageBus.syncPublisher(SingleMRToolWindowNotifier.TOPIC) override val reworkManager: ReworkManager = ReworkManagerImpl(this) final override val reworkEditorManager: ReworkEditorManager = ReworkEditorManagerImpl(this) override val componentFactory: ComponentFactory = DefaultComponentFactory(this) private val myBranchChangeListener = object: BranchChangeListener { override fun branchWillChange(branchName: String) { } override fun branchHasChanged(branchName: String) { debug("BranchChangeListener triggered, request create ReworkWatcher for $branchName") reworkManager.requestCreateReworkWatcher(providerStorage.registeredProviders, branchName) } } private val myFileEditorManagerListener = object: FileEditorManagerListener { override fun selectionChanged(event: FileEditorManagerEvent) { val editor = event.newEditor if (editor !is TextEditor) { return } val providers = providerStorage.registeredProviders for (provider in providers) { val reworkWatcher = reworkManager.findActiveReworkWatcher(provider) if (null !== reworkWatcher) { ApplicationManager.getApplication().invokeLater { reworkEditorManager.bootstrap(editor, reworkWatcher) } } } } } init { messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, myFileEditorManagerListener) messageBus.connect().subscribe(ReworkEditorNotifier.TOPIC, reworkEditorManager) } protected fun initWithApplicationServiceProvider(applicationSP: ApplicationServiceProvider) { applicationSP.watcherManager.addWatcher(reviewContextManager) val connection = messageBus.connect(project) connection.subscribe(MergeRequestDataNotifier.TOPIC, MergeRequestDataProvider(this, messageBus)) connection.subscribe(BranchChangeListener.VCS_BRANCH_CHANGED, myBranchChangeListener) } override fun readStateItem(item: Element, id: String, settings: ProviderSettings) { super.readStateItem(item, id, settings) filtersStorage.readFrom(item, id) } override fun writeStateItem(item: Element, id: String, settings: ProviderSettings) { super.writeStateItem(item, id, settings) filtersStorage.writeTo(item, id) } override fun addProviderConfiguration( id: String, info: ProviderInfo, credentials: ApiCredentials, repository: String ) { providerSettingsData[id] = ProviderSettingsImpl( id = id, info = info, credentials = encryptCredentials(info, credentials), repository = repository ) } override fun removeProviderConfiguration(id: String) { providerSettingsData.remove(id) } override fun getProviderConfigurations(): List { return providerSettingsData.values.map { ProviderSettingsImpl( id = it.id, info = it.info, credentials = decryptCredentials(it.info, it.credentials), repository = it.repository ) } } override fun initialize() { myIsInitialized = false providerStorage.clear() reworkManager.clear() projectNotifierTopic.starting() getProviderConfigurations().forEach { registerProviderSettings(it) } projectNotifierTopic.initialized() myIsInitialized = true } override fun isInitialized(): Boolean { return myIsInitialized } override fun openSingleMRToolWindow(invoker: (() -> Unit)?) { val toolWindow = ToolWindowManager.getInstance(project).getToolWindow( applicationServiceProvider.singleMRToolWindowName ) if (null !== toolWindow) { toolWindow.show(invoker) } } override fun hideSingleMRToolWindow(invoker: (() -> Unit)?) { val toolWindow = ToolWindowManager.getInstance(project).getToolWindow( applicationServiceProvider.singleMRToolWindowName ) if (null !== toolWindow) { toolWindow.hide(invoker) } } override fun isDoingCodeReview(): Boolean = null !== reviewContextManager.findDoingCodeReviewContext() override fun onApplicationSettingsChanged(old: ApplicationSettings, new: ApplicationSettings) { if (!old.enableReworkProcess && new.enableReworkProcess) { for (provider in providerStorage.registeredProviders) { reworkManager.createBranchWatcher(provider) } } } override fun startCodeReview(providerData: ProviderData, mergeRequest: MergeRequest) { reviewContextManager.setContextToDoingCodeReview(providerData.id, mergeRequest.id) val reviewContext = reviewContextManager.findDoingCodeReviewContext() if (null !== reviewContext) { projectNotifierTopic.startCodeReview(reviewContext) openSingleMRToolWindow { singleMRToolWindowNotifierTopic.showChangesWhenDoingCodeReview( reviewContext.providerData, reviewContext.changes ) } } } override fun stopCodeReview() { val reviewContext = reviewContextManager.findDoingCodeReviewContext() if (null !== reviewContext) { infrastructure.serviceBus() process PublishAllCommentsRequest.make( providerId = reviewContext.providerData.id, mergeRequestId = reviewContext.mergeRequestInfo.id ) ifError { notify( "There was an error from server. \n\n ${it.message}", NotificationType.ERROR ) throw ProviderException(it) } projectNotifierTopic.stopCodeReview(reviewContext) reviewContext.closeAllChanges() hideSingleMRToolWindow { singleMRToolWindowNotifierTopic.hideChangesAfterDoingCodeReview() singleMRToolWindowNotifierTopic.clearReworkTabs() } } println("After doing code review") providerStorage.registeredProviders.forEach { reworkManager.createBranchWatcher(it) } reviewContextManager.clearContextDoingCodeReview() } override fun notify(message: String) { notify(message, NotificationType.INFORMATION) } override fun notify(message: String, type: NotificationType) { val notification = myNotification.createNotification(message, type) notification.notify(project) } private fun registerProviderSettings(settings: ProviderSettings) { var name = "" if (settings.info.id == Gitlab.id) { name = GitlabConnectionsConfigurableBase.findNameFromId(settings.id) } if (settings.info.id == Github.id) { name = GithubConnectionsConfigurableBase.findNameFromId(settings.id) } val task = RegisterProviderTask( this, id = UUIDGenerator.generate(), name = name, settings = settings, listener = object : RegisterProviderTask.Listener { override fun providerRegistered(providerData: ProviderData) { projectNotifierTopic.providerRegistered(providerData) reworkManager.createBranchWatcher(providerData) } } ) task.start() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/ApplicationServiceProvider.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure import com.intellij.openapi.project.Project import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.ProviderInfo import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegrationIde.compatibility.IntellijIdeApi import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsManager import net.ntworld.mergeRequestIntegrationIde.watcher.WatcherManager interface ApplicationServiceProvider { val intellijIdeApi: IntellijIdeApi val settingsManager: ApplicationSettingsManager val watcherManager: WatcherManager val singleMRToolWindowName: String fun findProjectServiceProvider(project: Project): ProjectServiceProvider fun addProviderConfiguration(id: String, info: ProviderInfo, credentials: ApiCredentials) fun removeAllProviderConfigurations() fun getProviderConfigurations(): List fun isLegal(providerData: ProviderData): Boolean fun getAllProjectServiceProviders(): List } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/ProjectServiceProvider.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure import com.intellij.notification.NotificationType import com.intellij.util.messages.MessageBus import net.ntworld.foundation.Infrastructure import net.ntworld.mergeRequest.MergeRequest import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.ProviderInfo import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.ProviderStorage import net.ntworld.mergeRequestIntegrationIde.ComponentFactory import net.ntworld.mergeRequestIntegrationIde.compatibility.IntellijIdeApi import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.SingleMRToolWindowNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.service.FiltersStorageService import net.ntworld.mergeRequestIntegrationIde.infrastructure.service.RepositoryFileService import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings import net.ntworld.mergeRequestIntegrationIde.rework.ReworkEditorManager import net.ntworld.mergeRequestIntegrationIde.rework.ReworkManager import com.intellij.openapi.project.Project as IdeaProject interface ProjectServiceProvider { val applicationServiceProvider: ApplicationServiceProvider val infrastructure: Infrastructure val providerStorage: ProviderStorage val applicationSettings: ApplicationSettings val intellijIdeApi: IntellijIdeApi val project: IdeaProject val messageBus: MessageBus val repositoryFile: RepositoryFileService val reworkManager: ReworkManager val reworkEditorManager: ReworkEditorManager val filtersStorage: FiltersStorageService val reviewContextManager: ReviewContextManager val projectNotifierTopic: ProjectNotifier val singleMRToolWindowNotifierTopic: SingleMRToolWindowNotifier val componentFactory: ComponentFactory fun addProviderConfiguration(id: String, info: ProviderInfo, credentials: ApiCredentials, repository: String) fun removeProviderConfiguration(id: String) fun getProviderConfigurations(): List fun openSingleMRToolWindow(invoker: (() -> Unit)?) fun hideSingleMRToolWindow(invoker: (() -> Unit)?) fun initialize() fun isInitialized(): Boolean fun startCodeReview(providerData: ProviderData, mergeRequest: MergeRequest) fun stopCodeReview() fun isDoingCodeReview(): Boolean fun onApplicationSettingsChanged(old: ApplicationSettings, new: ApplicationSettings) fun notify(message: String) fun notify(message: String, type: NotificationType) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/ProviderSettings.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure import net.ntworld.mergeRequest.ProviderInfo import net.ntworld.mergeRequest.api.ApiCredentials interface ProviderSettings { val id: String val info: ProviderInfo val credentials: ApiCredentials val repository: String val sharable: Boolean } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/ReviewContext.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key import com.intellij.openapi.vcs.changes.Change import com.intellij.util.messages.MessageBusConnection import git4idea.repo.GitRepository import net.ntworld.mergeRequest.* interface ReviewContext { val project: Project val providerData: ProviderData val mergeRequestInfo: MergeRequestInfo val messageBusConnection: MessageBusConnection val diffReference: DiffReference? val repository: GitRepository? val commits: List val comments: List val changes: List val reviewingCommits: List val reviewingChanges: List fun findChangeByPath(path: String): Change? fun getCommentsByPath(path: String): List fun openChange(change: Change, focus: Boolean, displayMergeRequestId: Boolean) fun hasAnyChangeOpened(): Boolean fun closeAllChanges() fun getChangeData(change: Change, key: Key): T? fun putChangeData(change: Change, key: Key, value: T?) companion object { val KEY = Key.create("mri.ReviewContext") } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/ReviewContextManager.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure import com.intellij.openapi.vcs.changes.Change import net.ntworld.mergeRequest.* import net.ntworld.mergeRequestIntegrationIde.watcher.Watcher interface ReviewContextManager : Watcher { fun initContext(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, selected: Boolean) fun isDoingCodeReview(providerId: String, mergeRequestId: String): Boolean fun findDoingCodeReviewContext(): ReviewContext? fun findSelectedContext(): ReviewContext? fun findContext(providerId: String, mergeRequestId: String): ReviewContext? fun setSelected(providerId: String, mergeRequestId: String) fun clearContextDoingCodeReview() fun setContextToDoingCodeReview(providerId: String, mergeRequestId: String) fun getDraftCommentsCount(providerId: String, mergeRequestId: String): Int fun updateComments(providerId: String, mergeRequestId: String, comments: List) fun updateCommits(providerId: String, mergeRequestId: String, commits: List) fun updateChanges(providerId: String, mergeRequestId: String, changes: List) fun updateReviewingCommits(providerId: String, mergeRequestId: String, commits: List) fun updateReviewingChanges(providerId: String, mergeRequestId: String, changes: List) fun updateMergeRequest(providerId: String, mergeRequest: MergeRequest) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/ApiCredentialsImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal import net.ntworld.mergeRequest.api.ApiCredentials data class ApiCredentialsImpl( override val url: String, override val login: String, override val token: String, override val projectId: String, override val version: String, override val info: String, override val ignoreSSLCertificateErrors: Boolean ): ApiCredentials ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/DiffPreviewProviderImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal import com.intellij.diff.impl.DiffRequestProcessor import com.intellij.openapi.project.Project import com.intellij.openapi.vcs.changes.Change import com.intellij.openapi.vcs.changes.ChangesUtil import com.intellij.openapi.vcs.changes.DiffPreviewProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext class DiffPreviewProviderImpl( private val project: Project, val change: Change, val reviewContext: ReviewContext? = null, private val showMergeRequestId: Boolean ) : DiffPreviewProvider { override fun getOwner(): Any { return this } override fun createDiffRequestProcessor(): DiffRequestProcessor { return DiffRequestProcessorImpl(project, change, reviewContext) } override fun getEditorTabName(): String { if (null !== reviewContext && showMergeRequestId) { return "!${reviewContext.mergeRequestInfo.id}: ${ChangesUtil.getFilePath(change).name}" } return ChangesUtil.getFilePath(change).name } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/DiffRequestProcessorImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal import com.intellij.diff.chains.DiffRequestProducer import com.intellij.diff.impl.CacheDiffRequestProcessor import com.intellij.diff.requests.NoDiffRequest import com.intellij.diff.util.DiffPlaces import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key import com.intellij.openapi.vcs.changes.Change import com.intellij.openapi.vcs.changes.DiffPreviewUpdateProcessor import com.intellij.vcs.log.ui.frame.VcsLogChangesBrowser import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext class DiffRequestProcessorImpl( project: Project, private val change: Change, val reviewContext: ReviewContext? = null ) : CacheDiffRequestProcessor.Simple(project, DiffPlaces.DEFAULT), DiffPreviewUpdateProcessor { override fun clear() { applyRequest(NoDiffRequest.INSTANCE, false, null) } override fun getFastLoadingTimeMillis(): Int { return 10 } override fun refresh(fromModelRefresh: Boolean) { updateRequest() } override fun getCurrentRequestProvider(): DiffRequestProducer? { val context = if (null === reviewContext) { HashMap() } else { mutableMapOf, Any>( ReviewContext.KEY to reviewContext ) } return VcsLogChangesBrowser.createDiffRequestProducer(project!!, change, context, true) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/ProviderSettingsImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal import net.ntworld.mergeRequest.ProviderInfo import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProviderSettings data class ProviderSettingsImpl( override val id: String, override val info: ProviderInfo, override val credentials: ApiCredentials, override val repository: String, override val sharable: Boolean = false ) : ProviderSettings ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/ReviewContextImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key import com.intellij.openapi.util.UserDataHolderBase import com.intellij.openapi.vcs.changes.Change import com.intellij.openapi.vcs.changes.ChangesUtil import com.intellij.openapi.vcs.changes.PreviewDiffVirtualFile import com.intellij.util.messages.MessageBusConnection import git4idea.repo.GitRepository import net.ntworld.mergeRequest.* import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext import net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil class ReviewContextImpl( val projectServiceProvider: ProjectServiceProvider, override val providerData: ProviderData, override val mergeRequestInfo: MergeRequestInfo, override val messageBusConnection: MessageBusConnection ) : ReviewContext { private val myLogger = Logger.getInstance(this.javaClass) // private val myPreviewDiffVirtualFileMap = mutableMapOf() private val myPreviewDiffVirtualFileMap = mutableMapOf() private val myCommentsMap = mutableMapOf>() private val myChangesMap = mutableMapOf>() private val myChangesDataMap = mutableMapOf() override val project: Project = projectServiceProvider.project override var diffReference: DiffReference? = null override val repository: GitRepository? = RepositoryUtil.findRepository(projectServiceProvider, providerData) override var commits: List = listOf() override var comments: List = listOf() set(value) { field = value buildCommentsMap(value) } override var changes: List = listOf() set(value) { field = value buildChangesMap(value) } override var reviewingCommits: List = listOf() override var reviewingChanges: List = listOf() override fun findChangeByPath(path: String): Change? { val absolutePath = RepositoryUtil.findAbsoluteCrossPlatformsPath(repository, path) val changes = myChangesMap[absolutePath] if (null === changes) { return null } // TODO: do something when has more than 1 change if (changes.size > 1) { myLogger.info("There is more than 1 changes for $path") } return changes.first() } override fun getCommentsByPath(path: String): List { val crossPlatformsPath = RepositoryUtil.transformToCrossPlatformsPath(path) val comments = myCommentsMap[crossPlatformsPath] if (null !== comments) { return comments } myLogger.info("There is no comments for $crossPlatformsPath") return listOf() } override fun openChange(change: Change, focus: Boolean, displayMergeRequestId: Boolean) { val diffFile = myPreviewDiffVirtualFileMap[change.hashCode()] if (null === diffFile) { val provider = DiffPreviewProviderImpl(project, change, this, displayMergeRequestId) val created = PreviewDiffVirtualFile(provider) myPreviewDiffVirtualFileMap[change.hashCode()] = created FileEditorManagerEx.getInstanceEx(project).openFile(created, focus) } else { FileEditorManagerEx.getInstanceEx(project).openFile(diffFile, focus) } } override fun hasAnyChangeOpened(): Boolean { return myPreviewDiffVirtualFileMap.isNotEmpty() } override fun closeAllChanges() { val fileEditorManagerEx = FileEditorManagerEx.getInstanceEx(project) myPreviewDiffVirtualFileMap.forEach { (_, diffFile) -> fileEditorManagerEx.closeFile(diffFile) } myPreviewDiffVirtualFileMap.clear() } override fun getChangeData(change: Change, key: Key): T? { val userDataHolder = myChangesDataMap[change] return if (null !== userDataHolder) { userDataHolder.getUserData(key) } else null } override fun putChangeData(change: Change, key: Key, value: T?) { val userDataHolder = myChangesDataMap[change] if (null === userDataHolder) { val dataHolder = UserDataHolderBase() dataHolder.putUserData(key, value) myChangesDataMap[change] = dataHolder } else { userDataHolder.putUserData(key, value) } } private fun buildCommentsMap(value: Collection) { if (null === repository) { return } myCommentsMap.clear() for (comment in value) { val position = comment.position if (null === position) { continue } if (null !== position.newPath) { doHashComment(repository, position.newPath!!, comment) } if (null !== position.oldPath) { doHashComment(repository, position.oldPath!!, comment) } } myLogger.info("myCommentsMap was built successfully") myCommentsMap.forEach { (path, comments) -> val commentIds = comments.map { it.id } myLogger.info("$path contains ${commentIds.joinToString(",")}") } } private fun doHashComment(repository: GitRepository, path: String, comment: Comment) { val fullPath = RepositoryUtil.findAbsoluteCrossPlatformsPath(repository, path) val list = myCommentsMap[fullPath] if (null === list) { myCommentsMap[fullPath] = mutableListOf(comment) } else { if (!list.contains(comment)) { list.add(comment) } } } private fun buildChangesMap(value: Collection) { myChangesMap.clear() for (change in value) { val filePaths = ChangesUtil.getPathsCaseSensitive(change) for (filePath in filePaths) { val path = filePath.path val list = myChangesMap.get(path) if (null === list) { myChangesMap[path] = mutableListOf(change) } else { if (!list.contains(change)) { list.add(change) } } } } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/ReviewContextManagerImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.vcs.changes.Change import net.ntworld.mergeRequest.* import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContextManager class ReviewContextManagerImpl( private val projectServiceProvider: ProjectServiceProvider ) : ReviewContextManager { private val myLogger = Logger.getInstance(this.javaClass) private val myContexts = mutableMapOf() private var mySelected: String? = null private var myDoingCodeReviewContext: String? = null override val interval: Long = 60000 private fun keyOf(providerId: String, mergeRequestId: String) = "$providerId:$mergeRequestId" override fun initContext(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, selected: Boolean) { val key = keyOf(providerData.id, mergeRequestInfo.id) myLogger.info("Init context $key") if (!myContexts.contains(key)) { myContexts[key] = ReviewContextImpl( projectServiceProvider, providerData, mergeRequestInfo, projectServiceProvider.messageBus.connect() ) } if (selected) { mySelected = key } } override fun isDoingCodeReview(providerId: String, mergeRequestId: String): Boolean { val key = myDoingCodeReviewContext return if (null === key) false else keyOf(providerId, mergeRequestId) == key } override fun findSelectedContext() : ReviewContext? { val key = mySelected if (null === key) { return null } return myContexts[key] } override fun findDoingCodeReviewContext(): ReviewContext? { val key = myDoingCodeReviewContext if (null === key) { return null } return myContexts[key] } override fun findContext(providerId: String, mergeRequestId: String): ReviewContext? { return myContexts[keyOf(providerId, mergeRequestId)] } override fun setSelected(providerId: String, mergeRequestId: String) { mySelected = keyOf(providerId, mergeRequestId) } override fun clearContextDoingCodeReview() { myDoingCodeReviewContext = null } override fun setContextToDoingCodeReview(providerId: String, mergeRequestId: String) { myDoingCodeReviewContext = keyOf(providerId, mergeRequestId) } override fun getDraftCommentsCount(providerId: String, mergeRequestId: String): Int { val key = keyOf(providerId, mergeRequestId) val context = myContexts[key] if (null !== context) { return context.comments.filter { it.isDraft }.count() } return 0 } override fun updateComments(providerId: String, mergeRequestId: String, comments: List) { val key = keyOf(providerId, mergeRequestId) val context = myContexts[key] if (null !== context) { myLogger.info("Update comments for $key") context.comments = comments } } override fun updateCommits(providerId: String, mergeRequestId: String, commits: List) { val key = keyOf(providerId, mergeRequestId) val context = myContexts[key] if (null !== context) { myLogger.info("Update commits for $key") context.commits = commits } } override fun updateChanges(providerId: String, mergeRequestId: String, changes: List) { val key = keyOf(providerId, mergeRequestId) val context = myContexts[key] if (null !== context) { myLogger.info("Update changes for $key") context.changes = changes } } override fun updateReviewingCommits(providerId: String, mergeRequestId: String, commits: List) { val key = keyOf(providerId, mergeRequestId) val context = myContexts[key] if (null !== context) { myLogger.info("Update reviewingCommits for $key") context.reviewingCommits = commits } } override fun updateReviewingChanges(providerId: String, mergeRequestId: String, changes: List) { val key = keyOf(providerId, mergeRequestId) val context = myContexts[key] if (null !== context) { myLogger.info("Update reviewingChanges for $key") context.reviewingChanges = changes } } override fun updateMergeRequest(providerId: String, mergeRequest: MergeRequest) { val key = keyOf(providerId, mergeRequest.id) val context = myContexts[key] if (null !== context) { myLogger.info("Update diffReference for $key") context.diffReference = mergeRequest.diffReference } } override fun canExecute(): Boolean { return myContexts.isNotEmpty() } override fun shouldTerminate(): Boolean { return false } override fun execute() { val ids = myContexts .filter { it.key != mySelected && it.key != myDoingCodeReviewContext && !it.value.hasAnyChangeOpened() } .map { it.key } if (ids.isNotEmpty()) { ids.forEach { val context = myContexts.remove(it) if (null !== context) { context.messageBusConnection.disconnect() } } myLogger.info("Clear inactive contexts Id: ${ids.joinToString(", ")}") } } override fun terminate() { } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/ServiceBase.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal import com.intellij.credentialStore.CredentialAttributes import com.intellij.ide.passwordSafe.PasswordSafe import com.intellij.openapi.components.PersistentStateComponent import net.ntworld.mergeRequest.ProviderInfo import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.Gitlab import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProviderSettings import org.jdom.Element open class ServiceBase : PersistentStateComponent { protected val providerSettingsData = mutableMapOf() private val supportedProviders: List = listOf( Gitlab // Gitlab, Github ) override fun getState(): Element? { val element = Element("Provider") providerSettingsData.values.map { val item = Element("Item") item.setAttribute("id", it.id) writeStateItem(item, it.id, it) element.addContent(item) } return element } protected open fun writeStateItem(item: Element, id: String, settings: ProviderSettings) { item.setAttribute("providerId", settings.info.id) item.setAttribute("url", settings.credentials.url) item.setAttribute("login", settings.credentials.login) item.setAttribute("projectId", settings.credentials.projectId) item.setAttribute("version", settings.credentials.version) item.setAttribute("info", settings.credentials.info) item.setAttribute("ignoreSSLCertificateErrors", if (settings.credentials.ignoreSSLCertificateErrors) "1" else "0") item.setAttribute("repository", settings.repository) } override fun loadState(state: Element) { for (item in state.children) { if (item.name != "Item") { continue } val info = supportedProviders.firstOrNull { it.id == item.getAttribute("providerId").value } if (null === info) { continue } val credentials = ApiCredentialsImpl( url = item.getAttribute("url").value, login = item.getAttribute("login").value, token = "", projectId = item.getAttribute("projectId").value, version = item.getAttribute("version").value, info = item.getAttribute("info").value, ignoreSSLCertificateErrors = shouldIgnoreSSLCertificateErrors(item) ) val id = item.getAttribute("id").value val settings = ProviderSettingsImpl( id = id.trim(), info = info, credentials = decryptCredentials(info, credentials), repository = item.getAttribute("repository").value ) readStateItem(item, id, settings) } } protected open fun readStateItem(item: Element, id: String, settings: ProviderSettings) { providerSettingsData[id] = settings } private fun shouldIgnoreSSLCertificateErrors(item: Element): Boolean { val attribute = item.getAttribute("ignoreSSLCertificateErrors") if (null === attribute) { return false } return attribute.value == "1" || attribute.value.toLowerCase() == "true" } protected fun encryptCredentials(info: ProviderInfo, credentials: ApiCredentials): ApiCredentials { encryptPassword(info, credentials, credentials.token) return ApiCredentialsImpl( url = credentials.url, // ----------------------------------------------------------------- // Always bind login and token because if we don't the token will be // empty if the state not stored to the storage yet. // It's safe because the secret is stored on memory only. login = credentials.login, token = credentials.token, // ----------------------------------------------------------------- projectId = credentials.projectId, version = credentials.version, info = credentials.info, ignoreSSLCertificateErrors = credentials.ignoreSSLCertificateErrors ) } protected fun decryptCredentials(info: ProviderInfo, credentials: ApiCredentials): ApiCredentials { return ApiCredentialsImpl( url = credentials.url, // ----------------------------------------------------------------- // Always bind login and token because if we don't the token will be // empty if the state not stored to the storage yet. // It's safe because the secret is stored on memory only. login = credentials.login, token = decryptPassword(info, credentials) ?: credentials.token, // ----------------------------------------------------------------- projectId = credentials.projectId, version = credentials.version, info = credentials.info, ignoreSSLCertificateErrors = credentials.ignoreSSLCertificateErrors ) } private fun encryptPassword(info: ProviderInfo, credentials: ApiCredentials, password: String) { PasswordSafe.instance.setPassword(makeCredentialAttribute(info, credentials), password) } private fun decryptPassword(info: ProviderInfo, credentials: ApiCredentials): String? { val password = PasswordSafe.instance.getPassword(makeCredentialAttribute(info, credentials)) if (null === password || password.isEmpty()) { // Handle legacy CredentialAttribute return PasswordSafe.instance.getPassword(makeLegacyCredentialAttribute(info, credentials)) } return password } /** * For Windows, Intellij is using KeePass which have a 36 chars limitation on the group name, therefore I have * to shorten the group name since v2019.3.3 */ private fun makeCredentialAttribute(info: ProviderInfo, credentials: ApiCredentials): CredentialAttributes { if (credentials.url == credentials.login) { return CredentialAttributes("MRI:${info.id}", credentials.url) } return CredentialAttributes("MRI:${info.id}", "${credentials.login}:${credentials.url}") } /** * I have to keep legacy credential attribute otherwise current users have to input the token again * which is not available anymore. I meant can't see the token again after refreshing Gitlab's page. */ private fun makeLegacyCredentialAttribute(info: ProviderInfo, credentials: ApiCredentials): CredentialAttributes { return CredentialAttributes( "MRI - ${info.id} - ${credentials.url} - ${credentials.login}" ) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/DiffNotifier.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier import com.intellij.openapi.util.Key import com.intellij.openapi.vcs.changes.Change import com.intellij.util.messages.Topic import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext interface DiffNotifier { companion object { val TOPIC = Topic.create("MRI:DiffNotifier", DiffNotifier::class.java) val ScrollPosition = Key.create("DiffNotifier.ScrollPosition") val ScrollShowComments = Key.create("DiffNotifier.ScrollShowComments") } fun scrollToPositionRequested( reviewContext: ReviewContext, change: Change, position: CommentPosition, showComments: Boolean? ) fun hideAllCommentsRequested(reviewContext: ReviewContext, change: Change) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/MergeRequestDataNotifier.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier import com.intellij.util.messages.Topic import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData interface MergeRequestDataNotifier { companion object { val TOPIC = Topic.create("MRI:MergeRequestDataNotifier", MergeRequestDataNotifier::class.java) } fun fetchCommentsRequested(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) fun onCommentsUpdated(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/ProjectNotifier.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier import com.intellij.util.messages.Topic import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings interface ProjectNotifier { companion object { val TOPIC = Topic.create("MRI:ProjectNotifier", ProjectNotifier::class.java) } fun starting() fun initialized() fun providerRegistered(providerData: ProviderData) fun startCodeReview(reviewContext: ReviewContext) fun stopCodeReview(reviewContext: ReviewContext) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/ProjectNotifierAdapter.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext open class ProjectNotifierAdapter : ProjectNotifier { override fun starting() { } override fun initialized() { } override fun providerRegistered(providerData: ProviderData) { } override fun startCodeReview(reviewContext: ReviewContext) { } override fun stopCodeReview(reviewContext: ReviewContext) { } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/ReworkEditorNotifier.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier import com.intellij.openapi.fileEditor.TextEditor import com.intellij.util.messages.Topic import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher interface ReworkEditorNotifier { companion object { val TOPIC = Topic.create("MRI:ReworkEditorNotifier", ReworkEditorNotifier::class.java) } fun bootstrap(reworkWatcher: ReworkWatcher) fun bootstrap(editor: TextEditor, reworkWatcher: ReworkWatcher) fun open(providerData: ProviderData, path: String, line: Int? = null) fun commentsUpdated(providerData: ProviderData) fun shutdown(providerData: ProviderData) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/ReworkWatcherNotifier.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier import com.intellij.util.messages.Topic import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeView import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.Node interface ReworkWatcherNotifier { companion object { val TOPIC = Topic.create("MRI:ReworkWatcherNotifier", ReworkWatcherNotifier::class.java) } fun requestFetchComment(providerData: ProviderData) fun changeDisplayResolvedComments(providerData: ProviderData, value: Boolean) fun changeOnlyShowDraftComments(providerData: ProviderData, value: Boolean) fun commentTreeNodeSelected(providerData: ProviderData, node: Node, type: CommentTreeView.TreeSelectType) fun openCreateGeneralCommentForm(providerData: ProviderData) fun requestReplyComment(providerData: ProviderData, content: String, repliedComment: Comment) fun requestCreateComment(providerData: ProviderData, content: String, position: GutterPosition?, isDraft: Boolean) fun requestEditComment(providerData: ProviderData, comment: Comment, content: String) fun requestPublishComment(providerData: ProviderData, comment: Comment) fun requestDeleteComment(providerData: ProviderData, comment: Comment) fun requestResolveComment(providerData: ProviderData, comment: Comment) fun requestUnresolveComment(providerData: ProviderData, comment: Comment) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/SingleMRToolWindowNotifier.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier import com.intellij.openapi.vcs.changes.Change import com.intellij.util.messages.Topic import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher interface SingleMRToolWindowNotifier { companion object { val TOPIC = Topic.create("MRI:SingleMRToolWindowNotifier", SingleMRToolWindowNotifier::class.java) } fun registerReworkWatcher(reworkWatcher: ReworkWatcher) fun removeReworkWatcher(reworkWatcher: ReworkWatcher) fun hideChangesAfterDoingCodeReview() fun clearReworkTabs() fun showChangesWhenDoingCodeReview(providerData: ProviderData, changes: List) fun showReworkChanges(reworkWatcher: ReworkWatcher, changes: List) fun showReworkComments(reworkWatcher: ReworkWatcher, comments: List, displayResolvedComments: Boolean) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/provider/MergeRequestDataProvider.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.provider import com.intellij.openapi.application.ApplicationManager import com.intellij.util.messages.MessageBus import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.MergeRequestDataNotifier import net.ntworld.mergeRequestIntegrationIde.task.GetCommentsTask class MergeRequestDataProvider( private val projectServiceProvider: ProjectServiceProvider, private val messageBus: MessageBus ) : MergeRequestDataNotifier { private val getCommentsTaskListener = object : GetCommentsTask.Listener { override fun dataReceived( providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List ) { ApplicationManager.getApplication().invokeLater { messageBus.syncPublisher(MergeRequestDataNotifier.TOPIC) .onCommentsUpdated(providerData, mergeRequestInfo, comments) } } } override fun fetchCommentsRequested(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) { val task = GetCommentsTask( projectServiceProvider, providerData, mergeRequestInfo, getCommentsTaskListener ) task.start() } override fun onCommentsUpdated(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List) { } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/service/FiltersStorageService.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.service import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import org.jdom.Element interface FiltersStorageService { fun find(providerDataKey: String): Pair fun save(providerDataKey: String, filters: GetMergeRequestFilter, ordering: MergeRequestOrdering) fun writeTo(element: Element, providerDataKey: String) fun readFrom(element: Element, providerDataKey: String) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/service/RepositoryFileService.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.service import com.intellij.openapi.vcs.changes.Change import net.ntworld.mergeRequest.ProviderData import javax.swing.Icon interface RepositoryFileService { fun findChanges(providerData: ProviderData, hashes: List): List fun findIcon(providerData: ProviderData, path: String) : Icon } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/service/internal/FiltersStorageServiceImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.service.internal import net.ntworld.mergeRequest.MergeRequestState import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequest.query.generated.GetMergeRequestFilterImpl import net.ntworld.mergeRequestIntegration.util.SavedFiltersUtil import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.service.FiltersStorageService import org.jdom.Element class FiltersStorageServiceImpl( private val serviceProvider: ProjectServiceProvider ) : FiltersStorageService { private val myFiltersData: MutableMap> = mutableMapOf() override fun find(providerDataKey: String): Pair { val data = myFiltersData[providerDataKey] return if (null !== data && serviceProvider.applicationSettings.saveMRFilterState) { data } else { Pair( GetMergeRequestFilterImpl( id = null, state = MergeRequestState.OPENED, search = "", authorId = "", assigneeId = "", approverIds = listOf(""), sourceBranch = "" ), MergeRequestOrdering.RECENTLY_UPDATED ) } } override fun save(providerDataKey: String, filters: GetMergeRequestFilter, ordering: MergeRequestOrdering) { if (serviceProvider.applicationSettings.saveMRFilterState) { myFiltersData[providerDataKey] = Pair(filters, ordering) } } override fun readFrom(element: Element, providerDataKey: String) { val attribute = element.getAttribute("savedFilters") if (null !== attribute) { val data = SavedFiltersUtil.parse(attribute.value) if (null !== data) { myFiltersData[providerDataKey] = data } } } override fun writeTo(element: Element, providerDataKey: String) { val data = myFiltersData[providerDataKey] if (null !== data) { element.setAttribute("savedFilters", SavedFiltersUtil.stringify(data.first, data.second)) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/service/repositoryFile/CachedRepositoryFile.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.service.repositoryFile import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.vcs.changes.Change import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.api.Cache import net.ntworld.mergeRequestIntegrationIde.infrastructure.service.RepositoryFileService import javax.swing.Icon class CachedRepositoryFile( service: RepositoryFileService, private val cache: Cache ) : RepositoryFileDecorator(service) { private val myLogger = Logger.getInstance(this.javaClass) override fun findChanges(providerData: ProviderData, hashes: List): List { val key = "${providerData.id}:${hashes.joinToString(",")}" return cache.getOrRun(key) { myLogger.info("Cache $key not found") val result = super.findChanges(providerData, hashes) cache.set(key, result, FIND_CHANGES_TTL) result } } override fun findIcon(providerData: ProviderData, path: String): Icon { val key = "${providerData.id}:icon:${path}" return cache.getOrRun(key) { myLogger.info("Cache $key not found") val result = super.findIcon(providerData, path) cache.set(key, result, FIND_ICON_TTL) result } } companion object { const val FIND_CHANGES_TTL = 30000 const val FIND_ICON_TTL = 60000 } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/service/repositoryFile/LocalRepositoryFileService.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.service.repositoryFile import com.intellij.icons.AllIcons import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.util.Iconable import com.intellij.openapi.vcs.changes.Change import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager import com.intellij.vcs.log.impl.VcsLogContentUtil import com.intellij.vcs.log.util.VcsLogUtil import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.service.RepositoryFileService import net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil import javax.swing.Icon class LocalRepositoryFileService( private val projectServiceProvider: ProjectServiceProvider ) : RepositoryFileService { private val myLogger = Logger.getInstance(this.javaClass) override fun findChanges(providerData: ProviderData, hashes: List): List { try { val repository = RepositoryUtil.findRepository(projectServiceProvider, providerData) if (null === repository) { return listOf() } val log = VcsLogContentUtil.getOrCreateLog(projectServiceProvider.project) if (null === log) { return listOf() } val details = VcsLogUtil.getDetails( log.dataManager.getLogProvider(repository.root), repository.root, hashes ) return VcsLogUtil.collectChanges(details) { it.changes } } catch (exception: Exception) { myLogger.info("Cannot findChanges for ${providerData.repository}, hashes: ${hashes.joinToString(",")}") throw exception } } override fun findIcon(providerData: ProviderData, path: String): Icon { val repository = RepositoryUtil.findRepository(projectServiceProvider, providerData) val psiFile = findPsiFile(RepositoryUtil.findAbsoluteCrossPlatformsPath(repository, path)) if (null === psiFile) { return AllIcons.FileTypes.Any_type } return try { psiFile.getIcon(Iconable.ICON_FLAG_READ_STATUS) } catch (exception: Exception) { AllIcons.FileTypes.Any_type } } private fun findPsiFile(path: String): PsiFile? { val file = findVirtualFileByPath(path) if (null === file) { return null } return PsiManager.getInstance(projectServiceProvider.project).findFile(file) } fun findVirtualFileByPath(path: String): VirtualFile? { val file = LocalFileSystem.getInstance().findFileByPath(path) if (null !== file) { return file } return null } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/service/repositoryFile/RepositoryFileDecorator.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.service.repositoryFile import com.intellij.openapi.vcs.changes.Change import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.service.RepositoryFileService import javax.swing.Icon open class RepositoryFileDecorator( private val service: RepositoryFileService ) : RepositoryFileService { override fun findChanges(providerData: ProviderData, hashes: List): List { return service.findChanges(providerData, hashes) } override fun findIcon(providerData: ProviderData, path: String): Icon { return service.findIcon(providerData, path) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/ApplicationSettings.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting import net.ntworld.mergeRequest.api.ApiOptions import net.ntworld.mergeRequestIntegration.internal.ApiOptionsImpl interface ApplicationSettings { val enableRequestCache: Boolean val saveMRFilterState: Boolean val displayCommentsInDiffView: Boolean val showAddCommentIconsInDiffViewGutter: Boolean val checkoutTargetBranch: Boolean val maxDiffChangesOpenedAutomatically: Int val displayUpVotesAndDownVotes: Boolean val displayMergeRequestState: Boolean val enableReworkProcess: Boolean fun toApiOptions(): ApiOptions { return ApiOptionsImpl( enableRequestCache = this.enableRequestCache ) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/ApplicationSettingsImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting data class ApplicationSettingsImpl( override val enableRequestCache: Boolean, override val saveMRFilterState: Boolean, override val displayCommentsInDiffView: Boolean, override val showAddCommentIconsInDiffViewGutter: Boolean, override val checkoutTargetBranch: Boolean, override val maxDiffChangesOpenedAutomatically: Int, override val displayUpVotesAndDownVotes: Boolean, override val displayMergeRequestState: Boolean, override val enableReworkProcess: Boolean ) : ApplicationSettings { companion object { val DEFAULT = ApplicationSettingsImpl( enableRequestCache = true, saveMRFilterState = true, displayCommentsInDiffView = false, showAddCommentIconsInDiffViewGutter = true, checkoutTargetBranch = false, maxDiffChangesOpenedAutomatically = 10, displayUpVotesAndDownVotes = false, displayMergeRequestState = true, enableReworkProcess = true ) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/ApplicationSettingsManager.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting import org.jdom.Element interface ApplicationSettingsManager : ApplicationSettings { fun writeTo(element: Element) fun readFrom(elements: List): ApplicationSettings fun update(settings: ApplicationSettings) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/ApplicationSettingsManagerImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option.* import org.jdom.Element class ApplicationSettingsManagerImpl( private val changedInvoker: ((ApplicationSettings, ApplicationSettings) -> Unit) ) : ApplicationSettingsManager { private var mySettings: ApplicationSettings = ApplicationSettingsImpl.DEFAULT private val myOptionEnableRequestCache = EnableRequestCacheOption() private val myOptionSaveMRFilterState = SaveMRFilterStateOption() private val myOptionDisplayCommentsInDiffView = DisplayCommentsInDiffViewOption() private val myOptionShowAddCommentIconsInDiffViewGutter = ShowAddCommentIconsInDiffViewGutterOption() private val myOptionCheckoutTargetBranch = CheckoutTargetBranchOption() private val myOptionMaxDiffChangesOpenedAutomatically = MaxDiffChangesOpenedAutomaticallyOption() private val myOptionDisplayUpVotesAndDownVotes = DisplayUpVotesAndDownVotesOption() private val myOptionDisplayMergeRequestState = DisplayMergeRequestStateOption() private val myOptionEnableReworkProcess = EnableReworkProcessOption() private val myAllSettingOptions = listOf>( myOptionEnableRequestCache, myOptionSaveMRFilterState, myOptionDisplayCommentsInDiffView, myOptionShowAddCommentIconsInDiffViewGutter, myOptionCheckoutTargetBranch, myOptionMaxDiffChangesOpenedAutomatically, myOptionDisplayUpVotesAndDownVotes, myOptionDisplayMergeRequestState, myOptionEnableReworkProcess ) val settings: ApplicationSettings get() = mySettings override val enableRequestCache: Boolean get() = mySettings.enableRequestCache override val saveMRFilterState: Boolean get() = mySettings.saveMRFilterState override val displayCommentsInDiffView: Boolean get() = mySettings.displayCommentsInDiffView override val showAddCommentIconsInDiffViewGutter: Boolean get() = mySettings.showAddCommentIconsInDiffViewGutter override val checkoutTargetBranch: Boolean get() = mySettings.checkoutTargetBranch override val maxDiffChangesOpenedAutomatically: Int get() = mySettings.maxDiffChangesOpenedAutomatically override val displayUpVotesAndDownVotes: Boolean get() = mySettings.displayUpVotesAndDownVotes override val displayMergeRequestState: Boolean get() = mySettings.displayMergeRequestState override val enableReworkProcess: Boolean get() = mySettings.enableReworkProcess override fun readFrom(elements: List): ApplicationSettings { var settings = ApplicationSettingsImpl.DEFAULT for (item in elements) { if (item.name != "Setting") { continue } val nameAttribute = item.getAttribute("name") if (null === nameAttribute) { continue } val valueAttribute = item.getAttribute("value") if (null === valueAttribute) { continue } for (option in myAllSettingOptions) { if (option.name == nameAttribute.value.trim()) { settings = option.readValue(valueAttribute.value, settings) } } } mySettings = settings return settings } override fun writeTo(element: Element) { writeOption(element, myOptionEnableRequestCache, mySettings.enableRequestCache) writeOption(element, myOptionSaveMRFilterState, mySettings.saveMRFilterState) writeOption(element, myOptionDisplayCommentsInDiffView, mySettings.displayCommentsInDiffView) writeOption(element, myOptionShowAddCommentIconsInDiffViewGutter, mySettings.showAddCommentIconsInDiffViewGutter) writeOption(element, myOptionCheckoutTargetBranch, mySettings.checkoutTargetBranch) writeOption(element, myOptionMaxDiffChangesOpenedAutomatically, mySettings.maxDiffChangesOpenedAutomatically) writeOption(element, myOptionDisplayUpVotesAndDownVotes, mySettings.displayUpVotesAndDownVotes) writeOption(element, myOptionDisplayMergeRequestState, mySettings.displayMergeRequestState) writeOption(element, myOptionEnableReworkProcess, mySettings.enableReworkProcess) } override fun update(settings: ApplicationSettings) { val oldSettings = mySettings mySettings = settings changedInvoker.invoke(oldSettings, settings) } private fun writeOption(root: Element, option: SettingOption, value: T) { val item = Element("Setting") item.setAttribute("name", option.name) item.setAttribute("value", option.writeValue(value)) root.addContent(item) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/BooleanOption.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl abstract class BooleanOption : SettingOption { protected abstract fun getOptionValueFromSettings(settings: ApplicationSettingsImpl) : Boolean protected abstract fun copySettings(settings: ApplicationSettingsImpl, value: Boolean) : ApplicationSettingsImpl final override fun writeValue(value: Boolean): String { return if (value) "1" else "0" } final override fun readValue(input: String, currentSettings: ApplicationSettingsImpl): ApplicationSettingsImpl { val value = input.trim() == "1" if (getOptionValueFromSettings(currentSettings) != value) { return copySettings(currentSettings, value) } return currentSettings } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/CheckoutTargetBranchOption.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl class CheckoutTargetBranchOption : BooleanOption() { override val name: String = "code-review:checkout-target-branch" override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean { return settings.checkoutTargetBranch } override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl { return settings.copy(checkoutTargetBranch = value) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/DisplayCommentsInDiffViewOption.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl class DisplayCommentsInDiffViewOption : BooleanOption() { override val name: String = "comments:display-comments-in-diff-view" override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean { return settings.displayCommentsInDiffView } override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl { return settings.copy(displayCommentsInDiffView = value) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/DisplayMergeRequestStateOption.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl class DisplayMergeRequestStateOption : BooleanOption() { override val name: String = "merge-request:display-state" override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean { return settings.displayMergeRequestState } override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl { return settings.copy(displayMergeRequestState = value) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/DisplayUpVotesAndDownVotesOption.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl class DisplayUpVotesAndDownVotesOption : BooleanOption() { override val name: String = "merge-request:display-upvotes-downvotes" override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean { return settings.displayUpVotesAndDownVotes } override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl { return settings.copy(displayUpVotesAndDownVotes = value) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/EnableRequestCacheOption.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl class EnableRequestCacheOption : BooleanOption() { override val name: String = "enable-request-cache" override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean { return settings.enableRequestCache } override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl { return settings.copy(enableRequestCache = value) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/EnableReworkProcessOption.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl class EnableReworkProcessOption : BooleanOption() { override val name: String = "rework:enabled" override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean { return settings.enableReworkProcess } override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl { return settings.copy(enableReworkProcess = value) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/MaxDiffChangesOpenedAutomaticallyOption.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl class MaxDiffChangesOpenedAutomaticallyOption : SettingOption { override val name: String = "code-review:max-diff-change-opened-automatically" override fun writeValue(value: Int): String { return if (!isValid(value)) DEFAULT_VALUE.toString() else value.toString() } override fun readValue(input: String, currentSettings: ApplicationSettingsImpl): ApplicationSettingsImpl { val value = parse(input) if (currentSettings.maxDiffChangesOpenedAutomatically != value) { return currentSettings.copy(maxDiffChangesOpenedAutomatically = value) } return currentSettings } companion object { const val DEFAULT_VALUE = 10 fun parse(input: String) : Int { val rawValue = input.trim().toIntOrNull() return if (null === rawValue || !isValid(rawValue)) DEFAULT_VALUE else rawValue } private fun isValid(value: Int) = value in 0..1000 } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/SaveMRFilterStateOption.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl class SaveMRFilterStateOption : BooleanOption() { override val name: String = "save-mr-filter-state" override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean { return settings.saveMRFilterState } override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl { return settings.copy(saveMRFilterState = value) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/SettingOption.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl interface SettingOption { val name: String fun writeValue(value: T): String fun readValue(input: String, currentSettings: ApplicationSettingsImpl): ApplicationSettingsImpl } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/ShowAddCommentIconsInDiffViewGutterOption.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl class ShowAddCommentIconsInDiffViewGutterOption : BooleanOption() { override val name: String = "comments:show-add-comment-icons-in-diff-view-gutter" override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean { return settings.showAddCommentIconsInDiffViewGutter } override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl { return settings.copy(showAddCommentIconsInDiffViewGutter = value) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabFactory.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider object CommentsTabFactory { fun makeCommentsTabView( projectServiceProvider: ProjectServiceProvider, providerData: ProviderData ): CommentsTabView { return CommentsTabViewImpl(projectServiceProvider, providerData) } fun makeCommentsTabModel( projectServiceProvider: ProjectServiceProvider, providerData: ProviderData ): CommentsTabModel { return CommentsTabModelImpl(projectServiceProvider, providerData) } fun makeCommentsTabPresenter( projectServiceProvider: ProjectServiceProvider, model: CommentsTabModel, view: CommentsTabView ): CommentsTabPresenter { return CommentsTabPresenterImpl(projectServiceProvider, model, view) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabModel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments import com.intellij.openapi.Disposable import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.DataChangedSource import net.ntworld.mergeRequestIntegrationIde.Model import java.util.* interface CommentsTabModel : Model, Disposable { val providerData: ProviderData var mergeRequestInfo: MergeRequestInfo val comments: List var displayResolvedComments: Boolean var onlyShowDraftComments: Boolean interface DataListener : EventListener { fun onMergeRequestInfoChanged() fun onCommentsUpdated(source: DataChangedSource) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabModelImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.* import net.ntworld.mergeRequestIntegrationIde.AbstractModel import net.ntworld.mergeRequestIntegrationIde.DataChangedSource import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.MergeRequestDataNotifier import net.ntworld.mergeRequestIntegrationIde.mergeRequest.Empty import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider class CommentsTabModelImpl( private val projectServiceProvider: ProjectServiceProvider, override val providerData: ProviderData ) : AbstractModel(), CommentsTabModel { override val dispatcher = EventDispatcher.create(CommentsTabModel.DataListener::class.java) private val myComments: MutableList = mutableListOf() override var comments: MutableList = mutableListOf() private set override var mergeRequestInfo: MergeRequestInfo = MergeRequestInfo.Empty set(value) { field = value dispatcher.multicaster.onMergeRequestInfoChanged() } override var displayResolvedComments: Boolean = false set(value) { if (field != value) { field = value buildComments() dispatcher.multicaster.onCommentsUpdated(DataChangedSource.UI) } } override var onlyShowDraftComments: Boolean = false set(value) { if (field != value) { field = value buildComments() dispatcher.multicaster.onCommentsUpdated(DataChangedSource.UI) } } private val myMessageBusConnection = projectServiceProvider.messageBus.connect() private val myMergeRequestDataNotifier = object : MergeRequestDataNotifier { override fun fetchCommentsRequested(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) { } override fun onCommentsUpdated( providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List ) { val currentProviderData = this@CommentsTabModelImpl.providerData val currentMergeRequestInfo = this@CommentsTabModelImpl.mergeRequestInfo if (providerData.id != currentProviderData.id || mergeRequestInfo.id != currentMergeRequestInfo.id) { return } myComments.clear() myComments.addAll(comments) buildComments() dispatcher.multicaster.onCommentsUpdated(DataChangedSource.NOTIFIER) } } init { myMessageBusConnection.subscribe(MergeRequestDataNotifier.TOPIC, myMergeRequestDataNotifier) } override fun dispose() { myMessageBusConnection.disconnect() } private fun buildComments() { comments.clear() if (onlyShowDraftComments) { comments.addAll(myComments.filter { it.isDraft }) } else { if (displayResolvedComments) { comments.addAll(myComments) } else { comments.addAll(myComments.filter { !it.resolved }) } } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabPresenter.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments import com.intellij.openapi.Disposable import com.intellij.ui.tabs.TabInfo import net.ntworld.mergeRequestIntegrationIde.SimplePresenter interface CommentsTabPresenter : SimplePresenter, Disposable { val model: CommentsTabModel val view: CommentsTabView val tabInfo: TabInfo get() = view.tabInfo } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabPresenterImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments import com.intellij.notification.NotificationType import com.intellij.openapi.application.ApplicationManager import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.command.DeleteCommentCommand import net.ntworld.mergeRequest.command.ResolveCommentCommand import net.ntworld.mergeRequest.command.UnresolveCommentCommand import net.ntworld.mergeRequest.request.CreateCommentRequest import net.ntworld.mergeRequest.request.PublishCommentsRequest import net.ntworld.mergeRequest.request.ReplyCommentRequest import net.ntworld.mergeRequest.request.UpdateCommentRequest import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegration.provider.ProviderException import net.ntworld.mergeRequestIntegrationIde.AbstractPresenter import net.ntworld.mergeRequestIntegrationIde.DataChangedSource import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.DiffNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.MergeRequestDataNotifier import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.* import net.ntworld.mergeRequestIntegrationIde.mergeRequest.isEmpty import java.util.* class CommentsTabPresenterImpl( private val projectServiceProvider: ProjectServiceProvider, override val model: CommentsTabModel, override val view: CommentsTabView ) : AbstractPresenter(), CommentsTabPresenter, CommentsTabModel.DataListener, CommentsTabView.ActionListener { override val dispatcher = EventDispatcher.create(EventListener::class.java) private val myDiffPublisher = projectServiceProvider.messageBus.syncPublisher(DiffNotifier.TOPIC) init { model.addDataListener(this) view.addActionListener(this) } override fun onMergeRequestInfoChanged() { if (model.mergeRequestInfo.isEmpty()) { view.hideThread() } else { requestFetchComments() } } override fun onCommentsUpdated(source: DataChangedSource) { if (source == DataChangedSource.NOTIFIER) { ApplicationManager.getApplication().invokeLater { handleWhenCommentsGetUpdated() } } else { handleWhenCommentsGetUpdated() } } override fun dispose() { view.dispose() model.dispose() } /** * Node tree structure: * * + Root (hidden) * - GeneralCommentNode * - ThreadNode * - CommentNode * - FileNode * - FileLineNode * - ThreadNode * - CommentNode */ override fun onTreeNodeSelected(node: Node) = assertMergeRequestInfoIsAvailable { if (node is GeneralCommentsNode) { collectAndDisplayCommentThread(it, node) return@assertMergeRequestInfoIsAvailable } if (node is ThreadNode) { collectAndDisplayCommentThread(it, node.parent!!) return@assertMergeRequestInfoIsAvailable } if (node is CommentNode) { collectAndDisplayCommentThread(it, node.parent!!.parent!!) return@assertMergeRequestInfoIsAvailable } if (node is FileNode) { val reviewContext = projectServiceProvider.reviewContextManager.findContext(model.providerData.id, it.id) if (null !== reviewContext) { val change = reviewContext.findChangeByPath(node.path) if (null !== change) { reviewContext.openChange(change, focus = false, displayMergeRequestId = !projectServiceProvider.isDoingCodeReview()) view.hideThread() } } return@assertMergeRequestInfoIsAvailable } if (node is FileLineNode) { collectAndDisplayCommentThread(it, node) val reviewContext = projectServiceProvider.reviewContextManager.findContext(model.providerData.id, it.id) if (null !== reviewContext) { val change = reviewContext.findChangeByPath(node.path) if (null !== change) { reviewContext.putChangeData(change, DiffNotifier.ScrollPosition, node.position) reviewContext.putChangeData(change, DiffNotifier.ScrollShowComments, true) reviewContext.openChange(change, focus = false, displayMergeRequestId = !projectServiceProvider.isDoingCodeReview()) myDiffPublisher.hideAllCommentsRequested(reviewContext, change) myDiffPublisher.scrollToPositionRequested(reviewContext, change, node.position, true) } } } } override fun onShowResolvedCommentsToggled(displayResolvedComments: Boolean) { model.displayResolvedComments = displayResolvedComments } override fun onShowDraftCommentsOnlyToggled(onlyShowDraftComments: Boolean) { model.onlyShowDraftComments = onlyShowDraftComments } override fun onCreateGeneralCommentClicked() { assertMergeRequestInfoIsAvailable { if (view.hasGeneralCommentsTreeNode()) { view.selectGeneralCommentsTreeNode() } else { view.renderThread(it, mapOf()) } view.focusToMainEditor() } } override fun onRefreshButtonClicked() = requestFetchComments() override fun onDeleteCommentRequested(comment: Comment) = assertMergeRequestInfoIsAvailable { projectServiceProvider.infrastructure.commandBus() process DeleteCommentCommand.make( providerId = model.providerData.id, mergeRequestId = it.id, comment = comment ) requestFetchComments() } override fun onResolveCommentRequested(comment: Comment) = assertMergeRequestInfoIsAvailable { projectServiceProvider.infrastructure.commandBus() process ResolveCommentCommand.make( providerId = model.providerData.id, mergeRequestId = it.id, comment = comment ) requestFetchComments() } override fun onUnresolveCommentRequested(comment: Comment) = assertMergeRequestInfoIsAvailable { projectServiceProvider.infrastructure.commandBus() process UnresolveCommentCommand.make( providerId = model.providerData.id, mergeRequestId = it.id, comment = comment ) requestFetchComments() } override fun onReplyCommentRequested(repliedComment: Comment, content: String) = assertMergeRequestInfoIsAvailable { projectServiceProvider.infrastructure.serviceBus() process ReplyCommentRequest.make( providerId = model.providerData.id, mergeRequestId = it.id, repliedComment = repliedComment, body = content ) ifError { exception -> projectServiceProvider.notify( "There was an error from server. \n\n ${exception.message}", NotificationType.ERROR ) throw ProviderException(exception) } view.clearMainEditorText() requestFetchComments() } override fun onEditCommentRequested(comment: Comment, content: String) = assertMergeRequestInfoIsAvailable { projectServiceProvider.infrastructure.serviceBus() process UpdateCommentRequest.make( providerId = model.providerData.id, mergeRequestId = it.id, comment = comment, body = content ) ifError { exception -> projectServiceProvider.notify( "There was an error from server. \n\n ${exception.message}", NotificationType.ERROR ) throw ProviderException(exception) } requestFetchComments() } override fun onPublishDraftCommentRequested(comment: Comment) { projectServiceProvider.infrastructure.serviceBus() process PublishCommentsRequest.make( providerId = model.providerData.id, mergeRequestId = model.mergeRequestInfo.id, draftCommentIds = listOf(comment.id) ) ifError { projectServiceProvider.notify( "There was an error from server. \n\n ${it.message}", NotificationType.ERROR ) throw ProviderException(it) } requestFetchComments() } override fun onCreateCommentRequested(content: String, position: CommentPosition?, isDraft: Boolean) = assertMergeRequestInfoIsAvailable { projectServiceProvider.infrastructure.serviceBus() process CreateCommentRequest.make( providerId = model.providerData.id, mergeRequestId = model.mergeRequestInfo.id, position = position, body = content, isDraft = isDraft ) ifError { projectServiceProvider.notify( "There was an error from server. \n\n ${it.message}", NotificationType.ERROR ) throw ProviderException(it) } view.clearMainEditorText() requestFetchComments() } private fun requestFetchComments() = assertMergeRequestInfoIsAvailable { projectServiceProvider.messageBus.syncPublisher(MergeRequestDataNotifier.TOPIC).fetchCommentsRequested( model.providerData, it ) } private fun handleWhenCommentsGetUpdated() { view.displayCommentCount(model.comments.size) view.hideThread() view.renderTree(model.mergeRequestInfo, model.comments, model.displayResolvedComments) } private fun assertMergeRequestInfoIsAvailable(invoker: ((MergeRequestInfo) -> Unit)) { val mergeRequestInfo = model.mergeRequestInfo if (!mergeRequestInfo.isEmpty()) { invoker.invoke(mergeRequestInfo) } } private fun collectAndDisplayCommentThread(mergeRequestInfo: MergeRequestInfo, parent: Node) { view.renderThread(mergeRequestInfo, parent.groupComments()) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabView.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments import com.intellij.openapi.Disposable import com.intellij.ui.tabs.TabInfo import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequestIntegrationIde.Component import net.ntworld.mergeRequestIntegrationIde.View import net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEvent import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.Node import java.util.* interface CommentsTabView : View, Component, Disposable { val tabInfo: TabInfo fun displayCommentCount(count: Int) fun renderTree(mergeRequestInfo: MergeRequestInfo, comments: List, displayResolvedComments: Boolean) fun hideThread() fun renderThread(mergeRequestInfo: MergeRequestInfo, groupedComments: Map>) fun hasGeneralCommentsTreeNode(): Boolean fun selectGeneralCommentsTreeNode() fun focusToMainEditor() fun clearMainEditorText() interface ActionListener : EventListener, CommentEvent { fun onTreeNodeSelected(node: Node) fun onShowResolvedCommentsToggled(displayResolvedComments: Boolean) fun onShowDraftCommentsOnlyToggled(onlyShowDraftComments: Boolean) fun onCreateGeneralCommentClicked() fun onRefreshButtonClicked() fun onReplyCommentRequested(repliedComment: Comment, content: String) fun onEditCommentRequested(comment: Comment, content: String) fun onPublishDraftCommentRequested(comment: Comment) fun onCreateCommentRequested(content: String, position: CommentPosition?, isDraft: Boolean) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabViewImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments import com.intellij.openapi.ui.Messages import com.intellij.ui.JBColor import com.intellij.ui.OnePixelSplitter import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.components.Panel import com.intellij.ui.tabs.TabInfo import com.intellij.util.EventDispatcher import com.intellij.util.ui.JBUI import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.AbstractView import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeFactory import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreePresenter import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.component.Icons import net.ntworld.mergeRequestIntegrationIde.component.comment.EditorComponent import net.ntworld.mergeRequestIntegrationIde.component.comment.GroupComponent import net.ntworld.mergeRequestIntegrationIde.component.comment.Options import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeView import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.Node import javax.swing.BoxLayout import javax.swing.JComponent import javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER class CommentsTabViewImpl( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData ) : AbstractView(), CommentsTabView { override val dispatcher = EventDispatcher.create(CommentsTabView.ActionListener::class.java) private val mySplitter = OnePixelSplitter( this::class.java.canonicalName, 0.5f ) private val myTreePresenter: CommentTreePresenter = CommentTreeFactory.makePresenter( CommentTreeFactory.makeModel(providerData), CommentTreeFactory.makeView(projectServiceProvider, providerData, showOpenDiffViewDescription = false) ) private val myTreeListener = object : CommentTreePresenter.Listener { override fun onTreeNodeSelected(node: Node, type: CommentTreeView.TreeSelectType) { dispatcher.multicaster.onTreeNodeSelected(node) } override fun onShowResolvedCommentsToggled(displayResolvedComments: Boolean) { dispatcher.multicaster.onShowResolvedCommentsToggled(displayResolvedComments) } override fun onShowDraftCommentsOnlyToggled(onlyShowDraftComments: Boolean) { dispatcher.multicaster.onShowDraftCommentsOnlyToggled(onlyShowDraftComments) } override fun onCreateGeneralCommentClicked() { dispatcher.multicaster.onCreateGeneralCommentClicked() } override fun onRefreshButtonClicked() { dispatcher.multicaster.onRefreshButtonClicked() } } private var myCommentPosition: CommentPosition? = null private val myGroupCommentCollection = mutableListOf() private val myThreadPanel = Panel() private val myThreadBoxLayout = JBUI.Panels.simplePanel() private val myThreadWrapper = ScrollPaneFactory.createScrollPane(myThreadBoxLayout, true) private val myMainEditorEventListener = object : EditorComponent.EventListener { override fun onEditorFocused(editor: EditorComponent) { myThreadWrapper.verticalScrollBar.value = myThreadWrapper.verticalScrollBar.maximum } override fun onEditorResized(editor: EditorComponent) {} override fun onCancelClicked(editor: EditorComponent) { if (editor.text.isNotBlank()) { val result = Messages.showYesNoDialog( "Do you want to delete the whole content?", "Are you sure", Messages.getQuestionIcon() ) result == Messages.YES } editor.text = "" } override fun onSubmitClicked(editor: EditorComponent, isDraft: Boolean) { val text = editor.text.trim() val position = myCommentPosition if (text.isNotEmpty()) { dispatcher.multicaster.onCreateCommentRequested(editor.text, position, isDraft) } } } private val myMainEditor by lazy { val editor = projectServiceProvider.componentFactory.commentComponents.makeEditor( projectServiceProvider.project, EditorComponent.Type.NEW_DISCUSSION, 0, borderLeftRight = 0, showCancelAction = false, isDoingCodeReview = projectServiceProvider.isDoingCodeReview() ) editor.isVisible = true editor.addListener(myMainEditorEventListener) editor } private val myGroupComponentEventListener = object : GroupComponent.EventListener { override fun onResized() {} override fun onOpenDialogClicked() { } override fun onEditorCreated(groupId: String, editor: EditorComponent) { } override fun onEditorDestroyed(groupId: String, editor: EditorComponent) { } override fun onReplyCommentRequested(comment: Comment, content: String) { if (content.trim().isNotEmpty()) { dispatcher.multicaster.onReplyCommentRequested(comment, content) } } override fun onEditCommentRequested(comment: Comment, content: String) { dispatcher.multicaster.onEditCommentRequested(comment, content) } override fun onPublishDraftCommentRequested(comment: Comment) { dispatcher.multicaster.onPublishDraftCommentRequested(comment) } override fun onDeleteCommentRequested(comment: Comment) { dispatcher.multicaster.onDeleteCommentRequested(comment) } override fun onResolveCommentRequested(comment: Comment) { dispatcher.multicaster.onResolveCommentRequested(comment) } override fun onUnresolveCommentRequested(comment: Comment) { dispatcher.multicaster.onUnresolveCommentRequested(comment) } } override val component: JComponent = mySplitter override val tabInfo: TabInfo by lazy { val tabInfo = TabInfo(component) tabInfo.text = "Comments" tabInfo.icon = Icons.Comments tabInfo } init { myThreadPanel.background = JBColor.border() myThreadBoxLayout.background = JBColor.border() myThreadWrapper.background = JBColor.border() myThreadWrapper.horizontalScrollBarPolicy = HORIZONTAL_SCROLLBAR_NEVER myThreadBoxLayout.addToCenter(myThreadPanel) myThreadPanel.layout = BoxLayout(myThreadPanel, BoxLayout.Y_AXIS) myThreadBoxLayout.addToBottom(myMainEditor.component) mySplitter.firstComponent = myTreePresenter.component mySplitter.secondComponent = null myTreePresenter.addListener(myTreeListener) } override fun displayCommentCount(count: Int) { tabInfo.text = if (0 == count) "Comments" else "Comments · $count" } override fun dispose() { } override fun renderTree( mergeRequestInfo: MergeRequestInfo, comments: List, displayResolvedComments: Boolean ) { myTreePresenter.model.mergeRequestInfo = mergeRequestInfo myTreePresenter.model.comments = comments myTreePresenter.model.displayResolvedComments = displayResolvedComments } override fun hideThread() { mySplitter.secondComponent = null } override fun renderThread(mergeRequestInfo: MergeRequestInfo, groupedComments: Map>) { myThreadPanel.removeAll() myGroupCommentCollection.forEach { it.dispose() } myGroupCommentCollection.clear() myCommentPosition = null groupedComments.forEach { (groupId, comments) -> if (comments.isEmpty()) { return@forEach } val group = projectServiceProvider.componentFactory.commentComponents.makeGroup( providerData, mergeRequestInfo, projectServiceProvider.project, false, groupId, comments, Options(borderLeftRight = 0, showMoveToDialog = false) ) myCommentPosition = comments.first().position group.addListener(myGroupComponentEventListener) myThreadPanel.add(group.component) myGroupCommentCollection.add(group) } if (null !== myCommentPosition) { myMainEditor.addCommentNowButtonText = "Add comment" myMainEditor.addCommentNowButtonDesc = "Add comment to current line" } else { myMainEditor.addCommentNowButtonText = "Add general comment" myMainEditor.addCommentNowButtonDesc = "Create new thread of general comment" } mySplitter.secondComponent = myThreadWrapper } override fun hasGeneralCommentsTreeNode(): Boolean = myTreePresenter.hasGeneralCommentsTreeNode() override fun selectGeneralCommentsTreeNode() = myTreePresenter.selectGeneralCommentsTreeNode() override fun focusToMainEditor() = myMainEditor.focus() override fun clearMainEditorText() { myMainEditor.text = "" } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreeFactory.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider object CommentTreeFactory { fun makeModel(providerData: ProviderData): CommentTreeModel { return CommentTreeModelImpl(providerData) } fun makeView( projectServiceProvider: ProjectServiceProvider, providerData: ProviderData, showOpenDiffViewDescription: Boolean ): CommentTreeView { return CommentTreeViewImpl(projectServiceProvider, providerData, showOpenDiffViewDescription) } fun makePresenter( model: CommentTreeModel, view: CommentTreeView ): CommentTreePresenter { return CommentTreePresenterImpl(model, view) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreeModel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.Model import java.util.* interface CommentTreeModel : Model { val providerData: ProviderData var mergeRequestInfo: MergeRequestInfo var comments: List var displayResolvedComments: Boolean interface DataListener : EventListener { fun onMergeRequestInfoChanged() fun onCommentsUpdated() fun onDisplayResolvedCommentsChanged() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreeModelImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.AbstractModel import net.ntworld.mergeRequestIntegrationIde.mergeRequest.Empty class CommentTreeModelImpl( override val providerData: ProviderData ) : AbstractModel(), CommentTreeModel { override val dispatcher = EventDispatcher.create(CommentTreeModel.DataListener::class.java) override var mergeRequestInfo: MergeRequestInfo = MergeRequestInfo.Empty set(value) { field = value dispatcher.multicaster.onMergeRequestInfoChanged() } override var comments: List = listOf() set(value) { field = value dispatcher.multicaster.onCommentsUpdated() } override var displayResolvedComments: Boolean = false set(value) { field = value dispatcher.multicaster.onDisplayResolvedCommentsChanged() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreePresenter.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree import net.ntworld.mergeRequestIntegrationIde.Component import net.ntworld.mergeRequestIntegrationIde.Presenter import javax.swing.JComponent interface CommentTreePresenter : Presenter, Component { val model: CommentTreeModel val view: CommentTreeView override val component: JComponent get() = view.component fun setToolbarMode(mode: CommentTreeView.ToolbarMode) = view.setToolbarMode(mode) fun hasGeneralCommentsTreeNode(): Boolean fun selectGeneralCommentsTreeNode() interface Listener : CommentTreeView.ActionListener } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreePresenterImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree class CommentTreePresenterImpl( override val model: CommentTreeModel, override val view: CommentTreeView ) : CommentTreePresenter, CommentTreeModel.DataListener { init { model.addDataListener(this) } override fun onMergeRequestInfoChanged() { } override fun onCommentsUpdated() { view.renderTree(model.mergeRequestInfo, model.comments) } override fun onDisplayResolvedCommentsChanged() { view.setShowResolvedCommentState(model.displayResolvedComments) } override fun hasGeneralCommentsTreeNode(): Boolean { return view.hasGeneralCommentsTreeNode() } override fun selectGeneralCommentsTreeNode() { view.selectGeneralCommentsTreeNode() } override fun addListener(listener: CommentTreePresenter.Listener) = view.addActionListener(listener) override fun removeListener(listener: CommentTreePresenter.Listener) = view.removeActionListener(listener) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreeView.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequestIntegrationIde.Component import net.ntworld.mergeRequestIntegrationIde.View import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.Node import java.util.* interface CommentTreeView : View, Component { fun renderTree(mergeRequestInfo: MergeRequestInfo, comments: List) fun setShowResolvedCommentState(selected: Boolean) fun hasGeneralCommentsTreeNode(): Boolean fun selectGeneralCommentsTreeNode() fun setToolbarMode(mode: ToolbarMode) interface ActionListener : EventListener { fun onTreeNodeSelected(node: Node, type: TreeSelectType) fun onShowResolvedCommentsToggled(displayResolvedComments: Boolean) fun onShowDraftCommentsOnlyToggled(onlyShowDraftComments: Boolean) fun onCreateGeneralCommentClicked() fun onRefreshButtonClicked() } enum class ToolbarMode { FULL, MINI } enum class TreeSelectType { NORMAL, DOUBLE_CLICK, PRESS_ENTER } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreeViewImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree import com.intellij.ide.util.treeView.NodeRenderer import com.intellij.ide.util.treeView.PresentableNodeDescriptor import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.treeStructure.Tree import com.intellij.util.EventDispatcher import com.intellij.util.ui.tree.TreeUtil import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.AbstractView import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.* import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.ui.util.CustomSimpleToolWindowPanel import java.awt.event.KeyAdapter import java.awt.event.KeyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.JComponent import javax.swing.event.TreeSelectionListener import javax.swing.tree.* class CommentTreeViewImpl( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val showOpenDiffViewDescription: Boolean ) : AbstractView(), CommentTreeView { override val dispatcher = EventDispatcher.create(CommentTreeView.ActionListener::class.java) private val myComponent = CustomSimpleToolWindowPanel(vertical = true, borderless = true) private val myTree = Tree() private val myRoot = DefaultMutableTreeNode() private val myModel = DefaultTreeModel(myRoot) private val myRenderer = NodeRenderer() private var myIsTreeRendering = false private val myTreeCellRenderer = TreeCellRenderer { tree, value, selected, expanded, leaf, row, hasFocus -> myRenderer.getTreeCellRendererComponent( tree, value, selected, expanded, leaf, row, hasFocus ) } private val myTreeSelectionListener = TreeSelectionListener { if (null !== it && !myIsTreeRendering) { handleOnTreeNodeSelectedEvent(it.path, CommentTreeView.TreeSelectType.NORMAL) } } private val myTreeMouseListener = object : MouseAdapter() { override fun mousePressed(e: MouseEvent?) { if (null === e) { return } if (e.clickCount == 2) { handleOnTreeNodeSelectedEvent(myTree.selectionPath, CommentTreeView.TreeSelectType.DOUBLE_CLICK) } } } private val myKeyListener = object: KeyAdapter() { override fun keyPressed(e: KeyEvent?) { if (null === e) { return } if (e.keyCode == KeyEvent.VK_ENTER) { handleOnTreeNodeSelectedEvent(myTree.selectionPath, CommentTreeView.TreeSelectType.PRESS_ENTER) } } } private val myToolbar = CommentTreeViewToolbar(myTree, dispatcher) private val nodeSyncManager: NodeSyncManager by lazy { NodeSyncManagerImpl(NodeDescriptorServiceImpl(projectServiceProvider, providerData)) } private val mySyncedTree: SyncedTree by lazy { nodeSyncManager.makeSyncedTree(myTree, myModel, myRoot) } init { val treeSelectionModel = DefaultTreeSelectionModel() treeSelectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION myTree.model = myModel myTree.cellRenderer = myTreeCellRenderer myTree.isRootVisible = false myTree.selectionModel = treeSelectionModel myTree.addTreeSelectionListener(myTreeSelectionListener) myTree.addMouseListener(myTreeMouseListener) myTree.addKeyListener(myKeyListener) myComponent.setContent(ScrollPaneFactory.createScrollPane(myTree, true)) myComponent.toolbar = myToolbar.component } override fun renderTree(mergeRequestInfo: MergeRequestInfo, comments: List) { myIsTreeRendering = true val builder = RootNodeBuilder(comments, showOpenDiffViewDescription) val root = builder.build() nodeSyncManager.sync(mergeRequestInfo, root, mySyncedTree) handleOnTreeNodeSelectedEvent(myTree.selectionPath, CommentTreeView.TreeSelectType.NORMAL) myIsTreeRendering = false } override fun setShowResolvedCommentState(selected: Boolean) { myToolbar.showResolved = selected } override fun hasGeneralCommentsTreeNode(): Boolean { val children = myRoot.children() for (child in children) { if (isGeneralCommentsTreeNode(child as TreeNode)) { return true } } return false } override fun selectGeneralCommentsTreeNode() { val children = myRoot.children() for (child in children) { if (isGeneralCommentsTreeNode(child as TreeNode)) { myTree.selectionPath = TreeUtil.getPath(myRoot, child as @org.jetbrains.annotations.NotNull TreeNode) break } } } override fun setToolbarMode(mode: CommentTreeView.ToolbarMode) { myToolbar.setMode(mode) } private fun isGeneralCommentsTreeNode(node: TreeNode) : Boolean { val treeNode = node as? DefaultMutableTreeNode ?: return false val descriptor = treeNode.userObject as? PresentableNodeDescriptor<*> ?: return false return descriptor.element is GeneralCommentsNode } private fun handleOnTreeNodeSelectedEvent(selectedPath: TreePath?, type: CommentTreeView.TreeSelectType) { if (null === selectedPath) { return } val lastPath = selectedPath.lastPathComponent as? DefaultMutableTreeNode ?: return val descriptor = lastPath.userObject as? PresentableNodeDescriptor<*> ?: return val element = descriptor.element as? Node ?: return dispatcher.multicaster.onTreeNodeSelected(element, type) } override val component: JComponent = myComponent } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreeViewToolbar.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.* import com.intellij.ui.treeStructure.actions.CollapseAllAction import com.intellij.ui.treeStructure.actions.ExpandAllAction import com.intellij.util.EventDispatcher import net.miginfocom.swing.MigLayout import net.ntworld.mergeRequestIntegrationIde.Component import net.ntworld.mergeRequestIntegrationIde.component.Icons import javax.swing.JComponent import javax.swing.JPanel import javax.swing.JTree internal class CommentTreeViewToolbar( private val myTree: JTree, private val dispatcher: EventDispatcher ) : Component { var showResolved: Boolean = false var onlyShowDraftComments: Boolean = false private var myMode = CommentTreeView.ToolbarMode.FULL private val mySkipResolvedButton = MySkipResolvedButton(this) private val myToggleDraftsButton = MyToggleDraftsButton(this) private val myRefreshButton = MyRefreshButton(this) private val myAddGeneralComment = MyAddGeneralComment(this) private val myPanel by lazy { val panel = JPanel(MigLayout("ins 0, fill", "[left]push[right]", "center")) val mainActionGroup = DefaultActionGroup() mainActionGroup.add(mySkipResolvedButton) mainActionGroup.addSeparator() mainActionGroup.add(myToggleDraftsButton) val mainToolbar = ActionManager.getInstance().createActionToolbar( "${this::class.java.canonicalName}/toolbar", mainActionGroup, true ) val rightActionGroup = DefaultActionGroup() rightActionGroup.add(myRefreshButton) rightActionGroup.addSeparator() rightActionGroup.add(ExpandAllAction(myTree)) rightActionGroup.add(CollapseAllAction(myTree)) rightActionGroup.addSeparator() rightActionGroup.add(myAddGeneralComment) val rightToolbar = ActionManager.getInstance().createActionToolbar( "${this::class.java.canonicalName}/toolbar-right", rightActionGroup, true ) panel.add(mainToolbar.component) panel.add(rightToolbar.component) panel } override val component: JComponent = myPanel fun setMode(mode: CommentTreeView.ToolbarMode) { if (mode != myMode) { myMode = mode } } private class MySkipResolvedButton(private val self: CommentTreeViewToolbar) : ToggleAction("Resolved Comments", "Toggle resolved comments", null) { override fun isSelected(e: AnActionEvent): Boolean { return self.showResolved } override fun setSelected(e: AnActionEvent, state: Boolean) { self.showResolved = state self.dispatcher.multicaster.onShowResolvedCommentsToggled(self.showResolved) } override fun update(e: AnActionEvent) { super.update(e) e.presentation.isEnabled = !self.onlyShowDraftComments if (self.myMode == CommentTreeView.ToolbarMode.FULL) { e.presentation.icon = null } else { e.presentation.icon = Icons.Resolved } } override fun displayTextInToolbar(): Boolean { return self.myMode == CommentTreeView.ToolbarMode.FULL } override fun useSmallerFontForTextInToolbar() = true } private class MyToggleDraftsButton(private val self: CommentTreeViewToolbar) : ToggleAction("Only Draft", "Only show draft comments", null) { override fun isSelected(e: AnActionEvent): Boolean { return self.onlyShowDraftComments } override fun setSelected(e: AnActionEvent, state: Boolean) { self.onlyShowDraftComments = state self.dispatcher.multicaster.onShowDraftCommentsOnlyToggled(self.onlyShowDraftComments) } override fun displayTextInToolbar(): Boolean = true override fun useSmallerFontForTextInToolbar() = true } private class MyRefreshButton(private val self: CommentTreeViewToolbar) : AnAction("Refresh", "Refresh comment list", AllIcons.Actions.Refresh) { override fun actionPerformed(e: AnActionEvent) { self.dispatcher.multicaster.onRefreshButtonClicked() } } private class MyAddGeneralComment(private val self: CommentTreeViewToolbar) : AnAction( "General Comment", "Add a general comment", AllIcons.General.Add ) { override fun actionPerformed(e: AnActionEvent) { self.dispatcher.multicaster.onCreateGeneralCommentClicked() } override fun update(e: AnActionEvent) { super.update(e) if (self.myMode == CommentTreeView.ToolbarMode.FULL) { e.presentation.text = "General Comment" } else { e.presentation.text = "Add a general comment" } } override fun displayTextInToolbar(): Boolean { return self.myMode == CommentTreeView.ToolbarMode.FULL } override fun useSmallerFontForTextInToolbar() = true } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/AbstractNode.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node abstract class AbstractNode : Node { override var parent: Node? = null override val children: MutableList = mutableListOf() override fun add(node: Node) { node.parent = this children.add(node) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/CommentNode.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import com.intellij.ide.projectView.PresentationData import com.intellij.ui.SimpleTextAttributes import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequestIntegration.util.DateTimeUtil import net.ntworld.mergeRequestIntegrationIde.component.Icons open class CommentNode( val comment: Comment, val position: CommentPosition? ) : AbstractNode() { override val id: String = "comment[${comment.id}]" override fun updatePresentation(presentation: PresentationData) { presentation.setIcon(if (comment.resolved) Icons.TreeNode.ResolvedComment else Icons.TreeNode.UnresolvedComment) presentation.addText("${comment.author.name} ", SimpleTextAttributes.REGULAR_ATTRIBUTES) presentation.addText("@${comment.author.username} · ", SimpleTextAttributes.GRAYED_ATTRIBUTES) if (comment.isDraft) { presentation.addText("draft", SimpleTextAttributes.REGULAR_ATTRIBUTES) } else { presentation.addText(DateTimeUtil.toPretty(comment.createdAt), SimpleTextAttributes.REGULAR_ATTRIBUTES) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/FileLineNode.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import com.intellij.ide.projectView.PresentationData import com.intellij.ui.SimpleTextAttributes import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequestIntegrationIde.util.TextChoiceUtil class FileLineNode( val path: String, val line: Int, val position: CommentPosition, private val totalCount: Int, private val draftCount: Int, private val showOpenDiffViewDescription: Boolean ) : AbstractNode() { override val id: String = "line[$path:$line]" private val openDiffViewDescription = if (null !== position.newLine) "" else " (open diff view)" override fun updatePresentation(presentation: PresentationData) { presentation.addText("Line $line", SimpleTextAttributes.REGULAR_ATTRIBUTES) presentation.addText(" · " + TextChoiceUtil.commentWithDraft(totalCount, draftCount), SimpleTextAttributes.GRAY_ATTRIBUTES) if (openDiffViewDescription.isNotEmpty() && showOpenDiffViewDescription) { presentation.addText(openDiffViewDescription, SimpleTextAttributes.GRAY_ATTRIBUTES) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/FileNode.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import com.intellij.ide.projectView.PresentationData import com.intellij.ui.SimpleTextAttributes import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.util.TextChoiceUtil class FileNode( val path: String, private val draftCount: Int ) : AbstractNode() { override fun updatePresentation( projectServiceProvider: ProjectServiceProvider, providerData: ProviderData, presentation: PresentationData ) { super.updatePresentation(projectServiceProvider, providerData, presentation) presentation.setIcon(projectServiceProvider.repositoryFile.findIcon(providerData, path)) } override val id: String = "file[$path]" override fun updatePresentation(presentation: PresentationData) { val fileName = findFileName(path) presentation.addText(fileName, SimpleTextAttributes.REGULAR_ATTRIBUTES) if (draftCount > 0) { presentation.addText(" · " + TextChoiceUtil.draftComment(draftCount), SimpleTextAttributes.GRAYED_ATTRIBUTES) } if (fileName != path) { presentation.addText(" · $path", SimpleTextAttributes.GRAYED_ATTRIBUTES) } } private fun findFileName(path: String): String { return path.split("/").last() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/GeneralCommentsNode.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import com.intellij.ide.projectView.PresentationData import com.intellij.ui.SimpleTextAttributes import net.ntworld.mergeRequestIntegrationIde.util.TextChoiceUtil class GeneralCommentsNode(private val totalCount: Int, private val draftCount: Int) : AbstractNode() { override val id: String = "general-comments" override fun updatePresentation(presentation: PresentationData) { presentation.addText("General ", SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES) presentation.addText( " · " + TextChoiceUtil.commentWithDraft(totalCount, draftCount), SimpleTextAttributes.GRAY_ATTRIBUTES ) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/Node.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import com.intellij.ide.projectView.PresentationData import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider interface Node { val id: String var parent: Node? val children: List val childCount get() = children.size fun add(node: Node) fun updatePresentation(presentation: PresentationData) fun updatePresentation(projectServiceProvider: ProjectServiceProvider, providerData: ProviderData, presentation: PresentationData) { updatePresentation(presentation) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/NodeDescriptorService.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import com.intellij.ide.util.treeView.PresentableNodeDescriptor interface NodeDescriptorService { fun make(node: Node): PresentableNodeDescriptor fun findNode(input: Any?): Node? } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/NodeDescriptorServiceImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import com.intellij.ide.projectView.PresentationData import com.intellij.ide.util.treeView.PresentableNodeDescriptor import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider class NodeDescriptorServiceImpl( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData ) : NodeDescriptorService { override fun make(node: Node): PresentableNodeDescriptor { val presentation = MyPresentableNodeDescriptor(projectServiceProvider, providerData, node) presentation.update() return presentation } override fun findNode(input: Any?): Node? { return if (null !== input && input is MyPresentableNodeDescriptor) { input.element } else null } private class MyPresentableNodeDescriptor( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val element: Node ) : PresentableNodeDescriptor(projectServiceProvider.project, null) { override fun update(presentation: PresentationData) { element.updatePresentation(projectServiceProvider, providerData, presentation) } override fun getElement(): Node = element } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/NodeFactory.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition object NodeFactory { fun makeRoot(): RootNode = RootNode() fun makeGeneralComments(root: RootNode, totalCount: Int, draftCount: Int): GeneralCommentsNode { val node = GeneralCommentsNode(totalCount, draftCount) root.add(node) return node } fun makeThread(parent: Node, threadId: String, repliedCount: Int, draftCount: Int, comment: Comment): ThreadNode { val node = ThreadNode(threadId, repliedCount, draftCount, comment, comment.position) parent.add(node) return node } fun makeComment(parent: ThreadNode, comment: Comment): CommentNode { val node = CommentNode(comment, parent.position) parent.add(node) return node } fun makeFile(parent: RootNode, path: String, draftCount: Int): FileNode { val node = FileNode(path, draftCount) parent.add(node) return node } fun makeFileLine( parent: FileNode, path: String, line: Int, totalCount: Int, draftCount: Int, position: CommentPosition, showOpenDiffViewDescription: Boolean ): FileLineNode { val node = FileLineNode(path, line, position, totalCount, draftCount, showOpenDiffViewDescription) parent.add(node) return node } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/NodeSyncManager.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import net.ntworld.mergeRequest.MergeRequestInfo import javax.swing.JTree import javax.swing.tree.DefaultMutableTreeNode import javax.swing.tree.DefaultTreeModel interface NodeSyncManager { fun sync(mergeRequestInfo: MergeRequestInfo, root: RootNode, tree: SyncedTree) fun makeSyncedTree(tree: JTree, treeModel: DefaultTreeModel, treeRoot: DefaultMutableTreeNode): SyncedTree } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/NodeSyncManagerImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import com.intellij.ide.util.treeView.PresentableNodeDescriptor import com.intellij.util.ui.tree.TreeUtil import net.ntworld.mergeRequest.MergeRequestInfo import java.lang.NullPointerException import javax.swing.JTree import javax.swing.tree.* import kotlin.math.min class NodeSyncManagerImpl( private val nodeDescriptorService: NodeDescriptorService ) : NodeSyncManager { override fun sync(mergeRequestInfo: MergeRequestInfo, root: RootNode, tree: SyncedTree) { val expandingNodes = mutableSetOf() val selectedNodeToTopIds = mutableSetOf() val selectedTreeNode = tree.selectedTreeNode() if (null !== selectedTreeNode) { val node = nodeDescriptorService.findNode(selectedTreeNode.userObject) var currentNode: Node? = node while (null !== currentNode) { selectedNodeToTopIds.add(currentNode.id) currentNode = currentNode.parent } } syncStructure(root, tree.treeRoot) { node, treeNode -> if (tree.isExpand(treeNode)) { expandingNodes.add(node.id) } } tree.nodeStructureChanged(tree.treeRoot) loopStructure(tree.treeRoot) { node, treeNode -> if (expandingNodes.contains(node.id)) { tree.expand(treeNode) } if (selectedNodeToTopIds.contains(node.id)) { tree.select(treeNode) } } } @Suppress("UNCHECKED_CAST") private fun loopStructure(treeNode: DefaultMutableTreeNode, visitor: (Node, DefaultMutableTreeNode) -> Unit) { visitor((treeNode.userObject as PresentableNodeDescriptor).element, treeNode) val children = treeNode.children(); if (children != null) { while (children.hasMoreElements()) { val child = children.nextElement() loopStructure(child as DefaultMutableTreeNode, visitor) } } } override fun makeSyncedTree( tree: JTree, treeModel: DefaultTreeModel, treeRoot: DefaultMutableTreeNode ): SyncedTree { return MySyncedTree(tree, treeModel, treeRoot) } fun syncStructure( parent: Node, treeNode: DefaultMutableTreeNode, visitor: ((Node, DefaultMutableTreeNode) -> Unit) ) { visitor(parent, treeNode) treeNode.userObject = nodeDescriptorService.make(parent) val treeNodeChildren = treeNode.children().toList() val treeNodeChildCount = treeNodeChildren.size val index = min(treeNodeChildCount, parent.childCount) for (i in 0 until index) { val treeNodeChild = treeNodeChildren[i] syncStructure(parent.children[i], treeNodeChild as DefaultMutableTreeNode, visitor) } if (treeNodeChildCount < parent.childCount && index < parent.childCount) { for (i in index until parent.childCount) { val child = parent.children[i] val userObject = nodeDescriptorService.make(child) val childTreeNode = DefaultMutableTreeNode(userObject) syncStructure(child, childTreeNode, visitor) treeNode.add(childTreeNode) } return } if (treeNodeChildCount > parent.childCount && index < treeNode.childCount) { for (i in index until treeNodeChildCount) { // Always remove index because after removing 1 item the list is has 1 item less, then index // is the same :D treeNode.remove(index) } return } } private class MySyncedTree( private val tree: JTree, private val treeModel: DefaultTreeModel, override val treeRoot: DefaultMutableTreeNode ) : SyncedTree { private fun findPath(treeNode: TreeNode): TreePath? { return try { TreeUtil.getPath(treeRoot, treeNode) } catch (exception: NullPointerException) { null } } override fun isExpand(treeNode: TreeNode): Boolean { val path = findPath(treeNode) return if (null !== path) tree.isExpanded(path) else false } override fun selectedTreeNode(): DefaultMutableTreeNode? { return if (null === tree.selectionPath) { null } else { tree.selectionPath!!.lastPathComponent as DefaultMutableTreeNode? } } override fun select(treeNode: TreeNode) { val path = findPath(treeNode) if (null !== path) { tree.selectionPath = path } } override fun expand(treeNode: TreeNode) { val path = findPath(treeNode) if (null !== path) { tree.expandPath(path) } } override fun nodeStructureChanged(treeNode: TreeNode) { treeModel.nodeStructureChanged(treeNode) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/RootNode.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import com.intellij.ide.projectView.PresentationData class RootNode : AbstractNode() { override val id: String = "root" override fun updatePresentation(presentation: PresentationData) {} } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/RootNodeBuilder.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequestIntegrationIde.util.CommentUtil class RootNodeBuilder( comments: List, private val showOpenDiffViewDescription: Boolean ) { private val generalComments = comments.filter { null === it.position } private val positionComments = comments.filter { null !== it.position } fun build(): RootNode { val root = NodeFactory.makeRoot() if (generalComments.isNotEmpty()) { buildGeneralComments(root) } if (positionComments.isNotEmpty()) { buildPositionComments(root) } return root } private fun buildGeneralComments(root: RootNode) { val generalCommentsNode = NodeFactory.makeGeneralComments( root, generalComments.count(), generalComments.filter { it.isDraft }.count() ) buildThreadComments(generalCommentsNode, generalComments) } private fun buildThreadComments(parent: Node, comments: List) { val groups = CommentUtil.groupCommentsByThreadId(comments) groups.forEach { (id, items) -> if (items.isEmpty()) { return@forEach } val draftCount = items.filter { it.isDraft }.count() - 1 val threadNode = NodeFactory.makeThread(parent, id, items.count() - 1, draftCount, items.first()) for (i in 1..items.lastIndex) { NodeFactory.makeComment(threadNode, items[i]) } } } private fun buildPositionComments(root: RootNode) { val groupedByPath = CommentUtil.groupCommentsByPositionPath(positionComments) groupedByPath.forEach { (path, items) -> if (items.isEmpty()) { return@forEach } val draftCountOfFile = items.filter { it.isDraft }.count() val fileNode = NodeFactory.makeFile(root, path, draftCountOfFile) val groupedByLine = CommentUtil.groupCommentsByPositionLine(items) groupedByLine.forEach { (line, comments) -> if (comments.isNotEmpty()) { val draftCountOfFileLine = comments.filter { it.isDraft }.count() val fileLine = NodeFactory.makeFileLine( fileNode, path, line, comments.count(), draftCountOfFileLine, comments.last().position!!, showOpenDiffViewDescription ) buildThreadComments(fileLine, comments) } } } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/SyncedTree.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import javax.swing.tree.DefaultMutableTreeNode import javax.swing.tree.TreeNode interface SyncedTree { val treeRoot: DefaultMutableTreeNode fun isExpand(treeNode: TreeNode): Boolean fun selectedTreeNode(): DefaultMutableTreeNode? fun expand(treeNode: TreeNode) fun select(treeNode: TreeNode) fun nodeStructureChanged(treeNode: TreeNode) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/ThreadNode.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import com.intellij.ide.projectView.PresentationData import com.intellij.ui.SimpleTextAttributes import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.CommentPosition import net.ntworld.mergeRequestIntegrationIde.util.TextChoiceUtil class ThreadNode( val threadId: String, private val repliedCount: Int, private val draftCount: Int, comment: Comment, position: CommentPosition? ) : CommentNode(comment, position) { override val id: String = "thread[${comment.id}]" override fun updatePresentation(presentation: PresentationData) { super.updatePresentation(presentation) val text = buildReplyAndDraftText() if (null !== text) { presentation.addText( " · $text", SimpleTextAttributes.GRAYED_ATTRIBUTES ) } } private fun buildReplyAndDraftText(): String? { if (repliedCount > 0 && draftCount > 0) { return TextChoiceUtil.replyWithDraft(repliedCount, draftCount) } if (repliedCount > 0) { return TextChoiceUtil.reply(repliedCount) } if (draftCount > 0) { return TextChoiceUtil.draft(draftCount) } return null } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/_fn.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import net.ntworld.mergeRequest.Comment fun Node.groupComments(): Map> { val groupedComments = mutableMapOf>() this.children.forEach { if (it !is ThreadNode) { return@forEach } groupedComments[it.threadId] = mutableListOf(it.comment) it.children.forEach { node -> if (node is CommentNode) { groupedComments[it.threadId]!!.add(node.comment) } } } return groupedComments } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/fn.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest import net.ntworld.mergeRequest.DateTime import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.MergeRequestState private class MergeRequestInfoEmpty : MergeRequestInfo { override val id: String = "" override val provider: String = "" override val projectId: String = "" override val title: String = "" override val description: String = "" override val url: String = "" override val state: MergeRequestState = MergeRequestState.CLOSED override val createdAt: DateTime = "" override val updatedAt: DateTime = "" } fun MergeRequestInfo.isEmpty(): Boolean { return this.id.isEmpty() } val MergeRequestInfo.Companion.Empty: MergeRequestInfo get() = MergeRequestInfoEmpty() ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/BranchWatcher.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.rework import git4idea.repo.GitRepository import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.watcher.Watcher interface BranchWatcher : Watcher { val providerData: ProviderData val repository: GitRepository val currentBranchName: String? fun shutdown() } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/ReworkEditorController.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.rework import com.intellij.openapi.Disposable import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.vcs.changes.Change import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData interface ReworkEditorController : Disposable { val textEditor: TextEditor val editor: EditorEx val providerData: ProviderData val mergeRequestInfo: MergeRequestInfo val path: String val revisionNumber: String fun updateComments() fun hideAllComments() fun scrollToLine(visibleLine: Int) fun displayCommentsOnLine(visibleLine: Int) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/ReworkEditorManager.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.rework import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkEditorNotifier interface ReworkEditorManager: ReworkEditorNotifier { } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/ReworkManager.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.rework import com.intellij.openapi.vcs.changes.Change import git4idea.repo.GitRepository import net.ntworld.mergeRequest.ProviderData interface ReworkManager { fun clear() fun markBranchWatcherTerminated(branchWatcher: BranchWatcher) fun markReworkWatcherTerminated(reworkWatcher: ReworkWatcher) fun createBranchWatcher(providerData: ProviderData) fun requestCreateReworkWatcher(providers: List, branchName: String) fun requestCreateReworkWatcher(providerData: ProviderData, repository: GitRepository, branchName: String) fun findReworkWatcherByChange(providerData: ProviderData, change: Change): ReworkWatcher? fun findActiveReworkWatcher(providerData: ProviderData): ReworkWatcher? fun getActiveReworkWatchers(): List } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/ReworkWatcher.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.rework import com.intellij.openapi.vcs.changes.Change import git4idea.repo.GitRepository import net.ntworld.mergeRequest.* import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.watcher.Watcher interface ReworkWatcher : Watcher { val projectServiceProvider: ProjectServiceProvider val repository: GitRepository val branchName: String val providerData: ProviderData val mergeRequestInfo: MergeRequestInfo val commits: List val changes: List val comments: List val onlyShowDraftComments: Boolean val displayResolvedComments: Boolean fun isChangesBuilt(): Boolean fun isFetchedComments(): Boolean fun shutdown() fun openChange(change: Change) fun findChangeByPath(absolutePath: String): Change? fun findCommentsByPath(absolutePath: String): List fun fetchComments() fun key(): String { return keyOf(providerData, branchName) } companion object { fun keyOf(providerData: ProviderData, branchName: String): String { return "${providerData.id}:${branchName}" } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/internal/BranchWatcherImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.rework.internal import git4idea.repo.GitRepository import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.debug import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.rework.BranchWatcher import net.ntworld.mergeRequestIntegrationIde.rework.ReworkManager class BranchWatcherImpl( private val projectServiceProvider: ProjectServiceProvider, private val reworkManager: ReworkManager, override val providerData: ProviderData, override val repository: GitRepository ) : BranchWatcher { private var myTerminate = false private var myPrevBranchName: String? = null override val currentBranchName: String? get() = repository.currentBranchName override val interval: Long = 3000 override fun canExecute(): Boolean { return !projectServiceProvider.project.isDisposed } override fun shouldTerminate(): Boolean { return myTerminate || projectServiceProvider.project.isDisposed || projectServiceProvider.isDoingCodeReview() || !projectServiceProvider.applicationSettings.enableReworkProcess } override fun execute() { val branchName = currentBranchName if (null !== branchName && myPrevBranchName != branchName) { myPrevBranchName = branchName debug("${providerData.id}: BranchWatcher found branch changed, request create watcher") reworkManager.requestCreateReworkWatcher(providerData, repository, branchName) } } override fun terminate() { debug("${providerData.id}: BranchWatcher is terminated") reworkManager.markBranchWatcherTerminated(this) } override fun shutdown() { debug("${providerData.id}: terminate BranchWatcher") myTerminate = true } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/internal/ReworkEditorControllerImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.rework.internal import com.intellij.diff.util.Side import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.markup.HighlighterLayer import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.util.Disposer import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.component.gutter.* import net.ntworld.mergeRequestIntegrationIde.component.thread.ThreadFactory import net.ntworld.mergeRequestIntegrationIde.component.thread.ThreadModel import net.ntworld.mergeRequestIntegrationIde.component.thread.ThreadPresenter import net.ntworld.mergeRequestIntegrationIde.diff.DiffView import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkWatcherNotifier import net.ntworld.mergeRequestIntegrationIde.rework.ReworkEditorController import net.ntworld.mergeRequestIntegrationIde.util.CommentUtil class ReworkEditorControllerImpl( private val projectServiceProvider: ProjectServiceProvider, override val textEditor: TextEditor, override val editor: EditorEx, override val providerData: ProviderData, override val mergeRequestInfo: MergeRequestInfo, override val path: String, override val revisionNumber: String ) : ReworkEditorController { private val shouldDisplayAddIcon = false private val myReworkRequester = projectServiceProvider.messageBus.syncPublisher(ReworkWatcherNotifier.TOPIC) private val myGutterIconRenderers = mutableMapOf() private val myThreadPresenters = mutableMapOf() init { registerGutterIconRenderers() val reworkWatcher = projectServiceProvider.reworkManager.findActiveReworkWatcher(providerData) if (null !== reworkWatcher) { val commentsByLines = CommentUtil.groupCommentsByPositionNewLine( reworkWatcher.findCommentsByPath(path) ) commentsByLines.forEach { (visibleLine, comments) -> initializeLine(visibleLine, comments) } } Disposer.register(textEditor, this) } override fun dispose() { val makeupModel = textEditor.editor.markupModel val highlighters = makeupModel.allHighlighters for (highlighter in highlighters) { if (highlighter.gutterIconRenderer is GutterIconRenderer) { makeupModel.removeHighlighter(highlighter) highlighter.dispose() } } myGutterIconRenderers.clear() myThreadPresenters.forEach { it.value.dispose() } myThreadPresenters.clear() } override fun updateComments() { resetGutterIcons() val reworkWatcher = projectServiceProvider.reworkManager.findActiveReworkWatcher(providerData) if (null !== reworkWatcher) { val commentsByLines = CommentUtil.groupCommentsByPositionNewLine( reworkWatcher.findCommentsByPath(path) ) destroyExistingComments(commentsByLines.keys) commentsByLines.forEach { (visibleLine, comments) -> initializeLine(visibleLine, comments) assertThreadPresenterAvailable(visibleLine - 1) { it.model.comments = comments } } } } override fun hideAllComments() { for (presenter in myThreadPresenters) { presenter.value.model.visible = false } } override fun displayCommentsOnLine(visibleLine: Int) { this.assertThreadPresenterAvailable(visibleLine - 1) { this.displayComments(it.model, visibleLine - 1, DiffView.DisplayCommentMode.SHOW) } } override fun scrollToLine(visibleLine: Int) { editor.scrollingModel.scrollTo(LogicalPosition(visibleLine - 1, 0), ScrollType.MAKE_VISIBLE) } private fun initializeLine(visibleLine: Int, comments: List) { val logicalLine = visibleLine - 1 initializeThreadPresenterOnLineIfNotAvailable(logicalLine, comments) val renderer = myGutterIconRenderers[logicalLine] if (null !== renderer) { updateGutterIcon(renderer, comments) } } private fun resetGutterIcons() { for (renderer in myGutterIconRenderers.values) { renderer.setState(GutterState.NO_COMMENT) } } private fun destroyExistingComments(excludedVisibleLines: Set) { val excludedLogicalLines = excludedVisibleLines .map { it - 1 } .filter { it >= 0 } val removedKeys = mutableListOf() for (entry in myThreadPresenters) { if (excludedLogicalLines.contains(entry.key)) { continue } entry.value.model.comments = listOf() removedKeys.add(entry.key) } removedKeys.forEach { myThreadPresenters.remove(it) } } private fun resetEditorOnLine(logicalLine: Int, repliedComment: Comment?) { val thread = myThreadPresenters[logicalLine] ?: return thread.model.resetEditor(repliedComment) } // TODO: Duplicate AbstractDiffView.updateGutterIcon maybe move to Util class private fun updateGutterIcon(renderer: GutterIconRenderer, comments: List) { val state = if (comments.isEmpty()) { GutterState.NO_COMMENT } else { if (comments.filter { it.isDraft }.isNotEmpty()) { GutterState.HAS_DRAFT } else { if (comments.size == 1) GutterState.THREAD_HAS_SINGLE_COMMENT else GutterState.THREAD_HAS_MULTI_COMMENTS } } renderer.setState(state) } private fun registerGutterIconRenderers() { val lineCount = editor.document.lineCount for (logicalLine in 0 until lineCount) { myGutterIconRenderers[logicalLine] = GutterIconRendererFactory.makeGutterIconRenderer( editor.markupModel.addLineHighlighter(logicalLine, HighlighterLayer.LAST, null), shouldDisplayAddIcon, logicalLine, visibleLineLeft = null, visibleLineRight = logicalLine + 1, side = Side.RIGHT, actionListener = MyGutterIconRendererActionListener(this) ) } } private fun initializeThreadPresenterOnLineIfNotAvailable(logicalLine: Int, comments: List) { if (!myThreadPresenters.containsKey(logicalLine)) { val model = ThreadFactory.makeModel(comments) val view = ThreadFactory.makeView( projectServiceProvider, editor, providerData, mergeRequestInfo, logicalLine, Side.RIGHT, GutterPosition( editorType = DiffView.EditorType.SINGLE_SIDE, changeType = DiffView.ChangeType.UNKNOWN, newLine = logicalLine + 1, newPath = path, oldLine = null, oldPath = null, headHash = revisionNumber ), replyInDialog = true ) val presenter = ThreadFactory.makePresenter(model, view) presenter.addListener(MyThreadPresenterEventListener(this)) Disposer.register(textEditor, presenter) myThreadPresenters[logicalLine] = presenter } } private fun assertThreadPresenterAvailable(logicalLine: Int, invoker: ((ThreadPresenter) -> Unit)) { val thread = myThreadPresenters[logicalLine] ?: return invoker.invoke(thread) } private fun toggleComments(logicalLine: Int) { this.assertThreadPresenterAvailable(logicalLine) { this.displayComments(it.model, logicalLine, DiffView.DisplayCommentMode.TOGGLE) } } private fun displayComments(model: ThreadModel, logicalLine: Int, mode: DiffView.DisplayCommentMode) { when (mode) { DiffView.DisplayCommentMode.TOGGLE -> model.visible = !model.visible DiffView.DisplayCommentMode.SHOW -> model.visible = true DiffView.DisplayCommentMode.HIDE -> model.visible = false } setWritingStateOfGutterIconRenderer(model, logicalLine) } private fun setWritingStateOfGutterIconRenderer(model: ThreadModel, logicalLine: Int) { val renderer = myGutterIconRenderers[logicalLine] if (null !== renderer && model.showEditor) { renderer.setState(GutterState.WRITING) } } private fun collectCommentsOfLine(logicalLine: Int): List { val visibleLine = logicalLine + 1 val reworkWatcher = projectServiceProvider.reworkManager.findActiveReworkWatcher(providerData) if (null !== reworkWatcher) { val comments = reworkWatcher.findCommentsByPath(path) return comments.filter { val position = it.position ?: return@filter false position.newLine == visibleLine } } return listOf() } private class MyGutterIconRendererActionListener( private val self: ReworkEditorControllerImpl ) : GutterIconRendererActionListener { override fun performGutterIconRendererAction(gutterIconRenderer: GutterIconRenderer, type: GutterActionType) { when (type) { GutterActionType.ADD -> { } GutterActionType.TOGGLE -> { self.initializeThreadPresenterOnLineIfNotAvailable( gutterIconRenderer.logicalLine, self.collectCommentsOfLine(gutterIconRenderer.logicalLine) ) self.toggleComments(gutterIconRenderer.logicalLine) } } } } private class MyThreadPresenterEventListener( private val self: ReworkEditorControllerImpl ) : ThreadPresenter.EventListener { override fun onMainEditorClosed(threadPresenter: ThreadPresenter) { } override fun onEditCommentRequested(comment: Comment, content: String) { self.myReworkRequester.requestEditComment(self.providerData, comment, content) } override fun onReplyCommentRequested(content: String, repliedComment: Comment, logicalLine: Int, side: Side) { self.myReworkRequester.requestReplyComment(self.providerData, content, repliedComment) self.resetEditorOnLine(logicalLine, repliedComment) } override fun onCreateCommentRequested(content: String, position: GutterPosition, logicalLine: Int, side: Side, isDraft: Boolean) { self.myReworkRequester.requestCreateComment(self.providerData, content, position, isDraft) self.resetEditorOnLine(logicalLine, null) } override fun onPublishDraftCommentRequested(comment: Comment) { self.myReworkRequester.requestPublishComment(self.providerData, comment) } override fun onDeleteCommentRequested(comment: Comment) { self.myReworkRequester.requestDeleteComment(self.providerData, comment) } override fun onResolveCommentRequested(comment: Comment) { self.myReworkRequester.requestResolveComment(self.providerData, comment) } override fun onUnresolveCommentRequested(comment: Comment) { self.myReworkRequester.requestUnresolveComment(self.providerData, comment) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/internal/ReworkEditorManagerImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.rework.internal import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.vfs.LocalFileSystem import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.rework.ReworkEditorController import net.ntworld.mergeRequestIntegrationIde.rework.ReworkEditorManager import net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher class ReworkEditorManagerImpl( private val projectServiceProvider: ProjectServiceProvider ) : ReworkEditorManager { private val myFileEditorManagerEx = FileEditorManagerEx.getInstanceEx(projectServiceProvider.project) private val myControllerMap = mutableMapOf() override fun bootstrap(reworkWatcher: ReworkWatcher) { val editors = FileEditorManagerEx.getInstance(projectServiceProvider.project).allEditors for (editor in editors) { if (editor is TextEditor) { bootstrap(editor, reworkWatcher) } } } override fun bootstrap(editor: TextEditor, reworkWatcher: ReworkWatcher) { bindControllerToTextEditor(editor, reworkWatcher) } override fun open(providerData: ProviderData, path: String, line: Int?) { val file = LocalFileSystem.getInstance().findFileByPath(path) ?: return ApplicationManager.getApplication().invokeLater { val editors = myFileEditorManagerEx.openFile(file, true) for (fileEditor in editors) { if (fileEditor is TextEditor) { val controller = bindControllerToTextEditor(fileEditor, providerData) if (null !== line && null !== controller) { scrollAndDisplayCommentsOnLine(controller, line) } } } } } private fun scrollAndDisplayCommentsOnLine(controller: ReworkEditorController, line: Int) { controller.hideAllComments() controller.scrollToLine(line) controller.displayCommentsOnLine(line) } override fun commentsUpdated(providerData: ProviderData) { ApplicationManager.getApplication().invokeLater { val editors = myFileEditorManagerEx.allEditors for (editor in editors) { if (editor is TextEditor) { val controller = myControllerMap[editor] ?: continue controller.updateComments() } } } } override fun shutdown(providerData: ProviderData) { val editors = myFileEditorManagerEx.allEditors for (editor in editors) { if (editor is TextEditor) { val controller = myControllerMap[editor] ?: continue controller.dispose() } } } private fun bindControllerToTextEditor( textEditor: TextEditor, providerData: ProviderData ): ReworkEditorController? { if (myControllerMap.contains(textEditor)) { return myControllerMap[textEditor] } val reworkWatcher = projectServiceProvider.reworkManager.findActiveReworkWatcher(providerData) if (null === reworkWatcher) { return null } return bindControllerToTextEditor(textEditor, reworkWatcher) } private fun bindControllerToTextEditor( textEditor: TextEditor, reworkWatcher: ReworkWatcher ): ReworkEditorController? { if (myControllerMap.contains(textEditor)) { return myControllerMap[textEditor] } val editor = textEditor.editor as? EditorEx ?: return null val virtualFile = textEditor.file ?: return null val change = reworkWatcher.findChangeByPath(virtualFile.path) ?: return null val afterRevision = change.afterRevision ?: return null val instance = ReworkEditorControllerImpl( projectServiceProvider, textEditor, editor, reworkWatcher.providerData, reworkWatcher.mergeRequestInfo, virtualFile.path, afterRevision.revisionNumber.toString() ) myControllerMap[textEditor] = instance return instance } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/internal/ReworkGeneralCommentsView.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.rework.internal import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.ui.Messages import com.intellij.ui.JBColor import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.components.Panel import com.intellij.util.ui.JBUI import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.component.comment.EditorComponent import net.ntworld.mergeRequestIntegrationIde.component.comment.GroupComponent import net.ntworld.mergeRequestIntegrationIde.component.comment.Options import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkWatcherNotifier import net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher import java.awt.Dimension import javax.swing.BorderFactory import javax.swing.BoxLayout import javax.swing.ScrollPaneConstants class ReworkGeneralCommentsView( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val reworkWatcher: ReworkWatcher ) { private val myReworkRequester = projectServiceProvider.messageBus.syncPublisher(ReworkWatcherNotifier.TOPIC) private val myGroupCommentCollection = mutableListOf() private val myThreadPanel = Panel() private val myThreadBoxLayout = JBUI.Panels.simplePanel() private val myThreadWrapper = ScrollPaneFactory.createScrollPane(myThreadBoxLayout, true) private val myMainEditorEventListener = object : EditorComponent.EventListener { override fun onEditorFocused(editor: EditorComponent) { myThreadWrapper.verticalScrollBar.value = myThreadWrapper.verticalScrollBar.maximum } override fun onEditorResized(editor: EditorComponent) {} override fun onCancelClicked(editor: EditorComponent) { if (editor.text.isNotBlank()) { val result = Messages.showYesNoDialog( "Do you want to delete the whole content?", "Are you sure", Messages.getQuestionIcon() ) result == Messages.YES } editor.text = "" } override fun onSubmitClicked(editor: EditorComponent, isDraft: Boolean) { val text = editor.text.trim() if (text.isNotEmpty()) { myReworkRequester.requestCreateComment(providerData, text, null, isDraft) } } } private val myMainEditor by lazy { val editor = projectServiceProvider.componentFactory.commentComponents.makeEditor( projectServiceProvider.project, EditorComponent.Type.NEW_DISCUSSION, 0, borderLeftRight = 0, showCancelAction = false, isDoingCodeReview = projectServiceProvider.isDoingCodeReview() ) editor.isVisible = true editor.addListener(myMainEditorEventListener) editor } private val myGroupComponentEventListener = object : GroupComponent.EventListener { override fun onResized() {} override fun onOpenDialogClicked() { } override fun onEditorCreated(groupId: String, editor: EditorComponent) { } override fun onEditorDestroyed(groupId: String, editor: EditorComponent) { } override fun onReplyCommentRequested(comment: Comment, content: String) { if (content.trim().isNotEmpty()) { myReworkRequester.requestReplyComment(providerData, content, comment) } } override fun onEditCommentRequested(comment: Comment, content: String) { myReworkRequester.requestEditComment(providerData, comment, content) } override fun onPublishDraftCommentRequested(comment: Comment) { myReworkRequester.requestPublishComment(providerData, comment) } override fun onDeleteCommentRequested(comment: Comment) { myReworkRequester.requestDeleteComment(providerData, comment) } override fun onResolveCommentRequested(comment: Comment) { myReworkRequester.requestResolveComment(providerData, comment) } override fun onUnresolveCommentRequested(comment: Comment) { myReworkRequester.requestUnresolveComment(providerData, comment) } } init { myMainEditor.addCommentNowButtonText = "Add general comment" myMainEditor.addCommentNowButtonDesc = "Create new thread of general comment" myThreadPanel.background = JBColor.border() myThreadBoxLayout.background = JBColor.border() myThreadWrapper.background = JBColor.border() myThreadWrapper.border = BorderFactory.createMatteBorder(1, 1, 0, 1, JBColor.border()) myThreadWrapper.minimumSize = Dimension(600, 100) myThreadWrapper.maximumSize = Dimension(1200, -1) myThreadWrapper.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER myThreadBoxLayout.addToCenter(myThreadPanel) myThreadPanel.layout = BoxLayout(myThreadPanel, BoxLayout.Y_AXIS) myThreadBoxLayout.addToBottom(myMainEditor.component) } fun focusMainEditor() { myMainEditor.focus() } fun render(groupedComments: Map>) { myThreadPanel.removeAll() myGroupCommentCollection.forEach { it.dispose() } myGroupCommentCollection.clear() groupedComments.forEach { (groupId, comments) -> if (comments.isEmpty()) { return@forEach } val group = projectServiceProvider.componentFactory.commentComponents.makeGroup( reworkWatcher.providerData, reworkWatcher.mergeRequestInfo, projectServiceProvider.project, false, groupId, comments, Options(borderLeftRight = 0, showMoveToDialog = false) ) group.addListener(myGroupComponentEventListener) myThreadPanel.add(group.component) myGroupCommentCollection.add(group) } val builder = DialogBuilder() builder.setCenterPanel(myThreadWrapper) builder.removeAllActions() builder.addOkAction() builder.resizable(true) builder.show() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/internal/ReworkManagerImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.rework.internal import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.vcs.changes.Change import git4idea.repo.GitRepository import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.MergeRequestState import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.ProviderStatus import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequestIntegration.exception.ProviderNotFoundException import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegrationIde.debug import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.rework.BranchWatcher import net.ntworld.mergeRequestIntegrationIde.rework.ReworkManager import net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher import net.ntworld.mergeRequestIntegrationIde.task.SearchMergeRequestTask import net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil import java.util.* internal class ReworkManagerImpl( private val projectServiceProvider: ProjectServiceProvider ) : ReworkManager { private val myBranchWatchers = Collections.synchronizedMap(mutableMapOf()) private val myReworkWatchers = Collections.synchronizedMap(mutableMapOf()) override fun clear() { myBranchWatchers.forEach { entry -> entry.value.shutdown() } myBranchWatchers.clear() myReworkWatchers.forEach { entry -> entry.value.shutdown() } myReworkWatchers.clear() } override fun markBranchWatcherTerminated(branchWatcher: BranchWatcher) { myBranchWatchers.remove(branchWatcher.providerData.id) debug("${branchWatcher.providerData.id}: clear BranchWatcher") } override fun markReworkWatcherTerminated(reworkWatcher: ReworkWatcher) { val key = reworkWatcher.key() myReworkWatchers.remove(key) debug("$key: clear ReworkManager") } override fun createBranchWatcher(providerData: ProviderData) { if (!projectServiceProvider.applicationSettings.enableReworkProcess) { return } if (providerData.status != ProviderStatus.ACTIVE || myBranchWatchers.containsKey(providerData.id)) { return } var gitRepository = RepositoryUtil.findRepository(projectServiceProvider, providerData) var count = 0 while (gitRepository === null && count < 100) { debug("${providerData.id}: cannot find repository, retry in 10s") count++ Thread.sleep(10000) gitRepository = RepositoryUtil.findRepository(projectServiceProvider, providerData) } val repository = gitRepository if (null !== repository) { val branchWatcher = BranchWatcherImpl( projectServiceProvider, this, providerData, repository ) myBranchWatchers[providerData.id] = branchWatcher debug("${providerData.id}: create BranchWatcher") projectServiceProvider.applicationServiceProvider.watcherManager.addWatcher(branchWatcher) } } override fun requestCreateReworkWatcher(providers: List, branchName: String) { if (!projectServiceProvider.applicationSettings.enableReworkProcess) { return } val pair = findProviderData(providers, branchName) val providerData = pair.first val repository = pair.second if (null !== providerData && null !== repository) { requestCreateReworkWatcher(providerData, repository, branchName) } } override fun requestCreateReworkWatcher(providerData: ProviderData, repository: GitRepository, branchName: String) { if (!projectServiceProvider.applicationSettings.enableReworkProcess) { return } val key = ReworkWatcher.keyOf(providerData, branchName) if (myReworkWatchers.contains(key)) { return } val task = SearchMergeRequestTask( projectServiceProvider, providerData, GetMergeRequestFilter.make( state = MergeRequestState.OPENED, id = null, search = "", authorId = "", assigneeId = "", approverIds = listOf(), sourceBranch = branchName ), MergeRequestOrdering.RECENTLY_UPDATED, object : SearchMergeRequestTask.Listener { override fun onError(exception: Exception) { if (exception !is ProviderNotFoundException) { throw exception } } override fun dataReceived(list: List, page: Int, totalPages: Int, totalItems: Int) { if (myReworkWatchers.contains(key)) { return } ApplicationManager.getApplication().invokeLater { if (list.isNotEmpty()) { val mergeRequestInfo = list.first() debug("${providerData.id}:${branchName}: create watcher") val reworkWatcher = ReworkWatcherImpl( projectServiceProvider, repository, branchName, providerData, mergeRequestInfo ) myReworkWatchers[key] = reworkWatcher projectServiceProvider.applicationServiceProvider.watcherManager.addWatcher(reworkWatcher) } } } } ) task.start() } override fun findReworkWatcherByChange(providerData: ProviderData, change: Change): ReworkWatcher? { for (entry in myReworkWatchers) { if (entry.value.providerData.id != providerData.id) { continue } if (entry.value.changes.contains(change)) { return entry.value } } return null } override fun findActiveReworkWatcher(providerData: ProviderData): ReworkWatcher? { for (entry in myReworkWatchers) { if (entry.value.providerData.id != providerData.id) { continue } return entry.value } return null } override fun getActiveReworkWatchers(): List { return myReworkWatchers.values.toList() } private fun findProviderData( providers: List, branchName: String ): Pair { for (provider in providers) { val repository = RepositoryUtil.findRepository(projectServiceProvider, provider) if (null === repository) { continue } if (repository.currentBranchName != branchName) { continue } return Pair(provider, repository) } return Pair(null, null) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/internal/ReworkWatcherImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.rework.internal import com.intellij.notification.NotificationType import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.vcs.changes.Change import com.intellij.openapi.vcs.changes.ChangesUtil import com.intellij.openapi.vcs.changes.PreviewDiffVirtualFile import git4idea.repo.GitRepository import net.ntworld.mergeRequest.* import net.ntworld.mergeRequest.command.DeleteCommentCommand import net.ntworld.mergeRequest.command.ResolveCommentCommand import net.ntworld.mergeRequest.command.UnresolveCommentCommand import net.ntworld.mergeRequest.request.CreateCommentRequest import net.ntworld.mergeRequest.request.PublishCommentsRequest import net.ntworld.mergeRequest.request.ReplyCommentRequest import net.ntworld.mergeRequest.request.UpdateCommentRequest import net.ntworld.mergeRequestIntegration.internal.CommentPositionImpl import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegration.provider.ProviderException import net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition import net.ntworld.mergeRequestIntegrationIde.debug import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.DiffPreviewProviderImpl import net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ReviewContextImpl import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.DiffNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkEditorNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkWatcherNotifier import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeView import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.* import net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher import net.ntworld.mergeRequestIntegrationIde.task.FindMergeRequestTask import net.ntworld.mergeRequestIntegrationIde.task.GetCommentsTask import net.ntworld.mergeRequestIntegrationIde.task.GetCommitsTask import net.ntworld.mergeRequestIntegrationIde.util.CommentUtil import net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil import java.util.* class ReworkWatcherImpl( override val projectServiceProvider: ProjectServiceProvider, override val repository: GitRepository, override val branchName: String, override val providerData: ProviderData, override val mergeRequestInfo: MergeRequestInfo ) : ReworkWatcher, ReworkWatcherNotifier { private val myConnection = projectServiceProvider.messageBus.connect() private val myEditorManager = projectServiceProvider.messageBus.syncPublisher( ReworkEditorNotifier.TOPIC ) private val myDiffPublisher = projectServiceProvider.messageBus.syncPublisher(DiffNotifier.TOPIC) private val myPreviewDiffVirtualFileMap = mutableMapOf() private val myCommentsMap = Collections.synchronizedMap(mutableMapOf>()) private val myChangesMap = Collections.synchronizedMap(mutableMapOf>()) private val myComments = mutableListOf() private val myReworkGeneralCommentsView = ReworkGeneralCommentsView(projectServiceProvider, providerData, this) @Volatile private var myTerminate = false private var myIsChangesBuilt = false private var myIsFetchedComments = false private var myRunCount = 0 private var myMergeRequest: MergeRequest? = null override var commits: List = listOf() override var changes: List = listOf() override var comments: MutableList = mutableListOf() private set override var onlyShowDraftComments: Boolean = false private set override var displayResolvedComments: Boolean = false private set override val interval: Long = 10000 private val myGetCommitsTaskListener = object : GetCommitsTask.Listener { override fun dataReceived(mergeRequestInfo: MergeRequestInfo, commits: List) { this@ReworkWatcherImpl.commits = commits changes = projectServiceProvider.repositoryFile.findChanges(providerData, commits.map { it.id }) buildChangesMap() myIsChangesBuilt = true ApplicationManager.getApplication().invokeLater { projectServiceProvider.openSingleMRToolWindow { projectServiceProvider.singleMRToolWindowNotifierTopic.showReworkChanges( this@ReworkWatcherImpl, changes ) } myEditorManager.bootstrap(this@ReworkWatcherImpl) } } } private val myGetCommentsTaskListener = object : GetCommentsTask.Listener { override fun dataReceived( providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List ) { myComments.clear() myComments.addAll(comments) buildCommentsAndSendCommentsUpdatedSignal() myIsFetchedComments = true } } private val myFindMergeRequestTaskListener = object : FindMergeRequestTask.Listener { override fun dataReceived(mergeRequest: MergeRequest) { myMergeRequest = mergeRequest } } private val reviewContext = ReviewContextImpl( projectServiceProvider, providerData, mergeRequestInfo, projectServiceProvider.messageBus.connect() ) init { myConnection.subscribe(ReworkWatcherNotifier.TOPIC, this) projectServiceProvider.singleMRToolWindowNotifierTopic.registerReworkWatcher(this) fetchCommits() fetchMergeRequest() } override fun isChangesBuilt(): Boolean { return myIsChangesBuilt } override fun isFetchedComments(): Boolean { return myIsFetchedComments } override fun canExecute(): Boolean { return !myTerminate && !projectServiceProvider.project.isDisposed } override fun shouldTerminate(): Boolean { return myTerminate || projectServiceProvider.project.isDisposed || repository.currentBranchName != branchName || projectServiceProvider.isDoingCodeReview() || !projectServiceProvider.applicationSettings.enableReworkProcess } override fun execute() { // the watcher runs every 10 seconds, so 18x10s = 3 minutes if (myRunCount % 18 == 0) { fetchComments() } myRunCount += 1 } override fun terminate() { debug("${providerData.id}:$branchName: ReworkWatcher is terminated") if (!projectServiceProvider.messageBus.isDisposed) { projectServiceProvider.singleMRToolWindowNotifierTopic.removeReworkWatcher(this) } projectServiceProvider.reworkManager.markReworkWatcherTerminated(this) myConnection.disconnect() } override fun shutdown() { debug("${providerData.id}:$branchName: terminate ReworkWatcher") if (!projectServiceProvider.messageBus.isDisposed) { projectServiceProvider.singleMRToolWindowNotifierTopic.removeReworkWatcher(this) } ApplicationManager.getApplication().invokeLater { val fileEditorManagerEx = FileEditorManagerEx.getInstanceEx(projectServiceProvider.project) myPreviewDiffVirtualFileMap.forEach { (_, diffFile) -> fileEditorManagerEx.closeFile(diffFile) } myPreviewDiffVirtualFileMap.clear() myEditorManager.shutdown(providerData) } myTerminate = true } override fun openChange(change: Change) { val afterRevision = change.afterRevision if (null === afterRevision) { updateDataToReviewContext() return openChangeAsDiffView(change) } myEditorManager.open(providerData, afterRevision.file.path) } override fun findChangeByPath(absolutePath: String): Change? { return if (myTerminate) null else { val changes = myChangesMap[absolutePath] if (null === changes) { return null } return changes.first() } } override fun findCommentsByPath(absolutePath: String): List { return myCommentsMap[absolutePath] ?: listOf() } override fun fetchComments() { debug("${providerData.id}:$branchName: fetching comments") val task = GetCommentsTask( projectServiceProvider = projectServiceProvider, providerData = providerData, mergeRequestInfo = mergeRequestInfo, listener = myGetCommentsTaskListener ) task.start() } override fun requestFetchComment(providerData: ProviderData) = assertHasSameProvider(providerData) { fetchComments() } override fun changeDisplayResolvedComments( providerData: ProviderData, value: Boolean ) = assertHasSameProvider(providerData) { if (value != displayResolvedComments) { displayResolvedComments = value buildCommentsAndSendCommentsUpdatedSignal() } } override fun changeOnlyShowDraftComments(providerData: ProviderData, value: Boolean) = assertHasSameProvider(providerData) { if (value != onlyShowDraftComments) { onlyShowDraftComments = value buildCommentsAndSendCommentsUpdatedSignal() } } override fun commentTreeNodeSelected( providerData: ProviderData, node: Node, type: CommentTreeView.TreeSelectType ) = assertHasSameProvider(providerData) { if (type == CommentTreeView.TreeSelectType.NORMAL) { return@assertHasSameProvider } when (node) { is GeneralCommentsNode -> displayGeneralComments(node.groupComments(), false) is ThreadNode -> when (val parent = node.parent) { is GeneralCommentsNode -> displayGeneralComments(parent.groupComments(), false) is FileLineNode -> openFileAndDisplayThreadByLineNode(parent) } is CommentNode -> when (val parent = node.parent!!.parent) { is GeneralCommentsNode -> displayGeneralComments(parent.groupComments(), false) is FileLineNode -> openFileAndDisplayThreadByLineNode(parent) } is FileNode -> openFileByNode(node) is FileLineNode -> openFileAndDisplayThreadByLineNode(node) } } override fun openCreateGeneralCommentForm(providerData: ProviderData) = assertHasSameProvider(providerData) { val groupedComments = mutableMapOf>() comments.forEach { if (null !== it.position) { return@forEach } if (!groupedComments.containsKey(it.parentId)) { groupedComments[it.parentId] = mutableListOf() } groupedComments[it.parentId]!!.add(it) } displayGeneralComments(groupedComments, true) } override fun requestReplyComment( providerData: ProviderData, content: String, repliedComment: Comment ) = assertHasSameProvider(providerData) { projectServiceProvider.infrastructure.serviceBus() process ReplyCommentRequest.make( providerId = providerData.id, mergeRequestId = mergeRequestInfo.id, repliedComment = repliedComment, body = content ) ifError { projectServiceProvider.notify( "There was an error from server. \n\n ${it.message}", NotificationType.ERROR ) throw ProviderException(it) } fetchComments() } override fun requestCreateComment( providerData: ProviderData, content: String, position: GutterPosition?, isDraft: Boolean ) = assertHasSameProvider(providerData) { val commentPosition = convertGutterPositionToCommentPosition(position) projectServiceProvider.infrastructure.serviceBus() process CreateCommentRequest.make( providerId = providerData.id, mergeRequestId = mergeRequestInfo.id, position = commentPosition, body = content, isDraft = false ) ifError { projectServiceProvider.notify( "There was an error from server. \n\n ${it.message}", NotificationType.ERROR ) throw ProviderException(it) } fetchComments() } override fun requestEditComment(providerData: ProviderData, comment: Comment, content: String) { projectServiceProvider.infrastructure.serviceBus() process UpdateCommentRequest.make( providerId = providerData.id, mergeRequestId = mergeRequestInfo.id, comment = comment, body = content ) ifError { projectServiceProvider.notify( "There was an error from server. \n\n ${it.message}", NotificationType.ERROR ) throw ProviderException(it) } fetchComments() } override fun requestPublishComment(providerData: ProviderData, comment: Comment) { projectServiceProvider.infrastructure.serviceBus() process PublishCommentsRequest.make( providerId = providerData.id, mergeRequestId = mergeRequestInfo.id, draftCommentIds = listOf(comment.id) ) ifError { projectServiceProvider.notify( "There was an error from server. \n\n ${it.message}", NotificationType.ERROR ) throw ProviderException(it) } fetchComments() } override fun requestDeleteComment( providerData: ProviderData, comment: Comment ) = assertHasSameProvider(providerData) { projectServiceProvider.infrastructure.commandBus() process DeleteCommentCommand.make( providerId = providerData.id, mergeRequestId = mergeRequestInfo.id, comment = comment ) fetchComments() } override fun requestResolveComment( providerData: ProviderData, comment: Comment ) = assertHasSameProvider(providerData) { projectServiceProvider.infrastructure.commandBus() process ResolveCommentCommand.make( providerId = providerData.id, mergeRequestId = mergeRequestInfo.id, comment = comment ) fetchComments() } override fun requestUnresolveComment( providerData: ProviderData, comment: Comment ) = assertHasSameProvider(providerData) { projectServiceProvider.infrastructure.commandBus() process UnresolveCommentCommand.make( providerId = providerData.id, mergeRequestId = mergeRequestInfo.id, comment = comment ) fetchComments() } private fun convertGutterPositionToCommentPosition(input: GutterPosition?): CommentPosition? { if (null === input) return null return CommentPositionImpl( oldLine = input.oldLine, oldPath = if (null === input.oldPath) null else RepositoryUtil.findRelativePath(repository, input.oldPath), newLine = input.newLine, newPath = if (null === input.newPath) null else RepositoryUtil.findRelativePath(repository, input.newPath), baseHash = if (input.baseHash.isNullOrEmpty()) findBaseHash() else input.baseHash, headHash = if (input.headHash.isNullOrEmpty()) findHeadHash() else input.headHash, startHash = if (input.startHash.isNullOrEmpty()) findStartHash() else input.startHash, source = CommentPositionSource.UNKNOWN, changeType = CommentPositionChangeType.UNKNOWN ) } private fun findBaseHash(): String { if (commits.isNotEmpty()) { return commits.last().id } val mr = myMergeRequest ?: return "" val diff = mr.diffReference return if (null === diff) "" else diff.baseHash } private fun findStartHash(): String { val mr = myMergeRequest ?: return "" val diff = mr.diffReference return if (null === diff) "" else diff.startHash } private fun findHeadHash(): String { if (commits.isNotEmpty()) { return commits.first().id } val mr = myMergeRequest ?: return "" val diff = mr.diffReference return if (null === diff) "" else diff.headHash } private fun openFileByNode(node: FileNode) { val fullPath = RepositoryUtil.findAbsoluteCrossPlatformsPath(repository, node.path) openFileAndDisplayThread(absolutePath = fullPath, line = null, position = null) } private fun openFileAndDisplayThreadByLineNode(node: FileLineNode) { val fullPath = RepositoryUtil.findAbsoluteCrossPlatformsPath(repository, node.path) if (null === node.position.newLine) { // edge case, open diff view instead of editor view if there is no newLine val change = findChangeByPath(fullPath) if (null !== change) { updateDataToReviewContext() openChangeAsDiffView(change) reviewContext.putChangeData(change, DiffNotifier.ScrollPosition, node.position) reviewContext.putChangeData(change, DiffNotifier.ScrollShowComments, true) myDiffPublisher.hideAllCommentsRequested(reviewContext, change) myDiffPublisher.scrollToPositionRequested(reviewContext, change, node.position, true) } } else { openFileAndDisplayThread(absolutePath = fullPath, line = node.line, position = node.position) } } private fun openFileAndDisplayThread(absolutePath: String, line: Int?, position: CommentPosition?) { val change = findChangeByPath(absolutePath) if (null === change) { return myEditorManager.open(providerData, absolutePath, line) } val afterRevision = change.afterRevision if (null === afterRevision) { updateDataToReviewContext() openChangeAsDiffView(change) if (null !== position) { reviewContext.putChangeData(change, DiffNotifier.ScrollPosition, position) reviewContext.putChangeData(change, DiffNotifier.ScrollShowComments, true) myDiffPublisher.hideAllCommentsRequested(reviewContext, change) myDiffPublisher.scrollToPositionRequested(reviewContext, change, position, true) } return } return myEditorManager.open(providerData, absolutePath, line) } private fun displayGeneralComments(groupedComments: Map>, focusToEditor: Boolean) { myReworkGeneralCommentsView.render(groupedComments) if (focusToEditor) { myReworkGeneralCommentsView.focusMainEditor() } } private fun buildCommentsMap() { myCommentsMap.clear() val grouped = CommentUtil.groupCommentsByNewPath(comments) grouped.forEach { (relativePath, list) -> val fullPath = RepositoryUtil.findAbsoluteCrossPlatformsPath(repository, relativePath) myCommentsMap[fullPath] = list } } private fun buildChangesMap() { myChangesMap.clear() for (change in changes) { val filePaths = ChangesUtil.getPathsCaseSensitive(change) for (filePath in filePaths) { val path = filePath.path val list = myChangesMap.get(path) if (null === list) { myChangesMap[path] = mutableListOf(change) } else { if (!list.contains(change)) { list.add(change) } } } } } private fun openChangeAsDiffView(change: Change) { val project = projectServiceProvider.project val diffFile = myPreviewDiffVirtualFileMap[change] if (null === diffFile) { val provider = DiffPreviewProviderImpl(project, change, reviewContext, false) val created = PreviewDiffVirtualFile(provider) myPreviewDiffVirtualFileMap[change] = created FileEditorManagerEx.getInstanceEx(project).openFile(created, true) } else { FileEditorManagerEx.getInstanceEx(project).openFile(diffFile, true) } } private fun updateDataToReviewContext() { val mr = myMergeRequest if (null !== mr) { reviewContext.diffReference = mr.diffReference } reviewContext.commits = commits reviewContext.reviewingCommits = commits reviewContext.changes = changes reviewContext.reviewingChanges = changes reviewContext.comments = comments } private fun fetchCommits() { debug("${providerData.id}:$branchName: fetching commits") val task = GetCommitsTask( projectServiceProvider = projectServiceProvider, providerData = providerData, mergeRequestInfo = mergeRequestInfo, listener = myGetCommitsTaskListener ) task.start() } private fun fetchMergeRequest() { debug("${providerData.id}:$branchName: fetching mergeRequest") val task = FindMergeRequestTask( projectServiceProvider = projectServiceProvider, providerData = providerData, mergeRequestInfo = mergeRequestInfo, listener = myFindMergeRequestTaskListener ) task.start() } private fun assertHasSameProvider(providerData: ProviderData, invoker: (() -> Unit)) { if (this.providerData.id == providerData.id) { invoker() } } private fun buildCommentsAndSendCommentsUpdatedSignal() { comments.clear() if (onlyShowDraftComments) { comments.addAll(myComments.filter { it.isDraft }) } else { if (displayResolvedComments) { comments.addAll(myComments) } else { comments.addAll(myComments.filter { !it.resolved }) } } buildCommentsMap() myEditorManager.commentsUpdated(providerData) ApplicationManager.getApplication().invokeLater { projectServiceProvider.singleMRToolWindowNotifierTopic.showReworkComments( this, comments, displayResolvedComments ) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/FetchProjectMembersTask.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.task import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.UserInfo import net.ntworld.mergeRequest.UserStatus import net.ntworld.mergeRequest.query.GetProjectMembersQuery import net.ntworld.mergeRequestIntegration.internal.UserInfoImpl import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import com.intellij.openapi.project.Project as IdeaProject class FetchProjectMembersTask( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val addEmptyMember: Boolean, private val listener: Listener ) : Task.Backgroundable(projectServiceProvider.project, "Fetching project members...", true) { fun start() { ProgressManager.getInstance().runProcessWithProgressAsynchronously( this, Indicator(this) ) } override fun run(indicator: ProgressIndicator) { try { listener.taskStarted() val query = GetProjectMembersQuery.make(providerData.id) val result = projectServiceProvider.infrastructure.queryBus() process query val data = result.members .filter { it.status == UserStatus.ACTIVE } .sortedBy { it.name } if (addEmptyMember) { val dataWithEmptyMember = mutableListOf(UserInfoImpl.None) listener.dataReceived(dataWithEmptyMember + data) } else { listener.dataReceived(data) } listener.taskEnded() } catch (exception: Exception) { listener.onError(exception) } } private class Indicator(private val task: FetchProjectMembersTask) : BackgroundableProcessIndicator(task) interface Listener { fun onError(exception: Exception) fun taskStarted() fun dataReceived(collection: List) fun taskEnded() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/FindApprovalTask.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.task import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator import net.ntworld.mergeRequest.Approval import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.query.FindApprovalQuery import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider class FindApprovalTask( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val mergeRequestInfo: MergeRequestInfo, private val listener: Listener ) : Task.Backgroundable(projectServiceProvider.project, "Fetching approval data...", true) { fun start() { ProgressManager.getInstance().runProcessWithProgressAsynchronously( this, Indicator(this) ) } private class Indicator(private val task: FindApprovalTask) : BackgroundableProcessIndicator(task) override fun run(indicator: ProgressIndicator) { try { listener.taskStarted() val result = projectServiceProvider.infrastructure.queryBus() process FindApprovalQuery.make( providerId = providerData.id, mergeRequestId = mergeRequestInfo.id ) listener.dataReceived(mergeRequestInfo, result.approval) listener.taskEnded() } catch (exception: Exception) { listener.onError(exception) } } interface Listener { fun onError(exception: Exception) { throw exception } fun taskStarted() {} fun dataReceived(mergeRequestInfo: MergeRequestInfo, approval: Approval) fun taskEnded() {} } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/FindMergeRequestTask.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.task import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator import net.ntworld.mergeRequest.MergeRequest import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.query.FindMergeRequestQuery import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider class FindMergeRequestTask( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val mergeRequestInfo: MergeRequestInfo, private val listener: Listener ) : Task.Backgroundable(projectServiceProvider.project, "Fetching merge request...", true) { fun start() { ProgressManager.getInstance().runProcessWithProgressAsynchronously( this, Indicator(this) ) } private class Indicator(private val task: FindMergeRequestTask) : BackgroundableProcessIndicator(task) override fun run(indicator: ProgressIndicator) { try { listener.taskStarted() val result = projectServiceProvider.infrastructure.queryBus() process FindMergeRequestQuery.make( providerId = providerData.id, mergeRequestId = mergeRequestInfo.id ) listener.dataReceived(result.mergeRequest) projectServiceProvider.reviewContextManager.updateMergeRequest(providerData.id, result.mergeRequest) listener.taskEnded() } catch (exception: Exception) { listener.onError(exception) } } interface Listener { fun onError(exception: Exception) { throw exception } fun taskStarted() {} fun dataReceived(mergeRequest: MergeRequest) fun taskEnded() {} } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/GetAvailableUpdatesTask.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.task import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator import com.intellij.openapi.project.Project import net.ntworld.mergeRequestIntegration.update.UpdateManager class GetAvailableUpdatesTask( ideaProject: Project, private val listener: Listener ) : Task.Backgroundable(ideaProject, "Check available updates...", true) { fun start() { ProgressManager.getInstance().runProcessWithProgressAsynchronously( this, Indicator(this) ) } private class Indicator(private val task: GetAvailableUpdatesTask) : BackgroundableProcessIndicator(task) override fun run(indicator: ProgressIndicator) { try { listener.dataReceived(UpdateManager.getAvailableUpdates()) } catch (exception: Exception) { listener.onError(exception) } } interface Listener { fun onError(exception: Exception) { throw exception } fun dataReceived(updates: List) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/GetCommentsTask.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.task import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.query.GetCommentsQuery import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider class GetCommentsTask( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val mergeRequestInfo: MergeRequestInfo, private val listener: Listener ): Task.Backgroundable(projectServiceProvider.project, "Fetching comment data...", true) { fun start() { ProgressManager.getInstance().runProcessWithProgressAsynchronously( this, Indicator(this) ) } private class Indicator(private val task: GetCommentsTask) : BackgroundableProcessIndicator(task) override fun run(indicator: ProgressIndicator) { try { listener.taskStarted() val result = projectServiceProvider.infrastructure.queryBus() process GetCommentsQuery.make( providerId = providerData.id, mergeRequestId = mergeRequestInfo.id ) listener.dataReceived(providerData, mergeRequestInfo, result.comments) projectServiceProvider.reviewContextManager.updateComments( providerData.id, mergeRequestInfo.id, result.comments ) listener.taskEnded() } catch (exception: Exception) { listener.onError(exception) } } interface Listener { fun onError(exception: Exception) { throw exception } fun taskStarted() {} fun dataReceived(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List) fun taskEnded() {} } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/GetCommitsTask.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.task import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator import net.ntworld.mergeRequest.Commit import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.query.GetCommitsQuery import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider class GetCommitsTask( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val mergeRequestInfo: MergeRequestInfo, private val listener: Listener ) : Task.Backgroundable(projectServiceProvider.project, "Fetching commit data...", true) { fun start() { ProgressManager.getInstance().runProcessWithProgressAsynchronously( this, Indicator(this) ) } private class Indicator(private val task: GetCommitsTask) : BackgroundableProcessIndicator(task) override fun run(indicator: ProgressIndicator) { try { listener.taskStarted() val result = projectServiceProvider.infrastructure.queryBus() process GetCommitsQuery.make( providerId = providerData.id, mergeRequestId = mergeRequestInfo.id ) listener.dataReceived(mergeRequestInfo, result.commits) projectServiceProvider.reviewContextManager.updateCommits( providerData.id, mergeRequestInfo.id, result.commits ) listener.taskEnded() } catch (exception: Exception) { listener.onError(exception) } } interface Listener { fun onError(exception: Exception) { throw exception } fun taskStarted() {} fun dataReceived(mergeRequestInfo: MergeRequestInfo, commits: List) fun taskEnded() {} } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/GetPipelinesTask.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.task import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.Pipeline import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.query.GetPipelinesQuery import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider class GetPipelinesTask( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val mergeRequestInfo: MergeRequestInfo, private val listener: Listener ) : Task.Backgroundable(projectServiceProvider.project, "Fetching pipeline data...", true) { fun start() { ProgressManager.getInstance().runProcessWithProgressAsynchronously( this, Indicator(this) ) } private class Indicator(private val task: GetPipelinesTask) : BackgroundableProcessIndicator(task) override fun run(indicator: ProgressIndicator) { try { listener.taskStarted() val result = projectServiceProvider.infrastructure.queryBus() process GetPipelinesQuery.make( providerId = providerData.id, mergeRequestId = mergeRequestInfo.id ) listener.dataReceived(mergeRequestInfo, result.pipelines) listener.taskEnded() } catch (exception: Exception) { listener.onError(exception) } } interface Listener { fun onError(exception: Exception) { throw exception } fun taskStarted() {} fun dataReceived(mergeRequestInfo: MergeRequestInfo, pipelines: List) fun taskEnded() {} } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/RegisterProviderTask.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.task import com.intellij.notification.NotificationType import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.ProviderStatus import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProviderSettings class RegisterProviderTask( private val projectServiceProvider: ProjectServiceProvider, private val id: String, private val name: String, private val settings: ProviderSettings, private val listener: Listener ) : Task.Backgroundable(projectServiceProvider.project, "Fetching provider information...", true) { fun start() { ProgressManager.getInstance().runProcessWithProgressAsynchronously( this, Indicator(this) ) } override fun run(indicator: ProgressIndicator) { val job = GlobalScope.launch { val pair = projectServiceProvider.providerStorage.register( infrastructure = projectServiceProvider.infrastructure, id = id, key = settings.id, name = name, info = settings.info, credentials = settings.credentials, repository = settings.repository ) if (!indicator.isCanceled) { listener.providerRegistered(pair.first) if (pair.first.status == ProviderStatus.ERROR) { projectServiceProvider.notify(pair.first.errorMessage ?: "", NotificationType.ERROR) } if (null !== pair.second) { throw pair.second!! } } } job.start() while (job.isActive && !indicator.isCanceled) { Thread.sleep(100); } if (job.isActive) { job.cancel() } indicator.checkCanceled() } private class Indicator(private val task: RegisterProviderTask) : BackgroundableProcessIndicator(task) interface Listener { fun providerRegistered(providerData: ProviderData) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/RepositoryFetchAllRemotesTask.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.task import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.Task import git4idea.fetch.GitFetchSupport import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil class RepositoryFetchAllRemotesTask( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData ) : Task.Backgroundable(projectServiceProvider.project, "Fetching...", true) { override fun run(indicator: ProgressIndicator) { val repository = RepositoryUtil.findRepository(projectServiceProvider, providerData) if (null !== repository) { val service = ServiceManager.getService(projectServiceProvider.project, GitFetchSupport::class.java) service.fetchAllRemotes(listOf(repository)) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/SearchMergeRequestTask.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.task import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequest.query.GetMergeRequestsQuery import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegrationIde.SEARCH_MERGE_REQUEST_ITEMS_PER_PAGE import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider class SearchMergeRequestTask( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val filtering: GetMergeRequestFilter, private val ordering: MergeRequestOrdering, private val listener: Listener ) : Task.Backgroundable(projectServiceProvider.project, "Fetching merge requests...", true) { var page: Int = 1 fun start() = start(1) fun start(page: Int) { this.page = page ProgressManager.getInstance().runProcessWithProgressAsynchronously( this, Indicator(this) ) } override fun run(indicator: ProgressIndicator) { try { val currentPage = page listener.taskStarted() val query = GetMergeRequestsQuery.make( providerId = providerData.id, filterBy = filtering, orderBy = ordering, page = currentPage, itemsPerPage = SEARCH_MERGE_REQUEST_ITEMS_PER_PAGE ) val result = projectServiceProvider.infrastructure.queryBus() process query listener.dataReceived(result.mergeRequests, currentPage, result.totalPages, result.totalItems) listener.taskEnded() } catch (exception: Exception) { listener.onError(exception) } } private class Indicator(private val task: SearchMergeRequestTask) : BackgroundableProcessIndicator(task) interface Listener { fun onError(exception: Exception) { throw exception } fun taskStarted() {} fun dataReceived(list: List, page: Int, totalPages: Int, totalItems: Int) fun taskEnded() {} } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/CommentsToolWindowTab.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.toolWindow import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo interface CommentsToolWindowTab: ReworkToolWindowTab { fun setMergeRequestInfo(mergeRequestInfo: MergeRequestInfo) fun setComments(comments: List) fun setDisplayResolvedComments(value: Boolean) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/FilesToolWindowTab.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.toolWindow import com.intellij.openapi.vcs.changes.Change import net.ntworld.mergeRequest.ProviderData interface FilesToolWindowTab: ReworkToolWindowTab { val isCodeReviewChanges: Boolean fun setChanges(providerData: ProviderData, changes: List) fun hide() } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/ReworkToolWindowTab.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.toolWindow import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.Component interface ReworkToolWindowTab: Component { val providerData: ProviderData } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/SingleMRToolWindowFactoryBase.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.toolWindow import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider abstract class SingleMRToolWindowFactoryBase( private val applicationServiceProvider: ApplicationServiceProvider ) : ToolWindowFactory { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { val projectServiceProvider = applicationServiceProvider.findProjectServiceProvider(project) if (!projectServiceProvider.isInitialized()) { projectServiceProvider.initialize() } SingleMRToolWindowManager(projectServiceProvider, toolWindow) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/SingleMRToolWindowManager.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.toolWindow import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.vcs.changes.Change import com.intellij.openapi.wm.ToolWindow import com.intellij.ui.content.Content import com.intellij.ui.content.ContentFactory import com.intellij.util.ContentUtilEx import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.SINGLE_MR_CHANGES_WHEN_DOING_CODE_REVIEW_NAME import net.ntworld.mergeRequestIntegrationIde.SINGLE_MR_REWORK_CHANGES_PREFIX import net.ntworld.mergeRequestIntegrationIde.SINGLE_MR_REWORK_COMMENTS_PREFIX import net.ntworld.mergeRequestIntegrationIde.debug import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.SingleMRToolWindowNotifier import net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher class SingleMRToolWindowManager( private val projectServiceProvider: ProjectServiceProvider, private val toolWindow: ToolWindow ) : SingleMRToolWindowNotifier { private var myReviewChangesTab: FilesToolWindowTab? = null private val myReworkChangesTabs = mutableSetOf() private val myReworkCommentsTabs = mutableSetOf() private val mySupportedProviders = mutableMapOf() init { projectServiceProvider.messageBus.connect().subscribe(SingleMRToolWindowNotifier.TOPIC, this) val activeReworkWatchers = projectServiceProvider.reworkManager.getActiveReworkWatchers() activeReworkWatchers.forEach { registerReworkWatcher(it) if (it.isChangesBuilt()) { showReworkChanges(it, it.changes) } if (it.isFetchedComments()) { showReworkComments(it, it.comments, it.displayResolvedComments) } } } override fun registerReworkWatcher(reworkWatcher: ReworkWatcher) { val key = reworkWatcher.key() debug("$key: add watcher to SupportedProviders") mySupportedProviders[reworkWatcher.providerData.id] = reworkWatcher.branchName } override fun removeReworkWatcher(reworkWatcher: ReworkWatcher) { val key = reworkWatcher.key() val currentBranch = mySupportedProviders[reworkWatcher.providerData.id] if (currentBranch == reworkWatcher.branchName) { debug("$key: remove ReworkWatcher out SupportedWatchers") mySupportedProviders.remove(reworkWatcher.providerData.id) ApplicationManager.getApplication().invokeLater { debug("$key: remove tab out of tool window") val tab = myReworkChangesTabs.firstOrNull { it.providerData.id == reworkWatcher.providerData.id } if (null !== tab) { removeTabOutOfToolWindow(tab, myReworkChangesTabs, SINGLE_MR_REWORK_CHANGES_PREFIX) } val commentsTab = myReworkCommentsTabs.firstOrNull { it.providerData.id == reworkWatcher.providerData.id } if (null !== commentsTab) { removeTabOutOfToolWindow(commentsTab, myReworkCommentsTabs, SINGLE_MR_REWORK_COMMENTS_PREFIX) } if (myReworkChangesTabs.isEmpty() && myReworkCommentsTabs.isEmpty()) { projectServiceProvider.hideSingleMRToolWindow { debug("$key: hide tool window because there is nothing left") } } } } else { debug("$key: current provider have another branch, do nothing") } } override fun hideChangesAfterDoingCodeReview() { val reviewTab = myReviewChangesTab if (null !== reviewTab) { reviewTab.hide() } } override fun clearReworkTabs() { removeAllReworkToolWindowTabs() } override fun showChangesWhenDoingCodeReview(providerData: ProviderData, changes: List) { removeAllReworkToolWindowTabs() val currentTab = myReviewChangesTab if (null !== currentTab) { currentTab.setChanges(providerData, changes) return } val newTab = makeFilesToolWindowTab(providerData, true) newTab.setChanges(providerData, changes) toolWindow.contentManager.addContent( ContentFactory.SERVICE.getInstance().createContent( newTab.component, SINGLE_MR_CHANGES_WHEN_DOING_CODE_REVIEW_NAME, true ) ) myReviewChangesTab = newTab } override fun showReworkChanges( reworkWatcher: ReworkWatcher, changes: List ) = fillingDataForReworkTab(reworkWatcher) { val currentTab = myReworkChangesTabs.firstOrNull { it.providerData.id == reworkWatcher.providerData.id } if (null !== currentTab) { debug("${reworkWatcher.key()}: update current Files tab's data") currentTab.setChanges(reworkWatcher.providerData, changes) return@fillingDataForReworkTab } val newTab = makeFilesToolWindowTab(reworkWatcher.providerData, false) newTab.setChanges(reworkWatcher.providerData, changes) debug("${reworkWatcher.key()}: add new Files tab of to tool window") addTabToToolWindow(newTab, myReworkChangesTabs, SINGLE_MR_REWORK_CHANGES_PREFIX) } override fun showReworkComments( reworkWatcher: ReworkWatcher, comments: List, displayResolvedComments: Boolean ) = fillingDataForReworkTab(reworkWatcher) { val currentTab = myReworkCommentsTabs.firstOrNull { it.providerData.id == reworkWatcher.providerData.id } if (null !== currentTab) { debug("${reworkWatcher.key()}: update current Comments tab's data") currentTab.setMergeRequestInfo(reworkWatcher.mergeRequestInfo) currentTab.setDisplayResolvedComments(displayResolvedComments) currentTab.setComments(comments) return@fillingDataForReworkTab } val newTab = makeCommentsToolWindowTab(reworkWatcher.providerData, reworkWatcher.mergeRequestInfo, comments) debug("${reworkWatcher.key()}: add new Comments tab to tool window") addTabToToolWindow(newTab, myReworkCommentsTabs, SINGLE_MR_REWORK_COMMENTS_PREFIX) } private fun removeToolWindowTabsForCodeReview() { val content = toolWindow.contentManager.findContent(SINGLE_MR_CHANGES_WHEN_DOING_CODE_REVIEW_NAME) if (null !== content) { toolWindow.contentManager.removeContent(content, true) myReviewChangesTab = null } } private fun fillingDataForReworkTab(reworkWatcher: ReworkWatcher, invoker: (() -> Unit)) { val key = reworkWatcher.key() if (mySupportedProviders.contains(reworkWatcher.providerData.id)) { removeToolWindowTabsForCodeReview() debug("$key: ReworkWatcher is supported") invoker() } else { debug("$key: ReworkWatcher is NOT supported") } } private fun addTabToToolWindow( tab: T, bucket: MutableSet, prefix: String ): Boolean where T : ReworkToolWindowTab { // There are 3 scenarios when removing a tab: // A) 0 -> 1: we should add content // B) 1 -> 2: we should remove content, add old tab to tabbed content // C) 2 -> n: just add tabbed content if (bucket.isEmpty()) { toolWindow.contentManager.addContent( ContentFactory.SERVICE.getInstance().createContent(tab.component, prefix, true) ) return bucket.add(tab) } if (bucket.size == 1) { val oldTab = bucket.first() val oldContent = toolWindow.contentManager.findContent(prefix) toolWindow.contentManager.removeContent(oldContent, false) ContentUtilEx.addTabbedContent( toolWindow.contentManager, oldContent.component, prefix, oldTab.providerData.name, true, null ) } ContentUtilEx.addTabbedContent( toolWindow.contentManager, tab.component, prefix, tab.providerData.name, true, null ) return bucket.add(tab) } private fun removeTabOutOfToolWindow( tab: T, bucket: MutableSet, prefix: String ) where T : ReworkToolWindowTab { // There are 3 scenarios when removing a tab: // A) 1 -> 0: we should remove content // C) 2 -> 1: we should remove tabbed content, create new content with remaining tab // B) n -> 2: just remove tab of tabbed content if (!bucket.contains(tab)) { return } if (bucket.size == 1) { val content = toolWindow.contentManager.findContent(prefix) toolWindow.contentManager.removeContent(content, true) return bucket.clear() } val tabbedContent = ContentUtilEx.findTabbedContent(toolWindow.contentManager, prefix) if (null === tabbedContent) { return } if (bucket.size == 2) { toolWindow.contentManager.removeContent(tabbedContent, false) bucket.remove(tab) val remainingTab = bucket.first() toolWindow.contentManager.addContent( ContentFactory.SERVICE.getInstance().createContent(remainingTab.component, prefix, true) ) return } val tabInTabbedContent = tabbedContent.tabs.firstOrNull { it.second === tab.component } if (null !== tabInTabbedContent) { bucket.remove(tab) tabbedContent.tabs.remove(tabInTabbedContent) } } private fun removeAllReworkToolWindowTabs() { // There are 2 scenarios when removing all tab: // A) bucket > 1: we should remove tabbed content // B) bucket = 1: we should remove content val changes = if (myReworkChangesTabs.size > 1) { ContentUtilEx.findTabbedContent(toolWindow.contentManager, SINGLE_MR_REWORK_CHANGES_PREFIX) } else { toolWindow.contentManager.findContent(SINGLE_MR_REWORK_CHANGES_PREFIX) } if (null !== changes) { toolWindow.contentManager.removeContent(changes, true) } myReworkChangesTabs.clear() val comments = if (myReworkCommentsTabs.size > 1) { ContentUtilEx.findTabbedContent(toolWindow.contentManager, SINGLE_MR_REWORK_COMMENTS_PREFIX) } else { toolWindow.contentManager.findContent(SINGLE_MR_REWORK_COMMENTS_PREFIX) } if (null !== comments) { toolWindow.contentManager.removeContent(comments, true) } myReworkCommentsTabs.clear() } private fun makeFilesToolWindowTab(providerData: ProviderData, isCodeReviewChange: Boolean): FilesToolWindowTab { return projectServiceProvider.componentFactory.toolWindowTabs.makeFilesToolWindowTab( providerData, isCodeReviewChange ) } private fun makeCommentsToolWindowTab( providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List ): CommentsToolWindowTab { return projectServiceProvider.componentFactory.toolWindowTabs.makeCommentsToolWindowTab( providerData, mergeRequestInfo, comments ) } private data class ComponentContainer( val content: Content, val isTabbed: Boolean ) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/internal/CommentsToolWindowTabImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.toolWindow.internal import net.ntworld.mergeRequest.Comment import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkWatcherNotifier import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeFactory import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreePresenter import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeView import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.Node import net.ntworld.mergeRequestIntegrationIde.toolWindow.CommentsToolWindowTab import javax.swing.JComponent class CommentsToolWindowTabImpl( private val projectServiceProvider: ProjectServiceProvider, override val providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List ): CommentsToolWindowTab { private val myPublisher = projectServiceProvider.messageBus.syncPublisher( ReworkWatcherNotifier.TOPIC ) private val myTreePresenter: CommentTreePresenter = CommentTreeFactory.makePresenter( CommentTreeFactory.makeModel(providerData), CommentTreeFactory.makeView(projectServiceProvider, providerData, showOpenDiffViewDescription = true) ) private val myTreePresenterListener = object: CommentTreePresenter.Listener { override fun onTreeNodeSelected(node: Node, type: CommentTreeView.TreeSelectType) { myPublisher.commentTreeNodeSelected(providerData, node, type) } override fun onShowResolvedCommentsToggled(displayResolvedComments: Boolean) { myPublisher.changeDisplayResolvedComments(providerData, displayResolvedComments) } override fun onShowDraftCommentsOnlyToggled(onlyShowDraftComments: Boolean) { myPublisher.changeOnlyShowDraftComments(providerData, onlyShowDraftComments) } override fun onCreateGeneralCommentClicked() { myPublisher.openCreateGeneralCommentForm(providerData) } override fun onRefreshButtonClicked() { myPublisher.requestFetchComment(providerData) } } init { myTreePresenter.setToolbarMode(CommentTreeView.ToolbarMode.MINI) myTreePresenter.model.displayResolvedComments = false myTreePresenter.model.comments = comments myTreePresenter.model.mergeRequestInfo = mergeRequestInfo myTreePresenter.addListener(myTreePresenterListener) } override val component: JComponent = myTreePresenter.component override fun setMergeRequestInfo(mergeRequestInfo: MergeRequestInfo) { myTreePresenter.model.mergeRequestInfo = mergeRequestInfo } override fun setComments(comments: List) { myTreePresenter.model.comments = comments } override fun setDisplayResolvedComments(value: Boolean) { myTreePresenter.model.displayResolvedComments = value } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/internal/FilesToolWindowTabImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.toolWindow.internal import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project import com.intellij.openapi.ui.SimpleToolWindowPanel import com.intellij.openapi.vcs.changes.Change import com.intellij.openapi.vcs.changes.ui.ChangesTreeImpl import com.intellij.openapi.vcs.changes.ui.TreeModelBuilder import com.intellij.ui.JBColor import com.intellij.ui.ScrollPaneFactory import net.miginfocom.swing.MigLayout import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeView import net.ntworld.mergeRequestIntegrationIde.toolWindow.FilesToolWindowTab import net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit.CommitChanges import net.ntworld.mergeRequestIntegrationIde.ui.util.CustomSimpleToolWindowPanel import net.ntworld.mergeRequestIntegrationIde.ui.util.ToolbarUtil import java.awt.GridBagLayout import java.awt.event.KeyAdapter import java.awt.event.KeyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JPanel import javax.swing.SwingConstants import javax.swing.event.TreeSelectionListener import javax.swing.tree.DefaultMutableTreeNode import javax.swing.tree.DefaultTreeModel import javax.swing.tree.TreePath class FilesToolWindowTabImpl( private val projectServiceProvider: ProjectServiceProvider, override val providerData: ProviderData, override val isCodeReviewChanges: Boolean ) : FilesToolWindowTab { private var myProviderData: ProviderData? = null private val myComponentEmpty = JPanel() private val myLabelEmpty = JLabel("", SwingConstants.CENTER) private val myComponent = CustomSimpleToolWindowPanel(vertical = true, borderless = true) private val myTree = MyTree(projectServiceProvider.project) private val myTreeWrapper = ScrollPaneFactory.createScrollPane(myTree, true) private val myToolbar by lazy { val panel = JPanel(MigLayout("ins 0, fill", "[left]0[left, fill]push[right]", "center")) val leftActionGroup = DefaultActionGroup() val leftToolbar = ActionManager.getInstance().createActionToolbar( "${CommitChanges::class.java.canonicalName}/toolbar-left", leftActionGroup, true ) panel.add(JPanel()) panel.add(leftToolbar.component) panel.add( ToolbarUtil.createExpandAndCollapseToolbar( "${this::class.java.canonicalName}/toolbar-right", myTree ) ) panel } private val myTreeMouseListener = object : MouseAdapter() { override fun mousePressed(e: MouseEvent?) { if (null === e) { return } if (e.clickCount == 2) { handleTreeItemSelected(myTree.selectionPath) } } } private val myKeyListener = object: KeyAdapter() { override fun keyPressed(e: KeyEvent?) { if (null === e) { return } if (e.keyCode == KeyEvent.VK_ENTER) { handleTreeItemSelected(myTree.selectionPath) } } } init { myLabelEmpty.text = "Files' changes will be displayed when you do Code Review
or
open a branch which has an opened Merge Request" myComponentEmpty.background = JBColor.background() myComponentEmpty.layout = GridBagLayout() myComponentEmpty.add(myLabelEmpty) myComponent.toolbar = myToolbar myTree.addMouseListener(myTreeMouseListener) myTree.addKeyListener(myKeyListener) val reviewContext = projectServiceProvider.reviewContextManager.findDoingCodeReviewContext() if (null !== reviewContext) { setChanges(reviewContext.providerData, reviewContext.reviewingChanges) } else { hide() } } override val component: JComponent = myComponent private fun handleTreeItemSelected(path: TreePath?) { if (null === path) { return } val node = path.lastPathComponent as? DefaultMutableTreeNode ?: return val change = node.userObject as? Change ?: return val reviewContext = projectServiceProvider.reviewContextManager.findDoingCodeReviewContext() if (null !== reviewContext) { reviewContext.openChange(change, focus = true, displayMergeRequestId = false) return } val providerData = myProviderData if (null !== providerData) { val reworkWatcher = projectServiceProvider.reworkManager.findReworkWatcherByChange( providerData, change ) if (null !== reworkWatcher) { reworkWatcher.openChange(change) } } } override fun setChanges(providerData: ProviderData, changes: List) { myProviderData = providerData ApplicationManager.getApplication().invokeLater { myTree.setChangesToDisplay(changes) } myToolbar.isVisible = true myComponent.setContent(myTreeWrapper) } override fun hide() { myTree.setChangesToDisplay(listOf()) myToolbar.isVisible = false myComponent.setContent(myComponentEmpty) } private class MyTree(ideaProject: Project) : ChangesTreeImpl( ideaProject, false, false, Change::class.java ) { override fun buildTreeModel(changes: MutableList): DefaultTreeModel { return TreeModelBuilder.buildFromChanges(myProject, grouping, changes, null) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/Component.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui import javax.swing.JComponent interface Component { fun createComponent(): JComponent } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/MainToolWindowFactoryBase.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory import com.intellij.ui.content.ContentFactory import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.ui.toolWindowTab.HomeToolWindowTab open class MainToolWindowFactoryBase( private val applicationServiceProvider: ApplicationServiceProvider ) : ToolWindowFactory { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { val home = ContentFactory.SERVICE.getInstance().createContent( HomeToolWindowTab( applicationServiceProvider.findProjectServiceProvider(project), toolWindow ).createComponent(), "Home", true ) home.isCloseable = false toolWindow.contentManager.addContent(home) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/README.md ================================================ All classes in this folder is LEGACY, will be moved to outside and distributed based on domain. ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/AbstractConnectionsConfigurable.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.configuration import com.intellij.icons.AllIcons import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.options.SearchableConfigurable import com.intellij.ui.tabs.TabInfo import git4idea.repo.GitRepository import net.ntworld.mergeRequest.Project import com.intellij.openapi.project.Project as IdeaProject import net.ntworld.mergeRequest.ProviderInfo import net.ntworld.mergeRequest.api.ApiConnection import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegrationIde.configuration.vos.GitRemotePathInfo import net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ApiCredentialsImpl import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProviderSettings import net.ntworld.mergeRequestIntegrationIde.ui.util.Tabs import net.ntworld.mergeRequestIntegrationIde.ui.util.TabsUI import javax.swing.JComponent abstract class AbstractConnectionsConfigurable( private val projectServiceProvider: ProjectServiceProvider ) : SearchableConfigurable, Disposable { private val logger = Logger.getInstance(AbstractConnectionsConfigurable::class.java) private val myData = mutableMapOf() private val myTabInfos = mutableMapOf() private val myInitializedData = mutableMapOf() private var myIsInitialized = false private val myTabs: TabsUI by lazy { val tabs = Tabs(projectServiceProvider.project, projectServiceProvider.project) tabs.setCommonCenterActionGroupFactory { val actionGroup = DefaultActionGroup() actionGroup.add(myAddTabAction) actionGroup } tabs } private class MyAddTabAction(private val self: AbstractConnectionsConfigurable) : AnAction( "New Connection", "Add new connection", AllIcons.Actions.OpenNewTab ) { override fun actionPerformed(e: AnActionEvent) { perform() } fun perform() { var count = self.myTabs.getTabs().tabCount + 1 var name = "Connection $count" while (self.myData.containsKey(self.findIdFromName(name))) { count++ name = "Connection $count" } self.addNewConnection(name) } } private val myAddTabAction = MyAddTabAction(this) private val myConnectionListener = object : ConnectionUI.Listener { override fun test(connectionUI: ConnectionUI, name: String, connection: ApiConnection, shared: Boolean) { logger.debug("Start testing connection $name") try { assertConnectionIsValid(connection) logger.debug("Connection $name tested") connectionUI.onConnectionTested(name, connection, shared) } catch (exception: Exception) { logger.debug("Connection $name has error ${exception.message}") connectionUI.onConnectionError(name, connection, shared, exception) } } override fun verify(connectionUI: ConnectionUI, name: String, credentials: ApiCredentials, repository: String) { logger.debug("Start verifying credentials $name") try { val project = findProject(credentials) if (null !== project) { connectionUI.onCredentialsVerified(name, credentials, repository, project) } else { connectionUI.onCredentialsInvalid(name, credentials, repository) } } catch (exception: Exception) { logger.debug("Connection $name has error ${exception.message}") connectionUI.onCredentialsInvalid(name, credentials, repository) } } override fun delete(connectionUI: ConnectionUI, name: String) { logger.debug("Delete connection $name") val id = findIdFromName(name).trim() val settings = myData[id] if (null !== settings) { logger.debug("Update 'deleted' of connection $name to true") myData[id] = settings.copy(deleted = true) } val tabs = myTabs.getTabs() val tabInfo = myTabInfos[id] if (null !== tabInfo) { tabs.removeTab(tabInfo) } if (tabs.tabCount == 0) { myAddTabAction.perform() } else { tabs.select(tabs.getTabAt(0), true) } } override fun update( connectionUI: ConnectionUI, name: String, credentials: ApiCredentials, shared: Boolean, repository: String ) { val id = findIdFromName(name) val settings = MyProviderSettings( id = id, info = makeProviderInfo(), credentials = credentials, repository = repository, sharable = shared, deleted = false ) if (validateProviderSettings(settings)) { myData[id] = settings } } override fun changeName(connectionUI: ConnectionUI, oldName: String, newName: String) { logger.debug("Name of connection change from $oldName to $newName") val oldId = findIdFromName(oldName).trim() val newId = findIdFromName(newName).trim() val oldData = myData[oldId] if (null !== oldData) { myData[newId] = oldData.copy(id = newId, deleted = false) myData[oldId] = oldData.copy(deleted = true) } val tabInfo = myTabInfos[oldId] if (null !== tabInfo) { tabInfo.text = newName myTabInfos[newId] = tabInfo myTabInfos.remove(oldId) } } override fun guessProject(connectionUI: ConnectionUI, credentials: ApiCredentials, repository: GitRepository) { val remotes = mutableSetOf() repository.remotes.forEach { remote -> remote.urls.forEach { val path = parseRemotePath(it) if (null !== path) { remotes.add(path) } } remote.pushUrls.forEach { val path = parseRemotePath(it) if (null !== path) { remotes.add(path) } } } remotes.forEach { val project = findProjectByPath(credentials, it) connectionUI.onProjectGuessed(repository, project) } } private fun parseRemotePath(input: String): String? { val info = GitRemotePathInfo(input) return if (info.isValid) info.toString() else null } } abstract fun makeProviderInfo(): ProviderInfo abstract fun findNameFromId(id: String): String abstract fun findIdFromName(name: String): String abstract fun makeConnection(): ConnectionUI abstract fun validateConnection(connection: ApiConnection): Boolean abstract fun findProject(credentials: ApiCredentials): Project? abstract fun findProjectByPath(credentials: ApiCredentials, path: String): Project? abstract fun assertConnectionIsValid(connection: ApiConnection) private fun buildInitializedData(initUI: Boolean = true, initDataForCheckModification: Boolean = true) { if (myIsInitialized) { return } val currentProviderInfo = makeProviderInfo() myInitializedData.clear() val applicationServiceProvider = projectServiceProvider.applicationServiceProvider val appConnections = applicationServiceProvider.getProviderConfigurations() projectServiceProvider.getProviderConfigurations().forEach { if (it.info.id == currentProviderInfo.id) { myInitializedData[it.id] = MyProviderSettings( id = it.id, info = it.info, credentials = it.credentials, repository = it.repository, sharable = it.sharable, deleted = false ) } } for (appConnection in appConnections) { if (appConnection.info.id != currentProviderInfo.id) { continue } if (myInitializedData.containsKey(appConnection.id)) { myInitializedData[appConnection.id] = MyProviderSettings( id = myInitializedData[appConnection.id]!!.id, info = myInitializedData[appConnection.id]!!.info, credentials = appConnection.credentials, repository = myInitializedData[appConnection.id]!!.repository, sharable = true, deleted = false ) } else { myInitializedData[appConnection.id] = MyProviderSettings( id = appConnection.id, info = appConnection.info, credentials = MyProviderSettings.makeCredentialsFromApplicationLevel( appConnection.credentials ), repository = "", sharable = true, deleted = false ) } } myIsInitialized = true if (initUI) { if (myInitializedData.isEmpty()) { addNewConnection("Default") } myInitializedData.forEach { (key, value) -> val connectionUI = initConnection(value) addConnectionToTabPane(value, connectionUI) myData[key] = value } } if (initDataForCheckModification) { myInitializedData.forEach { (key, value) -> myData[key] = value } } } private fun makeConnectionWithEventListener(): ConnectionUI { val connection = makeConnection() connection.dispatcher.addListener(myConnectionListener) return connection } private fun initConnection(data: ProviderSettings): ConnectionUI { val ui = makeConnectionWithEventListener() ApplicationManager.getApplication().invokeLater { ui.initialize(findNameFromId(data.id), data.credentials, data.sharable, data.repository) } return ui } private fun addConnectionToTabPane(data: ProviderSettings, connectionUI: ConnectionUI, selected: Boolean = false) { val tabInfo = TabInfo(connectionUI.createComponent()) tabInfo.text = findNameFromId(data.id) myTabInfos[data.id.trim()] = tabInfo myTabs.addTab(tabInfo) if (selected) { myTabs.getTabs().select(tabInfo, true) } } private fun addNewConnection(name: String) { val id = findIdFromName(name) val data = MyProviderSettings.makeDefault(id, makeProviderInfo()) val connection = makeConnectionWithEventListener() connection.setName(name) myData[id] = data addConnectionToTabPane(data, connection, true) } override fun isModified(): Boolean { buildInitializedData() val initializedData = myInitializedData.filter { validateProviderSettings(it.value) } val validData = myData.filter { validateProviderSettings(it.value) } if (validData.size != initializedData.size) { logger.info("Size of valid data & initialized data is not match") return true } for (entry in validData) { if (!initializedData.containsKey(entry.key)) { logger.info("Initialized data does not contains ${entry.key}") return true } val initializedItem = initializedData[entry.key] if (null === initializedItem) { logger.info("Initialized data does not contains ${entry.key}") return true } if (!initializedItem.isEquals(entry.value)) { logger.info("Initialized data and data of ${entry.key} is not equals") return true } } logger.info("All data are matched, not modified") return false } override fun apply() { logger.info("Delete global connections") val applicationServiceProvider = projectServiceProvider.applicationServiceProvider applicationServiceProvider.removeAllProviderConfigurations() for (entry in myData) { if (entry.value.deleted) { logger.info("Delete connection ${entry.key}") projectServiceProvider.removeProviderConfiguration(entry.value.id.trim()) continue } if (entry.value.sharable) { logger.info("Save connection ${entry.key} to global") applicationServiceProvider.addProviderConfiguration( id = entry.value.id.trim(), info = entry.value.info, credentials = entry.value.credentials ) } if (validateProviderSettings(entry.value)) { logger.info("Save connection ${entry.key}") projectServiceProvider.addProviderConfiguration( id = entry.value.id.trim(), info = entry.value.info, credentials = entry.value.credentials, repository = entry.value.repository ) } } myData.clear() myIsInitialized = false projectServiceProvider.initialize() buildInitializedData(initUI = false, initDataForCheckModification = true) } override fun reset() { myData.clear() myTabInfos.clear() myTabs.getTabs().removeAllTabs() myIsInitialized = false buildInitializedData() } override fun dispose() { myData.clear() myTabInfos.clear() } override fun createComponent(): JComponent? { val component = myTabs.component return component } private fun validateProviderSettings(providerSettings: ProviderSettings): Boolean { return validateConnection(providerSettings.credentials) && providerSettings.repository.isNotEmpty() && providerSettings.credentials.projectId.isNotEmpty() } private data class MyProviderSettings( override val id: String, override val info: ProviderInfo, override val credentials: ApiCredentials, override val repository: String, override val sharable: Boolean, val deleted: Boolean ) : ProviderSettings { fun isEquals(other: MyProviderSettings): Boolean { return id == other.id && info.id == other.info.id && isCredentialsEquals(other.credentials) && repository == other.repository && sharable == other.sharable && deleted == other.deleted } private fun isCredentialsEquals(other: ApiCredentials): Boolean { return credentials.projectId == other.projectId && credentials.version == other.version && credentials.info == other.info && credentials.url == other.url && credentials.ignoreSSLCertificateErrors == other.ignoreSSLCertificateErrors && credentials.login == other.login && credentials.token == other.token } companion object { fun makeCredentialsFromApplicationLevel(credentials: ApiCredentials): ApiCredentials { return ApiCredentialsImpl( url = credentials.url, login = credentials.login, token = credentials.token, projectId = "", version = credentials.version, info = "", ignoreSSLCertificateErrors = credentials.ignoreSSLCertificateErrors ) } fun makeDefault(id: String, info: ProviderInfo): MyProviderSettings { return MyProviderSettings( id = id, info = info, credentials = ApiCredentialsImpl( url = "", login = "", token = "", projectId = "", version = "", info = "", ignoreSSLCertificateErrors = false ), repository = "", sharable = false, deleted = false ) } } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/ConfigurationBase.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.configuration import com.intellij.openapi.options.SearchableConfigurable import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings import javax.swing.JComponent abstract class ConfigurationBase( private val applicationServiceProvider: ApplicationServiceProvider ) : SearchableConfigurable { private var myInitializedSettings: ApplicationSettings = applicationServiceProvider.settingsManager private var myCurrentSettings: ApplicationSettings = applicationServiceProvider.settingsManager private val mySettingsUI: SettingsUI = SettingsConfiguration() private val mySettingsListener = object : SettingsUI.Listener { override fun change(settings: ApplicationSettings) { myCurrentSettings = settings } } init { mySettingsUI.initialize(myInitializedSettings) mySettingsUI.dispatcher.addListener(mySettingsListener) } override fun isModified(): Boolean { return myCurrentSettings != myInitializedSettings } override fun apply() { applicationServiceProvider.settingsManager.update(myCurrentSettings) myInitializedSettings = myCurrentSettings applicationServiceProvider.getAllProjectServiceProviders().forEach { it.providerStorage.updateApiOptions(myCurrentSettings.toApiOptions()) } } override fun reset() { myCurrentSettings = myInitializedSettings mySettingsUI.initialize(myInitializedSettings) } override fun createComponent(): JComponent? = mySettingsUI.createComponent() } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/ConnectionUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.configuration import com.intellij.util.EventDispatcher import git4idea.repo.GitRepository import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.api.ApiConnection import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegrationIde.ui.Component import java.util.* interface ConnectionUI : Component { val dispatcher: EventDispatcher fun initialize(name: String, credentials: ApiCredentials, shared: Boolean, repository: String) fun setName(name: String) fun onConnectionTested(name: String, connection: ApiConnection, shared: Boolean) fun onProjectGuessed(repository: GitRepository, project: Project?) fun onConnectionError(name: String, connection: ApiConnection, shared: Boolean, exception: Exception) fun onCredentialsVerified(name: String, credentials: ApiCredentials, repository: String, project: Project) fun onCredentialsInvalid(name: String, credentials: ApiCredentials, repository: String) interface Listener : EventListener { fun test(connectionUI: ConnectionUI, name: String, connection: ApiConnection, shared: Boolean) fun verify(connectionUI: ConnectionUI, name: String, credentials: ApiCredentials, repository: String) fun delete(connectionUI: ConnectionUI, name: String) fun update( connectionUI: ConnectionUI, name: String, credentials: ApiCredentials, shared: Boolean, repository: String ) fun changeName(connectionUI: ConnectionUI, oldName: String, newName: String) fun guessProject(connectionUI: ConnectionUI, credentials: ApiCredentials, repository: GitRepository) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GithubConnection.form ================================================
================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GithubConnection.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.configuration import com.intellij.openapi.project.Project as IdeaProject import com.intellij.openapi.ui.Messages import com.intellij.util.EventDispatcher import git4idea.repo.GitRepository import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.api.ApiConnection import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ApiCredentialsImpl import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil import javax.swing.* import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener class GithubConnection( private val projectServiceProvider: ProjectServiceProvider ) : ConnectionUI { var myWholePanel: JPanel? = null var mySettingsPanel: JPanel? = null var myName: JTextField? = null var myUrl: JTextField? = null var myUsername: JTextField? = null var myToken: JPasswordField? = null var myTestBtn: JButton? = null var myDeleteBtn: JButton? = null var myRepository: JComboBox? = null var myTerm: JTextField? = null var myProjectList: JList? = null var myShared: JCheckBox? = null var myIgnoreSSLError: JCheckBox? = null private var myCurrentName: String = "" private var mySelectedRepository: String? = null private var mySelectedProject: Project? = null private var myIsTested: Boolean = false private val myProjectFinder: ProjectFinderUI by lazy { GithubProjectFinder(projectServiceProvider, myTerm!!, myProjectList!!) } private val myNameFieldListener = object : DocumentListener { override fun changedUpdate(e: DocumentEvent?) { val newName = myName!!.text if (newName.trim() != myCurrentName) { dispatcher.multicaster.changeName(this@GithubConnection, myCurrentName, newName) myCurrentName = newName.trim() } } override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e) override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e) } private val myConnectionFieldsListener = object : DocumentListener { override fun changedUpdate(e: DocumentEvent?) { myTestBtn!!.isEnabled = myName!!.text.isNotEmpty() && myUrl!!.text.isNotEmpty() && myUsername!!.text.isNotEmpty() && getToken().isNotEmpty() updateConnectionData() } override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e) override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e) } override val dispatcher = EventDispatcher.create(ConnectionUI.Listener::class.java) init { myIgnoreSSLError!!.isVisible = false val repositories = RepositoryUtil.getRepositoriesByProject(projectServiceProvider.project) repositories.forEach { myRepository!!.addItem(it) } if (repositories.isNotEmpty()) { myRepository!!.selectedItem = repositories.first() } myTestBtn!!.addActionListener { onTestClicked() } myDeleteBtn!!.addActionListener { onDeleteClicked() } myName!!.document.addDocumentListener(myNameFieldListener) myName!!.document.addDocumentListener(myConnectionFieldsListener) myUrl!!.document.addDocumentListener(myConnectionFieldsListener) myUsername!!.document.addDocumentListener(myConnectionFieldsListener) myToken!!.document.addDocumentListener(myConnectionFieldsListener) myIgnoreSSLError!!.addChangeListener { updateConnectionData() } myRepository!!.addActionListener { updateConnectionData() } myProjectFinder.addProjectChangedListener(object: ProjectFinderUI.ProjectChangedListener { override fun projectChanged(projectId: String) { updateConnectionData() } }) } override fun initialize(name: String, credentials: ApiCredentials, shared: Boolean, repository: String) { myIsTested = true myCurrentName = name.trim() myName!!.text = name.trim() myUrl!!.text = if (credentials.url.isNotEmpty()) credentials.url else "https://api.github.com" myUsername!!.text = credentials.login myToken!!.text = credentials.token myIgnoreSSLError!!.isSelected = credentials.ignoreSSLCertificateErrors myShared!!.isSelected = shared mySelectedRepository = repository myTestBtn!!.isEnabled = false if (credentials.projectId.isNotEmpty()) { dispatcher.multicaster.verify(this, name, credentials, repository) } updateFieldsState() } override fun setName(name: String) { myName!!.text = name.trim() myUrl!!.text = "https://api.github.com" myCurrentName = name.trim() } override fun onConnectionTested(name: String, connection: ApiConnection, shared: Boolean) { myIsTested = true myTestBtn!!.isEnabled = false Messages.showInfoMessage("Successfully connected!", "Info") updateFieldsState() } override fun onProjectGuessed(repository: GitRepository, project: Project?) { } override fun onConnectionError(name: String, connection: ApiConnection, shared: Boolean, exception: Exception) { myIsTested = false Messages.showErrorDialog("Cannot connect, error: ${exception.message}", "Error") updateFieldsState() } override fun onCredentialsVerified( name: String, credentials: ApiCredentials, repository: String, project: Project ) { mySelectedProject = project mySelectedRepository = repository updateFieldsState() } override fun onCredentialsInvalid(name: String, credentials: ApiCredentials, repository: String) { updateFieldsState() } override fun createComponent(): JComponent = myWholePanel!! private fun updateConnectionData() { dispatcher.multicaster.update( this, myName!!.text.trim(), ApiCredentialsImpl( url = myUrl!!.text.trim(), login = myUsername!!.text.trim(), token = getToken().trim(), projectId = myProjectFinder.getSelectedProjectId(), version = "v3", info = "", ignoreSSLCertificateErrors = myIgnoreSSLError!!.isSelected ), myShared!!.isSelected, myRepository!!.selectedItem as String? ?: "" ) } private fun getToken(): String { return String(myToken!!.password) } private fun updateFieldsState() { myRepository!!.isEnabled = myIsTested myProjectFinder.setEnabled( myIsTested, ApiCredentialsImpl( url = myUrl!!.text.trim(), login = myUsername!!.text.trim(), token = getToken().trim(), projectId = "", version = "v3", info = "", ignoreSSLCertificateErrors = myIgnoreSSLError!!.isSelected ) ) if (null !== mySelectedRepository) { myRepository!!.selectedItem = mySelectedRepository } if (null !== mySelectedProject) { myProjectFinder.setSelectedProject(mySelectedProject) } } private fun onDeleteClicked() { val name = myName!!.text this.dispatcher.multicaster.delete(this, name) } private fun onTestClicked() { val name = myName!!.text val url = myUrl!!.text val username = myUsername!!.text val token = getToken() val shared = myShared!!.isSelected val ignoreSSLCertificateErrors = myIgnoreSSLError!!.isSelected if (name.isEmpty() || username.isEmpty() || url.isEmpty() || token.isEmpty()) { return } this.dispatcher.multicaster.test( this, name, ApiCredentialsImpl( url = url, login = myUsername!!.text.trim(), token = token, ignoreSSLCertificateErrors = ignoreSSLCertificateErrors, info = "", projectId = "", version = "v3" ), shared ) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GithubConnectionsConfigurableBase.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.configuration import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.api.ApiConnection import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.github.Github import net.ntworld.mergeRequestIntegration.provider.github.request.GithubFindCurrentUserRequest import net.ntworld.mergeRequestIntegration.provider.github.request.GithubFindRepositoryRequest import net.ntworld.mergeRequestIntegration.provider.github.transformer.GithubRepositoryTransformer import net.ntworld.mergeRequestIntegration.provider.github.vo.GithubProjectId import net.ntworld.mergeRequestIntegrationIde.exception.InvalidConnectionException import net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ApiCredentialsImpl import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider open class GithubConnectionsConfigurableBase( private val projectServiceProvider: ProjectServiceProvider ) : AbstractConnectionsConfigurable(projectServiceProvider) { override fun getId(): String = "MRI:github" override fun getDisplayName(): String = "Github" override fun makeProviderInfo() = Github override fun findNameFromId(id: String): String = Companion.findNameFromId(id) override fun findIdFromName(name: String): String = Companion.findIdFromName(name) override fun makeConnection(): ConnectionUI { return GithubConnection(projectServiceProvider) } override fun validateConnection(connection: ApiConnection): Boolean { return connection.url.isNotEmpty() && connection.login.isNotEmpty() && connection.token.isNotEmpty() } override fun findProject(credentials: ApiCredentials): Project? { val out = projectServiceProvider.infrastructure.serviceBus() process GithubFindRepositoryRequest( credentials = credentials, repositoryId = GithubProjectId.parseId(credentials.projectId).toString() ) val response = out.getResponse() return if (response.isSuccess) { GithubRepositoryTransformer.transform(response.repository) } else { null } } override fun findProjectByPath(credentials: ApiCredentials, path: String): Project? { return null } override fun assertConnectionIsValid(connection: ApiConnection) { val out = projectServiceProvider.infrastructure.serviceBus() process GithubFindCurrentUserRequest( credentials = ApiCredentialsImpl( url = connection.url, login = connection.login, token = connection.token, ignoreSSLCertificateErrors = connection.ignoreSSLCertificateErrors, info = "", projectId = "", version = "" ) ) val error = out.getResponse().error if (null !== error) { throw InvalidConnectionException(error.message) } } companion object { private const val PREFIX = "github:" fun findNameFromId(id: String): String { if (id.startsWith(PREFIX)) { return id.substring(PREFIX.length) } return id } fun findIdFromName(name: String): String { return "$PREFIX$name" } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GithubProjectFinder.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.configuration import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.github.request.GithubSearchRepositoriesRequest import net.ntworld.mergeRequestIntegration.provider.github.transformer.GithubRepositoryTransformer import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.ui.panel.ProjectPanel import java.awt.Component import java.awt.event.FocusEvent import java.awt.event.FocusListener import java.util.* import javax.swing.* import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener import com.intellij.openapi.project.Project as IdeaProject class GithubProjectFinder( private val projectServiceProvider: ProjectServiceProvider, private val myTerm: JTextField, private val myProjectList: JList ) : ProjectFinderUI { private var myIsTermTouched: Boolean = false private var mySelectedProject: Project? = null private var myCredentials: ApiCredentials? = null private val mySearchDispatcher = EventDispatcher.create(MySearchListener::class.java) private val myProjectChangedDispatcher = EventDispatcher.create(ProjectFinderUI.ProjectChangedListener::class.java) init { myTerm.text = "Search project..." myTerm.addFocusListener(object : FocusListener { override fun focusLost(e: FocusEvent?) { if (myTerm.text.isEmpty()) { myTerm.text = "Search project..." myIsTermTouched = false } } override fun focusGained(e: FocusEvent?) { if (!myIsTermTouched) { myTerm.text = "" } myIsTermTouched = true } }) myProjectList.selectionMode = ListSelectionModel.SINGLE_SELECTION myProjectList.cellRenderer = object : ListCellRenderer { override fun getListCellRendererComponent( list: JList?, value: Project?, index: Int, isSelected: Boolean, cellHasFocus: Boolean ): Component? { if (null === value) { return JPanel() } return ProjectPanel(value, isSelected).getComponent() } } myProjectList.addListSelectionListener { myProjectChangedDispatcher.multicaster.projectChanged(getSelectedProjectId()) } myTerm.document.addDocumentListener(object : DocumentListener { override fun changedUpdate(e: DocumentEvent?) { if (myTerm.text.isNotEmpty()) { triggerSearchTask(myTerm.text.trim()) } } override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e) override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e) }) mySearchDispatcher.addListener(object : MySearchListener { override fun searchFinished(term: String, projects: List) { if (myTerm.text == term) { myProjectList.setListData(projects.toTypedArray()) } } }) } private fun triggerSearchTask(term: String) { myProjectChangedDispatcher.multicaster.projectChanged("") MySearchTask(projectServiceProvider, term, this).start() } override fun addProjectChangedListener(listener: ProjectFinderUI.ProjectChangedListener) { myProjectChangedDispatcher.addListener(listener) } override fun getSelectedProjectId(): String { val value = myProjectList.selectedValue return if (null === value) "" else value.id } override fun setSelectedProject(project: Project?) { if (null !== project) { val current = mySelectedProject if (null !== current && current.id == project.id) { return } myProjectList.setListData(arrayOf(project)) myProjectList.selectedIndex = 0 mySelectedProject = project } } override fun setEnabled(value: Boolean, credentials: ApiCredentials) { myTerm.isEnabled = value myProjectList.isEnabled = value myCredentials = credentials } private interface MySearchListener : EventListener { fun searchFinished(term: String, projects: List) } private class MySearchTask( private val projectServiceProvider: ProjectServiceProvider, private val term: String, private val self: GithubProjectFinder ) : Task.Backgroundable(projectServiceProvider.project, "Searching github projects...", true) { fun start() { if (self.myIsTermTouched) { ProgressManager.getInstance().runProcessWithProgressAsynchronously(this, Indicator(this)) } } override fun run(indicator: ProgressIndicator) { val credentials = self.myCredentials ?: return Thread.sleep(300) if (term != self.myTerm.text) { return } val out = projectServiceProvider.infrastructure.serviceBus() process GithubSearchRepositoriesRequest( credentials = credentials, term = term ) if (!out.hasError()) { self.mySearchDispatcher.multicaster.searchFinished( term, out.getResponse().repositories.map { GithubRepositoryTransformer.transform(it) } ) } } private class Indicator(private val task: MySearchTask) : BackgroundableProcessIndicator(task) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GitlabConnection.form ================================================
================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GitlabConnection.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.configuration import com.intellij.openapi.ui.Messages import com.intellij.util.EventDispatcher import git4idea.repo.GitRepository import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.api.ApiConnection import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ApiCredentialsImpl import net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil import javax.swing.* import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener class GitlabConnection( private val projectServiceProvider: ProjectServiceProvider ) : ConnectionUI { var myWholePanel: JPanel? = null var mySettingsPanel: JPanel? = null var myName: JTextField? = null var myUrl: JTextField? = null var myToken: JPasswordField? = null var myTestBtn: JButton? = null var myDeleteBtn: JButton? = null var myRepository: JComboBox? = null var myTerm: JTextField? = null var myProjectList: JList? = null var myShared: JCheckBox? = null var mySearchStarred: JCheckBox? = null var mySearchMembership: JCheckBox? = null var mySearchOwn: JCheckBox? = null var myMergeApprovalsFeature: JCheckBox? = null var myIgnoreSSLError: JCheckBox? = null private var myCurrentName: String = "" private var mySelectedRepository: String? = null private var mySelectedProject: Project? = null private var myIsTested: Boolean = false private val myProjectFinder: ProjectFinderUI by lazy { GitlabProjectFinder( projectServiceProvider, myTerm!!, myProjectList!!, mySearchStarred!!, mySearchMembership!!, mySearchOwn!!, myMergeApprovalsFeature!! ) } private val myNameFieldListener = object : DocumentListener { override fun changedUpdate(e: DocumentEvent?) { val newName = myName!!.text if (newName.trim() != myCurrentName) { dispatcher.multicaster.changeName(this@GitlabConnection, myCurrentName, newName) myCurrentName = newName.trim() } } override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e) override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e) } private val myConnectionFieldsListener = object : DocumentListener { override fun changedUpdate(e: DocumentEvent?) { myTestBtn!!.isEnabled = myName!!.text.isNotEmpty() && myUrl!!.text.isNotEmpty() && getToken().isNotEmpty() updateConnectionData() } override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e) override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e) } override val dispatcher = EventDispatcher.create(ConnectionUI.Listener::class.java) init { val repositories = RepositoryUtil.getRepositoriesByProject(projectServiceProvider.project) repositories.forEach { myRepository!!.addItem(it) } if (repositories.isNotEmpty()) { myRepository!!.selectedItem = repositories.first() } myTestBtn!!.addActionListener { onTestClicked() } myDeleteBtn!!.addActionListener { onDeleteClicked() } myName!!.document.addDocumentListener(myNameFieldListener) myName!!.document.addDocumentListener(myConnectionFieldsListener) myUrl!!.document.addDocumentListener(myConnectionFieldsListener) myToken!!.document.addDocumentListener(myConnectionFieldsListener) myIgnoreSSLError!!.addChangeListener { updateConnectionData() } myRepository!!.addActionListener { requestGuessProjectByCurrentRepository() updateConnectionData() } myProjectFinder.addProjectChangedListener(object: ProjectFinderUI.ProjectChangedListener { override fun projectChanged(projectId: String) { updateConnectionData() } }) myMergeApprovalsFeature!!.addChangeListener { updateConnectionData() } } override fun initialize(name: String, credentials: ApiCredentials, shared: Boolean, repository: String) { myIsTested = true myCurrentName = name.trim() myName!!.text = name.trim() myUrl!!.text = if (credentials.url.isNotEmpty()) credentials.url else "https://gitlab.com" myToken!!.text = credentials.token myIgnoreSSLError!!.isSelected = credentials.ignoreSSLCertificateErrors myShared!!.isSelected = shared mySelectedRepository = repository myTestBtn!!.isEnabled = false myMergeApprovalsFeature!!.isSelected = GitlabUtil.hasMergeApprovalFeature(credentials) if (credentials.projectId.isNotEmpty()) { dispatcher.multicaster.verify(this, name, credentials, repository) } updateFieldsState() } override fun setName(name: String) { myName!!.text = name.trim() myUrl!!.text = "https://gitlab.com" myCurrentName = name.trim() } override fun onConnectionTested(name: String, connection: ApiConnection, shared: Boolean) { myIsTested = true myTestBtn!!.isEnabled = false updateFieldsState() requestGuessProjectByCurrentRepository() Messages.showInfoMessage("Successfully connected!", "Info") } override fun onProjectGuessed(repository: GitRepository, project: Project?) { if (null !== project && null === mySelectedProject) { mySelectedProject = project myProjectFinder.setSelectedProject(mySelectedProject) } } override fun onConnectionError(name: String, connection: ApiConnection, shared: Boolean, exception: Exception) { myIsTested = false Messages.showErrorDialog("Cannot connect, error: ${exception.message}", "Error") updateFieldsState() } override fun onCredentialsVerified( name: String, credentials: ApiCredentials, repository: String, project: Project ) { mySelectedProject = project mySelectedRepository = repository updateFieldsState() } override fun onCredentialsInvalid(name: String, credentials: ApiCredentials, repository: String) { updateFieldsState() } override fun createComponent(): JComponent = myWholePanel!! private fun requestGuessProjectByCurrentRepository() { val repository = RepositoryUtil.findRepositoryByPath( projectServiceProvider.project, myRepository!!.selectedItem as String? ?: "" ) if (null !== repository) { this.dispatcher.multicaster.guessProject(this, buildApiCredentials(), repository) } } private fun buildApiCredentials(): ApiCredentials { return ApiCredentialsImpl( url = myUrl!!.text.trim(), login = "", token = getToken().trim(), projectId = myProjectFinder.getSelectedProjectId(), version = "v4", info = if (myMergeApprovalsFeature!!.isSelected) GitlabUtil.getMergeApprovalFeatureInfo() else "", ignoreSSLCertificateErrors = myIgnoreSSLError!!.isSelected ) } private fun updateConnectionData() { dispatcher.multicaster.update( this, myName!!.text.trim(), buildApiCredentials(), myShared!!.isSelected, myRepository!!.selectedItem as String? ?: "" ) } private fun getToken(): String { return String(myToken!!.password) } private fun updateFieldsState() { myRepository!!.isEnabled = myIsTested myProjectFinder.setEnabled( myIsTested, ApiCredentialsImpl( url = myUrl!!.text.trim(), login = "", token = getToken().trim(), projectId = "", version = "v4", info = if (myMergeApprovalsFeature!!.isSelected) GitlabUtil.getMergeApprovalFeatureInfo() else "", ignoreSSLCertificateErrors = myIgnoreSSLError!!.isSelected ) ) if (null !== mySelectedRepository) { myRepository!!.selectedItem = mySelectedRepository } if (null !== mySelectedProject) { myProjectFinder.setSelectedProject(mySelectedProject) } } private fun onDeleteClicked() { val name = myName!!.text this.dispatcher.multicaster.delete(this, name) } private fun onTestClicked() { val name = myName!!.text val url = myUrl!!.text val token = getToken() val shared = myShared!!.isSelected val ignoreSSLCertificateErrors = myIgnoreSSLError!!.isSelected if (name.isEmpty() || url.isEmpty() || token.isEmpty()) { return } this.dispatcher.multicaster.test( this, name, ApiCredentialsImpl( url = url, login = "", token = token, ignoreSSLCertificateErrors = ignoreSSLCertificateErrors, info = "", projectId = "", version = "v4" ), shared ) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GitlabConnectionsConfigurableBase.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.configuration import net.ntworld.mergeRequest.ProviderInfo import net.ntworld.mergeRequest.api.ApiConnection import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.Gitlab import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindProjectRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabSearchProjectsRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabProjectTransformer import net.ntworld.mergeRequestIntegrationIde.exception.InvalidConnectionException import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ApiCredentialsImpl open class GitlabConnectionsConfigurableBase( private val projectServiceProvider: ProjectServiceProvider ) : AbstractConnectionsConfigurable(projectServiceProvider) { override fun makeProviderInfo(): ProviderInfo = Gitlab override fun findNameFromId(id: String) = Companion.findNameFromId(id) override fun findIdFromName(name: String) = Companion.findIdFromName(name) override fun makeConnection(): ConnectionUI { return GitlabConnection(projectServiceProvider) } override fun validateConnection(connection: ApiConnection): Boolean { return connection.url.isNotEmpty() && connection.token.isNotEmpty() } override fun findProject(credentials: ApiCredentials): net.ntworld.mergeRequest.Project? { val out = projectServiceProvider.infrastructure.serviceBus() process GitlabFindProjectRequest( credentials = credentials, projectId = credentials.projectId.toInt() ) val response = out.getResponse() return if (response.isSuccess) { GitlabProjectTransformer.transform(response.project) } else { null } } override fun findProjectByPath(credentials: ApiCredentials, path: String): net.ntworld.mergeRequest.Project? { val out = projectServiceProvider.infrastructure.serviceBus() process GitlabFindProjectRequest( credentials = credentials, projectId = 0, projectPath = path ) val response = out.getResponse() return if (response.isSuccess) { GitlabProjectTransformer.transform(response.project) } else { null } } override fun assertConnectionIsValid(connection: ApiConnection) { val out = projectServiceProvider.infrastructure.serviceBus() process GitlabSearchProjectsRequest( credentials = ApiCredentialsImpl( url = connection.url, login = connection.login, token = connection.token, ignoreSSLCertificateErrors = connection.ignoreSSLCertificateErrors, info = "", projectId = "", version = "" ), term = "" ) val error = out.getResponse().error if (null !== error) { throw InvalidConnectionException(error.message) } } override fun getId(): String = "MRI:gitlab" override fun getDisplayName(): String = "Gitlab new" companion object { private const val PREFIX = "gitlab:" fun findNameFromId(id: String): String { if (id.startsWith(PREFIX)) { return id.substring(PREFIX.length) } return id } fun findIdFromName(name: String): String { return "$PREFIX$name" } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GitlabProjectFinder.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.configuration import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.api.ApiCredentials import net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil import net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabSearchProjectsRequest import net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabProjectTransformer import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.ui.panel.ProjectPanel import java.awt.Component import java.awt.event.FocusEvent import java.awt.event.FocusListener import java.util.* import javax.swing.* import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener class GitlabProjectFinder( private val projectServiceProvider: ProjectServiceProvider, private val myTerm: JTextField, private val myProjectList: JList, private val mySearchStarred: JCheckBox, private val mySearchMembership: JCheckBox, private val mySearchOwn: JCheckBox, private val myMergeApprovalsFeature: JCheckBox ) : ProjectFinderUI { private var myIsTermTouched: Boolean = false private var mySelectedProject: Project? = null private var myCredentials: ApiCredentials? = null private val mySearchDispatcher = EventDispatcher.create(MySearchListener::class.java) private val myProjectChangedDispatcher = EventDispatcher.create(ProjectFinderUI.ProjectChangedListener::class.java) init { myTerm.text = "Search project..." myTerm.addFocusListener(object : FocusListener { override fun focusLost(e: FocusEvent?) { if (myTerm.text.isEmpty()) { myTerm.text = "Search project..." myIsTermTouched = false } } override fun focusGained(e: FocusEvent?) { if (!myIsTermTouched) { myTerm.text = "" } myIsTermTouched = true } }) myProjectList.selectionMode = ListSelectionModel.SINGLE_SELECTION myProjectList.cellRenderer = object : ListCellRenderer { override fun getListCellRendererComponent( list: JList?, value: Project?, index: Int, isSelected: Boolean, cellHasFocus: Boolean ): Component? { if (null === value) { return JPanel() } return ProjectPanel(value, isSelected).getComponent() } } myProjectList.addListSelectionListener { myProjectChangedDispatcher.multicaster.projectChanged(getSelectedProjectId()) } myTerm.document.addDocumentListener(object : DocumentListener { override fun changedUpdate(e: DocumentEvent?) { if (myTerm.text.isNotEmpty()) { triggerSearchTask(myTerm.text.trim()) } } override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e) override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e) }) mySearchDispatcher.addListener(object : MySearchListener { override fun searchFinished(term: String, projects: List) { if (myTerm.text == term) { myProjectList.setListData(projects.toTypedArray()) } } }) mySearchOwn.addChangeListener { triggerSearchTask(myTerm.text) } mySearchMembership.addChangeListener { triggerSearchTask(myTerm.text) } mySearchStarred.addChangeListener { triggerSearchTask(myTerm.text) } } private fun triggerSearchTask(term: String) { myProjectChangedDispatcher.multicaster.projectChanged("") MySearchTask(projectServiceProvider, term, this).start() } override fun addProjectChangedListener(listener: ProjectFinderUI.ProjectChangedListener) { myProjectChangedDispatcher.addListener(listener) } override fun getSelectedProjectId(): String { val value = myProjectList.selectedValue return if (null === value) "" else value.id } override fun setSelectedProject(project: Project?) { if (null !== project) { val current = mySelectedProject if (null !== current && current.id == project.id) { return } myProjectList.setListData(arrayOf(project)) myProjectList.selectedIndex = 0 mySelectedProject = project } } override fun setEnabled(value: Boolean, credentials: ApiCredentials) { myTerm.isEnabled = value myProjectList.isEnabled = value mySearchStarred.isEnabled = value mySearchMembership.isEnabled = value mySearchOwn.isEnabled = value myMergeApprovalsFeature.isEnabled = value myMergeApprovalsFeature.isSelected = GitlabUtil.hasMergeApprovalFeature(credentials) myCredentials = credentials } private interface MySearchListener : EventListener { fun searchFinished(term: String, projects: List) } private class MySearchTask( private val projectServiceProvider: ProjectServiceProvider, private val term: String, private val self: GitlabProjectFinder ) : Task.Backgroundable(projectServiceProvider.project, "Searching gitlab projects...", true) { fun start() { if (self.myIsTermTouched) { ProgressManager.getInstance().runProcessWithProgressAsynchronously(this, Indicator(this)) } } override fun run(indicator: ProgressIndicator) { val credentials = self.myCredentials ?: return Thread.sleep(300) if (term != self.myTerm.text) { return } val out = projectServiceProvider.infrastructure.serviceBus() process GitlabSearchProjectsRequest( credentials = credentials, term = term, owner = self.mySearchOwn.isSelected, membership = self.mySearchMembership.isSelected, starred = self.mySearchStarred.isSelected ) if (!out.hasError()) { self.mySearchDispatcher.multicaster.searchFinished( term, out.getResponse().projects.map { GitlabProjectTransformer.transform(it) } ) } } private class Indicator(private val task: MySearchTask) : BackgroundableProcessIndicator(task) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/ProjectFinderUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.configuration import net.ntworld.mergeRequest.Project import net.ntworld.mergeRequest.api.ApiCredentials import java.util.* interface ProjectFinderUI { fun setEnabled(value: Boolean, credentials: ApiCredentials) fun setSelectedProject(project: Project?) fun getSelectedProjectId(): String fun addProjectChangedListener(listener: ProjectChangedListener) interface ProjectChangedListener : EventListener { fun projectChanged(projectId: String) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/SettingsConfiguration.form ================================================
================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/SettingsConfiguration.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.configuration import com.intellij.util.EventDispatcher import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option.MaxDiffChangesOpenedAutomaticallyOption import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings import javax.swing.* import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener class SettingsConfiguration : SettingsUI { var myTabbedPane: JTabbedPane? = null var myWholePanel: JPanel? = null var myPerformancePanel: JPanel? = null var myEnableRequestCache: JCheckBox? = null var mySaveMRFilterState: JCheckBox? = null var myCommentOptionsPanel: JPanel? = null var myDisplayCommentsInDiffView: JCheckBox? = null var myShowAddCommentIconsInDiffViewGutter: JCheckBox? = null var myCodeReviewOptionsPanel: JPanel? = null var myCheckoutTargetBranch: JCheckBox? = null var myMergeRequestOptionsPanel: JPanel? = null var myDisplayUpVotesAndDownVotes: JCheckBox? = null var myDisplayMergeRequestState: JCheckBox? = null var myMaxDiffChangesOpenedAutomatically: JTextField? = null var myReworkProcessOptionsPanel: JPanel? = null var myEnableReworkProcess: JCheckBox? = null override val dispatcher = EventDispatcher.create(SettingsUI.Listener::class.java) init { // myPerformancePanel!!.border = BorderFactory.createTitledBorder("Performance") myEnableRequestCache!!.addActionListener { dispatchSettingsUpdated() } mySaveMRFilterState!!.addActionListener { dispatchSettingsUpdated() } myDisplayCommentsInDiffView!!.addActionListener { dispatchSettingsUpdated() } myShowAddCommentIconsInDiffViewGutter!!.addActionListener { dispatchSettingsUpdated() } myDisplayUpVotesAndDownVotes!!.addActionListener { dispatchSettingsUpdated() } myDisplayMergeRequestState!!.addActionListener { dispatchSettingsUpdated() } myEnableReworkProcess!!.addActionListener { dispatchSettingsUpdated() } myCheckoutTargetBranch!!.addActionListener { dispatchSettingsUpdated() } myMaxDiffChangesOpenedAutomatically!!.document.addDocumentListener(object : DocumentListener { override fun changedUpdate(e: DocumentEvent?) { dispatchSettingsUpdated() } override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e) override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e) }) } private fun dispatchSettingsUpdated() { val settings = ApplicationSettingsImpl( enableRequestCache = myEnableRequestCache!!.isSelected, saveMRFilterState = mySaveMRFilterState!!.isSelected, displayCommentsInDiffView = myDisplayCommentsInDiffView!!.isSelected, showAddCommentIconsInDiffViewGutter = myShowAddCommentIconsInDiffViewGutter!!.isSelected, checkoutTargetBranch = myCheckoutTargetBranch!!.isSelected, maxDiffChangesOpenedAutomatically = MaxDiffChangesOpenedAutomaticallyOption.parse( myMaxDiffChangesOpenedAutomatically!!.text ), displayUpVotesAndDownVotes = myDisplayUpVotesAndDownVotes!!.isSelected, displayMergeRequestState = myDisplayMergeRequestState!!.isSelected, enableReworkProcess = myEnableReworkProcess!!.isSelected ) dispatcher.multicaster.change(settings) } override fun initialize(settings: ApplicationSettings) { myEnableRequestCache!!.isSelected = settings.enableRequestCache mySaveMRFilterState!!.isSelected = settings.saveMRFilterState myDisplayCommentsInDiffView!!.isSelected = settings.displayCommentsInDiffView myShowAddCommentIconsInDiffViewGutter!!.isSelected = settings.showAddCommentIconsInDiffViewGutter myCheckoutTargetBranch!!.isSelected = settings.checkoutTargetBranch myMaxDiffChangesOpenedAutomatically!!.text = settings.maxDiffChangesOpenedAutomatically.toString() myDisplayUpVotesAndDownVotes!!.isSelected = settings.displayUpVotesAndDownVotes myDisplayMergeRequestState!!.isSelected = settings.displayMergeRequestState myEnableReworkProcess!!.isSelected = settings.enableReworkProcess } override fun createComponent(): JComponent = myWholePanel!! } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/SettingsUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.configuration import com.intellij.util.EventDispatcher import net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings import net.ntworld.mergeRequestIntegrationIde.ui.Component import java.util.* interface SettingsUI : Component { val dispatcher: EventDispatcher fun initialize(settings: ApplicationSettings) interface Listener: EventListener { fun change(settings: ApplicationSettings) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/AbstractMergeRequestCollection.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest import com.intellij.openapi.application.ApplicationManager import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.MergeRequestState import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.ProviderStatus import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegrationIde.component.PaginationToolbar import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.task.SearchMergeRequestTask import net.ntworld.mergeRequestIntegrationIde.ui.util.CustomSimpleToolWindowPanel import javax.swing.JComponent abstract class AbstractMergeRequestCollection( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData ) : MergeRequestCollectionUI { abstract fun makeContent(): JComponent abstract fun fetchDataStarted() abstract fun fetchDataStopped() abstract fun dataReceived(collection: List) open fun fetchDataError() { } override val eventDispatcher = EventDispatcher.create(MergeRequestCollectionEventListener::class.java) private val mySplitter = CustomSimpleToolWindowPanel(vertical = true, borderless = true) private val myPaginator: PaginationToolbar = projectServiceProvider.componentFactory.makePaginationToolbar( displayRefreshButton = true ) private val myContentDelegate = lazy { makeContent() } private val myContent: JComponent by myContentDelegate private var myFilter = GetMergeRequestFilter.make( state = MergeRequestState.OPENED, id = null, search = "", authorId = "", assigneeId = "", approverIds = listOf(), sourceBranch = "" ) private var myOrdering = MergeRequestOrdering.RECENTLY_UPDATED private val myListener = object : SearchMergeRequestTask.Listener { override fun onError(exception: Exception) { myPaginator.enable() fetchDataError() fetchDataStopped() } override fun taskStarted() { myPaginator.disable() fetchDataStarted() } override fun taskEnded() { myPaginator.enable() fetchDataStopped() } override fun dataReceived(list: List, page: Int, totalPages: Int, totalItems: Int) { ApplicationManager.getApplication().invokeLater { dataReceived(list) myPaginator.setData(page, totalPages, totalItems) } } } private val myPaginatorListener = object: PaginationToolbar.Listener { override fun changePage(page: Int) { fetchPage(page) } } init { myPaginator.addListener(myPaginatorListener) mySplitter.toolbar = myPaginator.component } final override fun setFilter(filter: GetMergeRequestFilter) { myFilter = filter } final override fun setOrder(ordering: MergeRequestOrdering) { myOrdering = ordering } override fun fetchData() = fetchPage(1) private fun fetchPage(page: Int) { if (providerData.status == ProviderStatus.ACTIVE) { val task = SearchMergeRequestTask( projectServiceProvider = projectServiceProvider, providerData = providerData, filtering = myFilter, ordering = myOrdering, listener = myListener ) task.start(page) } } override fun createComponent(): JComponent { if (!myContentDelegate.isInitialized()) { mySplitter.setContent(myContent) } return mySplitter } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollection.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest import com.intellij.openapi.Disposable import com.intellij.openapi.ui.SimpleToolWindowPanel import com.intellij.openapi.util.Disposer import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifierAdapter import javax.swing.JComponent class MergeRequestCollection( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData ) : MergeRequestCollectionUI, Disposable { private val myComponent = SimpleToolWindowPanel(true, true) private val myFilter: MergeRequestCollectionFilterUI by lazy { MergeRequestCollectionFilter(projectServiceProvider, providerData) } private val myTree: MergeRequestCollectionUI by lazy { val tree = MergeRequestCollectionTree(projectServiceProvider, providerData) if (projectServiceProvider.applicationSettings.saveMRFilterState) { val pair = projectServiceProvider.filtersStorage.find(providerData.key) tree.setFilter(pair.first) tree.setOrder(pair.second) } tree.fetchData() tree } private val myFilterListener = object: MergeRequestCollectionFilterEventListener { override fun filterChanged(filter: GetMergeRequestFilter) { myTree.setFilter(filter) myTree.fetchData() } override fun orderChanged(order: MergeRequestOrdering) { myTree.setOrder(order) myTree.fetchData() } } private val myProjectNotifier = object : ProjectNotifierAdapter() { override fun startCodeReview(reviewContext: ReviewContext) { myComponent.isVisible = false } override fun stopCodeReview(reviewContext: ReviewContext) { myComponent.isVisible = true } } private val myConnection = projectServiceProvider.messageBus.connect() init { myComponent.toolbar = myFilter.createComponent() myComponent.setContent(myTree.createComponent()) myFilter.eventDispatcher.addListener(myFilterListener) myTree.eventDispatcher.addListener(object: MergeRequestCollectionEventListener { override fun mergeRequestUnselected() { } override fun mergeRequestSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) { } }) myConnection.subscribe(ProjectNotifier.TOPIC, myProjectNotifier) Disposer.register(projectServiceProvider.project, this) } override val eventDispatcher = myTree.eventDispatcher override fun setFilter(filter: GetMergeRequestFilter) { myTree.setFilter(filter) } override fun setOrder(ordering: MergeRequestOrdering) { myTree.setOrder(ordering) } override fun fetchData() { myTree.fetchData() } override fun createComponent(): JComponent = myComponent override fun dispose() { myConnection.disconnect() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionEventListener.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import java.util.* interface MergeRequestCollectionEventListener : EventListener { fun mergeRequestUnselected() fun mergeRequestSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionFilter.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest import com.intellij.icons.AllIcons import com.intellij.ide.HelpTooltip import com.intellij.openapi.actionSystem.* import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.ui.SearchTextField import com.intellij.ui.components.panels.Wrapper import com.intellij.ui.popup.AbstractPopup import com.intellij.util.EventDispatcher import com.intellij.util.ui.JBUI import net.miginfocom.swing.MigLayout import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.ui.panel.MergeRequestFilterPropertiesPanel import java.awt.Point import java.awt.event.KeyEvent import java.awt.event.KeyListener import javax.swing.Icon import javax.swing.JComponent import javax.swing.JPanel import javax.swing.SwingUtilities class MergeRequestCollectionFilter( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData ) : MergeRequestCollectionFilterUI { override val eventDispatcher = EventDispatcher.create(MergeRequestCollectionFilterEventListener::class.java) private var myOrdering = MergeRequestOrdering.RECENTLY_UPDATED private val myFilterPropertiesChanged: (() -> Unit) = { val filter = myAdvanceFilterButton.buildFilter(mySearchField.text) eventDispatcher.multicaster.filterChanged(filter) saveFilterAndOrdering(filter, null) } private val mySearchField = MySearchTextField(this) private val myMainActionGroup = DefaultActionGroup() private val myToolbar = ActionManager.getInstance().createActionToolbar( "${MergeRequestCollectionFilter::class.java.canonicalName}${providerData.id}/toolbar", myMainActionGroup, true ) private val myAdvanceFilterButton: AdvanceFilterButton = AdvanceFilterButton( projectServiceProvider, providerData, myToolbar.component, myFilterPropertiesChanged ) private val myPanel by lazy { val panel = JPanel(MigLayout("ins 0, fill", "[left]0[left, fill]push[right]", "center")) myMainActionGroup.add(myAdvanceFilterButton) val rightCornerActionGroup = DefaultActionGroup() rightCornerActionGroup.add(myOrderByOldestAction) rightCornerActionGroup.add(myOrderByNewestAction) rightCornerActionGroup.add(myOrderByRecentUpdatedAction) val rightCornerToolbar = ActionManager.getInstance().createActionToolbar( "${MergeRequestCollectionFilter::class.java.canonicalName}${providerData.id}/toolbar-right", rightCornerActionGroup, true ) val textFilter = Wrapper(mySearchField) textFilter.setVerticalSizeReferent(myToolbar.component) textFilter.border = JBUI.Borders.emptyLeft(5) panel.add(textFilter) panel.add(myToolbar.component) panel.add(rightCornerToolbar.component) panel } private val myOrderByRecentUpdatedAction = OrderButton( this, MergeRequestOrdering.RECENTLY_UPDATED, "Order by recent updated", "Order by recent updated", AllIcons.Plugins.Updated ) private val myOrderByNewestAction = OrderButton( this, MergeRequestOrdering.NEWEST, "Order by newest", "Order by newest", AllIcons.General.Modified ) private val myOrderByOldestAction = OrderButton( this, MergeRequestOrdering.OLDEST, "Order by oldest", "Order by oldest", AllIcons.Vcs.History ) private val myKeyListener = object : KeyListener { private var searchingById: Boolean = false override fun keyTyped(e: KeyEvent?) { } override fun keyPressed(e: KeyEvent?) { } override fun keyReleased(e: KeyEvent?) { if (null === e || (e.keyCode != 10 && e.keyCode != 13)) { handleSearchingByIdIfCurrentInputIsAnInteger() return } val id = mySearchField.text.toIntOrNull() val filter = if (null !== id) myAdvanceFilterButton.buildFilterForSearchById(id) else myAdvanceFilterButton.buildFilter(mySearchField.text) eventDispatcher.multicaster.filterChanged(filter) saveFilterAndOrdering(filter, null) mySearchField.addCurrentTextToHistory() } private fun handleSearchingByIdIfCurrentInputIsAnInteger() { val id = mySearchField.text.toIntOrNull() if (null !== id) { // This is a special filter which filter by id, so do not save to history unless val filter = myAdvanceFilterButton.buildFilterForSearchById(id) eventDispatcher.multicaster.filterChanged(filter) searchingById = true return } if (searchingById) { // If searching by id we need to reload the old result val filter = myAdvanceFilterButton.buildFilter("") eventDispatcher.multicaster.filterChanged(filter) searchingById = false } } } init { mySearchField.addKeyboardListener(myKeyListener) val pair = projectServiceProvider.filtersStorage.find(providerData.key) myOrdering = pair.second mySearchField.text = pair.first.search myAdvanceFilterButton.setPreselectedValues(pair.first) } private fun saveFilterAndOrdering(filter: GetMergeRequestFilter?, ordering: MergeRequestOrdering?) { projectServiceProvider.filtersStorage.save( providerData.key, if (null === filter) myAdvanceFilterButton.buildFilter(mySearchField.text) else filter, if (null === ordering) myOrdering else ordering ) } override fun createComponent(): JComponent { return myPanel } private class OrderButton( private val self: MergeRequestCollectionFilter, private val order: MergeRequestOrdering, text: String, desc: String, icon: Icon ) : ToggleAction(text, desc, icon) { override fun isSelected(e: AnActionEvent): Boolean { return self.myOrdering == order } override fun setSelected(e: AnActionEvent, state: Boolean) { if (state) { self.myOrdering = order self.eventDispatcher.multicaster.orderChanged(order) self.saveFilterAndOrdering(null, order) } } } private class MySearchTextField(private val self: MergeRequestCollectionFilter): SearchTextField() { init { setHistoryPropertyName("${MergeRequestCollectionFilter::class.java.canonicalName}:${self.providerData.id}") } override fun onFieldCleared() { val filter = self.myAdvanceFilterButton.buildFilter(self.mySearchField.text) self.eventDispatcher.multicaster.filterChanged(filter) self.saveFilterAndOrdering(filter, null) } } private class AdvanceFilterButton( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val preferableFocusComponent: JComponent, private val onChanged: (() -> Unit) ) : AnAction(null, null, AllIcons.Actions.Properties) { private var myIsReady = false private var myPreselectedValue: GetMergeRequestFilter? = null private val myFilterPropertiesReady: (() -> Unit) = { myIsReady = true val preselected = myPreselectedValue if (null !== preselected) { myFilterPropertiesPanel.setPreselectedValues(preselected) myPreselectedValue = null } } private val myFilterPropertiesPanel = MergeRequestFilterPropertiesPanel( projectServiceProvider, providerData, onChanged, myFilterPropertiesReady ) override fun actionPerformed(e: AnActionEvent) { val component = myFilterPropertiesPanel.createComponent() val popup = JBPopupFactory.getInstance() .createComponentPopupBuilder( component, preferableFocusComponent ) .setResizable(true) .setMovable(false) .setRequestFocus(true) .createPopup() HelpTooltip.setMasterPopup(preferableFocusComponent, popup) val point = findPopupPoint(preferableFocusComponent) (popup as AbstractPopup).show(preferableFocusComponent, point.x, point.y, false) } override fun update(e: AnActionEvent) { super.update(e) e.presentation.isEnabled = myIsReady } private fun findPopupPoint(reference: JComponent): Point { val visibleBounds = reference.visibleRect val containerScreenPoint = visibleBounds.location SwingUtilities.convertPointToScreen(containerScreenPoint, reference) visibleBounds.location = containerScreenPoint return Point( visibleBounds.x, visibleBounds.y + reference.height ) } fun buildFilter(search: String) = myFilterPropertiesPanel.buildFilter(search) fun buildFilterForSearchById(id: Int) = myFilterPropertiesPanel.buildFilterForSearchById(id) fun setPreselectedValues(value: GetMergeRequestFilter) { if (myIsReady) { myFilterPropertiesPanel.setPreselectedValues(value) myPreselectedValue = null } else { myPreselectedValue = value } } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionFilterEventListener.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import java.util.* interface MergeRequestCollectionFilterEventListener: EventListener { fun filterChanged(filter: GetMergeRequestFilter) fun orderChanged(order: MergeRequestOrdering) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionFilterUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest import com.intellij.util.EventDispatcher import net.ntworld.mergeRequestIntegrationIde.ui.Component interface MergeRequestCollectionFilterUI : Component { val eventDispatcher: EventDispatcher } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionTree.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest import com.intellij.ide.util.treeView.NodeRenderer import com.intellij.openapi.application.ApplicationManager import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.treeStructure.Tree import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import java.awt.Component import javax.swing.JComponent import javax.swing.JTree import javax.swing.event.TreeSelectionEvent import javax.swing.event.TreeSelectionListener import javax.swing.tree.* class MergeRequestCollectionTree( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData ) : AbstractMergeRequestCollection(projectServiceProvider, providerData), TreeCellRenderer { private val myTree = Tree() private val myRoot = DefaultMutableTreeNode() private val myRenderer = NodeRenderer() private val myModel = DefaultTreeModel(myRoot) private val myTreeSelectionListener = object : TreeSelectionListener { override fun valueChanged(e: TreeSelectionEvent?) { if (null === e) { return } val path = e.path when (val node = (path.lastPathComponent as DefaultMutableTreeNode).userObject) { is MergeRequestCollectionTreeNode -> { eventDispatcher.multicaster.mergeRequestSelected( providerData = providerData, mergeRequestInfo = node.mergeRequestInfo ) } } } } init { val treeSelectionModel = DefaultTreeSelectionModel() treeSelectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION myTree.model = myModel myTree.cellRenderer = this myTree.isRootVisible = false myTree.selectionModel = treeSelectionModel myTree.addTreeSelectionListener(myTreeSelectionListener) } override fun makeContent(): JComponent { return ScrollPaneFactory.createScrollPane(myTree) } override fun fetchDataStarted() { myRoot.removeAllChildren() myTree.isVisible = false } override fun fetchDataStopped() { myTree.isVisible = true } override fun dataReceived(collection: List) { ApplicationManager.getApplication().invokeLater { myRoot.removeAllChildren() collection.forEach { val item = MergeRequestCollectionTreeNode( providerData, projectServiceProvider.project, it ) myRoot.add(DefaultMutableTreeNode(item)) item.update() } myModel.nodeStructureChanged(myRoot) myTree.isVisible = true } } override fun getTreeCellRendererComponent( tree: JTree?, value: Any?, selected: Boolean, expanded: Boolean, leaf: Boolean, row: Int, hasFocus: Boolean ): Component { return myRenderer.getTreeCellRendererComponent( tree, value, selected, expanded, leaf, row, hasFocus ) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionTreeNode.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest import com.intellij.ide.projectView.PresentationData import com.intellij.ide.util.treeView.PresentableNodeDescriptor import com.intellij.openapi.project.Project as IdeaProject import com.intellij.ui.SimpleTextAttributes import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData class MergeRequestCollectionTreeNode( private val providerData: ProviderData, ideaProject: IdeaProject, var mergeRequestInfo: MergeRequestInfo ): PresentableNodeDescriptor(ideaProject, null) { override fun update(presentation: PresentationData) { val id = providerData.info.formatMergeRequestId(mergeRequestInfo.id) presentation.addText("$id · ", SimpleTextAttributes.GRAYED_ATTRIBUTES) presentation.addText(mergeRequestInfo.title, SimpleTextAttributes.REGULAR_ATTRIBUTES) } override fun getElement(): MergeRequestInfo = mergeRequestInfo } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequestIntegrationIde.ui.Component interface MergeRequestCollectionUI : Component { val eventDispatcher: EventDispatcher fun setFilter(filter: GetMergeRequestFilter) fun setOrder(ordering: MergeRequestOrdering) fun fetchData() } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestDetails.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest import com.intellij.icons.AllIcons import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.ActionGroup import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.application.ApplicationManager import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.tabs.TabInfo import net.ntworld.mergeRequest.* import net.ntworld.mergeRequestIntegrationIde.component.Icons import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.CommentsTabFactory import net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.CommentsTabPresenter import net.ntworld.mergeRequestIntegrationIde.task.FindApprovalTask import net.ntworld.mergeRequestIntegrationIde.task.FindMergeRequestTask import net.ntworld.mergeRequestIntegrationIde.task.GetCommitsTask import net.ntworld.mergeRequestIntegrationIde.task.GetPipelinesTask import net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.* import net.ntworld.mergeRequestIntegrationIde.ui.service.FetchService import net.ntworld.mergeRequestIntegrationIde.ui.util.Tabs import net.ntworld.mergeRequestIntegrationIde.ui.util.TabsUI import javax.swing.JComponent class MergeRequestDetails( private val projectServiceProvider: ProjectServiceProvider, private val disposable: Disposable, private val providerData: ProviderData ) : MergeRequestDetailsUI { private val myToolbars = mutableListOf() private val myInfoTab : MergeRequestInfoTabUI = MergeRequestInfoTab() private val myInfoTabInfo: TabInfo by lazy { val tabInfo = TabInfo( ScrollPaneFactory.createScrollPane(myInfoTab.createComponent()) ) tabInfo.text = "Info" tabInfo.icon = AllIcons.General.ShowInfos tabInfo } private val myDescriptionTab: MergeRequestDescriptionTabUI = MergeRequestDescriptionTab() private val myDescriptionTabInfo: TabInfo by lazy { val tabInfo = TabInfo( ScrollPaneFactory.createScrollPane(myDescriptionTab.createComponent()) ) tabInfo.text = "Description" tabInfo.icon = Icons.Description tabInfo } private val myCommitsTab: MergeRequestCommitsTabUI = MergeRequestCommitsTab(projectServiceProvider) private val myCommitsTabInfo: TabInfo by lazy { val tabInfo = TabInfo(myCommitsTab.createComponent()) tabInfo.text = "Commit" tabInfo.icon = AllIcons.Vcs.CommitNode tabInfo } private val myCommentsTabPresenter: CommentsTabPresenter by lazy { val model = CommentsTabFactory.makeCommentsTabModel(projectServiceProvider, providerData) val view = CommentsTabFactory.makeCommentsTabView(projectServiceProvider, providerData) CommentsTabFactory.makeCommentsTabPresenter(projectServiceProvider, model, view) } private val myTabs: TabsUI by lazy { val tabs = Tabs(projectServiceProvider.project, disposable) tabs.setCommonCenterActionGroupFactory(this::toolbarCommonCenterActionGroupFactory) tabs.setCommonSideComponentFactory(this::toolbarCommonSideComponentFactory) tabs } private val myFindMRListener = object : FindMergeRequestTask.Listener { override fun dataReceived(mergeRequest: MergeRequest) { ApplicationManager.getApplication().invokeLater { setMergeRequest(mergeRequest) } } } private val myGetPipelinesListener = object : GetPipelinesTask.Listener { override fun onError(exception: Exception) { println(exception) } override fun dataReceived(mergeRequestInfo: MergeRequestInfo, pipelines: List) { ApplicationManager.getApplication().invokeLater { myToolbars.forEach { it.setPipelines(mergeRequestInfo, pipelines) } } } } private val myGetCommitsListener = object : GetCommitsTask.Listener { override fun onError(exception: Exception) { println(exception) } override fun taskStarted() { myCommitsTab.clear() } override fun dataReceived(mergeRequestInfo: MergeRequestInfo, commits: List) { ApplicationManager.getApplication().invokeLater { myToolbars.forEach { it.setCommits(mergeRequestInfo, commits) } myCommitsTab.setCommits(providerData, mergeRequestInfo, commits) if (commits.isEmpty()) { myCommitsTabInfo.text = "Commits" } else { myCommitsTabInfo.text = "Commits · ${commits.size}" } } } } private val mySelectCommitsListener = object: MergeRequestCommitsTabUI.Listener { override fun commitSelected( providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List ) { projectServiceProvider.reviewContextManager.updateReviewingCommits( providerData.id, mergeRequestInfo.id, commits ) myToolbars.forEach { it.setCommitsForReviewing(mergeRequestInfo, commits) } } } private val myFindApprovalListener = object : FindApprovalTask.Listener { override fun onError(exception: Exception) { println(exception) } override fun dataReceived(mergeRequestInfo: MergeRequestInfo, approval: Approval) { ApplicationManager.getApplication().invokeLater { myToolbars.forEach { it.setApproval(mergeRequestInfo, approval) } } } } private val myToolbarListener = object : MergeRequestDetailsToolbarUI.Listener { override fun refreshRequested(mergeRequestInfo: MergeRequestInfo) { setMergeRequestInfo(mergeRequestInfo) } } init { myTabs.addTab(myInfoTabInfo) myTabs.addTab(myDescriptionTabInfo) myTabs.addTab(myCommentsTabPresenter.tabInfo) myTabs.addTab(myCommitsTabInfo) myCommitsTab.dispatcher.addListener(mySelectCommitsListener) } override fun hide() { myTabs.component.isVisible = false } override fun setMergeRequest(mergeRequest: MergeRequest) { myInfoTab.setMergeRequest(mergeRequest) myToolbars.forEach { it.setMergeRequest(mergeRequest) } } override fun setMergeRequestInfo(mergeRequestInfo: MergeRequestInfo) { projectServiceProvider.reviewContextManager.initContext(providerData, mergeRequestInfo, true) FetchService.start( projectServiceProvider.applicationServiceProvider, projectServiceProvider.project, providerData, mergeRequestInfo ) myInfoTab.setMergeRequestInfo(providerData, mergeRequestInfo) myDescriptionTab.setMergeRequestInfo(providerData, mergeRequestInfo) myCommentsTabPresenter.model.mergeRequestInfo = mergeRequestInfo myTabs.getTabs().select(myInfoTabInfo, false) myTabs.component.isVisible = true myToolbars.forEach { it.setMergeRequestInfo(mergeRequestInfo) } FindMergeRequestTask(projectServiceProvider, providerData, mergeRequestInfo, myFindMRListener).start() GetPipelinesTask(projectServiceProvider, providerData, mergeRequestInfo, myGetPipelinesListener).start() GetCommitsTask(projectServiceProvider, providerData, mergeRequestInfo, myGetCommitsListener).start() if (providerData.hasApprovalFeature) { FindApprovalTask(projectServiceProvider, providerData, mergeRequestInfo, myFindApprovalListener).start() } } override fun createComponent(): JComponent = myTabs.component private fun toolbarCommonCenterActionGroupFactory(): ActionGroup { return DefaultActionGroup() } private fun toolbarCommonSideComponentFactory(): JComponent { val toolbar: MergeRequestDetailsToolbarUI = MergeRequestDetailsToolbar( projectServiceProvider, providerData, this ) toolbar.dispatcher.addListener(myToolbarListener) myToolbars.add(toolbar) return toolbar.createComponent() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestDetailsToolbar.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest import com.intellij.icons.AllIcons import com.intellij.ide.BrowserUtil import com.intellij.ide.HelpTooltip import com.intellij.openapi.actionSystem.* import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.ui.popup.AbstractPopup import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.* import net.ntworld.mergeRequest.command.ApproveMergeRequestCommand import net.ntworld.mergeRequest.command.UnapproveMergeRequestCommand import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegrationIde.component.Icons import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.ui.panel.ApprovalPanel import net.ntworld.mergeRequestIntegrationIde.ui.service.CodeReviewService import net.ntworld.mergeRequestIntegrationIde.ui.util.findVisibilityIconAndTextForApproval import java.awt.Dimension import java.awt.Point import javax.swing.JComponent import javax.swing.SwingUtilities class MergeRequestDetailsToolbar( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val details: MergeRequestDetailsUI ) : MergeRequestDetailsToolbarUI { override val dispatcher = EventDispatcher.create(MergeRequestDetailsToolbarUI.Listener::class.java) private val myActionGroup = DefaultActionGroup() private var myMergeRequest: MergeRequest? = null private var myMergeRequestInfo: MergeRequestInfo? = null private val myRefreshAction = MyRefreshAction(this) private var myUpVotes = -1 private var myDownVotes = -1 private val myUpVotesAction = MyUpVotesAction(this) private val myDownVotesAction = MyDownVotesAction(this) private var myPipelines: List = listOf() private val myPipelineAction = MyPipelineAction(this) private val myApprovalPanel = ApprovalPanel() private val myApprovalPanelListener = object : ApprovalPanel.Listener { override fun onApproveClicked() { val approval = myApproval val mr = myMergeRequest if (null !== approval && null !== mr) { myApprovalPanel.hide() projectServiceProvider.infrastructure.commandBus() process ApproveMergeRequestCommand.make( providerId = providerData.id, mergeRequestId = mr.id, sha = mr.diffReference!!.headHash ) details.setMergeRequestInfo(mr) } } override fun onUnapproveClicked() { val approval = myApproval val mr = myMergeRequest if (null !== approval && null !== mr) { myApprovalPanel.hide() projectServiceProvider.infrastructure.commandBus() process UnapproveMergeRequestCommand.make( providerId = providerData.id, mergeRequestId = mr.id ) details.setMergeRequestInfo(mr) } } } private var myApproval: Approval? = null private val myApprovalAction = MyApprovalAction(this) private var myUrl: String = "" private val myOpenUrlAction = MyOpenUrlAction(this) private var myState: MergeRequestState = MergeRequestState.ALL private val myStateAction = MyStateAction(this) private var myCommits: List = listOf() private var myReviewCommits: List = listOf() private val myCodeReviewAction = MyCodeReviewAction(this) private val myToolbar by lazy { myActionGroup.add(myRefreshAction) myActionGroup.addSeparator() myActionGroup.add(myUpVotesAction) myActionGroup.add(myDownVotesAction) myActionGroup.add(myStateAction) myActionGroup.add(myPipelineAction) myActionGroup.add(myApprovalAction) myActionGroup.addSeparator() myActionGroup.add(myOpenUrlAction) myActionGroup.addSeparator() myActionGroup.add(myCodeReviewAction) val toolbar = ActionManager.getInstance().createActionToolbar( "${MergeRequestDetailsToolbar::class.java.canonicalName}/toolbar-right", myActionGroup, true ) toolbar } init { myApprovalPanel.addListener(myApprovalPanelListener) } override fun setPipelines(mergeRequestInfo: MergeRequestInfo, pipelines: List) { val currentMR = myMergeRequestInfo if (currentMR != null && currentMR.id == mergeRequestInfo.id) { myPipelines = pipelines } } override fun setCommits(mergeRequestInfo: MergeRequestInfo, commits: List) { val currentMR = myMergeRequestInfo if (currentMR != null && currentMR.id == mergeRequestInfo.id) { myCommits = commits myReviewCommits = commits } } override fun setCommitsForReviewing(mergeRequestInfo: MergeRequestInfo, commits: List) { val currentMR = myMergeRequestInfo if (currentMR != null && currentMR.id == mergeRequestInfo.id) { myReviewCommits = commits } } override fun setApproval(mergeRequestInfo: MergeRequestInfo, approval: Approval) { val currentMR = myMergeRequestInfo if (currentMR != null && currentMR.id == mergeRequestInfo.id) { myApproval = approval } } override fun setMergeRequest(mergeRequest: MergeRequest) { myMergeRequest = mergeRequest myUpVotes = mergeRequest.upVotes myDownVotes = mergeRequest.downVotes myState = mergeRequest.state myUrl = mergeRequest.url myToolbar.component.isVisible = true } override fun setMergeRequestInfo(mergeRequestInfo: MergeRequestInfo) { myMergeRequestInfo = mergeRequestInfo myPipelines = listOf() myCommits = listOf() myReviewCommits = listOf() myApproval = null myToolbar.component.isVisible = false } override fun createComponent(): JComponent = myToolbar.component private class MyRefreshAction(private val self: MergeRequestDetailsToolbar) : AnAction("Refresh", "Refresh merge request info", AllIcons.Actions.Refresh) { override fun actionPerformed(e: AnActionEvent) { val mergeRequestInfo = self.myMergeRequestInfo if (null !== mergeRequestInfo) { self.dispatcher.multicaster.refreshRequested(mergeRequestInfo) } } override fun update(e: AnActionEvent) { e.presentation.isVisible = !self.projectServiceProvider.isDoingCodeReview() } } private class MyUpVotesAction(private val self: MergeRequestDetailsToolbar) : AnAction(null, "Up votes", Icons.ThumbsUp) { override fun actionPerformed(e: AnActionEvent) { } override fun update(e: AnActionEvent) { val enabled = self.projectServiceProvider.applicationSettings.displayUpVotesAndDownVotes e.presentation.text = self.myUpVotes.toString() e.presentation.isVisible = enabled && self.myUpVotes >= 0 } override fun displayTextInToolbar(): Boolean = true override fun useSmallerFontForTextInToolbar(): Boolean = true } private class MyDownVotesAction(private val self: MergeRequestDetailsToolbar) : AnAction(null, "Down votes", Icons.ThumbsDown) { override fun actionPerformed(e: AnActionEvent) { } override fun update(e: AnActionEvent) { val enabled = self.projectServiceProvider.applicationSettings.displayUpVotesAndDownVotes e.presentation.text = self.myDownVotes.toString() e.presentation.isVisible = enabled && self.myDownVotes >= 0 } override fun displayTextInToolbar(): Boolean = true override fun useSmallerFontForTextInToolbar(): Boolean = true } private class MyPipelineAction(private val self: MergeRequestDetailsToolbar) : AnAction( null, "Pipeline status · Click to open pipeline in browser", null ) { override fun actionPerformed(e: AnActionEvent) { val pipelines = self.myPipelines if (pipelines.isEmpty()) { return } BrowserUtil.open(pipelines.first().url) } override fun update(e: AnActionEvent) { if (self.myPipelines.isEmpty()) { e.presentation.isVisible = false return } val status = self.myPipelines.first().status e.presentation.icon = when (status) { PipelineStatus.FAILED -> Icons.PipelineFailed PipelineStatus.RUNNING -> Icons.PipelineRunning PipelineStatus.PARTIAL_FAILED -> Icons.PipelineFailed PipelineStatus.SUCCESS -> Icons.PipelineSuccess PipelineStatus.UNKNOWN -> Icons.PipelineRunning } e.presentation.text = when (status) { PipelineStatus.FAILED -> "failed" PipelineStatus.RUNNING -> "running" PipelineStatus.PARTIAL_FAILED -> "partial failed" PipelineStatus.SUCCESS -> "passed" PipelineStatus.UNKNOWN -> "" } e.presentation.isVisible = status != PipelineStatus.UNKNOWN } override fun displayTextInToolbar(): Boolean = true override fun useSmallerFontForTextInToolbar(): Boolean = true } private class MyApprovalAction(private val self: MergeRequestDetailsToolbar) : AnAction(null, "Approval", null) { override fun actionPerformed(e: AnActionEvent) { if (!self.myApprovalPanel.shouldDisplayApprovalPanel()) { return } val reference = self.createComponent() // toolbar component val popup = JBPopupFactory.getInstance() .createComponentPopupBuilder( self.myApprovalPanel.createComponent(), reference ) .setResizable(true) .setMovable(false) .setRequestFocus(true) .createPopup() HelpTooltip.setMasterPopup(reference, popup) val point = findPopupPoint(reference, popup.content.preferredSize) (popup as AbstractPopup).show(reference, point.x, point.y, false) } private fun findPopupPoint(reference: JComponent, popup: Dimension): Point { val visibleBounds = reference.visibleRect val containerScreenPoint = visibleBounds.location SwingUtilities.convertPointToScreen(containerScreenPoint, reference) visibleBounds.location = containerScreenPoint return Point( visibleBounds.x + reference.width - popup.width, visibleBounds.y + reference.height ) } override fun update(e: AnActionEvent) { val approval = self.myApproval if (null === approval) { e.presentation.isVisible = false return } self.myApprovalPanel.hide() self.myApprovalPanel.setApproval(approval) val triple = approval.findVisibilityIconAndTextForApproval() e.presentation.isVisible = triple.first e.presentation.icon = triple.second e.presentation.text = triple.third } override fun displayTextInToolbar(): Boolean = true override fun useSmallerFontForTextInToolbar(): Boolean = true } private class MyOpenUrlAction(private val self: MergeRequestDetailsToolbar) : AnAction("Open merge request in browser", "Open merge request in browser", Icons.ExternalLink) { override fun actionPerformed(e: AnActionEvent) { BrowserUtil.open(self.myUrl) } override fun update(e: AnActionEvent) { e.presentation.isVisible = self.myUrl.isNotEmpty() } } private class MyStateAction(private val self: MergeRequestDetailsToolbar) : AnAction(null, "Merge request state", null) { override fun actionPerformed(e: AnActionEvent) { } override fun update(e: AnActionEvent) { val enabled = self.projectServiceProvider.applicationSettings.displayMergeRequestState if (!enabled) { e.presentation.isVisible = false return } when (self.myState) { MergeRequestState.ALL -> { e.presentation.isVisible = false } MergeRequestState.OPENED -> { e.presentation.icon = Icons.StateOpened e.presentation.text = "opened" e.presentation.isVisible = true } MergeRequestState.CLOSED -> { e.presentation.icon = Icons.StateClosed e.presentation.text = "closed" e.presentation.isVisible = true } MergeRequestState.MERGED -> { e.presentation.icon = Icons.StateMerged e.presentation.text = "merged" e.presentation.isVisible = true } } } override fun displayTextInToolbar(): Boolean = true override fun useSmallerFontForTextInToolbar(): Boolean = true } private class MyCodeReviewAction(private val self: MergeRequestDetailsToolbar) : ToggleAction(null, null, null) { override fun isSelected(e: AnActionEvent): Boolean { return self.projectServiceProvider.isDoingCodeReview() } override fun setSelected(e: AnActionEvent, state: Boolean) { val mr = self.myMergeRequest if (null === mr) { return } if (state) { CodeReviewService.start(self.projectServiceProvider, self.providerData, mr, self.myReviewCommits) } else { CodeReviewService.stop(self.projectServiceProvider, self.providerData, mr) } } override fun update(e: AnActionEvent) { val mr = self.myMergeRequest if (null === mr || self.myReviewCommits.isEmpty()) { e.presentation.isVisible = false return } when (self.myState) { MergeRequestState.ALL, MergeRequestState.CLOSED, MergeRequestState.MERGED -> { e.presentation.isVisible = false } MergeRequestState.OPENED -> { e.presentation.text = getCodeReviewText() e.presentation.description = "Open diff in IDE and review" e.presentation.isVisible = true } // TODO: Support view diff // MergeRequestState.CLOSED, MergeRequestState.MERGED -> { // e.presentation.text = "View Diff" // e.presentation.isVisible = true // } } val isDoingCodeReview = self.projectServiceProvider.isDoingCodeReview() if (isDoingCodeReview) { val draftCount = self.projectServiceProvider.reviewContextManager.getDraftCommentsCount( self.providerData.id, mr.id ) if (draftCount > 0) { e.presentation.text = if (draftCount < 2) "Publish $draftCount Comment and Stop Reviewing" else "Publish $draftCount Comments and Stop Reviewing" e.presentation.description = "Publish all draft comments, stop reviewing and show other MRs" } else { e.presentation.text = "Stop Reviewing" e.presentation.description = "End reviewing and show other MRs" } e.presentation.isEnabled = self.projectServiceProvider.reviewContextManager.isDoingCodeReview( self.providerData.id, mr.id ) } else { e.presentation.isEnabled = true } } private fun getCodeReviewText(): String { if (self.myCommits.size == self.myReviewCommits.size) { return "Code Review" } return "Code Review ${self.myReviewCommits.size}/${self.myCommits.size} commits" } override fun displayTextInToolbar(): Boolean = true } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestDetailsToolbarUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.* import net.ntworld.mergeRequestIntegrationIde.ui.Component import java.util.* interface MergeRequestDetailsToolbarUI : Component { val dispatcher: EventDispatcher fun setPipelines(mergeRequestInfo: MergeRequestInfo, pipelines: List) fun setCommits(mergeRequestInfo: MergeRequestInfo, commits: List) fun setCommitsForReviewing(mergeRequestInfo: MergeRequestInfo, commits: List) fun setApproval(mergeRequestInfo: MergeRequestInfo, approval: Approval) fun setMergeRequest(mergeRequest: MergeRequest) fun setMergeRequestInfo(mergeRequestInfo: MergeRequestInfo) interface Listener: EventListener { fun refreshRequested(mergeRequestInfo: MergeRequestInfo) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestDetailsUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest import net.ntworld.mergeRequest.MergeRequest import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequestIntegrationIde.ui.Component interface MergeRequestDetailsUI : Component { fun hide() fun setMergeRequest(mergeRequest: MergeRequest) fun setMergeRequestInfo(mergeRequestInfo: MergeRequestInfo) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/MergeRequestCommitsTab.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab import com.intellij.openapi.Disposable import com.intellij.openapi.util.Disposer import com.intellij.ui.OnePixelSplitter import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.Commit import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifierAdapter import net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit.CommitChanges import net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit.CommitChangesUI import net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit.CommitCollection import net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit.CommitCollectionUI import javax.swing.JComponent class MergeRequestCommitsTab( private val projectServiceProvider: ProjectServiceProvider ) : MergeRequestCommitsTabUI, Disposable { override val dispatcher = EventDispatcher.create(MergeRequestCommitsTabUI.Listener::class.java) private val mySplitter = OnePixelSplitter( MergeRequestCommitsTab::class.java.canonicalName, 0.5f ) private val myCollection: CommitCollectionUI = CommitCollection() private val myChanges: CommitChangesUI = CommitChanges(projectServiceProvider) private val myCollectionListener = object : CommitCollectionUI.Listener { override fun commitsSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List) { if (commits.isEmpty()) { myChanges.clear() } else { myChanges.updateSelectedCommits(providerData, mergeRequestInfo, commits) } dispatcher.multicaster.commitSelected(providerData, mergeRequestInfo, commits) } } private val myProjectNotifier = object : ProjectNotifierAdapter() { override fun startCodeReview(reviewContext: ReviewContext) { myCollection.disable() myChanges.disable() } override fun stopCodeReview(reviewContext: ReviewContext) { myCollection.enable() myChanges.enable() } } private val myConnection = projectServiceProvider.messageBus.connect() init { mySplitter.firstComponent = myCollection.createComponent() mySplitter.secondComponent = myChanges.createComponent() myCollection.dispatcher.addListener(myCollectionListener) myConnection.subscribe(ProjectNotifier.TOPIC, myProjectNotifier) Disposer.register(projectServiceProvider.project, this) } override fun clear() { myCollection.clear() } override fun setCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List) { myCollection.setCommits(providerData, mergeRequestInfo, commits) myChanges.setCommits(providerData, mergeRequestInfo, commits) } override fun createComponent(): JComponent = mySplitter override fun dispose() { myConnection.disconnect() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/MergeRequestCommitsTabUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.Commit import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.ui.Component import java.util.* interface MergeRequestCommitsTabUI : Component { val dispatcher: EventDispatcher fun clear() fun setCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List) interface Listener : EventListener { fun commitSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/MergeRequestDescriptionTab.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab import com.intellij.ide.util.TipUIUtil import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.util.HtmlHelper import javax.swing.JComponent class MergeRequestDescriptionTab : MergeRequestDescriptionTabUI { private var myMR: MergeRequestInfo? = null private val myWebView = TipUIUtil.createBrowser() as TipUIUtil.Browser private val myHtmlTemplate = MergeRequestDescriptionTab::class.java.getResource( "/templates/mr.description.html" ).readText() override fun setMergeRequestInfo(providerData: ProviderData, mr: MergeRequestInfo) { val currentMR = myMR if (null === currentMR || currentMR.id != mr.id) { myMR = mr myWebView.text = buildHtml(providerData, mr) } } private fun buildHtml(providerData: ProviderData, mr: MergeRequestInfo): String { val output = myHtmlTemplate .replace("{{title}}", mr.title) .replace("{{description}}", HtmlHelper.convertFromMarkdown(mr.description)) return HtmlHelper.resolveRelativePath(providerData, output) } override fun createComponent(): JComponent { return myWebView.component } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/MergeRequestDescriptionTabUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.ui.Component interface MergeRequestDescriptionTabUI: Component { fun setMergeRequestInfo(providerData: ProviderData, mr: MergeRequestInfo) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/MergeRequestInfoTab.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab import net.ntworld.mergeRequest.MergeRequest import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.ui.panel.MergeRequestInfoPanel import javax.swing.JComponent class MergeRequestInfoTab : MergeRequestInfoTabUI { private val myInfoPanel = MergeRequestInfoPanel() override fun setMergeRequestInfo(providerData: ProviderData, mr: MergeRequestInfo) { myInfoPanel.setMergeRequestInfo(providerData, mr) } override fun setMergeRequest(mr: MergeRequest) = myInfoPanel.setMergeRequest(mr) override fun createComponent(): JComponent = myInfoPanel.createComponent() } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/MergeRequestInfoTabUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab import net.ntworld.mergeRequest.MergeRequest import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.ui.Component interface MergeRequestInfoTabUI : Component { fun setMergeRequestInfo(providerData: ProviderData, mr: MergeRequestInfo) fun setMergeRequest(mr: MergeRequest) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/commit/CommitChanges.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.vcs.changes.Change import com.intellij.openapi.vcs.changes.ui.ChangesBrowserChangeNode import com.intellij.openapi.vcs.changes.ui.ChangesTreeImpl import com.intellij.openapi.vcs.changes.ui.TreeModelBuilder import com.intellij.ui.ScrollPaneFactory import net.miginfocom.swing.MigLayout import net.ntworld.mergeRequest.Commit import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.ui.util.CustomSimpleToolWindowPanel import net.ntworld.mergeRequestIntegrationIde.ui.util.ToolbarUtil import javax.swing.JComponent import javax.swing.JPanel import javax.swing.event.TreeSelectionListener import javax.swing.tree.DefaultTreeModel import kotlin.concurrent.thread import com.intellij.openapi.project.Project as IdeaProject class CommitChanges(private val projectServiceProvider: ProjectServiceProvider) : CommitChangesUI { private val myComponent = CustomSimpleToolWindowPanel(vertical = true, borderless = true) private val myTree = MyTree(projectServiceProvider.project) private var myProviderData: ProviderData? = null private var myMergeRequestInfo: MergeRequestInfo? = null private val myTreeSelectionListener = TreeSelectionListener { if (null !== it && myTree.isVisible) { val lastPath = it.path.lastPathComponent val providerData = myProviderData val mergeRequestInfo = myMergeRequestInfo if (null !== providerData && null !== mergeRequestInfo && lastPath is ChangesBrowserChangeNode) { val selectedContext = projectServiceProvider.reviewContextManager.findSelectedContext() val reviewContext = projectServiceProvider.reviewContextManager.findContext( providerData.id, mergeRequestInfo.id ) if (null === reviewContext) { return@TreeSelectionListener } if (null !== selectedContext) { if (selectedContext.providerData.id != reviewContext.providerData.id || selectedContext.mergeRequestInfo.id != reviewContext.mergeRequestInfo.id) { return@TreeSelectionListener } } reviewContext.openChange(lastPath.userObject, focus = false, displayMergeRequestId = !projectServiceProvider.isDoingCodeReview()) } } } init { myComponent.setContent(ScrollPaneFactory.createScrollPane(myTree, true)) myComponent.toolbar = createToolbar() myTree.addTreeSelectionListener(myTreeSelectionListener) } override fun clear() { ApplicationManager.getApplication().invokeLater { myTree.setChangesToDisplay(listOf()) } } override fun disable() { myComponent.isVisible = false } override fun enable() { myComponent.isVisible = true } override fun setCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List) { myProviderData = providerData myMergeRequestInfo = mergeRequestInfo thread { myTree.isVisible = false val changes = projectServiceProvider.repositoryFile.findChanges(providerData, commits.map { it.id }) projectServiceProvider.reviewContextManager.updateChanges(providerData.id, mergeRequestInfo.id, changes) projectServiceProvider.reviewContextManager.updateReviewingChanges(providerData.id, mergeRequestInfo.id, changes) ApplicationManager.getApplication().invokeLater { myTree.setChangesToDisplay(changes) myTree.isVisible = true } } } override fun updateSelectedCommits( providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, selectedCommits: List ) { myProviderData = providerData myMergeRequestInfo = mergeRequestInfo thread { myTree.isVisible = false val partialChanges = projectServiceProvider.repositoryFile.findChanges(providerData, selectedCommits.map { it.id }) projectServiceProvider.reviewContextManager.updateReviewingChanges( providerData.id, mergeRequestInfo.id, partialChanges ) ApplicationManager.getApplication().invokeLater { myTree.setChangesToDisplay(partialChanges) myTree.isVisible = true } } } override fun createComponent(): JComponent = myComponent private fun createToolbar(): JComponent { val panel = JPanel(MigLayout("ins 0, fill", "[left]push[right]", "center")) val leftActionGroup = DefaultActionGroup() val leftToolbar = ActionManager.getInstance().createActionToolbar( "${CommitChanges::class.java.canonicalName}/toolbar-left", leftActionGroup, true ) panel.add(leftToolbar.component) panel.add( ToolbarUtil.createExpandAndCollapseToolbar( "${CommitChanges::class.java.canonicalName}/toolbar-right", myTree ) ) return panel } private class MyTree(ideaProject: IdeaProject) : ChangesTreeImpl( ideaProject, false, false, Change::class.java ) { override fun buildTreeModel(changes: MutableList): DefaultTreeModel { return TreeModelBuilder.buildFromChanges(myProject, grouping, changes, null) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/commit/CommitChangesUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit import net.ntworld.mergeRequest.Commit import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.ui.Component interface CommitChangesUI : Component { fun clear() fun disable() fun enable() fun setCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List) fun updateSelectedCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, selectedCommits: List) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/commit/CommitCollection.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.ui.JBColor import com.intellij.ui.ScrollPaneFactory import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.Commit import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.ui.panel.CommitItemPanel import net.ntworld.mergeRequestIntegrationIde.ui.util.CustomSimpleToolWindowPanel import javax.swing.BoxLayout import javax.swing.JComponent import javax.swing.JPanel import javax.swing.event.ChangeListener class CommitCollection : CommitCollectionUI { override val dispatcher = EventDispatcher.create(CommitCollectionUI.Listener::class.java) private val myComponent = CustomSimpleToolWindowPanel(vertical = true, borderless = true) private val myList = JPanel() private val myItems = mutableListOf() private var myProviderData: ProviderData? = null private var myMergeRequestInfo: MergeRequestInfo? = null private class MySelectAllButton(private val self: CommitCollection) : AnAction( "Select all", "Select all commits to see changes or do code review", null ) { override fun actionPerformed(e: AnActionEvent) { val commits = self.myItems.map { it.isSelectable(selectable = true, selected = true) it.commit } self.dispatchCommitSelectedEvent(commits) } override fun displayTextInToolbar() = true override fun useSmallerFontForTextInToolbar() = true } private val mySelectAllButton = MySelectAllButton(this) private class MyUnselectAllButton(private val self: CommitCollection): AnAction( "Unselect all", "Unselect all commits then pick a single one to see changes or do code review", null ) { override fun actionPerformed(e: AnActionEvent) { self.myItems.forEach { it.isSelectable(selectable = true, selected = false) } self.dispatchCommitSelectedEvent(listOf()) } override fun displayTextInToolbar() = true override fun useSmallerFontForTextInToolbar() = true } private val myUnselectAllButton = MyUnselectAllButton(this) private val myCommitSelectChangeListener = ChangeListener { val list = myItems.map { Pair(it.commit, it.isSelected()) } myItems.forEachIndexed { index, item -> item.isSelectable(CommitSelectUtil.canSelect(list, index)) } val selectedCommits = mutableListOf() for (item in myItems) { if (item.isSelected()) { selectedCommits.add(item.commit) } } dispatchCommitSelectedEvent(selectedCommits) } init { myList.layout = BoxLayout(myList, BoxLayout.Y_AXIS) myList.background = JBColor.background() myComponent.setContent(ScrollPaneFactory.createScrollPane(myList, true)) myComponent.toolbar = createToolbar() } override fun clear() { myList.removeAll() myItems.clear() } override fun disable() { myItems.forEach { it.disable() } } override fun enable() { myItems.forEach { it.enable() } } override fun setCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: Collection) { clear() myProviderData = providerData myMergeRequestInfo = mergeRequestInfo commits.forEach { val item = CommitItemPanel(it, myCommitSelectChangeListener) myItems.add(item) myList.add(item.createComponent()) } } override fun createComponent(): JComponent = myComponent private fun dispatchCommitSelectedEvent(commits: List) { val providerData = myProviderData val mergeRequestInfo = myMergeRequestInfo if (null !== providerData && null !== mergeRequestInfo) { dispatcher.multicaster.commitsSelected(providerData, mergeRequestInfo, commits) } } private fun createToolbar() : JComponent { val actionGroup = DefaultActionGroup() actionGroup.add(myUnselectAllButton) actionGroup.addSeparator() actionGroup.add(mySelectAllButton) return ActionManager.getInstance().createActionToolbar( "${CommitCollection::class.java.canonicalName}/toolbar", actionGroup, true ).component } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/commit/CommitCollectionUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.Commit import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.ui.Component import java.util.* interface CommitCollectionUI : Component { val dispatcher: EventDispatcher fun clear() fun disable() fun enable() fun setCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: Collection) interface Listener : EventListener { fun commitsSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/commit/CommitSelectUtil.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit import net.ntworld.mergeRequest.Commit object CommitSelectUtil { fun canSelect(list: List>, index: Int): Boolean { if (isAllSelectedOrUnselected(list)) { return true } val range = findSelectedCommitRangeIndex(list) return range.first - 1 <= index && index <= range.second + 1 } private fun findSelectedCommitRangeIndex(list: List>): Pair { var first = -1 var last = -1 var setFirst = false var index = 0 for (item in list) { if (item.second) { if (!setFirst) { first = index setFirst = true } last = index index++ continue } if (setFirst) { break } index++ } return Pair(first, last) } private fun isAllSelectedOrUnselected(list: List>): Boolean { if (list.isEmpty() || list.size == 1) { return true } val first = list[0].second for (i in 1..list.lastIndex) { if (list[i].second != first) { return false } } return true } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ApprovalPanel.form ================================================
================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ApprovalPanel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.panel import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.Approval import net.ntworld.mergeRequestIntegrationIde.ui.Component import net.ntworld.mergeRequestIntegrationIde.component.Icons import java.util.* import javax.swing.BoxLayout import javax.swing.JButton import javax.swing.JComponent import javax.swing.JPanel class ApprovalPanel : Component { var myWholePanel: JPanel? = null var myApproversWrapper: JPanel? = null var myApproveBtn: JButton? = null private var myApproval: Approval? = null private var canApprove = false private var canUnapprove = false private val dispatcher = EventDispatcher.create(Listener::class.java) init { myApproversWrapper!!.layout = BoxLayout(myApproversWrapper!!, BoxLayout.Y_AXIS) myApproveBtn!!.addActionListener { if (canApprove) { dispatcher.multicaster.onApproveClicked() } if (canUnapprove) { dispatcher.multicaster.onUnapproveClicked() } } } fun hide() { canApprove = false canUnapprove = false myWholePanel!!.isVisible = false } fun setApproval(approval: Approval) { myApproval = approval canApprove = false canUnapprove = false myApproveBtn!!.isVisible = approval.canApprove || approval.hasApproved if (approval.hasApproved) { myApproveBtn!!.text = "Revoke approval" canUnapprove = true } else { myApproveBtn!!.text = "Approve" canApprove = true } myApproversWrapper!!.removeAll() val approved = mutableListOf() for (approver in approval.approvedBy) { myApproversWrapper!!.add(UserInfoItemPanel(approver, Icons.Approved).createComponent()) approved.add(approver.id) } for (approver in approval.approvers) { if (approved.contains(approver.id)) { continue } myApproversWrapper!!.add(UserInfoItemPanel(approver, Icons.NoApproval).createComponent()) } myWholePanel!!.isVisible = true } fun addListener(listener: Listener) { dispatcher.addListener(listener) } fun shouldDisplayApprovalPanel() : Boolean { val approval = myApproval if (null === approval) { return false } return approval.canApprove || approval.hasApproved || approval.approvedBy.isNotEmpty() || approval.approvers.isNotEmpty() } override fun createComponent(): JComponent = myWholePanel!! interface Listener : EventListener { fun onApproveClicked() fun onUnapproveClicked() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/CommitItemPanel.form ================================================
================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/CommitItemPanel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.panel import com.intellij.ui.JBColor import net.ntworld.mergeRequest.Commit import net.ntworld.mergeRequestIntegration.util.DateTimeUtil import net.ntworld.mergeRequestIntegrationIde.ui.Component import java.awt.Dimension import javax.swing.* import javax.swing.event.ChangeListener class CommitItemPanel(val commit: Commit, private val changeListener: ChangeListener) : Component { var myWholePanel: JPanel? = null var myWrapperPanel: JPanel? = null var mySelectedWrapperPanel: JPanel? = null var myInfoWrapperPanel: JPanel? = null var myTitleWrapperPanel: JPanel? = null var myInfoDetailsWrapperPanel: JPanel? = null var mySelected: JCheckBox? = null var myCommitHash: JTextField? = null var myAuthorAndTime: JLabel? = null var myTitle: JLabel? = null init { mySelected!!.isSelected = true myTitle!!.text = commit.message myAuthorAndTime!!.text = "${commit.authorName} · ${DateTimeUtil.toPretty(DateTimeUtil.toDate(commit.createdAt))}" myCommitHash!!.text = commit.id myWholePanel!!.border = BorderFactory.createMatteBorder(0, 0, 1, 0, JBColor.border()) myWholePanel!!.maximumSize = Dimension(Int.MAX_VALUE, 60) mySelected!!.addChangeListener(changeListener) myWholePanel!!.background = JBColor.background() myWrapperPanel!!.background = JBColor.background() mySelectedWrapperPanel!!.background = JBColor.background() myInfoWrapperPanel!!.background = JBColor.background() myTitleWrapperPanel!!.background = JBColor.background() myInfoDetailsWrapperPanel!!.background = JBColor.background() } fun disable() { if (this.isSelected()) { mySelectedWrapperPanel!!.isVisible = false } else { myWholePanel!!.isVisible = false } } fun enable() { mySelectedWrapperPanel!!.isVisible = true myWholePanel!!.isVisible = true } fun isSelected(): Boolean = mySelected!!.isSelected fun isSelectable(selectable: Boolean, selected: Boolean? = null) { if (selectable) { mySelected!!.isEnabled = true if (null !== selected) { mySelected!!.isSelected = selected } } else { mySelected!!.isEnabled = false mySelected!!.toolTipText = "Cannot select this commit" mySelected!!.isSelected = false } } override fun createComponent(): JComponent = myWholePanel!! } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/MergeRequestFilterPropertiesPanel.form ================================================
================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/MergeRequestFilterPropertiesPanel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.panel import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.ui.ComboBox import com.intellij.util.ui.UIUtil import net.ntworld.mergeRequest.MergeRequestState import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.UserInfo import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.task.FetchProjectMembersTask import net.ntworld.mergeRequestIntegrationIde.ui.Component import java.awt.event.ActionListener import javax.swing.* class MergeRequestFilterPropertiesPanel( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val onChanged: (() -> Unit), private val onReady: (() -> Unit) ) : Component { var myWholePanel: JPanel? = null var myStateAll: JRadioButton? = null var myStateOpened: JRadioButton? = null var myStateMerged: JRadioButton? = null var myStateClosed: JRadioButton? = null var myAuthor: ComboBox? = null var myAssignee: ComboBox? = null var myApprover: ComboBox? = null private var isFetched = false private var isReady = false private var shouldDispatchChanges = true private val myData = mutableListOf() private val myListener = object : FetchProjectMembersTask.Listener { override fun onError(exception: Exception) { isFetched = false throw exception } override fun taskStarted() { myData.clear() } override fun dataReceived(collection: List) { ApplicationManager.getApplication().invokeLater { myData.addAll(collection) } } override fun taskEnded() { myAuthor!!.model = MyMemberModel(myData) if (providerData.hasAssigneeFeature) { myAssignee!!.model = MyMemberModel(myData) } if (providerData.hasApprovalFeature) { myApprover!!.model = MyMemberModel(myData) } isReady = true onReady() } } private val myListRenderer = ListCellRenderer { _, value, index, isSelected, cellHasFocus -> if (null === value) { JPanel() } else { val panel = UserInfoItemPanel(value) panel.setBackground(UIUtil.getListBackground(isSelected, cellHasFocus)) panel.createComponent() } } private val myActionListener = ActionListener { if (isReady && shouldDispatchChanges) { onChanged() } } init { val task = FetchProjectMembersTask(projectServiceProvider, providerData, true, myListener) if (!isFetched) { task.start() } myStateOpened!!.isSelected = true myAuthor!!.renderer = myListRenderer myAssignee!!.renderer = myListRenderer myApprover!!.renderer = myListRenderer myAuthor!!.isSwingPopup = false myAssignee!!.isSwingPopup = false myApprover!!.isSwingPopup = false myAuthor!!.addActionListener(myActionListener) myAssignee!!.addActionListener(myActionListener) myAssignee!!.addActionListener(myActionListener) myStateAll!!.addActionListener(myActionListener) myStateOpened!!.addActionListener(myActionListener) myStateMerged!!.addActionListener(myActionListener) myStateClosed!!.addActionListener(myActionListener) myAssignee!!.isEnabled = providerData.hasAssigneeFeature myApprover!!.isEnabled = providerData.hasApprovalFeature } override fun createComponent(): JComponent { return myWholePanel!! } fun buildFilter(search: String): GetMergeRequestFilter { return GetMergeRequestFilter.make( id = null, state = findState(), search = search.trim(), authorId = findMemberInComboBox(myAuthor!!), assigneeId = findMemberInComboBox(myAssignee!!), approverIds = listOf(findMemberInComboBox(myApprover!!)), sourceBranch = "" ) } fun buildFilterForSearchById(id: Int): GetMergeRequestFilter { return GetMergeRequestFilter.make( id = id, state = findState(), search = "", authorId = findMemberInComboBox(myAuthor!!), assigneeId = findMemberInComboBox(myAssignee!!), approverIds = listOf(findMemberInComboBox(myApprover!!)), sourceBranch = "" ) } fun setPreselectedValues(value: GetMergeRequestFilter) { shouldDispatchChanges = false when (value.state) { MergeRequestState.ALL -> myStateAll!!.isSelected = true MergeRequestState.OPENED -> myStateOpened!!.isSelected = true MergeRequestState.CLOSED -> myStateClosed!!.isSelected = true MergeRequestState.MERGED -> myStateMerged!!.isSelected = true } if (value.assigneeId.isNotBlank()) { selectMemberInComboBox(myAssignee!!, value.assigneeId) } if (value.authorId.isNotBlank()) { selectMemberInComboBox(myAuthor!!, value.authorId) } if (value.approverIds.isNotEmpty() && value.approverIds[0].isNotBlank()) { selectMemberInComboBox(myApprover!!, value.approverIds[0]) } shouldDispatchChanges = true } private fun findMemberInComboBox(comboBox: ComboBox) : String { val selected = comboBox.selectedItem as UserInfo? if (!comboBox.isEnabled || null === selected) { return "" } return selected.id } private fun selectMemberInComboBox(comboBox: ComboBox, userId: String) { for (i in 0 until comboBox.itemCount) { val item = comboBox.getItemAt(i) if (item.id == userId) { comboBox.selectedItem = item return } } } private fun findState(): MergeRequestState { if (myStateOpened!!.isSelected) { return MergeRequestState.OPENED } if (myStateMerged!!.isSelected) { return MergeRequestState.MERGED } if (myStateClosed!!.isSelected) { return MergeRequestState.CLOSED } return MergeRequestState.ALL } private class MyMemberModel( private val data: List ) : ComboBoxModel, AbstractListModel() { private var mySelected: UserInfo? = null override fun setSelectedItem(anItem: Any?) { mySelected = anItem as UserInfo? fireContentsChanged(this, -1, -1) } override fun getElementAt(index: Int): UserInfo { if (index < 0 || index >= data.size) { throw ArrayIndexOutOfBoundsException() } return data[index] } override fun getSelectedItem(): Any? { return mySelected } override fun getSize(): Int { return data.size } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/MergeRequestInfoPanel.form ================================================
================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/MergeRequestInfoPanel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.panel import com.intellij.icons.AllIcons import com.intellij.ui.JBColor import net.ntworld.mergeRequest.* import net.ntworld.mergeRequestIntegration.util.DateTimeUtil import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JPanel import javax.swing.JTextField class MergeRequestInfoPanel { var myWholePanel: JPanel? = null var myWrapperPanel: JPanel? = null var myBranchesWrapperPanel: JPanel? = null var myMergedByWrapperPanel: JPanel? = null var myClosedByWrapperPanel: JPanel? = null var myAssignedWrapperPanel: JPanel? = null var myAuthorWrapperPanel: JPanel? = null var myId: JLabel? = null var myTitle: JLabel? = null var myCreatedAt: JLabel? = null var myUpdatedAt: JLabel? = null var myAuthorFullName: JLabel? = null var myAuthorUsername: JLabel? = null var myAssigneeFullName: JLabel? = null var myAssigneeUsername: JLabel? = null var mySourceBranch: JTextField? = null var myBranchIcon: JLabel? = null var myTargetBranch: JTextField? = null var myStatus: JLabel? = null var myMergedByFullName: JLabel? = null var myMergedByUsername: JLabel? = null var myMergedAtText: JLabel? = null var myMergedAt: JLabel? = null var myClosedByFullName: JLabel? = null var myClosedByUsername: JLabel? = null var myClosedAtText: JLabel? = null var myClosedAt: JLabel? = null init { mySourceBranch!!.background = null mySourceBranch!!.border = null myTargetBranch!!.background = null myTargetBranch!!.border = null myBranchIcon!!.icon = AllIcons.Vcs.Arrow_right myTitle!!.text = "-" displayTime(null, myCreatedAt!!) displayTime(null, myUpdatedAt!!) hide() setBackground() } private fun setBackground() { myWholePanel!!.background = JBColor.background() myWrapperPanel!!.background = JBColor.background() myBranchesWrapperPanel!!.background = JBColor.background() myMergedByWrapperPanel!!.background = JBColor.background() myClosedByWrapperPanel!!.background = JBColor.background() myAssignedWrapperPanel!!.background = JBColor.background() myAuthorWrapperPanel!!.background = JBColor.background() } fun setMergeRequestInfo(providerData: ProviderData, mr: MergeRequestInfo) { myId!!.text = providerData.info.formatMergeRequestId(mr.id) myTitle!!.text = mr.title displayTime(mr.createdAt, myCreatedAt!!) displayTime(mr.updatedAt, myUpdatedAt!!) hide() } fun setMergeRequest(mr: MergeRequest) { myTitle!!.text = mr.title displayTime(mr.createdAt, myCreatedAt!!) displayTime(mr.updatedAt, myUpdatedAt!!) displayStatus(mr.state, myStatus!!) displayBranches(mr.sourceBranch, mr.targetBranch, mySourceBranch!!, myTargetBranch!!, myBranchIcon!!) displayUserInfo(mr.author, myAuthorFullName!!, myAuthorUsername!!) displayUserInfo(mr.assignee, myAssigneeFullName!!, myAssigneeUsername!!) displayUserInfoWithTime( mr.mergedBy, mr.mergedAt, myMergedByFullName!!, myMergedByUsername!!, myMergedAtText!!, myMergedAt!! ) displayUserInfoWithTime( mr.closedBy, mr.closedAt, myClosedByFullName!!, myClosedByUsername!!, myClosedAtText!!, myClosedAt!! ) } fun createComponent(): JComponent = myWholePanel!! private fun hide() { displayStatus(MergeRequestState.ALL, myStatus!!) displayBranches(null, null, mySourceBranch!!, myTargetBranch!!, myBranchIcon!!) displayUserInfo(null, myAuthorFullName!!, myAuthorUsername!!) displayUserInfo(null, myAssigneeFullName!!, myAssigneeUsername!!) displayUserInfoWithTime( null, null, myMergedByFullName!!, myMergedByUsername!!, myMergedAtText!!, myMergedAt!! ) displayUserInfoWithTime( null, null, myClosedByFullName!!, myClosedByUsername!!, myClosedAtText!!, myClosedAt!! ) } private fun displayBranches( source: String?, target: String?, sourceTextField: JTextField, targetTextField: JTextField, icon: JLabel ) { if (null === source || null === target) { sourceTextField.text = "-" targetTextField.isVisible = false icon.isVisible = false return } sourceTextField.text = source targetTextField.text = target targetTextField.isVisible = true icon.isVisible = true } private fun displayStatus(state: MergeRequestState, label: JLabel) { label.text = when (state) { MergeRequestState.ALL -> "-" MergeRequestState.OPENED -> "opened" MergeRequestState.CLOSED -> "closed" MergeRequestState.MERGED -> "merged" } } private fun displayUserInfoWithTime( user: UserInfo?, datetime: DateTime?, fullName: JLabel, username: JLabel, at: JLabel, datetimeLabel: JLabel ) { displayUserInfo(user, fullName, username) displayTime(datetime, datetimeLabel) at.isVisible = null !== datetime } private fun displayTime(datetime: DateTime?, label: JLabel) { if (null === datetime) { label.text = "" } else { val date = DateTimeUtil.toDate(datetime) label.text = "${DateTimeUtil.formatDate(date)} · ${DateTimeUtil.toPretty(date)}" } } private fun displayUserInfo(user: UserInfo?, fullName: JLabel, username: JLabel) { if (null === user) { fullName.text = "-" username.text = "" } else { fullName.text = user.name username.text = "@${user.username}" } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/MergeRequestItemPanel.form ================================================
================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/MergeRequestItemPanel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.panel import com.intellij.openapi.application.ApplicationManager import com.intellij.ui.JBColor import com.intellij.util.ui.UIUtil import net.ntworld.mergeRequest.Approval import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegration.util.DateTimeUtil import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.task.FindApprovalTask import net.ntworld.mergeRequestIntegrationIde.ui.Component import net.ntworld.mergeRequestIntegrationIde.ui.provider.ProviderDetailsMRList import net.ntworld.mergeRequestIntegrationIde.ui.util.findVisibilityIconAndTextForApproval import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JPanel class MergeRequestItemPanel( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val mergeRequestInfo: MergeRequestInfo, private val displayType: ProviderDetailsMRList.ApprovalStatusDisplayType ): Component { var myWholePanel: JPanel? = null var myWrapper: JPanel? = null var myContentWrapperPanel: JPanel? = null var myApprovalStatusWrapperPanel: JPanel? = null var myTimeWrapper: JPanel? = null var myId: JLabel? = null var myTitle: JLabel? = null var myCreated: JLabel? = null var myUpdated: JLabel? = null var myUserStatus: JLabel? = null var myAllStatuses: JLabel? = null private var myUserStatusColor: JBColor = JBColor.RED private val myFindApprovalTaskListener = object : FindApprovalTask.Listener { override fun dataReceived(mergeRequestInfo: MergeRequestInfo, approval: Approval) { ApplicationManager.getApplication().invokeLater { val triple = approval.findVisibilityIconAndTextForApproval() myAllStatuses!!.isVisible = triple.first myAllStatuses!!.icon = triple.second myAllStatuses!!.text = triple.third if (displayType == ProviderDetailsMRList.ApprovalStatusDisplayType.STATUSES_AND_MINE_APPROVAL) { val approved = approval.approvedBy.firstOrNull { it.id == providerData.currentUser.id } if (null !== approved) { myUserStatus!!.text = "Approved" myUserStatusColor = JBColor.GREEN } else { myUserStatus!!.text = "Waiting" myUserStatusColor = JBColor.RED } myUserStatus!!.foreground = myUserStatusColor myUserStatus!!.isVisible = true } myApprovalStatusWrapperPanel!!.isVisible = true } } } init { myId!!.text = providerData.info.formatMergeRequestId(mergeRequestInfo.id) myTitle!!.text = mergeRequestInfo.title val created = DateTimeUtil.toDate(mergeRequestInfo.createdAt) myCreated!!.text = "Created ${DateTimeUtil.toPretty(created)}" myCreated!!.toolTipText = DateTimeUtil.formatDate(created) val updated = DateTimeUtil.toDate(mergeRequestInfo.updatedAt) myUpdated!!.text = "updated ${DateTimeUtil.toPretty(updated)}" myUpdated!!.toolTipText = DateTimeUtil.formatDate(updated) if (displayType != ProviderDetailsMRList.ApprovalStatusDisplayType.NONE && providerData.hasApprovalFeature) { fetchApprovalData() } else { myApprovalStatusWrapperPanel!!.isVisible = false } } private fun fetchApprovalData() { val task = FindApprovalTask( projectServiceProvider = projectServiceProvider, providerData = providerData, mergeRequestInfo = mergeRequestInfo, listener = myFindApprovalTaskListener ) task.start() } fun changeStyle(selected: Boolean, hasFocus: Boolean) { val backgroundColor = UIUtil.getListBackground(selected, hasFocus) myWholePanel!!.background = backgroundColor myWrapper!!.background = backgroundColor myContentWrapperPanel!!.background = backgroundColor myApprovalStatusWrapperPanel!!.background = backgroundColor myTimeWrapper!!.background = backgroundColor val foregroundColor = UIUtil.getListForeground(selected, hasFocus) myTitle!!.foreground = foregroundColor val foregroundColorOrGray = if (selected && hasFocus) foregroundColor else JBColor.gray myCreated!!.foreground = foregroundColorOrGray myUpdated!!.foreground = foregroundColorOrGray if (displayType != ProviderDetailsMRList.ApprovalStatusDisplayType.NONE) { val foregroundColorOrCurrent = if (selected && hasFocus) foregroundColor else myUserStatusColor myUserStatus!!.foreground = foregroundColorOrCurrent myAllStatuses!!.foreground = foregroundColor } } override fun createComponent(): JComponent = myWholePanel!! } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ProjectPanel.form ================================================
================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ProjectPanel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.panel import net.ntworld.mergeRequest.Project import java.awt.Color import javax.swing.* class ProjectPanel(project: Project, isSelected: Boolean) { var myWrapper: JPanel? = null var myInner: JPanel? = null var myProjectName: JLabel? = null var myUrl: JLabel? = null init { myInner!!.border = BorderFactory.createLineBorder(Color(0, 0, 0, 0), 3) myProjectName!!.text = project.name myUrl!!.text = project.url setSelected(isSelected) } fun setSelected(value: Boolean) { if (!value) { myWrapper!!.border = BorderFactory.createLineBorder(Color(0, 0, 0, 0), 1) } else { myWrapper!!.border = BorderFactory.createLineBorder(Color(164, 55, 65), 1) } } fun getComponent(): JPanel = myWrapper!! } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ProviderInformationPanel.form ================================================
================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ProviderInformationPanel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.panel import com.intellij.ide.BrowserUtil import com.intellij.ui.JBColor import net.ntworld.mergeRequest.ProjectVisibility import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.ENTERPRISE_EDITION_URL import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.ui.Component import java.awt.Color import javax.swing.JButton import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JPanel class ProviderInformationPanel( private val applicationServiceProvider: ApplicationServiceProvider, private val providerData: ProviderData ): Component { var myWholePanel: JPanel? = null var myWrapperPanel: JPanel? = null var myLoggedUserWrapperPanel: JPanel? = null var myUrlWrapperPanel: JPanel? = null var myStatusWrapperPanel: JPanel? = null var myLicenseWarningWrapperPanel: JPanel? = null var myOpenURLButton: JButton? = null var myBuyButton: JButton? = null var myProjectName: JLabel? = null var myProjectUrl: JLabel? = null var myUserFullName: JLabel? = null var myUserUsername: JLabel? = null var myLocalRepository: JLabel? = null var myProjectVisibility: JLabel? = null var myLegalStatus: JLabel? = null var myLegalWarning: JLabel? = null var myWords1: JLabel? = null var myWords2: JLabel? = null var myWords3: JLabel? = null init { myProjectName!!.text = providerData.project.name myProjectUrl!!.text = providerData.project.url myUserFullName!!.text = providerData.currentUser.name myUserUsername!!.text = "@${providerData.currentUser.username}" myLocalRepository!!.text = providerData.repository myProjectVisibility!!.text = when (providerData.project.visibility) { ProjectVisibility.PUBLIC -> "Public" ProjectVisibility.PRIVATE -> "Private" } myOpenURLButton!!.addActionListener { BrowserUtil.open(providerData.project.url) } val isLegal = this.applicationServiceProvider.isLegal(providerData) if (!isLegal) { myLegalStatus!!.text = "Illegal" myLegalStatus!!.foreground = Color(0xBA, 0x3F, 0x3C) } else { myLegalStatus!!.text = "Legal" myLegalStatus!!.foreground = Color(0x68, 0x84, 0x57) } myBuyButton!!.isVisible = !isLegal myLegalWarning!!.isVisible = !isLegal myWords1!!.isVisible = !isLegal myWords2!!.isVisible = !isLegal myWords3!!.isVisible = !isLegal myBuyButton!!.addActionListener { BrowserUtil.open(ENTERPRISE_EDITION_URL) } setBackground() } private fun setBackground() { myWholePanel!!.background = JBColor.background() myOpenURLButton!!.isOpaque = true myOpenURLButton!!.background = JBColor.background() myBuyButton!!.isOpaque = true myBuyButton!!.background = JBColor.background() myWrapperPanel!!.background = JBColor.background() myLoggedUserWrapperPanel!!.background = JBColor.background() myUrlWrapperPanel!!.background = JBColor.background() myStatusWrapperPanel!!.background = JBColor.background() myLicenseWarningWrapperPanel!!.background = JBColor.background() } override fun createComponent(): JComponent = myWholePanel!! } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ProviderItemPanel.form ================================================
================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ProviderItemPanel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.panel import com.intellij.openapi.util.IconLoader import com.intellij.ui.JBColor import com.intellij.util.ui.UIUtil import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.ProviderStatus import net.ntworld.mergeRequestIntegrationIde.ui.Component import net.ntworld.mergeRequestIntegrationIde.ui.util.ImageUtil import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JPanel class ProviderItemPanel(private val providerData: ProviderData) : Component { var myWholePanel: JPanel? = null var myWrapperPanel: JPanel? = null var myAvatarWrapperPanel: JPanel? = null var myUserInfoWrapperPanel: JPanel? = null var myInfoWrapper: JPanel? = null var myStatusWrapperPanel: JPanel? = null var myProjectName: JLabel? = null var myName: JLabel? = null var myUsername: JLabel? = null var myAvatar: JLabel? = null var myStatus: JLabel? = null init { myAvatar!!.icon = ImageUtil.loadIconFromUrl(providerData.project.avatarUrl, providerData.info.icon3xPath, 48) myProjectName!!.text = providerData.name myName!!.text = providerData.currentUser.name myUsername!!.text = "@${providerData.currentUser.username}" if (providerData.status == ProviderStatus.ERROR) { myStatus!!.icon = IconLoader.getIcon("/icons/exclamation.2x.svg", ProviderItemPanel::class.java) myStatus!!.toolTipText = "Cannot fetch data from this provider" myStatus!!.isVisible = true } else { myStatus!!.isVisible = false } } fun changeStyle(selected: Boolean, hasFocus: Boolean) { val backgroundColor = UIUtil.getListBackground(selected, hasFocus) myWholePanel!!.background = backgroundColor myWrapperPanel!!.background = backgroundColor myStatusWrapperPanel!!.background = backgroundColor myInfoWrapper!!.background = backgroundColor myAvatarWrapperPanel!!.background = backgroundColor myUserInfoWrapperPanel!!.background = backgroundColor val foregroundColor = UIUtil.getListForeground(selected, hasFocus) myProjectName!!.foreground = foregroundColor myName!!.foreground = foregroundColor myUsername!!.foreground = if (selected && hasFocus) foregroundColor else JBColor.gray } override fun createComponent(): JComponent = myWholePanel!! } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/UserInfoItemPanel.form ================================================
================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/UserInfoItemPanel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.panel import net.ntworld.mergeRequest.UserInfo import net.ntworld.mergeRequestIntegrationIde.ui.Component import java.awt.Color import javax.swing.Icon import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JPanel class UserInfoItemPanel(private val userInfo: UserInfo, private val icon: Icon? = null): Component { var myWholePanel: JPanel? = null var myWrapper: JPanel? = null var myName: JLabel? = null var myUsername: JLabel? = null var myIcon: JLabel? = null init { if (null === icon) { myIcon!!.isVisible = false } else { myIcon!!.icon = icon myIcon!!.isVisible = true } myName!!.text = userInfo.name myUsername!!.text = "@${userInfo.username}" } override fun createComponent(): JComponent { return myWholePanel!! } fun setBackground(color: Color) { myWholePanel!!.background = color myWrapper!!.background = color } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollection.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.provider import com.intellij.openapi.Disposable import com.intellij.openapi.ui.SimpleToolWindowPanel import com.intellij.openapi.util.Disposer import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifierAdapter import net.ntworld.mergeRequestIntegrationIde.ui.Component import javax.swing.JComponent class ProviderCollection( private val projectServiceProvider: ProjectServiceProvider ): Component, Disposable { private val myListUI: ProviderCollectionListUI by lazy { val list = ProviderCollectionList() val registered = projectServiceProvider.providerStorage.registeredProviders if (registered.isNotEmpty()) { registered.forEach { list.addProvider(it) } } list } private val myToolbarUI: ProviderCollectionToolbarUI by lazy { ProviderCollectionToolbar() } private val myProjectNotifier = object : ProjectNotifierAdapter() { override fun providerRegistered(providerData: ProviderData) { myListUI.addProvider(providerData) } override fun startCodeReview(reviewContext: ReviewContext) { myComponent.isVisible = false } override fun stopCodeReview(reviewContext: ReviewContext) { myComponent.isVisible = true } } private val myConnection = projectServiceProvider.messageBus.connect() private val myToolbarEventListener = object: ProviderCollectionToolbarEventListener { override fun refreshClicked() { myListUI.clear() projectServiceProvider.initialize() } override fun helpClicked() { } } private val myComponent = SimpleToolWindowPanel(true, true) init { myConnection.subscribe(ProjectNotifier.TOPIC, myProjectNotifier) if (!projectServiceProvider.isInitialized()) { projectServiceProvider.initialize() } myComponent.setContent(myListUI.createComponent()) myComponent.toolbar = myToolbarUI.createComponent() myToolbarUI.eventDispatcher.addListener(myToolbarEventListener) Disposer.register(projectServiceProvider.project, this) } override fun createComponent(): JComponent = myComponent fun getListEventDispatcher() = myListUI.eventDispatcher fun addListEventListener(listener: ProviderCollectionListEventListener) { myListUI.eventDispatcher.addListener(listener) } fun addToolbarEventListener(listener: ProviderCollectionToolbarEventListener) { myToolbarUI.eventDispatcher.addListener(listener) } override fun dispose() { myConnection.disconnect() myListUI.clear() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollectionList.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.provider import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.components.JBList import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.ui.panel.ProviderItemPanel import java.awt.Component import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.* import javax.swing.event.ListSelectionEvent import javax.swing.event.ListSelectionListener class ProviderCollectionList : ProviderCollectionListUI { override val eventDispatcher = EventDispatcher.create(ProviderCollectionListEventListener::class.java) private val myProviderDataMap = mutableMapOf() private val myList = JBList() private val myProviderItemPanels = mutableMapOf() private val myCellRenderer = object: ListCellRenderer { override fun getListCellRendererComponent( list: JList?, value: ProviderData?, index: Int, isSelected: Boolean, cellHasFocus: Boolean ): Component { if (null === value) { return JPanel() } if (null === myProviderItemPanels[index]) { myProviderItemPanels[index] = ProviderItemPanel(value) } val panel = myProviderItemPanels[index]!! panel.changeStyle(isSelected, cellHasFocus) return panel.createComponent() } } private val myListSelectionListener = object: ListSelectionListener { override fun valueChanged(e: ListSelectionEvent?) { val value = myList.selectedValue if (null === value) { eventDispatcher.multicaster.providerUnselected() } else { eventDispatcher.multicaster.providerSelected(value) } } } private val myListMouseListener = object: MouseAdapter() { override fun mouseClicked(e: MouseEvent?) { if (null === e) { return } if (e.clickCount == 1) { val value = myList.selectedValue if (null === value) { eventDispatcher.multicaster.providerUnselected() } else { eventDispatcher.multicaster.providerSelected(value) } } if (e.clickCount == 2) { val value = myList.selectedValue if (null === value) { eventDispatcher.multicaster.providerUnselected() } else { eventDispatcher.multicaster.providerOpened(value) } } } } init { myProviderItemPanels.clear() myProviderDataMap.clear() myList.clearSelection() myList.selectionMode = ListSelectionModel.SINGLE_SELECTION myList.cellRenderer = myCellRenderer myList.addListSelectionListener(myListSelectionListener) myList.addMouseListener(myListMouseListener) updateList() } private fun updateList() { myProviderItemPanels.clear() val data = myProviderDataMap.values.sortedBy { it.name } myList.setListData(data.toTypedArray()) } override fun clear() { myProviderItemPanels.clear() myProviderDataMap.clear() myList.clearSelection() updateList() } override fun addProvider(providerData: ProviderData) { myProviderDataMap[providerData.id] = providerData updateList() } override fun createComponent(): JComponent = ScrollPaneFactory.createScrollPane(myList) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollectionListEventListener.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.provider import net.ntworld.mergeRequest.ProviderData import java.util.* interface ProviderCollectionListEventListener : EventListener { fun providerUnselected() fun providerSelected(providerData: ProviderData) fun providerOpened(providerData: ProviderData) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollectionListUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.provider import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.ui.Component interface ProviderCollectionListUI: Component { val eventDispatcher: EventDispatcher fun clear() fun addProvider(providerData: ProviderData) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollectionToolbar.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.provider import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.util.EventDispatcher import net.miginfocom.swing.MigLayout import net.ntworld.mergeRequestIntegrationIde.ui.Component import javax.swing.JComponent import javax.swing.JPanel class ProviderCollectionToolbar : ProviderCollectionToolbarUI, Component { override val eventDispatcher = EventDispatcher.create(ProviderCollectionToolbarEventListener::class.java) private val myPanel by lazy { val panel = JPanel(MigLayout("ins 0, fill", "[left, fill]push[right]", "center")) val mainActionGroup = DefaultActionGroup() mainActionGroup.add(myRefreshAction) // mainActionGroup.addSeparator() // mainActionGroup.add(myAddAction) val toolbar = ActionManager.getInstance().createActionToolbar( "${ProviderCollectionToolbar::class.java.canonicalName}/toolbar-left", mainActionGroup, true ) val rightCornerActionGroup = DefaultActionGroup() // rightCornerActionGroup.add(myHelpAction) val rightCornerToolbar = ActionManager.getInstance().createActionToolbar( "${ProviderCollectionToolbar::class.java.canonicalName}/toolbar-right", rightCornerActionGroup, true ) panel.add(toolbar.component) panel.add(rightCornerToolbar.component) panel } private class MyHelpAction(private val self: ProviderCollectionToolbar) : AnAction("Help", null, AllIcons.Actions.Help) { override fun actionPerformed(e: AnActionEvent) { self.eventDispatcher.multicaster.helpClicked() } } private val myHelpAction = MyHelpAction(this) private class MyRefreshAction(private val self: ProviderCollectionToolbar) : AnAction("Refresh", null, AllIcons.Actions.Refresh) { override fun actionPerformed(e: AnActionEvent) { self.eventDispatcher.multicaster.refreshClicked() } } private val myRefreshAction = MyRefreshAction(this) override fun createComponent(): JComponent = myPanel } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollectionToolbarEventListener.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.provider import java.util.* interface ProviderCollectionToolbarEventListener: EventListener { fun refreshClicked() fun helpClicked() } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollectionToolbarUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.provider import com.intellij.util.EventDispatcher import net.ntworld.mergeRequestIntegrationIde.ui.Component interface ProviderCollectionToolbarUI : Component { val eventDispatcher: EventDispatcher } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderDetails.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.provider import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.* import com.intellij.openapi.wm.ToolWindow import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.tabs.TabInfo import com.intellij.ui.tabs.TabsListener import com.intellij.util.EventDispatcher import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.MergeRequestState import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.ProviderStatus import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequestIntegration.make import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.MergeRequestCollectionEventListener import net.ntworld.mergeRequestIntegrationIde.ui.panel.ProviderInformationPanel import net.ntworld.mergeRequestIntegrationIde.ui.util.Tabs import net.ntworld.mergeRequestIntegrationIde.ui.util.TabsUI import javax.swing.JComponent class ProviderDetails( private val projectServiceProvider: ProjectServiceProvider, private val toolWindow: ToolWindow, private val dispatcher: EventDispatcher, private val providerData: ProviderData ) : ProviderDetailsUI { override val listEventDispatcher = EventDispatcher.create(MergeRequestCollectionEventListener::class.java) private val myListEventListener = object: MergeRequestCollectionEventListener { override fun mergeRequestUnselected() { listEventDispatcher.multicaster.mergeRequestUnselected() } override fun mergeRequestSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) { listEventDispatcher.multicaster.mergeRequestSelected(providerData, mergeRequestInfo) } } private val myCommonCenterActionGroupFactory: () -> ActionGroup = { DefaultActionGroup() } private class MyOpenAction(private val self: ProviderDetails): AnAction("Open", "Open provider in new tab", AllIcons.Actions.Menu_open) { override fun actionPerformed(e: AnActionEvent) { self.dispatcher.multicaster.providerOpened(self.providerData) } override fun update(e: AnActionEvent) { e.presentation.isVisible = self.providerData.status == ProviderStatus.ACTIVE } override fun displayTextInToolbar(): Boolean = true } private val myCommonSideComponentFactory: () -> JComponent = { val actionGroup = DefaultActionGroup() actionGroup.add(MyOpenAction(this)) val toolbar = ActionManager.getInstance().createActionToolbar( "${ProviderDetails::class.java.canonicalName}/toolbar-right", actionGroup, true ) toolbar.component } private val myTabs: TabsUI by lazy { val tabs = Tabs(projectServiceProvider.project, toolWindow.contentManager) tabs.setCommonCenterActionGroupFactory(myCommonCenterActionGroupFactory) tabs.setCommonSideComponentFactory(myCommonSideComponentFactory) tabs } private val myDetailsTabInfo by lazy { val tabInfo = TabInfo( ScrollPaneFactory.createScrollPane( ProviderInformationPanel(projectServiceProvider.applicationServiceProvider, providerData) .createComponent() ) ) tabInfo.text = "General Information" tabInfo } private val myOpeningMRList by lazy { val list = ProviderDetailsMRList( projectServiceProvider, providerData, GetMergeRequestFilter.make( state = MergeRequestState.OPENED, id = null, search = "", authorId = "", assigneeId = "", approverIds = listOf(), sourceBranch = "" ), MergeRequestOrdering.RECENTLY_UPDATED, displayType = ProviderDetailsMRList.ApprovalStatusDisplayType.NONE ) list.eventDispatcher.addListener(myListEventListener) list } private val myOpeningMRTabInfo by lazy { val tabInfo = TabInfo(myOpeningMRList.createComponent()) tabInfo.text = "Opening MRs" tabInfo } private val myMyMRList by lazy { val list = ProviderDetailsMRList( projectServiceProvider, providerData, GetMergeRequestFilter.make( state = MergeRequestState.OPENED, id = null, search = "", authorId = providerData.currentUser.id, assigneeId = "", approverIds = listOf(), sourceBranch = "" ), MergeRequestOrdering.RECENTLY_UPDATED, displayType = ProviderDetailsMRList.ApprovalStatusDisplayType.STATUSES ) list.eventDispatcher.addListener(myListEventListener) list } private val myMyMRTabInfo by lazy { val tabInfo = TabInfo(myMyMRList.createComponent()) tabInfo.text = "My MRs" tabInfo } private val myMyAssignedMRList by lazy { val list = ProviderDetailsMRList( projectServiceProvider, providerData, GetMergeRequestFilter.make( state = MergeRequestState.OPENED, id = null, search = "", authorId = "", assigneeId = providerData.currentUser.id, approverIds = listOf(), sourceBranch = "" ), MergeRequestOrdering.RECENTLY_UPDATED, displayType = ProviderDetailsMRList.ApprovalStatusDisplayType.STATUSES ) list.eventDispatcher.addListener(myListEventListener) list } private val myMyAssignedMRTabInfo by lazy { val tabInfo = TabInfo(myMyAssignedMRList.createComponent()) tabInfo.text = "MRs assigned to me" tabInfo } private val myWaitingForMyApprovalMRList by lazy { val list = ProviderDetailsMRList( projectServiceProvider, providerData, GetMergeRequestFilter.make( state = MergeRequestState.OPENED, id = null, search = "", authorId = "", assigneeId = "", approverIds = listOf(providerData.currentUser.id), sourceBranch = "" ), MergeRequestOrdering.RECENTLY_UPDATED, displayType = ProviderDetailsMRList.ApprovalStatusDisplayType.STATUSES_AND_MINE_APPROVAL ) list.eventDispatcher.addListener(myListEventListener) list } private val myWaitingForMyApprovalMRTabInfo by lazy { val tabInfo = TabInfo(myWaitingForMyApprovalMRList.createComponent()) tabInfo.text = "MRs waiting for my approval" tabInfo } init { myTabs.addTab(myDetailsTabInfo) myTabs.addTab(myOpeningMRTabInfo) myTabs.addTab(myMyMRTabInfo) if (providerData.hasAssigneeFeature) { myTabs.addTab(myMyAssignedMRTabInfo) } if (providerData.hasApprovalFeature) { myTabs.addTab(myWaitingForMyApprovalMRTabInfo) } myTabs.addListener(object : TabsListener { override fun selectionChanged(oldSelection: TabInfo?, newSelection: TabInfo?) { if (newSelection === myOpeningMRTabInfo) { myOpeningMRList.fetchIfNotLoaded() return } if (newSelection === myMyMRTabInfo) { myMyMRList.fetchData() return } if (providerData.hasAssigneeFeature && newSelection === myMyAssignedMRTabInfo) { myMyAssignedMRList.fetchData() return } if (providerData.hasApprovalFeature && newSelection === myWaitingForMyApprovalMRTabInfo) { myWaitingForMyApprovalMRList.fetchData() return } } }) } override fun hide() { myTabs.component.isVisible = false } override fun show() { myTabs.component.isVisible = true } override fun createComponent(): JComponent = myTabs.component } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderDetailsMRList.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.provider import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.components.JBList import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequest.ProviderStatus import net.ntworld.mergeRequest.api.MergeRequestOrdering import net.ntworld.mergeRequest.query.GetMergeRequestFilter import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.AbstractMergeRequestCollection import net.ntworld.mergeRequestIntegrationIde.ui.panel.MergeRequestItemPanel import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.JComponent import javax.swing.ListCellRenderer import javax.swing.ListSelectionModel class ProviderDetailsMRList( private val projectServiceProvider: ProjectServiceProvider, private val providerData: ProviderData, private val filterBy: GetMergeRequestFilter, private val orderBy: MergeRequestOrdering, private val displayType: ApprovalStatusDisplayType ): AbstractMergeRequestCollection(projectServiceProvider, providerData) { private var isLoaded = false private val myList = JBList() private val myItemPanels = mutableMapOf() private val myCellRenderer = ListCellRenderer { _, value, index, isSelected, cellHasFocus -> if (null === myItemPanels[index]) { myItemPanels[index] = MergeRequestItemPanel( projectServiceProvider, providerData, value, displayType ) } val panel = myItemPanels[index]!! panel.changeStyle(isSelected, cellHasFocus) panel.createComponent() } private val myListMouseListener = object: MouseAdapter() { override fun mouseClicked(e: MouseEvent?) { if (null === e) { return } if (e.clickCount == 2) { val value = myList.selectedValue if (null === value) { eventDispatcher.multicaster.mergeRequestUnselected() } else { eventDispatcher.multicaster.mergeRequestSelected(providerData, value) } } } } init { myList.selectionMode = ListSelectionModel.SINGLE_SELECTION myList.cellRenderer = myCellRenderer myList.addMouseListener(myListMouseListener) } override fun makeContent(): JComponent { setFilter(filterBy) setOrder(orderBy) return ScrollPaneFactory.createScrollPane(myList) } fun fetchIfNotLoaded() { if (!isLoaded && providerData.status == ProviderStatus.ACTIVE) { fetchData() isLoaded = true } } override fun fetchDataStarted() { myList.isVisible = false } override fun fetchDataStopped() { myList.isVisible = true } override fun dataReceived(collection: List) { myItemPanels.clear() myList.setListData(collection.toTypedArray()) myList.isVisible = true } enum class ApprovalStatusDisplayType { NONE, STATUSES_AND_MINE_APPROVAL, STATUSES } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderDetailsUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.provider import com.intellij.util.EventDispatcher import net.ntworld.mergeRequestIntegrationIde.ui.Component import net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.MergeRequestCollectionEventListener interface ProviderDetailsUI : Component { val listEventDispatcher: EventDispatcher fun hide() fun show() } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/service/CheckoutService.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.service import com.intellij.openapi.components.ServiceManager import git4idea.branch.GitBrancher import com.intellij.openapi.project.Project as IdeaProject import git4idea.repo.GitRepository import net.ntworld.mergeRequest.MergeRequest import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil import kotlin.Exception object CheckoutService { private var myCurrentBranch: String? = null fun stop(projectServiceProvider: ProjectServiceProvider, providerData: ProviderData) { val branch = myCurrentBranch val repository = RepositoryUtil.findRepository(projectServiceProvider, providerData) if (null !== branch && null !== repository) { doCheckout(projectServiceProvider, false, repository, providerData, branch, object : Listener { override fun onError(exception: Exception) { myCurrentBranch = null } override fun onSuccess() { myCurrentBranch = null } }) } } fun start(projectServiceProvider: ProjectServiceProvider, providerData: ProviderData, mergeRequest: MergeRequest, listener: Listener) { val repository = RepositoryUtil.findRepository(projectServiceProvider, providerData) if (null === repository) { return listener.onError(Exception("Repository not found")) } doCheckout(projectServiceProvider, true, repository, providerData, mergeRequest.sourceBranch, listener) } private fun doCheckout( projectServiceProvider: ProjectServiceProvider, useRemote: Boolean, repository: GitRepository, providerData: ProviderData, branch: String, listener: Listener ) { val currentBranch = repository.currentBranch if (null !== currentBranch) { if (currentBranch.name == branch) { return listener.onSuccess() } myCurrentBranch = currentBranch.name } val branchExecutor = ServiceManager.getService(projectServiceProvider.project, GitBrancher::class.java) try { branchExecutor.checkout(branch, false, listOf(repository)) { listener.onSuccess() } } catch (exception: Exception) { if (!useRemote) { return listener.onError(exception) } val remoteName = findRemoteName(repository, providerData) if (remoteName.isEmpty()) { return listener.onError(Exception("Cannot find remote of repository")) } try { branchExecutor.checkoutNewBranchStartingFrom( branch, "$remoteName/${branch}", false, listOf(repository) ) { listener.onSuccess() } } catch (exception: Exception) { listener.onError(exception) } } } private fun findRemoteName(repository: GitRepository, providerData: ProviderData): String { for (remote in repository.remotes) { for (url in remote.urls) { if (url == providerData.project.repositoryHttpUrl || url == providerData.project.repositorySshUrl) { return remote.name } } } return "" } interface Listener { fun onError(exception: Exception) fun onSuccess() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/service/CodeReviewService.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.service import com.intellij.notification.NotificationType import com.intellij.openapi.wm.ToolWindowManager import net.ntworld.mergeRequest.Commit import net.ntworld.mergeRequest.MergeRequest import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier import kotlin.Exception import com.intellij.openapi.project.Project as IdeaProject object CodeReviewService { var checkedOut = false fun start( projectServiceProvider: ProjectServiceProvider, providerData: ProviderData, mergeRequest: MergeRequest, commits: List ) { checkedOut = false projectServiceProvider.reviewContextManager.updateReviewingCommits(providerData.id, mergeRequest.id, commits) projectServiceProvider.startCodeReview(providerData, mergeRequest) checkout(projectServiceProvider, providerData, mergeRequest, commits) } fun stop( projectServiceProvider: ProjectServiceProvider, providerData: ProviderData, mergeRequest: MergeRequest ) { projectServiceProvider.stopCodeReview() if (checkedOut) { CheckoutService.stop(projectServiceProvider, providerData) DisplayChangesService.stop(projectServiceProvider.project, providerData, mergeRequest) } } private fun checkout( projectServiceProvider: ProjectServiceProvider, providerData: ProviderData, mergeRequest: MergeRequest, commits: List ) { if (!projectServiceProvider.applicationSettings.checkoutTargetBranch) { return checkoutSuccess(projectServiceProvider, providerData, mergeRequest, commits) } CheckoutService.start(projectServiceProvider, providerData, mergeRequest, object : CheckoutService.Listener { override fun onError(exception: Exception) { projectServiceProvider.notify( "Cannot checkout branch ${mergeRequest.sourceBranch}\n\nPlease do git checkout manually before click Code Review", NotificationType.ERROR ) this@CodeReviewService.stop(projectServiceProvider, providerData, mergeRequest) } override fun onSuccess() { checkoutSuccess(projectServiceProvider, providerData, mergeRequest, commits) } }) } private fun checkoutSuccess( projectServiceProvider: ProjectServiceProvider, providerData: ProviderData, mergeRequest: MergeRequest, commits: List ) { checkedOut = true EditorStateService.start(projectServiceProvider.project) DisplayChangesService.start( projectServiceProvider.applicationServiceProvider, projectServiceProvider.project, providerData, mergeRequest, commits ) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/service/DisplayChangesService.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.service import com.intellij.ide.ui.UISettings import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.vcs.changes.* import com.intellij.openapi.project.Project as IdeaProject import com.intellij.ui.tabs.JBTabsPosition import com.intellij.vcs.log.Hash import com.intellij.vcs.log.impl.VcsLogManager import com.intellij.vcs.log.util.VcsLogUtil import git4idea.repo.GitRepository import net.ntworld.mergeRequest.Commit import net.ntworld.mergeRequest.MergeRequest import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil import javax.swing.SwingConstants import kotlin.concurrent.thread object DisplayChangesService { private var myTabPlacement: JBTabsPosition? = null private val myChangePreviewDiffVirtualFileMap = mutableMapOf() fun stop(ideaProject: IdeaProject, providerData: ProviderData, mergeRequest: MergeRequest) { closeAllDiffsAndRestoreTabPlacement(ideaProject) myTabPlacement = null myChangePreviewDiffVirtualFileMap.clear() } fun start( applicationServiceProvider: ApplicationServiceProvider, ideaProject: IdeaProject, providerData: ProviderData, mergeRequest: MergeRequest, commits: List ) { if (myChangePreviewDiffVirtualFileMap.isNotEmpty()) { myChangePreviewDiffVirtualFileMap.clear() } myTabPlacement = null val diff = mergeRequest.diffReference val repository = RepositoryUtil.findRepository( applicationServiceProvider.findProjectServiceProvider(ideaProject), providerData ) if (null === repository || null === diff) { return } applicationServiceProvider.intellijIdeApi.getVcsLogManager(ideaProject) { if (commits.size <= 1) { thread { val hash = if (commits.isEmpty()) diff.headHash else commits.first().id displayChangesForOneCommit( applicationServiceProvider, ideaProject, providerData, mergeRequest, repository, it, hash ) } } else { thread { displayChangesForCommits( applicationServiceProvider, ideaProject, providerData, mergeRequest, repository, it, commits ) } } } } private fun displayChangesForOneCommit( applicationServiceProvider: ApplicationServiceProvider, ideaProject: IdeaProject, providerData: ProviderData, mergeRequest: MergeRequest, repository: GitRepository, log: VcsLogManager, hash: String ) { // TODO: Reduce repetition val details = VcsLogUtil.getDetails(log.dataManager, repository.root, MyHash(hash)) displayChanges( applicationServiceProvider, ideaProject, providerData, mergeRequest, details.changes.toList() ) } private fun displayChangesForCommits( applicationServiceProvider: ApplicationServiceProvider, ideaProject: IdeaProject, providerData: ProviderData, mergeRequest: MergeRequest, repository: GitRepository, log: VcsLogManager, commits: List ) { // TODO: Reduce repetition val details = VcsLogUtil.getDetails( log.dataManager.getLogProvider(repository.root), repository.root, commits.map { it.id } ) if (details.isEmpty()) { return } if (details.size == 1) { return displayChanges( applicationServiceProvider, ideaProject, providerData, mergeRequest, details.first().changes.toList() ) } val changes = VcsLogUtil.collectChanges(details) { it.changes } displayChanges(applicationServiceProvider, ideaProject, providerData, mergeRequest, changes) } private fun displayChanges( applicationServiceProvider: ApplicationServiceProvider, ideaProject: IdeaProject, providerData: ProviderData, mergeRequest: MergeRequest, changes: List ) { // TODO: call updateReviewingChanges too many time, let's save some resource applicationServiceProvider.findProjectServiceProvider(ideaProject) .reviewContextManager.updateReviewingChanges(providerData.id, mergeRequest.id, changes) ApplicationManager.getApplication().invokeLater { val projectServiceProvider = applicationServiceProvider.findProjectServiceProvider(ideaProject) val reviewContext = projectServiceProvider.reviewContextManager.findDoingCodeReviewContext() val max = projectServiceProvider.applicationSettings.maxDiffChangesOpenedAutomatically if (null !== reviewContext && max != 0 && changes.size < max) { val limit = UISettings().editorTabLimit changes.forEachIndexed { index, item -> if (index < limit) { reviewContext.openChange(item, focus = true, displayMergeRequestId = false) } } } } } private fun closeAllDiffsAndRestoreTabPlacement(ideaProject: IdeaProject) { val fileEditorManagerEx = FileEditorManagerEx.getInstanceEx(ideaProject) val openFiles = fileEditorManagerEx.openFiles for (openFile in openFiles) { fileEditorManagerEx.closeFile(openFile) } val tabPlacement = myTabPlacement if (null !== tabPlacement) { val editorWindows = fileEditorManagerEx.windows if (editorWindows.size == 1) { val editorWindow = editorWindows.first() editorWindow.tabbedPane.setTabPlacement( when (tabPlacement) { JBTabsPosition.top -> SwingConstants.TOP JBTabsPosition.left -> SwingConstants.LEFT JBTabsPosition.bottom -> SwingConstants.BOTTOM JBTabsPosition.right -> SwingConstants.RIGHT } ) } } } // TODO: Will add an option in configuration then call later private fun rearrangeTabPlacement(fileEditorManagerEx: FileEditorManagerEx) { val editorWindows = fileEditorManagerEx.windows if (editorWindows.size == 1) { val editorWindow = editorWindows.first() myTabPlacement = editorWindow.tabbedPane.tabs.presentation.tabsPosition editorWindow.tabbedPane.setTabPlacement(SwingConstants.LEFT) } } class MyHash(private val hash: String) : Hash { override fun toShortString(): String { return hash.substring(0, 6) } override fun asString(): String { return hash } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/service/EditorStateService.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.service import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.project.Project object EditorStateService { fun start(ideaProject: Project) { val fileManager = FileEditorManagerEx.getInstance(ideaProject) val openFiles = fileManager.openFiles for (openFile in openFiles) { fileManager.closeFile(openFile) } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/service/FetchService.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.service import git4idea.GitVcs import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegration.util.DateTimeUtil import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.task.RepositoryFetchAllRemotesTask import java.util.* import com.intellij.openapi.project.Project as IdeaProject object FetchService { private var myLastFetchedDateTime: Date? = null fun start(applicationServiceProvider: ApplicationServiceProvider, ideaProject: IdeaProject, providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) { val lastFetch = myLastFetchedDateTime val lastUpdated = DateTimeUtil.toDate(mergeRequestInfo.updatedAt) if (lastFetch !== null && lastFetch > lastUpdated) { return } try { GitVcs.runInBackground(RepositoryFetchAllRemotesTask( applicationServiceProvider.findProjectServiceProvider(ideaProject), providerData )) } catch (exception: Exception) { applicationServiceProvider.findProjectServiceProvider(ideaProject).notify( "Cannot fetch from remotes. The changes in Commits tab may not be displayed correctly. \n Please run 'git fetch' manually if you want to see the change list." ) } myLastFetchedDateTime = Date(System.currentTimeMillis()) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/toolWindowTab/HomeToolWindowTab.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.toolWindowTab import com.intellij.openapi.Disposable import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.ToolWindow import com.intellij.ui.OnePixelSplitter import com.intellij.ui.content.Content import com.intellij.ui.content.ContentManagerEvent import com.intellij.ui.content.ContentManagerListener import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier import net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifierAdapter import net.ntworld.mergeRequestIntegrationIde.ui.Component import net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.MergeRequestCollectionEventListener import net.ntworld.mergeRequestIntegrationIde.ui.provider.* import javax.swing.JComponent import javax.swing.JPanel class HomeToolWindowTab( private val projectServiceProvider: ProjectServiceProvider, private val toolWindow: ToolWindow ) : Component, Disposable { private val mySplitter = OnePixelSplitter(HomeToolWindowTab::class.java.canonicalName, 0.35f) private val myCollectionPanel = ProviderCollection(projectServiceProvider) private val myDetailPanels = mutableMapOf() private val myContents = mutableMapOf() private val myMRToolWindowTabs = mutableMapOf() private val myProjectNotifier = object : ProjectNotifierAdapter() { override fun startCodeReview(reviewContext: ReviewContext) { myDetailPanels.forEach { it.value.hide() } myContents.forEach { if (it.key == reviewContext.providerData.id) { it.value.isCloseable = false } } } override fun stopCodeReview(reviewContext: ReviewContext) { myDetailPanels.forEach { it.value.show() } myContents.forEach { if (it.key == reviewContext.providerData.id) { it.value.isCloseable = true } } } } private val myConnection = projectServiceProvider.messageBus.connect() private val myDetailsListEventListener = object : MergeRequestCollectionEventListener { override fun mergeRequestUnselected() { } override fun mergeRequestSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) { openTab(providerData, mergeRequestInfo) } } private val myCollectionPanelListEventListener = object : ProviderCollectionListEventListener { override fun providerUnselected() { mySplitter.secondComponent = null } override fun providerSelected(providerData: ProviderData) { if (null === myDetailPanels[providerData.id]) { val details = ProviderDetails( projectServiceProvider, toolWindow, myCollectionPanel.getListEventDispatcher(), providerData ) myDetailPanels[providerData.id] = details details.listEventDispatcher.addListener(myDetailsListEventListener) } mySplitter.secondComponent = myDetailPanels[providerData.id]!!.createComponent() } override fun providerOpened(providerData: ProviderData) { openTab(providerData) } } private val myContentManagerListener = object: ContentManagerListener { override fun contentAdded(event: ContentManagerEvent) { } override fun contentRemoveQuery(event: ContentManagerEvent) { } override fun selectionChanged(event: ContentManagerEvent) { } override fun contentRemoved(event: ContentManagerEvent) { var removedId: String? = null for (entry in myContents) { if (entry.value === event.content) { removedId = entry.key break } } if (null !== removedId) { myContents.remove(removedId) } } } private val myCollectionPanelToolbarEventListener = object: ProviderCollectionToolbarEventListener { override fun refreshClicked() { myContents.forEach { toolWindow.contentManager.removeContent(it.value, true) } myMRToolWindowTabs.clear() myContents.clear() } override fun helpClicked() { } } init { mySplitter.firstComponent = myCollectionPanel.createComponent() mySplitter.secondComponent = JPanel() toolWindow.contentManager.addContentManagerListener(myContentManagerListener) myCollectionPanel.addListEventListener(myCollectionPanelListEventListener) myCollectionPanel.addToolbarEventListener(myCollectionPanelToolbarEventListener) myConnection.subscribe(ProjectNotifier.TOPIC, myProjectNotifier) Disposer.register(projectServiceProvider.project, this) } private fun findNameForTab(providerData: ProviderData): String { if (projectServiceProvider.applicationServiceProvider.isLegal(providerData)) { return providerData.name } return if (providerData.name.length < 20) { "${providerData.name} · Not legal for private repository · please buy EE version · only 1\$/month" } else { "Not legal for private repository · please buy EE version · only 1\$/month · ${providerData.name}" } } private fun openTab(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo? = null) { if (null === myContents[providerData.id]) { val toolWindowTab = MergeRequestToolWindowTab( projectServiceProvider, toolWindow.contentManager, providerData ) myMRToolWindowTabs[providerData.id] = toolWindowTab val content = toolWindow.contentManager.factory.createContent( toolWindowTab.createComponent(), findNameForTab(providerData), false ) content.isCloseable = true myContents[providerData.id] = content toolWindow.contentManager.addContent(content) } val tab = myContents[providerData.id] if (null !== tab) { toolWindow.contentManager.setSelectedContent(tab) } if (null !== mergeRequestInfo) { val toolWindowTab = myMRToolWindowTabs[providerData.id] if (null !== toolWindowTab) { toolWindowTab.selectMergeRequest(mergeRequestInfo) } } } override fun createComponent(): JComponent = mySplitter override fun dispose() { myConnection.disconnect() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/toolWindowTab/MergeRequestToolWindowTab.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.toolWindowTab import com.intellij.ui.OnePixelSplitter import com.intellij.ui.content.ContentManager import net.ntworld.mergeRequest.MergeRequestInfo import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.ui.Component import net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.* import javax.swing.JComponent class MergeRequestToolWindowTab( private val projectServiceProvider: ProjectServiceProvider, private val contentManager: ContentManager, private val providerData: ProviderData ) : Component { private val mySplitter by lazy { OnePixelSplitter( "${MergeRequestToolWindowTab::class.java.canonicalName}:${providerData.info.id}:${providerData.name}", 0.35f ) } private val myCollection: MergeRequestCollectionUI = MergeRequestCollection(projectServiceProvider, providerData) private val myDetails: MergeRequestDetailsUI = MergeRequestDetails(projectServiceProvider, contentManager, providerData) private val myCollectionListener = object : MergeRequestCollectionEventListener { override fun mergeRequestUnselected() { myDetails.hide() } override fun mergeRequestSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) { myDetails.setMergeRequestInfo(mergeRequestInfo) } } init { myCollection.eventDispatcher.addListener(myCollectionListener) mySplitter.firstComponent = myCollection.createComponent() mySplitter.secondComponent = myDetails.createComponent() } override fun createComponent(): JComponent = mySplitter fun selectMergeRequest(mergeRequestInfo: MergeRequestInfo) { myDetails.setMergeRequestInfo(mergeRequestInfo) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/toolWindowTab/UpdateInfoTab.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.toolWindowTab import com.intellij.ide.util.TipUIUtil import com.intellij.ui.ScrollPaneFactory import net.ntworld.mergeRequestIntegrationIde.ui.Component import net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.MergeRequestDescriptionTab import javax.swing.JComponent class UpdateInfoTab(private val updates: List) : Component { private val myWebView = TipUIUtil.createBrowser() as TipUIUtil.Browser private val myHtmlTemplate = MergeRequestDescriptionTab::class.java.getResource( "/templates/update.html" ).readText() init { myWebView.text = buildHtml() } private fun buildHtml() : String { return myHtmlTemplate .replace("{{content}}", updates.joinToString("

")) } override fun createComponent(): JComponent = ScrollPaneFactory.createScrollPane(myWebView.component) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/util/CustomSimpleToolWindowPanel.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.util import com.intellij.openapi.actionSystem.ActionToolbar import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.DataProvider import com.intellij.ui.JBColor import com.intellij.ui.components.JBPanelWithEmptyText import com.intellij.ui.paint.LinePainter2D import com.intellij.ui.switcher.QuickActionProvider import com.intellij.util.ui.UIUtil import org.jetbrains.annotations.NonNls import java.awt.BorderLayout import java.awt.Container import java.awt.Graphics import java.awt.Graphics2D import java.awt.event.ContainerAdapter import java.awt.event.ContainerEvent import javax.swing.JComponent import javax.swing.SwingConstants class CustomSimpleToolWindowPanel( private val vertical: Boolean, private val borderless: Boolean ) : JBPanelWithEmptyText(), QuickActionProvider, DataProvider { private var myToolbar: JComponent? = null private var myContent: JComponent? = null private var myBorderless = borderless private var myVertical = vertical private var myProvideQuickActions = false init { layout = BorderLayout(if (vertical) 0 else 1, if (vertical) 1 else 0) setProvideQuickActions(true) addContainerListener(object : ContainerAdapter() { override fun componentAdded(e: ContainerEvent?) { val child = e!!.child if (child is Container) { child.addContainerListener(this) } if (myBorderless) { UIUtil.removeScrollBorder(this@CustomSimpleToolWindowPanel) } } override fun componentRemoved(e: ContainerEvent?) { val child = e!!.child if (child is Container) { child.removeContainerListener(this) } } }) } fun isVertical(): Boolean { return myVertical } fun setVertical(vertical: Boolean) { if (myVertical == vertical) return removeAll() myVertical = vertical setContent(myContent) toolbar = myToolbar } fun isToolbarVisible(): Boolean { return myToolbar != null && myToolbar!!.isVisible } var toolbar: JComponent? get() { return myToolbar } set(c) { if (c == null) { remove(myToolbar) } myToolbar = c if (myToolbar is ActionToolbar) { (myToolbar as ActionToolbar).setOrientation(if (myVertical) SwingConstants.HORIZONTAL else SwingConstants.VERTICAL) } if (c != null) { if (myVertical) { add(c, BorderLayout.SOUTH) } else { add(c, BorderLayout.EAST) } } revalidate() repaint() } override fun getData(@NonNls dataId: String): Any? { return if (QuickActionProvider.KEY.`is`(dataId) && myProvideQuickActions) this else null } fun setProvideQuickActions(provide: Boolean): CustomSimpleToolWindowPanel? { myProvideQuickActions = provide return this } override fun getActions(originalProvider: Boolean): List { val toolbars = UIUtil.uiTraverser(myToolbar).traverse().filter( ActionToolbar::class.java ) return if (toolbars.size() == 0) emptyList() else toolbars.flatten { toolbar: ActionToolbar -> toolbar.actions }.toList() } override fun getComponent(): JComponent? { return this } fun setContent(c: JComponent?) { if (myContent != null) { remove(myContent) } myContent = c add(c, BorderLayout.CENTER) if (myBorderless) { UIUtil.removeScrollBorder(c) } revalidate() repaint() } override fun paintComponent(g: Graphics) { super.paintComponent(g) if (myToolbar != null && myToolbar!!.parent === this && myContent != null && myContent!!.parent === this) { g.color = JBColor.border() if (myVertical) { val y = myContent!!.bounds.maxY.toInt() LinePainter2D.paint(g as Graphics2D, 0.0, y.toDouble(), width.toDouble(), y.toDouble()) } else { val x = myContent!!.bounds.maxX.toInt() LinePainter2D.paint(g as Graphics2D, x.toDouble(), 0.0, x.toDouble(), height.toDouble()) } } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/util/ImageUtil.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.util import com.intellij.openapi.util.IconLoader import com.intellij.util.IconUtil import com.intellij.util.ui.JBUI import java.net.URL import javax.swing.Icon object ImageUtil { fun loadIconFromUrl(url: String): Icon? { if (url.isEmpty()) { return null } return IconLoader.findIcon(URL(url), true) } fun loadIconFromUrl(url: String, resourceFallback: String): Icon { val icon = loadIconFromUrl(url) if (null === icon) { return IconLoader.getIcon(resourceFallback, ImageUtil.javaClass) } return icon } fun loadIconFromUrl(url: String, size: Int): Icon? { val icon = loadIconFromUrl(url) if (null !== icon) { return resize(icon, size) } return null } fun loadIconFromUrl(url: String, resourceFallback: String, size: Int): Icon { val icon = loadIconFromUrl(url, resourceFallback) return if (icon.iconWidth >= size) { resize(icon, size) } else { resize(IconLoader.getIcon(resourceFallback, ImageUtil.javaClass), size) } } private fun resize(icon: Icon, size: Int): Icon { val scale = JBUI.scale(size).toFloat() / icon.iconWidth.toFloat() return IconUtil.scale(icon, null, scale) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/util/Tabs.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.util import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.ActionGroup import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.wm.IdeFocusManager import com.intellij.ui.awt.RelativePoint import com.intellij.ui.tabs.* import com.intellij.ui.tabs.impl.* import com.intellij.ui.tabs.impl.singleRow.ScrollableSingleRowLayout import com.intellij.ui.tabs.impl.singleRow.SingleRowLayout import com.intellij.util.ui.JBUI import java.awt.* import javax.swing.JComponent import com.intellij.openapi.project.Project as IdeaProject class Tabs( private val ideaProject: IdeaProject, private val disposable: Disposable ) : TabsUI { private val myTabs: JBTabs = MyTabs(ideaProject, disposable) private var myCommonActionGroupInCenterFactory: (() -> ActionGroup)? = null private var myCommonSideComponentFactory: (() -> JComponent)? = null override fun getTabs(): JBTabs = myTabs override fun setCommonCenterActionGroupFactory(factory: () -> ActionGroup) { myCommonActionGroupInCenterFactory = factory } override fun setCommonSideComponentFactory(factory: () -> JComponent) { myCommonSideComponentFactory = factory } override fun addTab(tabInfo: TabInfo) { if (null !== myCommonActionGroupInCenterFactory) { tabInfo.setActions(myCommonActionGroupInCenterFactory!!.invoke(), null) } if (null !== myCommonSideComponentFactory) { tabInfo.sideComponent = myCommonSideComponentFactory!!.invoke() } myTabs.addTab(tabInfo) } override fun addListener(listener: TabsListener) { myTabs.addListener(listener) } private class MyTabs( private val ideaProject: IdeaProject, private val disposable: Disposable ) : JBEditorTabs( ideaProject, ActionManager.getInstance(), IdeFocusManager.getInstance(ideaProject), disposable ) { override fun createTabPainterAdapter(): TabPainterAdapter? { return DefaultTabPainterAdapter(JBTabPainter.DEBUGGER) } override fun createSingleRowLayout(): SingleRowLayout? { return ScrollableSingleRowLayout(this) } override fun createTabBorder(): JBTabsBorder? { return MyTabsBorder(this) } override fun useSmallLabels(): Boolean { return true } override fun getToolbarInset(): Int { return 0 } fun shouldAddToGlobal(point: Point?): Boolean { val label = selectedLabel if (label == null || point == null) { return true } val bounds = label.bounds return point.y <= bounds.y + bounds.height } override fun layout(c: JComponent?, bounds: Rectangle): Rectangle? { if (c is Toolbar) { bounds.height -= separatorWidth return super.layout(c, bounds) } return super.layout(c, bounds) } override fun processDropOver(over: TabInfo?, relativePoint: RelativePoint) { val point = relativePoint.getPoint(component) myShowDropLocation = shouldAddToGlobal(point) super.processDropOver(over, relativePoint) for ((key, label) in myInfo2Label) { if (label.bounds.contains(point) && myDropInfo != key) { select(key, false) break } } } override fun createTabLabel(info: TabInfo): TabLabel { return MyTabLabel(this, info) } private class MyTabLabel(tabs: JBTabsImpl, info: TabInfo) : TabLabel(tabs, info) { override fun getPreferredSize(): Dimension { val size = super.getPreferredSize() return Dimension(size.width, getPreferredHeight()) } private fun getPreferredHeight(): Int { return JBUI.scale(28) } } private class MyTabsBorder(private val jbTabs: JBTabsImpl) : JBTabsBorder(jbTabs) { override val effectiveBorder: Insets get() = Insets(jbTabs.borderThickness, 0, 0, 0) override fun paintBorder( c: Component, g: Graphics, x: Int, y: Int, width: Int, height: Int ) { if (jbTabs.isEmptyVisible) return jbTabs.tabPainter.paintBorderLine( g as Graphics2D, jbTabs.borderThickness, Point(x, y + jbTabs.myHeaderFitSize.height), Point(x + width, y + jbTabs.myHeaderFitSize.height) ) } } } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/util/TabsUI.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.util import com.intellij.openapi.actionSystem.ActionGroup import com.intellij.ui.tabs.JBTabs import com.intellij.ui.tabs.TabInfo import com.intellij.ui.tabs.TabsListener import javax.swing.JComponent interface TabsUI { val component get() = getTabs().component fun getTabs(): JBTabs fun setCommonCenterActionGroupFactory(factory: () -> ActionGroup) fun setCommonSideComponentFactory(factory: () -> JComponent) fun addTab(tabInfo: TabInfo) fun addListener(listener: TabsListener) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/util/ToolbarUtil.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.util import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.ui.treeStructure.actions.CollapseAllAction import com.intellij.ui.treeStructure.actions.ExpandAllAction import java.awt.Component import javax.swing.JTree object ToolbarUtil { fun createExpandAndCollapseToolbar(name: String, tree: JTree): Component { val actionGroup = DefaultActionGroup() actionGroup.add(ExpandAllAction(tree)) actionGroup.add(CollapseAllAction(tree)) val toolbar = ActionManager.getInstance().createActionToolbar( name, actionGroup, true ) return toolbar.component } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/util/fn.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.ui.util import net.ntworld.mergeRequest.Approval import net.ntworld.mergeRequestIntegrationIde.component.Icons import javax.swing.Icon fun Approval.findVisibilityIconAndTextForApproval() : Triple { val required = this.approvalsRequired val left = this.approvalsLeft val visibility = required > 0 var icon: Icon? = null var text: String? = null if (required == 0) { return Triple(visibility, icon, text) } if (left == 0) { icon = Icons.Approved text = "approved $required/$required" return Triple(visibility, icon, text) } if (left == required) { icon = Icons.NoApproval } else { icon = Icons.RequiredApproval } text = "approval ${required - left}/$required" return Triple(visibility, icon, text) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/util/CommentUtil.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.util import net.ntworld.mergeRequest.Comment object CommentUtil { fun groupCommentsByThreadId(comments: List): Map> { val groups = mutableMapOf>() for (comment in comments) { if (!groups.containsKey(comment.parentId)) { groups[comment.parentId] = mutableListOf() } groups[comment.parentId]!!.add(comment) } return groups } fun groupCommentsByNewPath(comments: List): Map> { val groups = mutableMapOf>() for (comment in comments) { val position = comment.position if (null === position) { continue } val path = position.newPath if (null === path) { continue } if (!groups.containsKey(path)) { groups[path] = mutableListOf() } groups[path]!!.add(comment) } return groups } fun groupCommentsByPositionNewLine(comments: List): Map> { val groups = mutableMapOf>() for (comment in comments) { val position = comment.position if (null === position) { continue } val line = position.newLine if (null === line) { continue } if (!groups.containsKey(line)) { groups[line] = mutableListOf() } groups[line]!!.add(comment) } return groups.toSortedMap() } fun groupCommentsByPositionPath(comments: List): Map> { val groups = mutableMapOf>() for (comment in comments) { val position = comment.position if (null === position) { continue } val path = if (null !== position.newPath) position.newPath else position.oldPath if (null === path) { continue } if (!groups.containsKey(path)) { groups[path] = mutableListOf() } groups[path]!!.add(comment) } return groups } fun groupCommentsByPositionLine(comments: List): Map> { val groups = mutableMapOf>() for (comment in comments) { val position = comment.position if (null === position) { continue } val line = if (null !== position.newLine) position.newLine else position.oldLine if (null === line) { continue } if (!groups.containsKey(line)) { groups[line] = mutableListOf() } groups[line]!!.add(comment) } return groups.toSortedMap() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/util/FileTypeUtil.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.util import com.intellij.lang.Language import com.intellij.openapi.fileTypes.LanguageFileType import javax.swing.Icon object FileTypeUtil { private class MarkdownLanguage : Language("Markdown", "text/x-markdown") private class MarkdownFileType(language: Language): LanguageFileType(language) { override fun getIcon(): Icon? = null override fun getName(): String { return "Markdown" } override fun getDefaultExtension(): String { return "md" } override fun getDescription(): String { return "Markdown" } } val markdownFileType : LanguageFileType by lazy { MarkdownFileType(getLanguage()) } private fun getLanguage() : Language { val definedLanguages = Language.findInstancesByMimeType("text/x-markdown") if (definedLanguages.isEmpty()) { return MarkdownLanguage() } return definedLanguages.first() } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/util/HtmlHelper.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.util import net.ntworld.mergeRequest.ProviderData import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer object HtmlHelper { private val myCommonMarkParser = Parser.builder().build() private val myHtmlRenderer = HtmlRenderer.builder().build() fun convertFromMarkdown(md: String): String { return myHtmlRenderer.render(myCommonMarkParser.parse(md)) } fun resolveRelativePath(providerData: ProviderData, html: String): String { return html.replace("() fun findRepository(projectServiceProvider: ProjectServiceProvider, providerData: ProviderData): GitRepository? { if (null === repositoryCached[providerData.id]) { val vcsRepositoryManager = VcsRepositoryManager.getInstance(projectServiceProvider.project) for (repository in vcsRepositoryManager.repositories) { if (repository.root.path == providerData.repository) { repositoryCached[providerData.id] = repository as GitRepository return repository } } } return repositoryCached[providerData.id] } fun transformToCrossPlatformsPath(input: String): String { return if (input.contains('\\')) input.replace('\\', '/') else input } fun findAbsoluteCrossPlatformsPath(repository: GitRepository?, relativePath: String): String { if (null === repository) { return transformToCrossPlatformsPath(relativePath) } return transformToCrossPlatformsPath("${repository.root.path}${File.separatorChar}$relativePath") } fun findRelativePath(repository: GitRepository?, absolutePath: String): String { if (null === repository) { return absolutePath.replace(File.separatorChar, '/') } val root = repository.root.path if (!absolutePath.startsWith(root)) { return absolutePath.replace(File.separatorChar, '/') } val path = absolutePath.substring(root.length).replace(File.separatorChar, '/') return if (path.startsWith('/')) { path.substring(1) } else { path } } fun getRepositoriesByProject(ideaProject: IdeaProject): List { val vcsRepositoryManager = VcsRepositoryManager.getInstance(ideaProject) return vcsRepositoryManager.repositories.map { it.root.path } } fun findRepositoryByPath(ideaProject: IdeaProject, path: String): GitRepository? { val vcsRepositoryManager = VcsRepositoryManager.getInstance(ideaProject) val repository = vcsRepositoryManager.repositories.find { it.root.path == path } if (null === repository || repository !is GitRepository) { return null } return repository } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/util/TextChoiceUtil.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.util object TextChoiceUtil { fun comment(count: Int): String { return if (count < 2) "$count comment" else "$count comments" } fun reply(count: Int): String { return if (count < 2) "$count reply" else "$count replies" } fun draft(count: Int): String { return if (count < 2) "$count draft" else "$count drafts" } fun draftComment(count: Int): String { return if (count < 2) "$count draft comment" else "$count draft comments" } fun commentWithDraft(totalCount: Int, draftCount: Int): String { return if (draftCount > 0) comment(totalCount) + " - ${draft(draftCount)}" else comment(totalCount) } fun replyWithDraft(totalCount: Int, draftCount: Int): String { return if (draftCount > 0) reply(totalCount) + " - ${draft(draftCount)}" else reply(totalCount) } } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/watcher/Watcher.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.watcher interface Watcher { val interval: Long fun canExecute(): Boolean fun shouldTerminate(): Boolean fun execute() fun terminate() } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/watcher/WatcherManager.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.watcher import com.intellij.openapi.Disposable interface WatcherManager : Disposable { fun addWatcher(watcher: Watcher) fun removeWatcher(watcher: Watcher) } ================================================ FILE: merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/watcher/WatcherManagerImpl.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.watcher import java.util.* class WatcherManagerImpl : WatcherManager { private val myWatchers = mutableMapOf() override fun addWatcher(watcher: Watcher) { val timer = Timer() timer.schedule(MyTimerTask(this, watcher), 0L, watcher.interval) myWatchers[watcher] = timer } override fun removeWatcher(watcher: Watcher) { val timer = myWatchers[watcher] if (null !== timer) { timer.cancel() myWatchers.remove(watcher) } } override fun dispose() { myWatchers.forEach { it.value.cancel() } myWatchers.clear() } internal class MyTimerTask( private val watcherManager: WatcherManager, private val watcher: Watcher ): TimerTask() { override fun run() { if (watcher.canExecute()) { watcher.execute() } if (watcher.shouldTerminate()) { watcher.terminate() watcherManager.removeWatcher(watcher) } } } } ================================================ FILE: merge-request-integration-core/src/main/resources/templates/mr.comment.html ================================================
{{content}}
================================================ FILE: merge-request-integration-core/src/main/resources/templates/mr.description.html ================================================

{{title}}


{{description}}
================================================ FILE: merge-request-integration-core/src/main/resources/templates/update.html ================================================
{{content}}
================================================ FILE: merge-request-integration-core/src/test/kotlin/net/ntworld/mergeRequestIntegrationIde/configuration/vos/GitRemotePathInfoTest.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.configuration.vos import com.intellij.util.VersionUtil import com.intellij.util.text.VersionComparatorUtil import org.junit.Test import kotlin.test.assertEquals class GitRemotePathInfoTest { private data class Item( val input: String, val valid: Boolean, val namespace: String = "", val project: String = "", val toStringValue: String = "" ) @Test fun testSshUrls() { val dataset = listOf( Item( input = "git@gitlab.test.de:namespace/project.git", valid = true, namespace = "namespace", project = "project", toStringValue = "namespace/project" ), Item( input = "git@gitlab.test.de:namespace/project", valid = true, namespace = "namespace", project = "project", toStringValue = "namespace/project" ), Item( input = "git@gitlab.test.de:namespace", valid = false ), Item( input = "git@gitlab.test.de:", valid = false ) ) dataset.forEach { val info = GitRemotePathInfo(it.input) assertEquals(it.valid, info.isValid, "failed case $it") if (it.valid) { assertEquals(it.namespace, info.namespace, "failed case $it") assertEquals(it.project, info.project, "failed case $it") assertEquals(it.toStringValue, info.toString(), "failed case $it") } } } @Test fun testHttpUrls() { val dataset = listOf( Item( input = "https://gitlab.test.de/namespace/project", valid = true, namespace = "namespace", project = "project", toStringValue = "namespace/project" ), Item( input = "https://gitlab.test.de/namespace/project.git", valid = true, namespace = "namespace", project = "project", toStringValue = "namespace/project" ), Item( input = "http://gitlab.test.de/namespace/project.git", valid = true, namespace = "namespace", project = "project", toStringValue = "namespace/project" ), Item( input = "HTTPS://gitlab.test.de/namespace/project", valid = true, namespace = "namespace", project = "project", toStringValue = "namespace/project" ), Item( input = "http://gitlab.test.de/namespace/PROJECT.git", valid = true, namespace = "namespace", project = "PROJECT", toStringValue = "namespace/PROJECT" ), Item( input = "https://github.com/nhat-phan/merge-request-integration", valid = true, namespace = "nhat-phan", project = "merge-request-integration", toStringValue = "nhat-phan/merge-request-integration" ), Item( input = "http://github.com/nhat-phan/merge-request-integration", valid = true, namespace = "nhat-phan", project = "merge-request-integration", toStringValue = "nhat-phan/merge-request-integration" ), Item( input = "http://github.com/nhat-phan", valid = false ), Item( input = "http:github.com/nhat-phan/merge-request-integration", valid = false ), Item( input = "http://:github.com:123\\:123nhat-phan/merge-request-integration", valid = false ) ) dataset.forEach { val info: GitRemotePathInfo try { info = GitRemotePathInfo(it.input) assertEquals(it.valid, info.isValid, "failed case $it") if (it.valid) { assertEquals(it.namespace, info.namespace, "failed case $it") assertEquals(it.project, info.project, "failed case $it") assertEquals(it.toStringValue, info.toString(), "failed case $it") } } catch (exception: Exception) { assertEquals(it.valid, false, "failed case $it") } } } @Test fun testVersion() { println(VersionComparatorUtil.compare("2020.2.0-eap-1-for-ide-2020.1.x", "2020.2.0-built-for-ide-2020.1.x")) println(VersionComparatorUtil.compare("2020.2.0-built-for-ide-2020.1.x", "2020.2.1-built-for-ide-2020.1.x")) println(VersionComparatorUtil.compare("2020.2.0-built-for-ide-2020.1.x", "2020.1.5-built-for-ide-2020.1")) println(VersionComparatorUtil.compare("2020.2.0-eap-1-for-ide-2020.1.x", "2020.1.5-built-for-ide-2020.1")) } } ================================================ FILE: merge-request-integration-core/src/test/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/DummyProjectServiceProvider.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.infrastructure import com.intellij.openapi.project.Project class DummyProjectServiceProvider(project: Project) : AbstractProjectServiceProvider(project) { override val applicationServiceProvider: ApplicationServiceProvider get() = TODO("Not yet implemented") } ================================================ FILE: merge-request-integration-core/src/test/kotlin/net/ntworld/mergeRequestIntegrationIde/internal/CodeReviewServiceImplTest.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.internal import org.junit.Test import kotlin.test.assertEquals class CodeReviewServiceImplTest ================================================ FILE: merge-request-integration-core/src/test/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/NodeSyncManagerImplTest.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node import com.intellij.ide.projectView.PresentationData import com.intellij.ide.util.treeView.PresentableNodeDescriptor import com.intellij.openapi.command.impl.DummyProject import net.ntworld.mergeRequest.* import net.ntworld.mergeRequest.generated.CommentImpl import net.ntworld.mergeRequest.generated.CommentPositionImpl import net.ntworld.mergeRequestIntegration.internal.UserImpl import javax.swing.tree.DefaultMutableTreeNode import kotlin.test.Test import kotlin.test.assertEquals class NodeSyncManagerImplTest { private val user = UserImpl( id = "1", name = "name", username = "username", email = "email", avatarUrl = "", url = "", status = UserStatus.ACTIVE, createdAt = "" ) private val commentPosition = CommentPositionImpl( "", "", "", null, null, null, null, CommentPositionSource.UNKNOWN, CommentPositionChangeType.UNKNOWN ) @Test fun `testSyncStructure can generate correct structure with empty tree`() { val service = makeService() val root = generateFullList() val rootTreeNode = DefaultMutableTreeNode() service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER) assertEquals(""" --root ----general-comments ------thread[1] --------comment[2] ------thread[3] ----file[/dir/file-1] ------line[/dir/file-1:1] --------thread[4] ------line[/dir/file-1:2] --------thread[5] ----------comment[6] ----------comment[7] --------thread[8] ----------comment[9] ----------comment[10] ----------comment[11] ----file[/file-2] ------line[/file-2:3] --------thread[12] !END """.trimIndent(), generateStructureFootprint(rootTreeNode)) } @Test fun `testSyncStructure can add more node into current structure`() { val service = makeService() val root = generateFullList() val rootTreeNode = DefaultMutableTreeNode() service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER) root.children[1].children[1].children[0].add(CommentNode(makeComment("Insert", "new"), null)) service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER) assertEquals(""" --root ----general-comments ------thread[1] --------comment[2] ------thread[3] ----file[/dir/file-1] ------line[/dir/file-1:1] --------thread[4] ------line[/dir/file-1:2] --------thread[5] ----------comment[6] ----------comment[7] ----------comment[Insert] --------thread[8] ----------comment[9] ----------comment[10] ----------comment[11] ----file[/file-2] ------line[/file-2:3] --------thread[12] !END """.trimIndent(), generateStructureFootprint(rootTreeNode)) } @Test fun `testSyncStructure can remove node out of current structure`() { val service = makeService() val root = generateFullList() val rootTreeNode = DefaultMutableTreeNode() service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER) (root.children[1].children[1].children as MutableList).removeAt(0) root.children.removeAt(0) (root.children[0].children[1].children[0].children as MutableList).removeAt(1) service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER) assertEquals(""" --root ----file[/dir/file-1] ------line[/dir/file-1:1] --------thread[4] ------line[/dir/file-1:2] --------thread[8] ----------comment[9] ----------comment[11] ----file[/file-2] ------line[/file-2:3] --------thread[12] !END """.trimIndent(), generateStructureFootprint(rootTreeNode)) } @Test fun `testSyncStructure can remove items and delete correct indexes`() { val service = makeService() val root = generateFullList() val rootTreeNode = DefaultMutableTreeNode() service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER) (root.children[1].children as MutableList).removeAt(0) service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER) assertEquals(""" --root ----general-comments ------thread[1] --------comment[2] ------thread[3] ----file[/dir/file-1] ------line[/dir/file-1:2] --------thread[5] ----------comment[6] ----------comment[7] --------thread[8] ----------comment[9] ----------comment[10] ----------comment[11] ----file[/file-2] ------line[/file-2:3] --------thread[12] !END """.trimIndent(), generateStructureFootprint(rootTreeNode)) (root.children as MutableList).removeAt(0) service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER) assertEquals(""" --root ----file[/dir/file-1] ------line[/dir/file-1:2] --------thread[5] ----------comment[6] ----------comment[7] --------thread[8] ----------comment[9] ----------comment[10] ----------comment[11] ----file[/file-2] ------line[/file-2:3] --------thread[12] !END """.trimIndent(), generateStructureFootprint(rootTreeNode)) } private fun generateStructureFootprint(treeNode: DefaultMutableTreeNode): String { val stringBuilder = StringBuilder() generateStructureFootprint(stringBuilder, treeNode, 0) stringBuilder.append("!END") return stringBuilder.toString() } @Suppress("UNCHECKED_CAST") private fun generateStructureFootprint(result: StringBuilder, treeNode: DefaultMutableTreeNode, deep: Int) { var padding = "" for (i in 0..deep) { padding += "--" } val userObject = treeNode.userObject as PresentableNodeDescriptor val node = userObject.element result.append("${padding}${node.id}") result.appendln() val children = treeNode.children() while (children.hasMoreElements()) { generateStructureFootprint(result, children.nextElement() as DefaultMutableTreeNode, deep+1) } } private fun generateFullList(): RootNode { val root = RootNode() val generalComments = GeneralCommentsNode(3, 0) val threadOne = ThreadNode("thread-one", 1, 0, makeComment("1", "thread-one"), null) threadOne.add(CommentNode(makeComment("2", "thread-one"), null)) generalComments.add(threadOne) val threadTwo = ThreadNode("thread-two", 0, 0, makeComment("3", "thread-two"), null) generalComments.add(threadTwo) root.add(generalComments) val fileOne = FileNode("/dir/file-1", 0) val line1 = FileLineNode("/dir/file-1", 1, commentPosition, 1, 0, false) val threadThree = ThreadNode("thread-three", 0, 0, makeComment("4", "thread-three"), null) line1.add(threadThree) val line2 = FileLineNode("/dir/file-1", 2, commentPosition, 0, 0, false) val threadFour = ThreadNode("thread-four", 2, 0, makeComment("5", "thread-four"), null) threadFour.add(CommentNode(makeComment("6", "thread-four"), null)) threadFour.add(CommentNode(makeComment("7", "thread-four"), null)) line2.add(threadFour) val threadFive = ThreadNode("thread-five", 3, 0, makeComment("8", "thread-four"), null) threadFive.add(CommentNode(makeComment("9", "thread-five"), null)) threadFive.add(CommentNode(makeComment("10", "thread-five"), null)) threadFive.add(CommentNode(makeComment("11", "thread-five"), null)) line2.add(threadFive) fileOne.add(line1) fileOne.add(line2) val fileTwo = FileNode("/file-2", 0) val line3 = FileLineNode("/file-2", 3, commentPosition, 1, 0, false) val threadSix = ThreadNode("thread-six", 0, 0, makeComment("12", "thread-six"), null) line3.add(threadSix) fileTwo.add(line3) root.add(fileOne) root.add(fileTwo) return root } private fun makeService(): NodeSyncManagerImpl { return NodeSyncManagerImpl(DummyNodeDescriptorService()) } private fun makeComment(id: String, threadId: String): Comment { return CommentImpl( id = id, parentId = threadId, replyId = threadId, body = "comment $id", author = user, position = null, createdAt = "", updatedAt = "", resolvable = true, resolved = false, resolvedBy = null, isDraft = true ) } private class DummyPresentableNodeDescriptor( private val element: Node ) : PresentableNodeDescriptor(DummyProject.getInstance(), null) { override fun update(presentation: PresentationData) { } override fun getElement(): Node = element } private class DummyNodeDescriptorService: NodeDescriptorService { override fun make(node: Node): PresentableNodeDescriptor { return DummyPresentableNodeDescriptor(node) } override fun findNode(input: Any?): Node? { return null } override fun isHolding(input: Any?, node: Node): Boolean { return false } } companion object { val DEFAULT_INVOKER: ((Node, DefaultMutableTreeNode) -> Unit) = { _, _ -> } } } ================================================ FILE: merge-request-integration-core/src/test/kotlin/net/ntworld/mergeRequestIntegrationIde/watcher/WatcherManagerImplTest.kt ================================================ package net.ntworld.mergeRequestIntegrationIde.watcher import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class WatcherManagerImplTest { @Test fun testCanScheduleInfinityWatcher() { var count = 0 class MyWatcher : Watcher { override val interval: Long = 100 override fun canExecute() = true override fun shouldTerminate() = false override fun execute() { count++ } override fun terminate() { } } val watcherManager = WatcherManagerImpl() watcherManager.addWatcher(MyWatcher()) Thread.sleep(950) assertTrue(count >= 10) } @Test fun testCanTerminateTheWatcherByShouldTerminateCondition() { var count = 0 class MyWatcher : Watcher { override val interval: Long = 100 override fun canExecute() = true override fun shouldTerminate() = count == 5 override fun execute() { count++ } override fun terminate() { count += 10 } } val watcherManager = WatcherManagerImpl() watcherManager.addWatcher(MyWatcher()) Thread.sleep(950) assertEquals(15, count) } @Test fun testNotTriggerExecuteIfThereIsCanExecuteReturnFalse() { var count = 0 class MyWatcher : Watcher { override val interval: Long = 100 override fun canExecute() = false override fun shouldTerminate() = count == 5 override fun execute() { count++ } override fun terminate() { } } val watcherManager = WatcherManagerImpl() watcherManager.addWatcher(MyWatcher()) Thread.sleep(950) assertEquals(0, count) } } ================================================ FILE: merge-request-integration-ee/LICENSE ================================================ Merge Request Integration Enterprise Edition (EE) (c) 2019-present Nhat Phan (https://github.com/nhat-phan) Merge Request Integration Enterprise Edition (EE) is licensed under a Creative Commons Attribution-NoDerivatives 4.0 International License. You should have received a copy of the license along with this work. If not, see . Attribution-NoDerivatives 4.0 International ======================================================================= Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC- licensed material, or material used under an exception or limitation to copyright. More considerations for licensors: wiki.creativecommons.org/Considerations_for_licensors Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason--for example, because of any applicable exception or limitation to copyright--then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More_considerations for the public: wiki.creativecommons.org/Considerations_for_licensees ======================================================================= Creative Commons Attribution-NoDerivatives 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NoDerivatives 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 -- Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. c. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. d. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. e. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. f. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. g. Licensor means the individual(s) or entity(ies) granting rights under this Public License. h. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. i. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. j. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 -- Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: a. reproduce and Share the Licensed Material, in whole or in part; and b. produce and reproduce, but not Share, Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. 5. Downstream recipients. a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. b. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 -- License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material, You must: a. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. For the avoidance of doubt, You do not have permission under this Public License to Share Adapted Material. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. Section 4 -- Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database, provided You do not Share Adapted Material; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 -- Disclaimer of Warranties and Limitation of Liability. a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 -- Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 -- Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 -- Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ======================================================================= Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. ================================================ FILE: merge-request-integration-ee/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile val artifactGroup: String by project val targetIDEVersion: String by project val enterpriseEditionVersion: String by project val intellijVersion: String by project val jvmTarget: String by project val foundationVersion: String by project val gitlab4jVersion: String by project val githubApiVersion: String by project val prettyTimeVersion: String by project val commonmarkVersion: String by project val intellijSinceBuild: String by project val intellijUntilBuild: String by project val eapRelease: String by project group = artifactGroup version = if (eapRelease == "false") { "$enterpriseEditionVersion-built-for-ide-$targetIDEVersion" } else { "$enterpriseEditionVersion-eap-$eapRelease-for-ide-$targetIDEVersion" } repositories { jcenter() mavenCentral() maven("https://jitpack.io") } dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib") implementation("com.github.nhat-phan.foundation:foundation-jvm:$foundationVersion") implementation("org.gitlab4j:gitlab4j-api:$gitlab4jVersion") implementation("org.kohsuke:github-api:$githubApiVersion") implementation("org.ocpsoft.prettytime:prettytime:$prettyTimeVersion") compile("com.atlassian.commonmark:commonmark:$commonmarkVersion") implementation(project(":contracts")) implementation(project(":merge-request-integration-core")) implementation(project(":merge-request-integration")) } // See https://github.com/JetBrains/gradle-intellij-plugin/ intellij { version = intellijVersion updateSinceUntilBuild = true setPlugins("git4idea") } val compileKotlin: KotlinCompile by tasks val compileTestKotlin: KotlinCompile by tasks compileKotlin.kotlinOptions { jvmTarget = jvmTarget } compileTestKotlin.kotlinOptions { jvmTarget = jvmTarget } tasks { named("compileKotlin") { kotlinOptions { jvmTarget = jvmTarget } } named("patchPluginXml") { val version = if (!enterpriseEditionVersion.endsWith("eap")) enterpriseEditionVersion else enterpriseEditionVersion.substring(0, enterpriseEditionVersion.length - 3) changeNotes(htmlFixer("./merge-request-integration-ee/doc/release-notes.$version.html")) pluginDescription(htmlFixer("./merge-request-integration-ee/doc/description.html")) sinceBuild(intellijSinceBuild) untilBuild(intellijUntilBuild) } } fun htmlFixer(filename: String): String { if (!File(filename).exists()) { throw Exception("File $filename not found.") } return File(filename).readText().replace("", "").replace("", "") } ================================================ FILE: merge-request-integration-ee/doc/description.html ================================================

Merge Request Integration EE is an open-source plugin for JetBrains IDEs which helps you

  • Do code review right in your IDE.
  • Address review comments from your colleagues.

What you can do:

  • Check approval statuses of merge requests which are waiting for your approval.
  • Filter Merge Requests which are assigned to you, belongs to your colleagues, etc
  • Check pipeline status and approval status.
  • Do code review, navigate code with Diff View right in your IDE.
  • Address review comments, navigate comments in editors.
  • Add and reply a comment
  • Approve/revoke your approval
  • More and more features will be coming soon :)

Currently the plugin supports GitLab only (gitlab cloud and self-hosted).

If you've installed Merge Request Integration CE, please uninstall before installing Enterprise Edition.

How to setup Gitlab connection

You need a personal api token. To get the token please follow these steps:

  • Log in to your Gitlab site
  • Go to Settings > Access Token and create a personal access token
  • Go to your IDE preferences, Merge Request Integration > Gitlab
  • Fill data, then save and click refresh button of Merge Request Integration CE window

About me

My name is Nhat, I'm a software developer at Personio (yes, we are hiring all around the world, relocation to Munich is of course possible).

Attribution

================================================ FILE: merge-request-integration-ee/doc/release-notes.2019.3.1.html ================================================
  • Initial release :)
================================================ FILE: merge-request-integration-ee/doc/release-notes.2019.3.2.html ================================================
  • Introduce commit list and changes tree
  • Resolve and delete comments
  • Display comments on editors
  • Bug fixes
================================================ FILE: merge-request-integration-ee/doc/release-notes.2019.3.3.html ================================================

Notable changes

  • Improved style of list
  • Improved approval panel which only opens if there is information to be displayed
  • Replaced deprecated api
  • Fixed bug #4: Cannot leave a comment in unified view or on Windows
  • Fixed bug #5: Cannot open comment editor if IDE does not support Markdown
  • Fixed bug: Token on Windows cannot be saved
  • Fixed grammatical error on GitLab configuration panel

Acknowledgement

  • Thank @Savak, @SToto98 for reporting bug #4
  • Thank @smallcreep for reporting bug #5
  • Thank Artem Kuchyn for reporting "Token on Windows cannot be saved" bug.
  • Thank Jay for correcting grammar.
================================================ FILE: merge-request-integration-ee/doc/release-notes.2019.3.4.html ================================================

Notable changes

  • Supported self-signed certificate
  • Supported cache option for finding merge request to improve performance
  • New implementation of configuration panel
  • Fixed #1: Support multi repositories project
  • Fixed #9: Connection list cannot be saved
  • Fixed #16: Pipeline is not displayed on Gitlab v12.2.4
  • Fixed bug: Empty description string on some actions
  • Fixed bug: UpdateManager requires weird format of metadata

Acknowledgement

================================================ FILE: merge-request-integration-ee/doc/release-notes.2019.3.5.html ================================================

Notable changes

  • Hot-fix #21: Gitlab configuration page is empty for new users who have no connections data

Acknowledgement

================================================ FILE: merge-request-integration-ee/doc/release-notes.2020.1.0.html ================================================

Notable changes

    • Feature: introduce create-general-comment button
    • Option: allow doing code review without checking out the target branch
    • Option: open diff changes if the number of changes is less than configurable value
    • Option: keep selected values of filters and order state in Merge Request list
    • UX: display a comment after creating/replying comment
    • UX: select project automatically based on remote url in configuration
    • UX: remove left border of Tabs
    • Bug fix: cannot start an IDE if CE and EE are installed simultaneously

Acknowledgement

================================================ FILE: merge-request-integration-ee/doc/release-notes.2020.1.1.html ================================================

Notable changes

  • Hotfix: Crashes on Intellij IDEs v2020.1 EAP

Acknowledgement

================================================ FILE: merge-request-integration-ee/doc/release-notes.2020.1.2.html ================================================

Notable changes

  • Feature: Display, resolve, delete, reply, write comments in diff view
  • Feature: Open diff view when selecting a group in comments tree
  • Bug fix: Duplicated/wrong comment position

Acknowledgement

================================================ FILE: merge-request-integration-ee/doc/release-notes.2020.1.3.html ================================================

Notable changes

  • Branch project based on IDE versions, deploy plugin to EAP channel for 2020.1 EAP IDEs
  • Display approval status in merge request list
  • Improve UI response when deleting and adding a new comment
  • Sync comment collection when a comment gets changed in diff view
  • Fix #44: error when comments get destroyed in the event thread

Acknowledgement

================================================ FILE: merge-request-integration-ee/doc/release-notes.2020.1.4.html ================================================

Notable changes

  • Bug fixed: comments are not shown in diff view on Windows
  • Bug fixed: comments are filtered out when the MR has too many commits
================================================ FILE: merge-request-integration-ee/doc/release-notes.2020.1.5.html ================================================

Notable changes

  • Support IDEA 2020.1, Android Studio 4.0.0
  • Introduce new Comments tab which
    • Has all current functionalities
    • Can open diff view when you select a file
    • Can jump to code when you travel the Comments tree
  • Open diff view when you click to Changes tree in Commits tab
  • Display merge request id in Home tab, Merge Request tree and Info tab
  • Fix #17: Cannot do Code Review on Android Studio
  • Fix #54: Merge Request tree should be invoked in application thread when get reloaded
  • Bug fixes

Acknowledgement

================================================ FILE: merge-request-integration-ee/doc/release-notes.2020.2.0.html ================================================

Notable changes

  • Support IDEA 2020.2
  • Introduce new Rework flow which helps
    • Display comments and file changes if current branch matches your MR
    • Jump and address comments more easily
    Note: because of technical difficulty Reply button only works when open a dialog
  • Introduce options to hide UpVote, DownVote or State buttons in toolbar.
  • Improve configuration area.
  • Throw all exceptions which may happen inside plugin.
  • Fix #65: loading data and refreshing not work properly
  • Fix #88: fetching merge requests error for some timezones

Acknowledgement

================================================ FILE: merge-request-integration-ee/doc/release-notes.2020.3.0.html ================================================

Notable changes

  • Support IDEA 2020.3.x
  • Support search by ID when ID is typed in the search box
  • Introduce edit comment functionality
  • Introduce draft comment feature which can temporarily save and push when Code Review is stopped
  • Fix #93: Crash with empty avatar
  • Fix #98: Crash when project gets closed

Acknowledgement

================================================ FILE: merge-request-integration-ee/settings.gradle.kts ================================================ rootProject.name = "merge-request-integration-ee" ================================================ FILE: merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/CheckLicense.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeEE import com.intellij.openapi.application.PermanentInstallationID import com.intellij.ui.LicensingFacade import java.io.ByteArrayInputStream import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.security.Signature import java.security.cert.* import java.util.* import java.util.Base64.getMimeDecoder /** * @author Eugene Zhuravlev * Date: 26-Jul-18 */ object CheckLicense { /** * PRODUCT_CODE must be the same specified in plugin.xml inside the tag */ private const val PRODUCT_CODE = "PMRINTEGEE" private const val KEY_PREFIX = "key:" private const val STAMP_PREFIX = "stamp:" private const val EVAL_PREFIX = "eval:" /** * Public root certificates needed to verify JetBrains-signed licenses */ private val ROOT_CERTIFICATES = arrayOf( "-----BEGIN CERTIFICATE-----\n" + "MIIFOzCCAyOgAwIBAgIJANJssYOyg3nhMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV\n" + "BAMMDUpldFByb2ZpbGUgQ0EwHhcNMTUxMDAyMTEwMDU2WhcNNDUxMDI0MTEwMDU2\n" + "WjAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMIICIjANBgkqhkiG9w0BAQEFAAOC\n" + "Ag8AMIICCgKCAgEA0tQuEA8784NabB1+T2XBhpB+2P1qjewHiSajAV8dfIeWJOYG\n" + "y+ShXiuedj8rL8VCdU+yH7Ux/6IvTcT3nwM/E/3rjJIgLnbZNerFm15Eez+XpWBl\n" + "m5fDBJhEGhPc89Y31GpTzW0vCLmhJ44XwvYPntWxYISUrqeR3zoUQrCEp1C6mXNX\n" + "EpqIGIVbJ6JVa/YI+pwbfuP51o0ZtF2rzvgfPzKtkpYQ7m7KgA8g8ktRXyNrz8bo\n" + "iwg7RRPeqs4uL/RK8d2KLpgLqcAB9WDpcEQzPWegbDrFO1F3z4UVNH6hrMfOLGVA\n" + "xoiQhNFhZj6RumBXlPS0rmCOCkUkWrDr3l6Z3spUVgoeea+QdX682j6t7JnakaOw\n" + "jzwY777SrZoi9mFFpLVhfb4haq4IWyKSHR3/0BlWXgcgI6w6LXm+V+ZgLVDON52F\n" + "LcxnfftaBJz2yclEwBohq38rYEpb+28+JBvHJYqcZRaldHYLjjmb8XXvf2MyFeXr\n" + "SopYkdzCvzmiEJAewrEbPUaTllogUQmnv7Rv9sZ9jfdJ/cEn8e7GSGjHIbnjV2ZM\n" + "Q9vTpWjvsT/cqatbxzdBo/iEg5i9yohOC9aBfpIHPXFw+fEj7VLvktxZY6qThYXR\n" + "Rus1WErPgxDzVpNp+4gXovAYOxsZak5oTV74ynv1aQ93HSndGkKUE/qA/JECAwEA\n" + "AaOBhzCBhDAdBgNVHQ4EFgQUo562SGdCEjZBvW3gubSgUouX8bMwSAYDVR0jBEEw\n" + "P4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2Zp\n" + "bGUgQ0GCCQDSbLGDsoN54TAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkq\n" + "hkiG9w0BAQsFAAOCAgEAjrPAZ4xC7sNiSSqh69s3KJD3Ti4etaxcrSnD7r9rJYpK\n" + "BMviCKZRKFbLv+iaF5JK5QWuWdlgA37ol7mLeoF7aIA9b60Ag2OpgRICRG79QY7o\n" + "uLviF/yRMqm6yno7NYkGLd61e5Huu+BfT459MWG9RVkG/DY0sGfkyTHJS5xrjBV6\n" + "hjLG0lf3orwqOlqSNRmhvn9sMzwAP3ILLM5VJC5jNF1zAk0jrqKz64vuA8PLJZlL\n" + "S9TZJIYwdesCGfnN2AETvzf3qxLcGTF038zKOHUMnjZuFW1ba/12fDK5GJ4i5y+n\n" + "fDWVZVUDYOPUixEZ1cwzmf9Tx3hR8tRjMWQmHixcNC8XEkVfztID5XeHtDeQ+uPk\n" + "X+jTDXbRb+77BP6n41briXhm57AwUI3TqqJFvoiFyx5JvVWG3ZqlVaeU/U9e0gxn\n" + "8qyR+ZA3BGbtUSDDs8LDnE67URzK+L+q0F2BC758lSPNB2qsJeQ63bYyzf0du3wB\n" + "/gb2+xJijAvscU3KgNpkxfGklvJD/oDUIqZQAnNcHe7QEf8iG2WqaMJIyXZlW3me\n" + "0rn+cgvxHPt6N4EBh5GgNZR4l0eaFEV+fxVsydOQYo1RIyFMXtafFBqQl6DDxujl\n" + "FeU3FZ+Bcp12t7dlM4E0/sS1XdL47CfGVj4Bp+/VbF862HmkAbd7shs7sDQkHbU=\n" + "-----END CERTIFICATE-----\n", "-----BEGIN CERTIFICATE-----\n" + "MIIFTDCCAzSgAwIBAgIJAMCrW9HV+hjZMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV\n" + "BAMMEkxpY2Vuc2UgU2VydmVycyBDQTAgFw0xNjEwMTIxNDMwNTRaGA8yMTE2MTIy\n" + "NzE0MzA1NFowHTEbMBkGA1UEAwwSTGljZW5zZSBTZXJ2ZXJzIENBMIICIjANBgkq\n" + "hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoT7LvHj3JKK2pgc5f02z+xEiJDcvlBi6\n" + "fIwrg/504UaMx3xWXAE5CEPelFty+QPRJnTNnSxqKQQmg2s/5tMJpL9lzGwXaV7a\n" + "rrcsEDbzV4el5mIXUnk77Bm/QVv48s63iQqUjVmvjQt9SWG2J7+h6X3ICRvF1sQB\n" + "yeat/cO7tkpz1aXXbvbAws7/3dXLTgAZTAmBXWNEZHVUTcwSg2IziYxL8HRFOH0+\n" + "GMBhHqa0ySmF1UTnTV4atIXrvjpABsoUvGxw+qOO2qnwe6ENEFWFz1a7pryVOHXg\n" + "P+4JyPkI1hdAhAqT2kOKbTHvlXDMUaxAPlriOVw+vaIjIVlNHpBGhqTj1aqfJpLj\n" + "qfDFcuqQSI4O1W5tVPRNFrjr74nDwLDZnOF+oSy4E1/WhL85FfP3IeQAIHdswNMJ\n" + "y+RdkPZCfXzSUhBKRtiM+yjpIn5RBY+8z+9yeGocoxPf7l0or3YF4GUpud202zgy\n" + "Y3sJqEsZksB750M0hx+vMMC9GD5nkzm9BykJS25hZOSsRNhX9InPWYYIi6mFm8QA\n" + "2Dnv8wxAwt2tDNgqa0v/N8OxHglPcK/VO9kXrUBtwCIfZigO//N3hqzfRNbTv/ZO\n" + "k9lArqGtcu1hSa78U4fuu7lIHi+u5rgXbB6HMVT3g5GQ1L9xxT1xad76k2EGEi3F\n" + "9B+tSrvru70CAwEAAaOBjDCBiTAdBgNVHQ4EFgQUpsRiEz+uvh6TsQqurtwXMd4J\n" + "8VEwTQYDVR0jBEYwRIAUpsRiEz+uvh6TsQqurtwXMd4J8VGhIaQfMB0xGzAZBgNV\n" + "BAMMEkxpY2Vuc2UgU2VydmVycyBDQYIJAMCrW9HV+hjZMAwGA1UdEwQFMAMBAf8w\n" + "CwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQCJ9+GQWvBS3zsgPB+1PCVc\n" + "oG6FY87N6nb3ZgNTHrUMNYdo7FDeol2DSB4wh/6rsP9Z4FqVlpGkckB+QHCvqU+d\n" + "rYPe6QWHIb1kE8ftTnwapj/ZaBtF80NWUfYBER/9c6To5moW63O7q6cmKgaGk6zv\n" + "St2IhwNdTX0Q5cib9ytE4XROeVwPUn6RdU/+AVqSOspSMc1WQxkPVGRF7HPCoGhd\n" + "vqebbYhpahiMWfClEuv1I37gJaRtsoNpx3f/jleoC/vDvXjAznfO497YTf/GgSM2\n" + "LCnVtpPQQ2vQbOfTjaBYO2MpibQlYpbkbjkd5ZcO5U5PGrQpPFrWcylz7eUC3c05\n" + "UVeygGIthsA/0hMCioYz4UjWTgi9NQLbhVkfmVQ5lCVxTotyBzoubh3FBz+wq2Qt\n" + "iElsBrCMR7UwmIu79UYzmLGt3/gBdHxaImrT9SQ8uqzP5eit54LlGbvGekVdAL5l\n" + "DFwPcSB1IKauXZvi1DwFGPeemcSAndy+Uoqw5XGRqE6jBxS7XVI7/4BSMDDRBz1u\n" + "a+JMGZXS8yyYT+7HdsybfsZLvkVmc9zVSDI7/MjVPdk6h0sLn+vuPC1bIi5edoNy\n" + "PdiG2uPH5eDO6INcisyPpLS4yFKliaO4Jjap7yzLU9pbItoWgCAYa2NpxuxHJ0tB\n" + "7tlDFnvaRnQukqSG+VqNWg==\n" + "-----END CERTIFICATE-----" ) private const val SECOND: Long = 1000 private const val MINUTE = 60 * SECOND private const val HOUR = 60 * MINUTE private const val TIMESTAMP_VALIDITY_PERIOD_MS = 1 * HOUR // configure period that suits your needs better// licensed via ticket obtained from JetBrains Floating License Server // the license is obtained via JetBrainsAccount or entered as an activation code val isLicensed: Boolean get() { val facade = LicensingFacade.getInstance() ?: return false val cstamp = facade.getConfirmationStamp(PRODUCT_CODE) ?: return false if (cstamp.startsWith(KEY_PREFIX)) { // the license is obtained via JetBrainsAccount or entered as an activation code return isKeyValid(cstamp.substring(KEY_PREFIX.length)) } if (cstamp.startsWith(STAMP_PREFIX)) { // licensed via ticket obtained from JetBrains Floating License Server return isLicenseServerStampValid(cstamp.substring(STAMP_PREFIX.length)) } return if (cstamp.startsWith(EVAL_PREFIX)) { isEvaluationValid(cstamp.substring(EVAL_PREFIX.length)) } else false } private fun isEvaluationValid(expirationTime: String): Boolean { return try { val now = Date() val expiration = Date(expirationTime.toLong()) now.before(expiration) } catch (e: NumberFormatException) { false } } private fun isKeyValid(key: String): Boolean { val licenseParts = key.split("-").toTypedArray() if (licenseParts.size != 4) { return false // invalid format } val licenseId = licenseParts[0] val licensePartBase64 = licenseParts[1] val signatureBase64 = licenseParts[2] val certBase64 = licenseParts[3] try { val sig = Signature.getInstance("SHA1withRSA") // the last parameter of 'createCertificate()' set to 'false' switches off certificate expiration checks. // This might be the case if the key is at the same time a perpetual fallback license for older IDE versions. // Here it is only important that the key was signed with an authentic JetBrains certificate. sig.initVerify( createCertificate( Base64.getMimeDecoder().decode(certBase64.toByteArray(StandardCharsets.UTF_8)), emptySet(), false ) ) val licenseBytes = Base64.getMimeDecoder() .decode(licensePartBase64.toByteArray(StandardCharsets.UTF_8)) sig.update(licenseBytes) if (!sig.verify(Base64.getMimeDecoder().decode(signatureBase64.toByteArray(StandardCharsets.UTF_8)))) { return false } // Optional additional check: the licenseId corresponds to the licenseId encoded in the signed license data // The following is a 'least-effort' code. It would be more accurate to parse json and then find there the value of the attribute "licenseId" val licenseData = String(licenseBytes, Charset.forName("UTF-8")) return licenseData.contains("\"licenseId\":\"$licenseId\"") } catch (ignored: Throwable) { } return false } private fun isLicenseServerStampValid(serverStamp: String): Boolean { try { val parts = serverStamp.split(":".toRegex()).toTypedArray() val base64 = getMimeDecoder() val expectedMachineId = parts[0] val timeStamp = parts[1].toLong() val machineId = parts[2] val signatureType = parts[3] val signatureBytes = base64.decode(parts[4].toByteArray(StandardCharsets.UTF_8)) val certBytes = base64.decode(parts[5].toByteArray(StandardCharsets.UTF_8)) val intermediate: MutableCollection = ArrayList() for (idx in 6 until parts.size) { intermediate.add(base64.decode(parts[idx].toByteArray(StandardCharsets.UTF_8))) } val sig = Signature.getInstance(signatureType) // the last parameter of 'createCertificate()' set to 'true' causes the certificate to be checked for // expiration. Expired certificates from a license server cannot be trusted sig.initVerify(createCertificate(certBytes, intermediate, true)) sig.update("$timeStamp:$machineId".toByteArray(StandardCharsets.UTF_8)) if (sig.verify(signatureBytes)) { // machineId must match the machineId from the server reply and // server reply should be relatively 'fresh' return expectedMachineId == machineId && Math.abs(System.currentTimeMillis() - timeStamp) < TIMESTAMP_VALIDITY_PERIOD_MS } } catch (ignored: Throwable) { // consider serverStamp invalid } return false } @Throws(Exception::class) private fun createCertificate( certBytes: ByteArray, intermediateCertsBytes: Collection, checkValidityAtCurrentDate: Boolean ): X509Certificate { val x509factory = CertificateFactory.getInstance("X.509") val cert = x509factory.generateCertificate(ByteArrayInputStream(certBytes)) as X509Certificate val allCerts: MutableCollection = HashSet() allCerts.add(cert) for (bytes in intermediateCertsBytes) { allCerts.add(x509factory.generateCertificate(ByteArrayInputStream(bytes))) } try { // Create the selector that specifies the starting certificate val selector = X509CertSelector() selector.certificate = cert // Configure the PKIX certificate builder algorithm parameters val trustAchors: MutableSet = HashSet() for (rc in ROOT_CERTIFICATES) { trustAchors.add( TrustAnchor( x509factory.generateCertificate(ByteArrayInputStream(rc.toByteArray(StandardCharsets.UTF_8))) as X509Certificate, null ) ) } val pkixParams = PKIXBuilderParameters(trustAchors, selector) pkixParams.isRevocationEnabled = false if (!checkValidityAtCurrentDate) { // deliberately check validity on the start date of cert validity period, so that we do not depend on // the actual moment when the check is performed pkixParams.date = cert.notBefore } pkixParams.addCertStore( CertStore.getInstance("Collection", CollectionCertStoreParameters(allCerts)) ) // Build and verify the certification chain val path = CertPathBuilder.getInstance("PKIX").build(pkixParams).certPath if (path != null) { CertPathValidator.getInstance("PKIX").validate(path, pkixParams) return cert } } catch (e: Exception) { // debug the reason here } throw Exception("Certificate used to sign the license is not signed by JetBrains root certificate") } } ================================================ FILE: merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/Configuration.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeEE import com.intellij.openapi.components.ServiceManager import net.ntworld.mergeRequestIntegrationIde.ui.configuration.ConfigurationBase class Configuration: ConfigurationBase(ServiceManager.getService(EnterpriseApplicationServiceProvider::class.java)) { override fun getId(): String { return "merge-request-integration-ee" } override fun getDisplayName(): String { return "Merge Request Integration EE" } } ================================================ FILE: merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/DiffExtension.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeEE import com.intellij.openapi.components.ServiceManager import net.ntworld.mergeRequestIntegrationIde.diff.DiffExtensionBase class DiffExtension : DiffExtensionBase( ServiceManager.getService(EnterpriseApplicationServiceProvider::class.java) ) ================================================ FILE: merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/DiffViewAddCommentAction.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeEE import net.ntworld.mergeRequestIntegrationIde.diff.DiffViewAddCommentActionBase class DiffViewAddCommentAction : DiffViewAddCommentActionBase() ================================================ FILE: merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/DiffViewToggleCommentsAction.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeEE import net.ntworld.mergeRequestIntegrationIde.diff.DiffViewToggleCommentsActionBase class DiffViewToggleCommentsAction : DiffViewToggleCommentsActionBase() ================================================ FILE: merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/EnterpriseApplicationServiceProvider.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeEE import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.openapi.project.Project import net.ntworld.mergeRequest.ProviderData import net.ntworld.mergeRequestIntegrationIde.infrastructure.AbstractApplicationServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider @State(name = "MergeRequestIntegrationApplicationLevel", storages = [(Storage("merge-request-integration.xml"))]) class EnterpriseApplicationServiceProvider: AbstractApplicationServiceProvider() { override fun isLegal(providerData: ProviderData): Boolean { if (!super.isLegal(providerData)) { return CheckLicense.isLicensed } return true } override fun findProjectServiceProvider(project: Project): ProjectServiceProvider { val service = ServiceManager.getService(project, EnterpriseProjectServiceProvider::class.java) registerProjectServiceProvider(service) return service } override val singleMRToolWindowName: String = "Merge Request" } ================================================ FILE: merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/EnterpriseProjectServiceProvider.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeEE import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.openapi.project.Project as IdeaProject import net.ntworld.mergeRequestIntegrationIde.infrastructure.AbstractProjectServiceProvider import net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider @State(name = "MergeRequestIntegrationProjectLevel", storages = [(Storage("merge-request-integration-ee.xml"))]) class EnterpriseProjectServiceProvider(ideaProject: IdeaProject) : AbstractProjectServiceProvider(ideaProject) { override val applicationServiceProvider: ApplicationServiceProvider = ServiceManager.getService( EnterpriseApplicationServiceProvider::class.java ) init { initWithApplicationServiceProvider(applicationServiceProvider) } } ================================================ FILE: merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/GithubConnectionsConfigurable.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeEE import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.project.Project import net.ntworld.mergeRequestIntegrationIde.ui.configuration.GithubConnectionsConfigurableBase class GithubConnectionsConfigurable(project: Project) : GithubConnectionsConfigurableBase( ServiceManager.getService(project, EnterpriseProjectServiceProvider::class.java) ) { override fun getId(): String = "MRI:github-ee" override fun getDisplayName(): String = "Github" } ================================================ FILE: merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/GitlabConnectionsConfigurable.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeEE import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.project.Project import net.ntworld.mergeRequestIntegrationIde.ui.configuration.GitlabConnectionsConfigurableBase class GitlabConnectionsConfigurable(project: Project) : GitlabConnectionsConfigurableBase( ServiceManager.getService(project, EnterpriseProjectServiceProvider::class.java) ) { override fun getId(): String = "MRI:gitlab-ee" override fun getDisplayName(): String = "Gitlab" } ================================================ FILE: merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/MainToolWindowFactory.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeEE import com.intellij.openapi.components.ServiceManager import net.ntworld.mergeRequestIntegrationIde.ui.MainToolWindowFactoryBase class MainToolWindowFactory : MainToolWindowFactoryBase( ServiceManager.getService(EnterpriseApplicationServiceProvider::class.java) ) ================================================ FILE: merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/SingleMRToolWindowFactory.kt ================================================ package net.ntworld.mergeRequestIntegrationIdeEE import com.intellij.openapi.components.ServiceManager import net.ntworld.mergeRequestIntegrationIde.toolWindow.SingleMRToolWindowFactoryBase class SingleMRToolWindowFactory : SingleMRToolWindowFactoryBase( ServiceManager.getService(EnterpriseApplicationServiceProvider::class.java) ) ================================================ FILE: merge-request-integration-ee/src/main/resources/META-INF/plugin.xml ================================================ net.ntworld.nhat-phan.merge-request-integration-ee Merge Request Integration EE - Code Review for GitLab ntworld 2019.3.2 com.intellij.modules.platform com.intellij.modules.lang com.intellij.modules.vcs Git4Idea ================================================ FILE: settings.gradle.kts ================================================ /* * This file was generated by the Gradle 'init' task. * * The settings file is used to specify which projects to include in your build. * * Detailed information about configuring a multi-project build in Gradle can be found * in the user manual at https://docs.gradle.org/5.3.1/userguide/multi_project_builds.html */ pluginManagement { resolutionStrategy { eachPlugin { if (requested.id.id == "kotlin-multiplatform") { useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}") } if (requested.id.id == "kotlinx-serialization") { useModule("org.jetbrains.kotlin:kotlin-serialization:${requested.version}") } } } } rootProject.name = "merge-request-integration-workspace" include("contracts") include("merge-request-integration") include("merge-request-integration-core") include("merge-request-integration-ce") include("merge-request-integration-ee")