Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 50 additions & 9 deletions Muxy/Services/RemoteServerDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,27 @@ final class RemoteServerDelegate: MuxyRemoteServerDelegate {
return VCSCreatePRResultDTO(url: info.url, number: info.number)
}

func vcsMergePullRequest(
projectID: UUID,
number: Int,
method: VCSMergeMethodDTO,
deleteBranch: Bool
) async throws {
let repoPath = try repoPath(projectID: projectID)
let mergeMethod: GitRepositoryService.PRMergeMethod = switch method {
case .merge: .merge
case .squash: .squash
case .rebase: .rebase
}
try await gitService.mergePullRequest(
repoPath: repoPath,
number: number,
method: mergeMethod,
deleteBranch: deleteBranch
)
notifyRepoDidChange(repoPath: repoPath)
}

func vcsAddWorktree(
projectID: UUID,
name: String,
Expand Down Expand Up @@ -585,15 +606,7 @@ final class RemoteServerDelegate: MuxyRemoteServerDelegate {

private static func toStatusDTO(_ state: VCSTabState) -> VCSStatusDTO? {
guard let branch = state.branchName else { return nil }
let pullRequest: VCSPullRequestDTO? = state.pullRequestInfo.map { info in
VCSPullRequestDTO(
url: info.url,
number: info.number,
state: info.state.rawValue,
isDraft: info.isDraft,
baseBranch: info.baseBranch
)
}
let pullRequest = state.pullRequestInfo.map(Self.toPullRequestDTO)
return VCSStatusDTO(
branch: branch,
aheadCount: state.aheadBehind.ahead,
Expand All @@ -606,6 +619,34 @@ final class RemoteServerDelegate: MuxyRemoteServerDelegate {
)
}

private static func toPullRequestDTO(_ info: GitRepositoryService.PRInfo) -> VCSPullRequestDTO {
VCSPullRequestDTO(
url: info.url,
number: info.number,
state: info.state.rawValue,
isDraft: info.isDraft,
baseBranch: info.baseBranch,
mergeable: info.mergeable,
mergeStateStatus: info.mergeStateStatus.rawValue,
checks: VCSPRChecksDTO(
status: Self.checksStatusString(info.checks.status),
passing: info.checks.passing,
failing: info.checks.failing,
pending: info.checks.pending,
total: info.checks.total
)
)
}

private static func checksStatusString(_ status: GitRepositoryService.PRChecksStatus) -> String {
switch status {
case .none: "none"
case .pending: "pending"
case .success: "success"
case .failure: "failure"
}
}

private static func toFileDTO(_ file: GitStatusFile, staged: Bool) -> GitFileDTO {
let statusChar = staged ? file.xStatus : file.yStatus
let isUntracked = file.xStatus == "?" && file.yStatus == "?"
Expand Down
17 changes: 17 additions & 0 deletions MuxyServer/MuxyRemoteServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public protocol MuxyRemoteServerDelegate: AnyObject {
func vcsSwitchBranch(projectID: UUID, branch: String) async throws
func vcsCreateBranch(projectID: UUID, name: String) async throws
func vcsCreatePR(projectID: UUID, title: String, body: String, baseBranch: String?, draft: Bool) async throws -> VCSCreatePRResultDTO
func vcsMergePullRequest(projectID: UUID, number: Int, method: VCSMergeMethodDTO, deleteBranch: Bool) async throws
func vcsAddWorktree(projectID: UUID, name: String, branch: String, createBranch: Bool) async throws -> WorktreeDTO
func vcsRemoveWorktree(projectID: UUID, worktreeID: UUID) async throws
func getProjectLogo(projectID: UUID) -> ProjectLogoDTO?
Expand Down Expand Up @@ -576,6 +577,22 @@ public final class MuxyRemoteServer: @unchecked Sendable {
return MuxyResponse(id: request.id, error: MuxyError(code: 500, message: error.localizedDescription))
}

case .vcsMergePullRequest:
guard case let .vcsMergePullRequest(params) = request.params else {
return MuxyResponse(id: request.id, error: .invalidParams)
}
do {
try await delegate.vcsMergePullRequest(
projectID: params.projectID,
number: params.number,
method: params.method,
deleteBranch: params.deleteBranch
)
return MuxyResponse(id: request.id, result: .ok)
} catch {
return MuxyResponse(id: request.id, error: MuxyError(code: 500, message: error.localizedDescription))
}

case .vcsAddWorktree:
guard case let .vcsAddWorktree(params) = request.params else {
return MuxyResponse(id: request.id, error: .invalidParams)
Expand Down
5 changes: 5 additions & 0 deletions MuxyShared/MuxyProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public enum MuxyMethod: String, Codable, Sendable {
case vcsSwitchBranch
case vcsCreateBranch
case vcsCreatePR
case vcsMergePullRequest
case vcsAddWorktree
case vcsRemoveWorktree
case getProjectLogo
Expand Down Expand Up @@ -131,6 +132,7 @@ public enum MuxyParams: Codable, Sendable {
case vcsSwitchBranch(VCSSwitchBranchParams)
case vcsCreateBranch(VCSCreateBranchParams)
case vcsCreatePR(VCSCreatePRParams)
case vcsMergePullRequest(VCSMergePullRequestParams)
case vcsAddWorktree(VCSAddWorktreeParams)
case vcsRemoveWorktree(VCSRemoveWorktreeParams)
case getProjectLogo(GetProjectLogoParams)
Expand Down Expand Up @@ -178,6 +180,7 @@ public enum MuxyParams: Codable, Sendable {
case "vcsSwitchBranch": self = try .vcsSwitchBranch(container.decode(VCSSwitchBranchParams.self, forKey: .value))
case "vcsCreateBranch": self = try .vcsCreateBranch(container.decode(VCSCreateBranchParams.self, forKey: .value))
case "vcsCreatePR": self = try .vcsCreatePR(container.decode(VCSCreatePRParams.self, forKey: .value))
case "vcsMergePullRequest": self = try .vcsMergePullRequest(container.decode(VCSMergePullRequestParams.self, forKey: .value))
case "vcsAddWorktree": self = try .vcsAddWorktree(container.decode(VCSAddWorktreeParams.self, forKey: .value))
case "vcsRemoveWorktree": self = try .vcsRemoveWorktree(container.decode(VCSRemoveWorktreeParams.self, forKey: .value))
case "getProjectLogo": self = try .getProjectLogo(container.decode(GetProjectLogoParams.self, forKey: .value))
Expand Down Expand Up @@ -253,6 +256,8 @@ public enum MuxyParams: Codable, Sendable {
try container.encode(v, forKey: .value)
case let .vcsCreatePR(v): try container.encode("vcsCreatePR", forKey: .type)
try container.encode(v, forKey: .value)
case let .vcsMergePullRequest(v): try container.encode("vcsMergePullRequest", forKey: .type)
try container.encode(v, forKey: .value)
case let .vcsAddWorktree(v): try container.encode("vcsAddWorktree", forKey: .type)
try container.encode(v, forKey: .value)
case let .vcsRemoveWorktree(v): try container.encode("vcsRemoveWorktree", forKey: .type)
Expand Down
19 changes: 19 additions & 0 deletions MuxyShared/ProtocolParams.swift
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,25 @@ public struct VCSCreatePRParams: Codable, Sendable {
}
}

public enum VCSMergeMethodDTO: String, Codable, Sendable {
case merge
case squash
case rebase
}

public struct VCSMergePullRequestParams: Codable, Sendable {
public let projectID: UUID
public let number: Int
public let method: VCSMergeMethodDTO
public let deleteBranch: Bool
public init(projectID: UUID, number: Int, method: VCSMergeMethodDTO, deleteBranch: Bool) {
self.projectID = projectID
self.number = number
self.method = method
self.deleteBranch = deleteBranch
}
}

public struct VCSAddWorktreeParams: Codable, Sendable {
public let projectID: UUID
public let name: String
Expand Down
33 changes: 32 additions & 1 deletion MuxyShared/VCSStatusDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,44 @@ public struct VCSPullRequestDTO: Codable, Sendable, Hashable {
public let state: String
public let isDraft: Bool
public let baseBranch: String
public let mergeable: Bool?
public let mergeStateStatus: String
public let checks: VCSPRChecksDTO

public init(url: String, number: Int, state: String, isDraft: Bool, baseBranch: String) {
public init(
url: String,
number: Int,
state: String,
isDraft: Bool,
baseBranch: String,
mergeable: Bool? = nil,
mergeStateStatus: String = "UNKNOWN",
checks: VCSPRChecksDTO = VCSPRChecksDTO(status: "none", passing: 0, failing: 0, pending: 0, total: 0)
) {
self.url = url
self.number = number
self.state = state
self.isDraft = isDraft
self.baseBranch = baseBranch
self.mergeable = mergeable
self.mergeStateStatus = mergeStateStatus
self.checks = checks
}
}

public struct VCSPRChecksDTO: Codable, Sendable, Hashable {
public let status: String
public let passing: Int
public let failing: Int
public let pending: Int
public let total: Int

public init(status: String, passing: Int, failing: Int, pending: Int, total: Int) {
self.status = status
self.passing = passing
self.failing = failing
self.pending = pending
self.total = total
}
}

Expand Down
39 changes: 39 additions & 0 deletions Tests/MuxyTests/Remote/MuxyRemoteServerRoutingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ private final class MockDelegate: MuxyRemoteServerDelegate {
var vcsSwitchBranchCalls: [(projectID: UUID, branch: String)] = []
var vcsCreateBranchCalls: [(projectID: UUID, name: String)] = []
var vcsCreatePRCalls: [(projectID: UUID, title: String, body: String, baseBranch: String?, draft: Bool)] = []
var vcsMergePullRequestCalls: [(projectID: UUID, number: Int, method: VCSMergeMethodDTO, deleteBranch: Bool)] = []
var vcsAddWorktreeCalls: [(projectID: UUID, name: String, branch: String, createBranch: Bool)] = []
var vcsRemoveWorktreeCalls: [(projectID: UUID, worktreeID: UUID)] = []

Expand Down Expand Up @@ -146,6 +147,15 @@ private final class MockDelegate: MuxyRemoteServerDelegate {
return stubCreatePRResult
}

func vcsMergePullRequest(
projectID: UUID,
number: Int,
method: VCSMergeMethodDTO,
deleteBranch: Bool
) async throws {
vcsMergePullRequestCalls.append((projectID, number, method, deleteBranch))
}

func vcsAddWorktree(
projectID: UUID,
name: String,
Expand Down Expand Up @@ -510,6 +520,35 @@ struct MuxyRemoteServerRoutingTests {
#expect(result.number == delegate.stubCreatePRResult.number)
}

@Test("vcs merge pull request route forwards params")
func vcsMergePullRequestRoute() async {
let (server, delegate) = makeServer()
let projectID = UUID()

let response = await server.processRequest(
MuxyRequest(
id: "8m",
method: .vcsMergePullRequest,
params: .vcsMergePullRequest(VCSMergePullRequestParams(
projectID: projectID,
number: 42,
method: .squash,
deleteBranch: true
))
),
clientID: authedClient(on: server)
)

#expect(delegate.vcsMergePullRequestCalls.first?.projectID == projectID)
#expect(delegate.vcsMergePullRequestCalls.first?.number == 42)
#expect(delegate.vcsMergePullRequestCalls.first?.method == .squash)
#expect(delegate.vcsMergePullRequestCalls.first?.deleteBranch == true)
guard case .ok = response.result else {
Issue.record("expected ok result")
return
}
}

@Test("vcs worktree routes forward input and output")
func vcsWorktreeRoutes() async {
let (server, delegate) = makeServer()
Expand Down
1 change: 1 addition & 0 deletions docs/features/remote-server/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Notes:
| `vcsSwitchBranch` | `projectID`, `branch` | `ok` |
| `vcsCreateBranch` | `projectID`, `name` | `ok` |
| `vcsCreatePR` | `projectID`, `title`, `body`, `baseBranch`, `draft` | `vcsPRCreated` |
| `vcsMergePullRequest` | `projectID`, `number`, `method`, `deleteBranch` | `ok` |
| `vcsAddWorktree` | `projectID`, `name`, `branch`, `createBranch` | `worktrees` |
| `vcsRemoveWorktree` | `projectID`, `worktreeID` | `ok` |

Expand Down
Loading