Remove Swift file length budget (#8125)
Co-authored-by: cmux reload-cloud <[email protected]>
This commit is contained in:
co-authored by
cmux reload-cloud
parent
671aff6812
commit
85ded20f53
+3
-3
@@ -36,7 +36,7 @@ reviews:
|
||||
Apply `.github/review-bot-rules/react-base-ui-accessibility.md` during review. For custom React UI, require `@base-ui-components/react` or an existing local component when it provides the relevant dialog, popover, menu, checkbox, select, switch, tabs, tooltip, combobox, focus, or keyboard behavior. Pass for native semantic controls and for cases with no relevant primitive where the PR owns complete accessibility and keyboard behavior.
|
||||
- path: "**/Package.swift"
|
||||
instructions: |
|
||||
Apply the cmux custom Swift lint rules in `.github/review-bot-rules/`, especially the concurrency modernization, actor isolation, blocking runtime, file/package boundary, and architectural rethink rules.
|
||||
Apply the cmux custom Swift lint rules in `.github/review-bot-rules/`, especially the concurrency modernization, actor isolation, blocking runtime, package boundary, and architectural rethink rules.
|
||||
- path: "**/Package.resolved"
|
||||
instructions: |
|
||||
Apply `.github/review-bot-rules/swiftpm-package-resolved.md` during review. cmux-owned SwiftPM lockfiles are intentional source-of-truth files, not accidental artifacts; dependency pin changes must be visible in PR diffs.
|
||||
@@ -142,10 +142,10 @@ reviews:
|
||||
mode: error
|
||||
instructions: |
|
||||
For Swift changes, fail when the diff violates `.github/review-bot-rules/swift-concurrent-annotation.md`: missing `@concurrent` on `nonisolated async` work that should leave the caller actor, invalid `@concurrent` on synchronous or actor-isolated functions, or CPU/file/network-heavy async helpers called from UI isolation without an explicit hop. Pass for intentionally UI-bound async work.
|
||||
- name: "cmux Swift file and package boundaries"
|
||||
- name: "cmux Swift package boundaries"
|
||||
mode: error
|
||||
instructions: |
|
||||
For production Swift changes, fail when the diff violates `.github/review-bot-rules/swift-file-package-boundaries.md`: new oversized files, large additions to already oversized files, mixed UI/state/persistence/network/parsing/protocol responsibilities in one file, or independently testable feature logic kept in the app target when it should live behind a small SwiftPM package target. Pass for existing oversized files touched incidentally, small UI/AppKit/Ghostty glue, generated/vendored/prototype/test code, and focused bug fixes that preserve a clear extraction path.
|
||||
For production Swift changes, fail when the diff violates `.github/review-bot-rules/swift-package-boundaries.md`: independently testable or reusable domain logic is kept in the app target when it should live behind a small SwiftPM package target. Pass for small UI/AppKit/Ghostty glue, generated/vendored/prototype/test code, and app-lifecycle composition.
|
||||
- name: "cmux SwiftPM lockfiles"
|
||||
mode: error
|
||||
instructions: |
|
||||
|
||||
@@ -27,7 +27,7 @@ Current rules:
|
||||
- `swift-concurrency-modernization.md`
|
||||
- `swift-concurrent-annotation.md`
|
||||
- `swift-expensive-sync-load.md`
|
||||
- `swift-file-package-boundaries.md`
|
||||
- `swift-package-boundaries.md`
|
||||
- `swift-logging.md`
|
||||
- `swiftui-state-layout.md`
|
||||
- `user-facing-errors.md`
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# Swift File And Package Boundaries
|
||||
|
||||
Flag Swift changes that add too much unrelated responsibility to one file or keep independently testable feature logic inside the app target when it should be isolated behind a SwiftPM package boundary.
|
||||
|
||||
cmux already has a checked-in Swift file length budget reference (`.github/swift-file-length-budget.tsv` and `scripts/swift_file_length_budget.py`). The CI gate is diff-aware: small incidental growth in an existing over-budget file is allowed, while new large files, meaningful PR-local growth, and hard-cap violations still fail. This rule is the semantic review layer: do not satisfy it by mechanically moving code around, and do not expand the TSV budget for a feature that should instead be split by responsibility or extracted into a package.
|
||||
|
||||
Report a failure when the diff introduces or materially expands:
|
||||
|
||||
- A new production Swift file over 400 lines without a clear single responsibility, or over 800 lines even when the responsibility is mostly coherent.
|
||||
- More than 250 lines added to an existing production Swift file that is already over 800 lines. Treat an extraction exception as met only when the PR removes or moves one of the mixed responsibilities listed below out of the file, or documents that responsibility behind a new package boundary, and the file's total line count decreases by more than 200 lines. A new SwiftPM package target can satisfy this exception when the oversized file also shrinks by more than 200 lines because code moved into that target.
|
||||
- A file that mixes UI rendering, state ownership, persistence, networking, parsing, subprocess/socket protocol, and platform bridge code in one place.
|
||||
- A feature implemented directly in the app target/module's root `Sources/` path when its core logic is independent of cmux app lifecycle and can compile/test without AppKit, SwiftUI view state, Ghostty globals, or process-wide singletons.
|
||||
- Reusable domain logic used by more than one surface (Mac app, CLI, daemon, tests, previews, debug tooling, future iOS/shared code) without a small SwiftPM package target.
|
||||
- Provider, auth, protocol, parsing, persistence, logging, or workstream logic that needs isolated fakes, fixtures, or unit tests but is hidden behind app-target globals.
|
||||
- A PR that primarily updates `.github/swift-file-length-budget.tsv` to accept meaningful growth instead of reducing the large file, splitting responsibilities, or adding a package boundary.
|
||||
|
||||
Line counting follows the existing budget script as a shared measurement convention: count physical lines including blank lines; scan cmux-owned Swift files under `Sources`, `CLI`, `Packages`, `cmuxTests`, and `cmuxUITests`; exclude whole path subtrees containing `/vendor/`, `/ghostty/`, `/homebrew-cmux/`, `/.build/`, `/SourcePackages/`, or `/.ci-source-packages/`; and use 500 lines as the tracked-file reference threshold from `.github/swift-file-length-budget.tsv`. For this LLM rule, use the post-change physical file length when visible; use PR added-line count only for the "more than 250 lines added" growth check. Do not object to a small focused bug fix merely because it adds a few lines to an existing oversized file.
|
||||
|
||||
Package-boundary signals:
|
||||
|
||||
- The code has a stable domain noun and public API that can be expressed without view types.
|
||||
- The code needs tests that should run without launching cmux or constructing app UI.
|
||||
- The code owns data formats, network/provider contracts, socket messages, credentials, persistence schemas, or cross-surface state transitions.
|
||||
- The feature would be safer if callers depended on a small protocol or value API instead of a concrete app singleton.
|
||||
|
||||
Allowed cases:
|
||||
|
||||
- Existing oversized files that the PR only touches incidentally.
|
||||
- Small UI-only views, AppKit bridges, app delegates, menu wiring, and Ghostty integration glue that are inherently app-target code.
|
||||
- Focused bug fixes that add a small amount of code to a large file while preserving a clear extraction path.
|
||||
- Generated files, vendored code, prototypes, and test fixtures.
|
||||
- New package creation that starts small and intentionally leaves app-specific UI composition in the app target/module-root `Sources/` directory.
|
||||
|
||||
When reporting, include the file or feature boundary, the approximate line-count pressure, the responsibilities being mixed, and the smallest extraction cut. If a package is the right shape, name the proposed package target and the first public type or protocol it should expose.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Swift Package Boundaries
|
||||
|
||||
Flag Swift changes that keep independently testable feature logic inside the app target when it should be isolated behind a SwiftPM package boundary.
|
||||
|
||||
Report a failure when the diff introduces or materially expands:
|
||||
|
||||
- A feature implemented directly in the app target/module's root `Sources/` path when its core logic is independent of cmux app lifecycle and can compile/test without AppKit, SwiftUI view state, Ghostty globals, or process-wide singletons.
|
||||
- Reusable domain logic used by more than one surface (Mac app, CLI, daemon, tests, previews, debug tooling, future iOS/shared code) without a small SwiftPM package target.
|
||||
- Provider, auth, protocol, parsing, persistence, logging, or workstream logic that needs isolated fakes, fixtures, or unit tests but is hidden behind app-target globals.
|
||||
|
||||
Package-boundary signals:
|
||||
|
||||
- The code has a stable domain noun and public API that can be expressed without view types.
|
||||
- The code needs tests that should run without launching cmux or constructing app UI.
|
||||
- The code owns data formats, network/provider contracts, socket messages, credentials, persistence schemas, or cross-surface state transitions.
|
||||
- The feature would be safer if callers depended on a small protocol or value API instead of a concrete app singleton.
|
||||
|
||||
Allowed cases:
|
||||
|
||||
- Small UI-only views, AppKit bridges, app delegates, menu wiring, and Ghostty integration glue that are inherently app-target code.
|
||||
- Generated files, vendored code, prototypes, and test fixtures.
|
||||
- New package creation that starts small and intentionally leaves app-specific UI composition in the app target/module-root `Sources/` directory.
|
||||
|
||||
When reporting, include the feature boundary and the smallest extraction cut. Name the proposed package target and the first public type or protocol it should expose.
|
||||
@@ -1,257 +0,0 @@
|
||||
# cmux-owned Swift file length budget.
|
||||
# Format: max_lines<TAB>relative path
|
||||
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
|
||||
35405 CLI/cmux.swift
|
||||
17782 Sources/AppDelegate.swift
|
||||
15818 Sources/ContentView.swift
|
||||
14701 Sources/TerminalController.swift
|
||||
12829 Sources/Workspace.swift
|
||||
12554 Sources/GhosttyTerminalView.swift
|
||||
12262 cmuxTests/AppDelegateShortcutRoutingTests.swift
|
||||
11332 Sources/Panels/BrowserPanel.swift
|
||||
9497 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
|
||||
7968 CLI/cmux_open.swift
|
||||
7756 Sources/Panels/BrowserPanelView.swift
|
||||
7489 cmuxTests/WorkspaceUnitTests.swift
|
||||
7473 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
|
||||
7310 cmuxTests/WorkspaceRemoteConnectionTests.swift
|
||||
6359 cmuxTests/SessionPersistenceTests.swift
|
||||
6255 cmuxTests/GhosttyConfigTests.swift
|
||||
6188 Sources/TabManager.swift
|
||||
5857 cmuxTests/TerminalAndGhosttyTests.swift
|
||||
5782 Sources/TextBoxInput.swift
|
||||
5571 cmuxTests/BrowserConfigTests.swift
|
||||
4735 Sources/cmuxApp.swift
|
||||
4482 Sources/Panels/FilePreviewPanel.swift
|
||||
4196 cmuxTests/BrowserPanelTests.swift
|
||||
4004 cmuxTests/TabManagerUnitTests.swift
|
||||
3965 Sources/BrowserWindowPortal.swift
|
||||
3953 cmuxTests/WindowAndDragTests.swift
|
||||
3934 Sources/Feed/FeedPanelView.swift
|
||||
3779 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
|
||||
3673 cmuxTests/TabManagerSessionSnapshotTests.swift
|
||||
3668 cmuxTests/CLIGenericHookPersistenceTests.swift
|
||||
3314 Sources/CmuxConfig.swift
|
||||
2882 Sources/Update/UpdateTitlebarAccessory.swift
|
||||
2875 Sources/SessionIndexView.swift
|
||||
2874 cmuxTests/CMUXOpenCommandTests.swift
|
||||
2561 Sources/Panels/CmuxWebView.swift
|
||||
2558 Sources/KeyboardShortcutSettings.swift
|
||||
2546 cmuxTests/WorkspaceManualUnreadTests.swift
|
||||
2524 cmuxTests/CommandPaletteSearchEngineTests.swift
|
||||
2328 cmuxTests/CJKIMEInputTests.swift
|
||||
2257 Sources/Mobile/MobileHostService.swift
|
||||
2229 Sources/TerminalWindowPortal.swift
|
||||
2216 Sources/TerminalNotificationStore.swift
|
||||
2186 Sources/RestorableAgentSession.swift
|
||||
2133 cmuxTests/ShortcutAndCommandPaletteTests.swift
|
||||
2126 cmuxTests/CmuxConfigTests.swift
|
||||
1999 Sources/SessionPersistence.swift
|
||||
1983 Sources/KeyboardShortcutSettingsFileStore.swift
|
||||
1900 cmuxTests/NotificationAndMenuBarTests.swift
|
||||
1866 Sources/Panels/BrowserWebAuthnSupport.swift
|
||||
1847 cmuxTests/TerminalControllerSocketSecurityTests.swift
|
||||
1810 Sources/SessionIndexStore.swift
|
||||
1760 Sources/WindowDragHandleView.swift
|
||||
1732 cmuxTests/WorkspacePullRequestSidebarTests.swift
|
||||
1687 cmuxTests/MarkdownPanelTests.swift
|
||||
1680 cmuxUITests/BrowserPaneNavigationKeybindUITests.swift
|
||||
1656 Sources/FileExplorerView.swift
|
||||
1652 cmuxTests/CMUXCLIErrorOutputRegressionTests.swift
|
||||
1597 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift
|
||||
1560 cmuxTests/TextBoxMentionCompletionTests.swift
|
||||
1523 cmuxTests/RestorableAgentSessionIndexTests.swift
|
||||
1499 cmuxTests/OmnibarAndToolsTests.swift
|
||||
1498 cmuxUITests/MultiWindowNotificationsUITests.swift
|
||||
1428 cmuxTests/AgentSessionAutoResumeSwiftTests.swift
|
||||
1420 cmuxTests/AppDelegateIssue2907RoutingTests.swift
|
||||
1384 cmuxTests/KeyboardShortcutSettingsFileStoreStartupTests.swift
|
||||
1380 cmuxUITests/MenuKeyEquivalentRoutingUITests.swift
|
||||
1363 Sources/CMUXInstalledExtensionSidebarHostView.swift
|
||||
1360 Sources/Feed/FeedButtonStyleDebugWindowController.swift
|
||||
1317 Sources/FileExplorerStore.swift
|
||||
1290 Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/Config/GhosttyConfig.swift
|
||||
1290 cmuxTests/TextBoxSubmitActionTests.swift
|
||||
1285 cmuxUITests/SidebarHelpMenuUITests.swift
|
||||
1270 cmuxTests/MobileHostAuthorizationTests.swift
|
||||
1258 Sources/Feed/FeedCoordinator.swift
|
||||
1240 cmuxTests/SidebarOrderingTests.swift
|
||||
1209 Packages/macOS/CmuxCommandPalette/Tests/CmuxCommandPaletteTests/CommandPaletteSearchEngineTests.swift
|
||||
1204 cmuxTests/FileExplorerStoreTests.swift
|
||||
1197 cmuxTests/CodexAppServerSessionTests.swift
|
||||
1197 cmuxTests/VMDefaultCloudCommandTests.swift
|
||||
1147 cmuxTests/PiVaultAgentPersistenceTests.swift
|
||||
1117 cmuxTests/AgentHibernationTests.swift
|
||||
1093 cmuxUITests/BonsplitTabDragUITests.swift
|
||||
1087 Packages/macOS/CmuxCommandPalette/Sources/CmuxCommandPalette/Search/CommandPaletteFuzzyMatcher.swift
|
||||
1049 cmuxTests/WorkspaceGroupTests.swift
|
||||
1038 Sources/AppDelegate+CmuxSSHURL.swift
|
||||
1030 Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalViewportResyncTests.swift
|
||||
1021 cmuxUITests/TerminalCmdClickUITests.swift
|
||||
1009 cmuxTests/CmuxTopSnapshotScopeTests.swift
|
||||
1006 cmuxTests/CmuxSSHURLRequestTests.swift
|
||||
1000 Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/PairedMacBackupTests.swift
|
||||
999 cmuxTests/DockSocketLifecycleTests.swift
|
||||
982 Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/ChatConversationStoreTests.swift
|
||||
951 Sources/App/TerminalDirectoryOpenSupport.swift
|
||||
948 Sources/App/ShortcutRoutingSupport.swift
|
||||
947 Sources/TerminalNotificationPolicy.swift
|
||||
945 Sources/SessionIndexRegisteredAgents.swift
|
||||
944 Sources/CommandPalette/CommandPaletteSettingsToggle.swift
|
||||
943 Sources/Cloud/VMClient.swift
|
||||
942 Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/WorkspaceMacSelectionTests.swift
|
||||
929 Sources/Panels/TerminalPanel.swift
|
||||
918 Sources/Panels/BrowserPopupWindowController.swift
|
||||
905 Sources/CmuxSSHURLRequest.swift
|
||||
904 Sources/VaultAgentProcessScanner.swift
|
||||
899 Sources/Panels/MarkdownWebRenderer.swift
|
||||
896 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift
|
||||
895 Sources/RemoteTmuxControlConnection.swift
|
||||
885 cmuxTests/SidebarWorkspaceDropPlannerTests.swift
|
||||
878 Sources/PortScanner.swift
|
||||
871 cmuxTests/ClaudeHookSurfaceResolutionSwiftTests.swift
|
||||
868 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Workspace/ControlCommandCoordinator+Workspace.swift
|
||||
868 Sources/Panels/BrowserScreenshotSnapshotter.swift
|
||||
865 Sources/DockSplitStore.swift
|
||||
864 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Store/ChatConversationStore.swift
|
||||
858 Sources/PricingPlansScreen.swift
|
||||
856 Sources/TextBoxMentionIndexStore.swift
|
||||
847 cmuxTests/AgentSessionAutoResumeSettingsTests.swift
|
||||
844 cmuxTests/SSHStartupSignalLifecycleTests.swift
|
||||
837 Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalOutputDeliveryQueueTests.swift
|
||||
834 Sources/MainWindowFocusController.swift
|
||||
825 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift
|
||||
822 Sources/WorkspaceContentView.swift
|
||||
812 Sources/TerminalController+ControlPaneContext.swift
|
||||
810 Packages/macOS/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/SwiftViewInterpreterTests.swift
|
||||
801 Sources/ClosedItemHistory.swift
|
||||
798 cmuxTests/CmuxEventBusTests.swift
|
||||
795 Sources/RemoteTmuxController.swift
|
||||
786 Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+Input.swift
|
||||
779 cmuxUITests/BrowserOmnibarSuggestionsUITests.swift
|
||||
773 Sources/App/MenuBarExtraController.swift
|
||||
768 cmuxUITests/BrowserFixtureInteractionUITests.swift
|
||||
766 Sources/Mobile/AgentChat/AgentChatSessionRegistry.swift
|
||||
756 Sources/Panels/AgentSessionWebRendererCoordinator.swift
|
||||
754 cmuxTests/GhosttyTerminalStartupEnvironmentTests.swift
|
||||
752 cmuxUITests/CloseWorkspaceCmdDUITests.swift
|
||||
747 Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
|
||||
738 Packages/macOS/CMUXProjectModel/Sources/CMUXProjectModel/XcodeProjectAdapter.swift
|
||||
736 Sources/TerminalController+ControlWorkspaceContext.swift
|
||||
722 Sources/TaskManagerTypes.swift
|
||||
716 Sources/TaskManagerSnapshot.swift
|
||||
714 Sources/AppleScriptSupport.swift
|
||||
713 cmuxTests/UpdatePillReleaseVisibilityTests.swift
|
||||
710 Sources/TerminalSSHSessionDetector.swift
|
||||
709 Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver.swift
|
||||
706 CLI/CMUXCLI+Config.swift
|
||||
699 cmuxTests/TerminalNotificationClearAllTests.swift
|
||||
698 cmuxTests/RestorableAgentHookProviderResumeTests.swift
|
||||
696 cmuxTests/KeyboardShortcutContextTests.swift
|
||||
691 Sources/NotificationSoundSettings.swift
|
||||
691 cmuxTests/TaskManagerResourcesTests.swift
|
||||
690 cmuxTests/SessionIndexViewTests.swift
|
||||
683 Packages/macOS/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift
|
||||
683 Sources/Panels/CodexAppServerSession.swift
|
||||
681 Sources/Panels/AgentSessionProcessStore.swift
|
||||
680 Sources/FileExplorerSearchController.swift
|
||||
677 Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+Bootstrap.swift
|
||||
672 cmuxTests/SessionPersistenceResumeBindingTests.swift
|
||||
668 cmuxTests/FeedCoordinatorTests.swift
|
||||
665 cmuxTests/CLICodexHookTimeoutRegressionTests.swift
|
||||
663 Packages/iOS/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift
|
||||
660 Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator.swift
|
||||
658 Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift
|
||||
657 Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridInputCatchUpTests.swift
|
||||
657 Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+RuntimeLifecycle.swift
|
||||
654 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift
|
||||
654 Sources/CmuxTopSnapshot.swift
|
||||
652 Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
|
||||
650 Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Import/Detection/BrowserInstalledBrowserDetector.swift
|
||||
650 Sources/Panels/MarkdownRemoteImageLoader.swift
|
||||
649 Packages/macOS/CmuxRemoteWorkspace/Sources/CmuxRemoteWorkspace/Tunnel/RemoteDaemonProxyTunnel.swift
|
||||
648 cmuxTests/TerminalNotificationQueueTests.swift
|
||||
644 Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentLaunchSanitizerTests.swift
|
||||
641 cmuxTests/CommandPaletteNucleoFFITests.swift
|
||||
637 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
|
||||
635 Sources/Panels/BrowserNavigationDelegate.swift
|
||||
635 cmuxUITests/RightSidebarChromeHeightUITests.swift
|
||||
633 Sources/SettingsNavigation.swift
|
||||
630 Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutWhenClause.swift
|
||||
627 Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift
|
||||
623 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface.swift
|
||||
620 cmuxTests/FinderFileDropRegressionTests.swift
|
||||
619 Sources/CmuxEventBus.swift
|
||||
608 Sources/SleepyFaceView.swift
|
||||
608 Sources/TextBoxSubmitActions.swift
|
||||
608 cmuxUITests/FeedSidebarUITests.swift
|
||||
607 Sources/SessionIndexModels.swift
|
||||
604 Packages/macOS/CmuxCommandPalette/Tests/CmuxCommandPaletteTests/CommandPaletteNucleoFFITests.swift
|
||||
601 Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift
|
||||
594 cmuxTests/PortalTabDragRoutingTests.swift
|
||||
591 Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/DefaultsValueModelLifecycleTests.swift
|
||||
591 cmuxTests/CmuxConfigContextMenuTests.swift
|
||||
588 cmuxTests/CommandPaletteShortcutCustomizationTests.swift
|
||||
586 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttyRuntime.swift
|
||||
586 Sources/JSONCParser.swift
|
||||
585 cmuxTests/SettingsWindowPresenterTests.swift
|
||||
583 Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTests.swift
|
||||
581 Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizerPrimaryPolicies.swift
|
||||
580 Packages/macOS/CmuxExtensionKit/Tests/CmuxExtensionKitTests/CmuxExtensionKitTests.swift
|
||||
580 cmuxTests/CLIHookNoResponseTests.swift
|
||||
579 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlCommandCoordinator+Pane.swift
|
||||
578 Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift
|
||||
577 cmuxTests/AppearanceSettingsTests.swift
|
||||
576 Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGrid.swift
|
||||
575 Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift
|
||||
572 Sources/Feed/FeedTextEditorDebugWindowController.swift
|
||||
567 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserSection.swift
|
||||
567 Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigDiscovery/GhosttyConfigDiscovery.swift
|
||||
562 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/BackingUpPairedMacStore.swift
|
||||
562 cmuxTests/AgentExecutableResolverTests.swift
|
||||
561 cmuxTests/GhosttyConfigPathResolverTests.swift
|
||||
560 cmuxTests/CLISSHPTYResizeInputTests.swift
|
||||
560 cmuxTests/RemoteTmuxControlParserTests.swift
|
||||
559 CLI/CMUXCLI+AgentHookDefinitions.swift
|
||||
558 Packages/macOS/CmuxGit/Sources/CmuxGit/Parsing/GitMetadataService+Config.swift
|
||||
553 Sources/RightSidebarPanelView.swift
|
||||
551 cmuxUITests/AutomationSocketUITests.swift
|
||||
550 Sources/CloudVMActionLauncher.swift
|
||||
549 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlCommandCoordinator+SidebarMetadataV1.swift
|
||||
549 Sources/Panels/BrowserAutomation.swift
|
||||
546 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AutomationSection.swift
|
||||
544 cmuxUITests/DisplayResolutionRegressionUITests.swift
|
||||
540 Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGridReplay.swift
|
||||
539 CLI/CMUXCLI+Themes.swift
|
||||
539 CLI/CodexTeamsApprovalBridge.swift
|
||||
538 Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/MobileTerminalRenderGridTests.swift
|
||||
538 Sources/TerminalController+ControlSurfaceContext2.swift
|
||||
537 Sources/App/WorkspaceRuntimeSettings.swift
|
||||
527 CLI/CLISocketPathResolver.swift
|
||||
527 cmuxTests/BrowserHTTPBasicAuthPromptTests.swift
|
||||
526 Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift
|
||||
525 Packages/macOS/CmuxSettings/Sources/CmuxSettings/SocketControl/SocketControlSettings.swift
|
||||
524 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Scene/SettingsWindowScene.swift
|
||||
522 Sources/TerminalPaneDropTargetView.swift
|
||||
520 CLI/CMUXCLI+AmpExtension.swift
|
||||
520 cmuxTests/MainWindowVisibilityControllerTests.swift
|
||||
519 CLI/CMUXCLI+AutoNaming.swift
|
||||
519 Packages/macOS/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/stress-two-column-cockpit-sidebar.swift
|
||||
518 Packages/macOS/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/stress-git-review-queue-command-deck.swift
|
||||
516 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
|
||||
516 Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift
|
||||
516 Sources/TerminalImageTransfer.swift
|
||||
514 Packages/macOS/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift
|
||||
514 cmuxUITests/UpdatePillUITests.swift
|
||||
511 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlCommandCoordinator+SidebarReportsV1.swift
|
||||
510 Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift
|
||||
510 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift
|
||||
509 Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizerAdditionalPolicies.swift
|
||||
507 Sources/TerminalControllerTopSupport.swift
|
||||
506 Sources/App/MainWindowVisibilityController.swift
|
||||
504 Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/UserDefaultsSettingsStoreTests.swift
|
||||
504 cmuxTests/TerminalNotificationSocketActionTests.swift
|
||||
503 Sources/Settings/ConfigSource.swift
|
||||
502 Sources/CmuxEventPublishing.swift
|
||||
501 Sources/TerminalNotificationQueue.swift
|
||||
500 Sources/KeyboardShortcutRecorder.swift
|
||||
|
@@ -207,9 +207,6 @@ jobs:
|
||||
- name: Validate Swift warning budget guard
|
||||
run: ./tests/test_ci_swift_warning_budget.sh
|
||||
|
||||
- name: Validate Swift file length budget guard
|
||||
run: ./tests/test_ci_swift_file_length_budget.sh
|
||||
|
||||
- name: Validate auxiliary window close shortcut lint
|
||||
run: ./tests/test_ci_auxiliary_window_close_shortcuts.sh
|
||||
|
||||
@@ -234,38 +231,6 @@ jobs:
|
||||
- name: Validate bash prompt bootstrap composes with user PROMPT_COMMAND (starship)
|
||||
run: python3 tests/test_issue_5164_starship_prompt_composition.py
|
||||
|
||||
- name: Validate Swift file length budget
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
BASE_BRANCH: ${{ github.event.pull_request.base.ref }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
BEFORE_SHA: ${{ github.event.before }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$EVENT_NAME" = "pull_request" ]; then
|
||||
BASE_REF="$BASE_SHA"
|
||||
if MERGE_BASE="$(git merge-base "$BASE_SHA" "$HEAD_SHA")"; then
|
||||
BASE_REF="$MERGE_BASE"
|
||||
fi
|
||||
python3 scripts/swift_file_length_budget.py \
|
||||
--budget .github/swift-file-length-budget.tsv \
|
||||
--base-ref "$BASE_REF" \
|
||||
--merge-ref "origin/${BASE_BRANCH:-main}" \
|
||||
--merge-head "$HEAD_SHA"
|
||||
elif [ "$EVENT_NAME" = "push" ] && [ -n "$BEFORE_SHA" ] && ! [[ "$BEFORE_SHA" =~ ^0+$ ]]; then
|
||||
python3 scripts/swift_file_length_budget.py \
|
||||
--budget .github/swift-file-length-budget.tsv \
|
||||
--base-ref "$BEFORE_SHA"
|
||||
elif git rev-parse --verify --quiet origin/main >/dev/null && MERGE_BASE="$(git merge-base origin/main HEAD)"; then
|
||||
python3 scripts/swift_file_length_budget.py \
|
||||
--budget .github/swift-file-length-budget.tsv \
|
||||
--base-ref "$MERGE_BASE"
|
||||
else
|
||||
python3 scripts/swift_file_length_budget.py \
|
||||
--budget .github/swift-file-length-budget.tsv
|
||||
fi
|
||||
|
||||
- name: Validate feature flags (naming, owners, expiry, single use, no reuse)
|
||||
run: python3 scripts/lint-feature-flags.py
|
||||
|
||||
|
||||
@@ -72,8 +72,8 @@
|
||||
"severity": "high"
|
||||
},
|
||||
{
|
||||
"id": "cmux-swift-file-package-boundaries",
|
||||
"rule": "Flag Swift changes that add too much unrelated responsibility to one file or miss a SwiftPM package boundary: new production Swift files over 400 lines without one responsibility, files over 800 lines, large additions to existing oversized files, mixed UI/state/persistence/network/parsing/protocol code, or independently testable feature logic kept in the app target instead of a small package.",
|
||||
"id": "cmux-swift-package-boundaries",
|
||||
"rule": "Flag independently testable or reusable Swift domain logic kept in the app target instead of a small SwiftPM package. Pass for app-lifecycle composition, small UI/AppKit/Ghostty glue, generated or vendored code, prototypes, and tests.",
|
||||
"scope": [
|
||||
"**/*.swift",
|
||||
"**/Package.swift",
|
||||
|
||||
@@ -114,8 +114,8 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".github/review-bot-rules/swift-file-package-boundaries.md",
|
||||
"description": "Source-of-truth cmux lint rule for Swift file size and SwiftPM package boundary review.",
|
||||
"path": ".github/review-bot-rules/swift-package-boundaries.md",
|
||||
"description": "Source-of-truth cmux lint rule for SwiftPM package boundary review.",
|
||||
"scope": [
|
||||
"**/*.swift",
|
||||
"**/Package.swift",
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ Review production Swift and runtime changes for:
|
||||
- Fixed sleeps, delays, and polling used as hacky synchronization.
|
||||
- Legacy concurrency patterns where Swift concurrency is available.
|
||||
- Incorrect `@concurrent` or `nonisolated async` behavior.
|
||||
- Swift file sprawl and missing SwiftPM package boundaries for independently testable feature logic.
|
||||
- Missing SwiftPM package boundaries for independently testable feature logic.
|
||||
- Production logging that bypasses unified logging or leaks sensitive data.
|
||||
- User-facing text that is not fully internationalized across every supported app or web locale.
|
||||
- SwiftUI state and layout patterns that cause stale state, broad invalidation, or render-time mutation.
|
||||
|
||||
+2
-3
@@ -6,9 +6,8 @@ import Foundation
|
||||
import Testing
|
||||
@testable import CmuxMobileShell
|
||||
|
||||
/// Regression tests for `stableMacColorSlots` resets on the account/team
|
||||
/// boundaries. Split from ``MobileShellCompositePreviewTests`` to keep that
|
||||
/// file under the Swift file length budget.
|
||||
/// Regression tests for `stableMacColorSlots` resets on account and team
|
||||
/// boundaries.
|
||||
@MainActor
|
||||
@Suite struct MobileShellCompositeColorSlotResetTests {
|
||||
@Test func signOutClearsStableMacColorSlots() {
|
||||
|
||||
@@ -5,8 +5,8 @@ import UserNotifications
|
||||
|
||||
// Notification sound selection, custom sound staging, Focus/DND suppression,
|
||||
// fallback playback, and notification custom-command execution.
|
||||
// Extracted from TerminalNotificationStore.swift to keep that file within the
|
||||
// Swift file length budget.
|
||||
// Kept separate from TerminalNotificationStore.swift so settings resolution
|
||||
// and notification delivery have distinct ownership.
|
||||
|
||||
enum NotificationSoundSettings {
|
||||
static let key = "notificationSound"
|
||||
|
||||
@@ -1,564 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check cmux-owned Swift file lengths against a checked-in budget."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
DEFAULT_ROOTS = ("Sources", "CLI", "Packages", "cmuxTests", "cmuxUITests")
|
||||
DEFAULT_THRESHOLD = 500
|
||||
DEFAULT_INCIDENTAL_GROWTH = 25
|
||||
DEFAULT_HARD_CAP = 900
|
||||
IGNORED_PATH_PARTS = (
|
||||
"/vendor/",
|
||||
"/ghostty/",
|
||||
"/homebrew-cmux/",
|
||||
"/.build/",
|
||||
"/SourcePackages/",
|
||||
"/.ci-source-packages/",
|
||||
)
|
||||
|
||||
|
||||
FileLengthBudget = dict[str, int]
|
||||
|
||||
|
||||
def is_ignored_path(path: pathlib.Path) -> bool:
|
||||
normalized = "/" + path.as_posix().lstrip("/")
|
||||
return any(part in normalized for part in IGNORED_PATH_PARTS)
|
||||
|
||||
|
||||
def count_lines(path: pathlib.Path) -> int:
|
||||
with path.open("r", encoding="utf-8", errors="replace") as handle:
|
||||
return sum(1 for _ in handle)
|
||||
|
||||
|
||||
def collect_file_lengths(repo_root: pathlib.Path, roots: tuple[str, ...]) -> FileLengthBudget:
|
||||
budget: FileLengthBudget = {}
|
||||
for root in roots:
|
||||
root_path = repo_root / root
|
||||
if not root_path.exists():
|
||||
continue
|
||||
|
||||
for path in sorted(root_path.rglob("*.swift")):
|
||||
rel_path = path.relative_to(repo_root)
|
||||
if is_ignored_path(rel_path):
|
||||
continue
|
||||
budget[rel_path.as_posix()] = count_lines(path)
|
||||
return budget
|
||||
|
||||
|
||||
def is_in_roots(rel_path: str, roots: tuple[str, ...]) -> bool:
|
||||
return any(rel_path == root or rel_path.startswith(f"{root}/") for root in roots)
|
||||
|
||||
|
||||
def count_blob_lines(content: bytes) -> int:
|
||||
return content.count(b"\n") + (0 if content.endswith(b"\n") or not content else 1)
|
||||
|
||||
|
||||
def list_tree_swift_paths(repo_root: pathlib.Path, tree: str, roots: tuple[str, ...]) -> list[str]:
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(repo_root), "ls-tree", "-r", "--name-only", "-z", tree],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr.decode("utf-8", errors="replace").strip())
|
||||
|
||||
paths: list[str] = []
|
||||
for raw_path in result.stdout.split(b"\0"):
|
||||
if not raw_path:
|
||||
continue
|
||||
rel_path = raw_path.decode("utf-8", errors="surrogateescape")
|
||||
if not rel_path.endswith(".swift"):
|
||||
continue
|
||||
if not is_in_roots(rel_path, roots):
|
||||
continue
|
||||
if is_ignored_path(pathlib.Path(rel_path)):
|
||||
continue
|
||||
paths.append(rel_path)
|
||||
return sorted(paths)
|
||||
|
||||
|
||||
def collect_file_lengths_at_ref(repo_root: pathlib.Path, ref: str, roots: tuple[str, ...]) -> FileLengthBudget:
|
||||
paths = list_tree_swift_paths(repo_root, ref, roots)
|
||||
if not paths:
|
||||
return {}
|
||||
|
||||
batch_input = "".join(f"{ref}:{rel_path}\n" for rel_path in paths).encode(
|
||||
"utf-8",
|
||||
errors="surrogateescape",
|
||||
)
|
||||
try:
|
||||
process = subprocess.run(
|
||||
["git", "-C", str(repo_root), "cat-file", "--batch"],
|
||||
input=batch_input,
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
except OSError as exc:
|
||||
raise RuntimeError(str(exc)) from exc
|
||||
|
||||
if process.returncode != 0:
|
||||
raise RuntimeError(process.stderr.decode("utf-8", errors="replace").strip())
|
||||
|
||||
budget: FileLengthBudget = {}
|
||||
offset = 0
|
||||
stdout = process.stdout
|
||||
for rel_path in paths:
|
||||
header_end = stdout.find(b"\n", offset)
|
||||
if header_end == -1:
|
||||
raise RuntimeError(f"missing git cat-file header for {rel_path}")
|
||||
header = stdout[offset:header_end]
|
||||
header_parts = header.split()
|
||||
if len(header_parts) == 2 and header_parts[1] == b"missing":
|
||||
raise RuntimeError(f"missing object for {ref}:{rel_path}")
|
||||
if len(header_parts) != 3:
|
||||
raise RuntimeError(f"unexpected git cat-file header for {rel_path}: {header!r}")
|
||||
try:
|
||||
size = int(header_parts[2])
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(f"invalid git cat-file size for {rel_path}: {header!r}") from exc
|
||||
content_start = header_end + 1
|
||||
content_end = content_start + size
|
||||
if content_end > len(stdout):
|
||||
raise RuntimeError(f"truncated git cat-file content for {rel_path}")
|
||||
budget[rel_path] = count_blob_lines(stdout[content_start:content_end])
|
||||
offset = content_end
|
||||
if offset < len(stdout) and stdout[offset : offset + 1] == b"\n":
|
||||
offset += 1
|
||||
return budget
|
||||
|
||||
|
||||
def tracked_file_lengths(file_lengths: FileLengthBudget, threshold: int) -> FileLengthBudget:
|
||||
return {
|
||||
rel_path: line_count
|
||||
for rel_path, line_count in file_lengths.items()
|
||||
if line_count >= threshold
|
||||
}
|
||||
|
||||
|
||||
def count_lines_at_ref(repo_root: pathlib.Path, ref: str, rel_path: str) -> int | None:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(repo_root), "show", f"{ref}:{rel_path}"],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
return count_blob_lines(result.stdout)
|
||||
|
||||
|
||||
def parse_budget(text: str, source: str) -> FileLengthBudget:
|
||||
budget: FileLengthBudget = {}
|
||||
for line_number, raw_line in enumerate(text.splitlines(), start=1):
|
||||
line = raw_line.rstrip("\n")
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
parts = line.split("\t", 1)
|
||||
if len(parts) != 2:
|
||||
raise ValueError(f"{source}:{line_number}: expected max_lines<TAB>relative path")
|
||||
|
||||
count_text, rel_path = parts
|
||||
try:
|
||||
count = int(count_text)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"{source}:{line_number}: invalid line count {count_text!r}") from exc
|
||||
|
||||
if count < 0:
|
||||
raise ValueError(f"{source}:{line_number}: line count must be non-negative")
|
||||
if rel_path in budget:
|
||||
raise ValueError(f"{source}:{line_number}: duplicate entry for {rel_path!r}")
|
||||
budget[rel_path] = count
|
||||
return budget
|
||||
|
||||
|
||||
def load_budget(path: pathlib.Path) -> FileLengthBudget:
|
||||
return parse_budget(path.read_text(encoding="utf-8"), str(path))
|
||||
|
||||
|
||||
def repo_relative_path(repo_root: pathlib.Path, path: pathlib.Path) -> str | None:
|
||||
try:
|
||||
return path.resolve(strict=False).relative_to(repo_root).as_posix()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def load_budget_at_ref(repo_root: pathlib.Path, ref: str, rel_path: str) -> FileLengthBudget | None:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(repo_root), "show", f"{ref}:{rel_path}"],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
return parse_budget(result.stdout, f"{ref}:{rel_path}")
|
||||
|
||||
|
||||
def write_budget(path: pathlib.Path, budget: FileLengthBudget) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as handle:
|
||||
handle.write("# cmux-owned Swift file length budget.\n")
|
||||
handle.write("# Format: max_lines<TAB>relative path\n")
|
||||
handle.write("# Reduce counts as files shrink. CI fails if tracked files exceed this budget.\n")
|
||||
for rel_path, line_count in sorted(budget.items(), key=lambda item: (-item[1], item[0])):
|
||||
handle.write(f"{line_count}\t{rel_path}\n")
|
||||
|
||||
|
||||
def print_file_summary(label: str, file_lengths: FileLengthBudget) -> None:
|
||||
total = sum(file_lengths.values())
|
||||
print(f"{label}: {total} line(s) across {len(file_lengths)} Swift file(s)")
|
||||
|
||||
|
||||
def speculative_merge_tree(repo_root: pathlib.Path, merge_ref: str, merge_head: str) -> str | None:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(repo_root), "merge-tree", "--write-tree", merge_ref, merge_head],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
except OSError as exc:
|
||||
print(
|
||||
f"Speculative merge of {merge_ref} and {merge_head} could not run; "
|
||||
"falling back to working-tree evaluation.",
|
||||
)
|
||||
print(str(exc), file=sys.stderr)
|
||||
return None
|
||||
|
||||
if result.returncode == 0:
|
||||
tree = result.stdout.splitlines()[0] if result.stdout.splitlines() else ""
|
||||
if tree:
|
||||
return tree
|
||||
print(
|
||||
f"Speculative merge of {merge_ref} and {merge_head} did not produce a tree; "
|
||||
"falling back to working-tree evaluation.",
|
||||
)
|
||||
if result.stderr.strip():
|
||||
print(result.stderr.strip(), file=sys.stderr)
|
||||
return None
|
||||
|
||||
if result.returncode == 1:
|
||||
print(
|
||||
f"Speculative merge of {merge_ref} and {merge_head} conflicts; "
|
||||
"falling back to working-tree evaluation.",
|
||||
)
|
||||
return None
|
||||
|
||||
print(
|
||||
f"Speculative merge of {merge_ref} and {merge_head} failed; "
|
||||
"falling back to working-tree evaluation.",
|
||||
)
|
||||
if result.stderr.strip():
|
||||
print(result.stderr.strip(), file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def compare_budget(
|
||||
actual: FileLengthBudget,
|
||||
allowed: FileLengthBudget,
|
||||
base_allowed: FileLengthBudget | None,
|
||||
threshold: int,
|
||||
all_file_lengths: FileLengthBudget,
|
||||
repo_root: pathlib.Path,
|
||||
base_ref: str | None,
|
||||
incidental_growth: int,
|
||||
hard_cap: int,
|
||||
) -> int:
|
||||
failures: list[tuple[str, int, int | None, str]] = []
|
||||
incidental: list[tuple[str, int, int, int]] = []
|
||||
reductions: list[tuple[str, int, int]] = []
|
||||
|
||||
for rel_path in sorted(set(actual) | set(allowed)):
|
||||
actual_count = actual.get(rel_path, all_file_lengths.get(rel_path, 0))
|
||||
allowed_count = allowed.get(rel_path)
|
||||
|
||||
if base_ref and actual_count >= threshold:
|
||||
base_count = count_lines_at_ref(repo_root, base_ref, rel_path)
|
||||
if base_count is None:
|
||||
failures.append((rel_path, actual_count, allowed_count, "new tracked file"))
|
||||
continue
|
||||
if base_count < threshold:
|
||||
failures.append((rel_path, actual_count, allowed_count, "newly tracked file"))
|
||||
continue
|
||||
|
||||
base_growth = actual_count - base_count if base_count is not None else None
|
||||
if actual_count > hard_cap and base_growth is not None and base_growth > 0:
|
||||
failures.append((rel_path, actual_count, allowed_count, f"hard cap {hard_cap}"))
|
||||
continue
|
||||
if base_growth is not None and base_growth > incidental_growth:
|
||||
failures.append(
|
||||
(
|
||||
rel_path,
|
||||
actual_count,
|
||||
allowed_count,
|
||||
f"PR growth +{base_growth} exceeds incidental allowance {incidental_growth}",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if allowed_count is None:
|
||||
failures.append((rel_path, actual_count, None, "missing budget entry"))
|
||||
continue
|
||||
base_allowed_count = base_allowed.get(rel_path) if base_allowed is not None else None
|
||||
if (
|
||||
base_allowed_count is not None
|
||||
and allowed_count < base_allowed_count
|
||||
and actual_count > allowed_count
|
||||
):
|
||||
failures.append(
|
||||
(
|
||||
rel_path,
|
||||
actual_count,
|
||||
allowed_count,
|
||||
f"budget lowered below actual count (base budget {base_allowed_count})",
|
||||
)
|
||||
)
|
||||
continue
|
||||
if actual_count > allowed_count and base_growth is not None and base_growth > 0:
|
||||
incidental.append((rel_path, actual_count, allowed_count, base_growth))
|
||||
continue
|
||||
if actual_count > allowed_count:
|
||||
continue
|
||||
if actual_count < allowed_count:
|
||||
reductions.append((rel_path, actual_count, allowed_count))
|
||||
continue
|
||||
continue
|
||||
|
||||
if allowed_count is None and actual_count >= threshold:
|
||||
failures.append((rel_path, actual_count, None, "untracked"))
|
||||
elif allowed_count is not None and actual_count > allowed_count:
|
||||
failures.append((rel_path, actual_count, allowed_count, "exceeds checked-in budget"))
|
||||
elif rel_path in allowed and actual_count < allowed_count:
|
||||
reductions.append((rel_path, actual_count, allowed_count))
|
||||
|
||||
if failures:
|
||||
print("Swift file length budget exceeded.")
|
||||
print("")
|
||||
for rel_path, actual_count, allowed_count, reason in sorted(
|
||||
failures,
|
||||
key=lambda item: ((item[2] if item[2] is not None else threshold) - item[1], item[0]),
|
||||
):
|
||||
comparison_count = allowed_count if allowed_count is not None else threshold
|
||||
delta = actual_count - comparison_count
|
||||
if allowed_count is None:
|
||||
prefix = f"+{delta}" if delta > 0 else "new"
|
||||
print(f"{prefix} {rel_path}")
|
||||
print(f" actual={actual_count} budget=untracked threshold={threshold}")
|
||||
else:
|
||||
print(f"+{delta} {rel_path}")
|
||||
print(f" actual={actual_count} budget={allowed_count}")
|
||||
print(f" reason={reason}")
|
||||
print("")
|
||||
print("Split the file, reduce the new growth, or refresh the budget only when accepting known debt.")
|
||||
return 1
|
||||
|
||||
print("Swift file length budget respected.")
|
||||
if incidental:
|
||||
print("")
|
||||
print("Incidental growth allowed by PR gate:")
|
||||
for rel_path, actual_count, allowed_count, base_growth in sorted(
|
||||
incidental,
|
||||
key=lambda item: (item[3], item[0]),
|
||||
reverse=True,
|
||||
)[:20]:
|
||||
print(f"+{base_growth} {rel_path}")
|
||||
print(f" actual={actual_count} budget={allowed_count} allowance={incidental_growth}")
|
||||
if reductions:
|
||||
print("")
|
||||
print("Budget can be reduced:")
|
||||
for rel_path, actual_count, allowed_count in sorted(
|
||||
reductions,
|
||||
key=lambda item: (item[2] - item[1], item[0]),
|
||||
reverse=True,
|
||||
)[:20]:
|
||||
delta = allowed_count - actual_count
|
||||
print(f"-{delta} {rel_path}")
|
||||
print(f" actual={actual_count} budget={allowed_count}")
|
||||
if len(reductions) > 20:
|
||||
print(f"... {len(reductions) - 20} more reduction(s)")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--repo-root",
|
||||
default=pathlib.Path.cwd(),
|
||||
type=pathlib.Path,
|
||||
help="repository root to scan",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--budget",
|
||||
default=pathlib.Path(".github/swift-file-length-budget.tsv"),
|
||||
type=pathlib.Path,
|
||||
help="checked-in file length budget",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--threshold",
|
||||
default=DEFAULT_THRESHOLD,
|
||||
type=int,
|
||||
help="minimum line count tracked by the budget",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--roots",
|
||||
nargs="+",
|
||||
default=list(DEFAULT_ROOTS),
|
||||
help="repo-relative roots to scan",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--write-budget",
|
||||
action="store_true",
|
||||
help="write the current file lengths as the budget instead of checking",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--base-ref",
|
||||
help="git ref used to allow small PR-local growth in files already over budget",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--merge-ref",
|
||||
help="git ref to speculatively merge with --merge-head before evaluating the budget",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--merge-head",
|
||||
default="HEAD",
|
||||
help="git ref for the PR side of a speculative merge",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--incidental-growth",
|
||||
default=DEFAULT_INCIDENTAL_GROWTH,
|
||||
type=int,
|
||||
help="max lines a PR may add to an existing tracked file without refreshing the budget",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hard-cap",
|
||||
default=DEFAULT_HARD_CAP,
|
||||
type=int,
|
||||
help="absolute max lines for an existing tracked file, even with incidental PR growth",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.threshold < 1:
|
||||
print("--threshold must be at least 1", file=sys.stderr)
|
||||
return 2
|
||||
if args.incidental_growth < 0:
|
||||
print("--incidental-growth must be non-negative", file=sys.stderr)
|
||||
return 2
|
||||
if args.hard_cap < args.threshold:
|
||||
print("--hard-cap must be at least --threshold", file=sys.stderr)
|
||||
return 2
|
||||
if args.write_budget and args.merge_ref:
|
||||
print("--write-budget cannot be used with --merge-ref", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
repo_root = args.repo_root.resolve(strict=False)
|
||||
budget_path = args.budget if args.budget.is_absolute() else repo_root / args.budget
|
||||
|
||||
merged_tree: str | None = None
|
||||
if args.merge_ref:
|
||||
merged_tree = speculative_merge_tree(repo_root, args.merge_ref, args.merge_head)
|
||||
|
||||
if merged_tree:
|
||||
print(
|
||||
f"Evaluating speculative merge of {args.merge_ref} and {args.merge_head} "
|
||||
f"(tree {merged_tree})."
|
||||
)
|
||||
try:
|
||||
file_lengths = collect_file_lengths_at_ref(repo_root, merged_tree, tuple(args.roots))
|
||||
except RuntimeError as exc:
|
||||
print(f"Error reading Swift files from merged tree: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
actual = tracked_file_lengths(file_lengths, args.threshold)
|
||||
print_file_summary("All scanned cmux-owned Swift files", file_lengths)
|
||||
print_file_summary(f"Tracked Swift files >= {args.threshold} lines", actual)
|
||||
|
||||
budget_ref_path = repo_relative_path(repo_root, budget_path)
|
||||
try:
|
||||
allowed = load_budget_at_ref(repo_root, merged_tree, budget_ref_path) if budget_ref_path else None
|
||||
except ValueError as exc:
|
||||
print(f"Error reading Swift file length budget: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
if allowed is None:
|
||||
print(f"Missing Swift file length budget: {budget_path}", file=sys.stderr)
|
||||
return 2
|
||||
base_allowed: FileLengthBudget | None = None
|
||||
try:
|
||||
base_allowed = load_budget_at_ref(repo_root, args.merge_ref, budget_ref_path)
|
||||
except ValueError as exc:
|
||||
print(f"Error reading base Swift file length budget: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
print_file_summary("Allowed Swift file length budget", allowed)
|
||||
return compare_budget(
|
||||
actual,
|
||||
allowed,
|
||||
base_allowed,
|
||||
args.threshold,
|
||||
file_lengths,
|
||||
repo_root,
|
||||
args.merge_ref,
|
||||
args.incidental_growth,
|
||||
args.hard_cap,
|
||||
)
|
||||
|
||||
file_lengths = collect_file_lengths(repo_root, tuple(args.roots))
|
||||
actual = tracked_file_lengths(file_lengths, args.threshold)
|
||||
print_file_summary("All scanned cmux-owned Swift files", file_lengths)
|
||||
print_file_summary(f"Tracked Swift files >= {args.threshold} lines", actual)
|
||||
|
||||
if args.write_budget:
|
||||
write_budget(budget_path, actual)
|
||||
print(f"Wrote {budget_path}")
|
||||
return 0
|
||||
|
||||
if not budget_path.exists():
|
||||
print(f"Missing Swift file length budget: {budget_path}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
try:
|
||||
allowed = load_budget(budget_path)
|
||||
except ValueError as exc:
|
||||
print(f"Error reading Swift file length budget: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
base_allowed: FileLengthBudget | None = None
|
||||
if args.base_ref:
|
||||
budget_ref_path = repo_relative_path(repo_root, budget_path)
|
||||
if budget_ref_path is not None:
|
||||
try:
|
||||
base_allowed = load_budget_at_ref(repo_root, args.base_ref, budget_ref_path)
|
||||
except ValueError as exc:
|
||||
print(f"Error reading base Swift file length budget: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
print_file_summary("Allowed Swift file length budget", allowed)
|
||||
return compare_budget(
|
||||
actual,
|
||||
allowed,
|
||||
base_allowed,
|
||||
args.threshold,
|
||||
file_lengths,
|
||||
repo_root,
|
||||
args.base_ref,
|
||||
args.incidental_growth,
|
||||
args.hard_cap,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -1,492 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
FIXTURE="$TMP_DIR/repo"
|
||||
BUDGET="$TMP_DIR/budget.tsv"
|
||||
|
||||
python3 - "$FIXTURE" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
root = pathlib.Path(sys.argv[1])
|
||||
|
||||
def write_lines(path: pathlib.Path, count: int) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("".join(f"line {index}\n" for index in range(count)), encoding="utf-8")
|
||||
|
||||
write_lines(root / "Sources" / "Big.swift", 5)
|
||||
write_lines(root / "Sources" / "Small.swift", 4)
|
||||
write_lines(root / "Sources" / "vendor" / "Ignored.swift", 100)
|
||||
write_lines(root / "CLI" / "Tool.swift", 6)
|
||||
write_lines(root / "Packages" / "Fixture" / "Sources" / "Fixture.swift", 7)
|
||||
write_lines(root / "Packages" / "Fixture" / ".build" / "checkouts" / "Ignored.swift", 100)
|
||||
PY
|
||||
|
||||
python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$BUDGET" \
|
||||
--threshold 5 \
|
||||
--write-budget
|
||||
|
||||
git -C "$FIXTURE" init -q
|
||||
git -C "$FIXTURE" add .
|
||||
git -C "$FIXTURE" -c user.name='cmux CI' -c user.email='[email protected]' commit -qm baseline
|
||||
BASE_REF="$(git -C "$FIXTURE" rev-parse HEAD)"
|
||||
|
||||
if ! grep -Fq $'5\tSources/Big.swift' "$BUDGET"; then
|
||||
echo "expected tracked Sources file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq $'6\tCLI/Tool.swift' "$BUDGET"; then
|
||||
echo "expected tracked CLI file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq $'7\tPackages/Fixture/Sources/Fixture.swift' "$BUDGET"; then
|
||||
echo "expected tracked Packages file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -Fq 'Sources/Small.swift' "$BUDGET"; then
|
||||
echo "small file should not be included" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -Fq 'vendor' "$BUDGET"; then
|
||||
echo "ignored source should not be included" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -Fq '.build' "$BUDGET"; then
|
||||
echo "SwiftPM build output should not be included" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$BUDGET" \
|
||||
--threshold 5
|
||||
|
||||
mkdir -p "$FIXTURE/.github"
|
||||
(
|
||||
cd "$TMP_DIR"
|
||||
python3 "$ROOT_DIR/scripts/swift_file_length_budget.py" \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget .github/relative-budget.tsv \
|
||||
--threshold 5 \
|
||||
--write-budget
|
||||
)
|
||||
|
||||
if [ ! -f "$FIXTURE/.github/relative-budget.tsv" ]; then
|
||||
echo "expected relative budget path to resolve inside repo root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$TMP_DIR/.github/relative-budget.tsv" ]; then
|
||||
echo "relative budget path should not resolve from current directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$FIXTURE/Sources/NewLarge.swift" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
path = pathlib.Path(sys.argv[1])
|
||||
path.write_text("".join(f"new line {index}\n" for index in range(5)), encoding="utf-8")
|
||||
PY
|
||||
|
||||
if python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$BUDGET" \
|
||||
--threshold 5 >"$TMP_DIR/new-file.out" 2>&1; then
|
||||
echo "expected new untracked file failure" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'new Sources/NewLarge.swift' "$TMP_DIR/new-file.out"; then
|
||||
echo "expected new untracked file output" >&2
|
||||
cat "$TMP_DIR/new-file.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'budget=untracked threshold=5' "$TMP_DIR/new-file.out"; then
|
||||
echo "expected untracked budget output" >&2
|
||||
cat "$TMP_DIR/new-file.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm "$FIXTURE/Sources/NewLarge.swift"
|
||||
|
||||
printf 'new growth\n' >>"$FIXTURE/Sources/Big.swift"
|
||||
|
||||
if python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$BUDGET" \
|
||||
--threshold 5 >"$TMP_DIR/fail.out" 2>&1; then
|
||||
echo "expected file length budget failure" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'Swift file length budget exceeded' "$TMP_DIR/fail.out"; then
|
||||
echo "expected budget failure output" >&2
|
||||
cat "$TMP_DIR/fail.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq '+1 Sources/Big.swift' "$TMP_DIR/fail.out"; then
|
||||
echo "expected file growth delta" >&2
|
||||
cat "$TMP_DIR/fail.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$BUDGET" \
|
||||
--threshold 5 \
|
||||
--base-ref "$BASE_REF" \
|
||||
--incidental-growth 1 \
|
||||
--hard-cap 10 >"$TMP_DIR/incidental.out"
|
||||
|
||||
if ! grep -Fq 'Incidental growth allowed by PR gate' "$TMP_DIR/incidental.out"; then
|
||||
echo "expected incidental growth output" >&2
|
||||
cat "$TMP_DIR/incidental.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git -C "$FIXTURE" add .
|
||||
git -C "$FIXTURE" -c user.name='cmux CI' -c user.email='[email protected]' commit -qm 'allow incidental growth'
|
||||
UNCHANGED_BASE_REF="$(git -C "$FIXTURE" rev-parse HEAD)"
|
||||
|
||||
python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$BUDGET" \
|
||||
--threshold 5 \
|
||||
--base-ref "$UNCHANGED_BASE_REF" \
|
||||
--incidental-growth 0 \
|
||||
--hard-cap 10 >"$TMP_DIR/unchanged-over-budget.out"
|
||||
|
||||
if grep -Fq 'Swift file length budget exceeded' "$TMP_DIR/unchanged-over-budget.out"; then
|
||||
echo "unchanged over-budget file should pass in base-ref mode" >&2
|
||||
cat "$TMP_DIR/unchanged-over-budget.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$FIXTURE/.github"
|
||||
printf '6\tSources/Big.swift\n6\tCLI/Tool.swift\n7\tPackages/Fixture/Sources/Fixture.swift\n' >"$FIXTURE/.github/swift-file-length-budget.tsv"
|
||||
git -C "$FIXTURE" add .github/swift-file-length-budget.tsv
|
||||
git -C "$FIXTURE" -c user.name='cmux CI' -c user.email='[email protected]' commit -qm 'record checked-in budget'
|
||||
CHECKED_IN_BUDGET_REF="$(git -C "$FIXTURE" rev-parse HEAD)"
|
||||
|
||||
printf '5\tSources/Big.swift\n6\tCLI/Tool.swift\n7\tPackages/Fixture/Sources/Fixture.swift\n' >"$FIXTURE/.github/swift-file-length-budget.tsv"
|
||||
if python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$FIXTURE/.github/swift-file-length-budget.tsv" \
|
||||
--threshold 5 \
|
||||
--base-ref "$CHECKED_IN_BUDGET_REF" \
|
||||
--incidental-growth 0 \
|
||||
--hard-cap 10 >"$TMP_DIR/lowered-budget.out" 2>&1; then
|
||||
echo "expected lowered checked-in budget to fail base-ref check" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'budget lowered below actual count (base budget 6)' "$TMP_DIR/lowered-budget.out"; then
|
||||
echo "expected lowered-budget reason" >&2
|
||||
cat "$TMP_DIR/lowered-budget.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'threshold crossing\n' >>"$FIXTURE/Sources/Small.swift"
|
||||
printf '6\tSources/Big.swift\n5\tSources/Small.swift\n6\tCLI/Tool.swift\n7\tPackages/Fixture/Sources/Fixture.swift\n' >"$FIXTURE/.github/swift-file-length-budget.tsv"
|
||||
if python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$FIXTURE/.github/swift-file-length-budget.tsv" \
|
||||
--threshold 5 \
|
||||
--base-ref "$CHECKED_IN_BUDGET_REF" \
|
||||
--incidental-growth 1 \
|
||||
--hard-cap 10 >"$TMP_DIR/threshold-crossing.out" 2>&1; then
|
||||
echo "expected below-threshold base file to fail base-ref check" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'reason=newly tracked file' "$TMP_DIR/threshold-crossing.out"; then
|
||||
echo "expected threshold-crossing reason" >&2
|
||||
cat "$TMP_DIR/threshold-crossing.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed -i.bak '$d' "$FIXTURE/Sources/Small.swift"
|
||||
rm "$FIXTURE/Sources/Small.swift.bak"
|
||||
|
||||
if python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$BUDGET" \
|
||||
--threshold 5 \
|
||||
--base-ref "$BASE_REF" \
|
||||
--incidental-growth 0 >"$TMP_DIR/growth-limit.out" 2>&1; then
|
||||
echo "expected incidental growth limit failure" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'PR growth +1 exceeds incidental allowance 0' "$TMP_DIR/growth-limit.out"; then
|
||||
echo "expected growth-limit reason" >&2
|
||||
cat "$TMP_DIR/growth-limit.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$BUDGET" \
|
||||
--threshold 5 \
|
||||
--base-ref "$BASE_REF" \
|
||||
--incidental-growth 1 \
|
||||
--hard-cap 5 >"$TMP_DIR/hard-cap.out" 2>&1; then
|
||||
echo "expected hard-cap failure" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'reason=hard cap 5' "$TMP_DIR/hard-cap.out"; then
|
||||
echo "expected hard-cap reason" >&2
|
||||
cat "$TMP_DIR/hard-cap.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'extra growth\n' >>"$FIXTURE/Sources/Big.swift"
|
||||
printf '7\tSources/Big.swift\n6\tCLI/Tool.swift\n7\tPackages/Fixture/Sources/Fixture.swift\n' >"$TMP_DIR/raised-budget.tsv"
|
||||
|
||||
if python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$TMP_DIR/raised-budget.tsv" \
|
||||
--threshold 5 \
|
||||
--base-ref "$BASE_REF" \
|
||||
--incidental-growth 1 >"$TMP_DIR/raised-budget-bypass.out" 2>&1; then
|
||||
echo "expected raised budget to still fail PR growth check" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'PR growth +2 exceeds incidental allowance 1' "$TMP_DIR/raised-budget-bypass.out"; then
|
||||
echo "expected raised-budget growth reason" >&2
|
||||
cat "$TMP_DIR/raised-budget-bypass.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$FIXTURE/Sources/NewBudgeted.swift" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
path = pathlib.Path(sys.argv[1])
|
||||
path.write_text("".join(f"budgeted new line {index}\n" for index in range(5)), encoding="utf-8")
|
||||
PY
|
||||
printf '5\tSources/NewBudgeted.swift\n7\tSources/Big.swift\n6\tCLI/Tool.swift\n7\tPackages/Fixture/Sources/Fixture.swift\n' >"$TMP_DIR/new-file-budget.tsv"
|
||||
|
||||
if python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$TMP_DIR/new-file-budget.tsv" \
|
||||
--threshold 5 \
|
||||
--base-ref "$BASE_REF" >"$TMP_DIR/new-file-budget-bypass.out" 2>&1; then
|
||||
echo "expected budgeted new large file to fail base-ref check" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'reason=new tracked file' "$TMP_DIR/new-file-budget-bypass.out"; then
|
||||
echo "expected new-file reason" >&2
|
||||
cat "$TMP_DIR/new-file-budget-bypass.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm "$FIXTURE/Sources/NewBudgeted.swift"
|
||||
sed -i.bak '$d' "$FIXTURE/Sources/Big.swift"
|
||||
rm "$FIXTURE/Sources/Big.swift.bak"
|
||||
|
||||
printf 'not-a-valid-budget-line\n' >"$TMP_DIR/bad-budget.tsv"
|
||||
if python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$TMP_DIR/bad-budget.tsv" \
|
||||
--threshold 5 >"$TMP_DIR/bad.out" 2>&1; then
|
||||
echo "expected malformed budget failure" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'Error reading Swift file length budget' "$TMP_DIR/bad.out"; then
|
||||
echo "expected malformed budget error output" >&2
|
||||
cat "$TMP_DIR/bad.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -Fq 'Traceback' "$TMP_DIR/bad.out"; then
|
||||
echo "malformed budget should not print a traceback" >&2
|
||||
cat "$TMP_DIR/bad.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '5\tSources/Big.swift\n6\tSources/Big.swift\n' >"$TMP_DIR/duplicate-budget.tsv"
|
||||
if python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$FIXTURE" \
|
||||
--budget "$TMP_DIR/duplicate-budget.tsv" \
|
||||
--threshold 5 >"$TMP_DIR/duplicate.out" 2>&1; then
|
||||
echo "expected duplicate budget failure" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'duplicate entry' "$TMP_DIR/duplicate.out"; then
|
||||
echo "expected duplicate budget error output" >&2
|
||||
cat "$TMP_DIR/duplicate.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git merge-tree --write-tree HEAD HEAD >/dev/null 2>&1; then
|
||||
echo "Skipping speculative merge-tree budget tests: git merge-tree --write-tree is unsupported by this git."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
RACE_FIXTURE="$TMP_DIR/race-repo"
|
||||
|
||||
python3 - "$RACE_FIXTURE" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
root = pathlib.Path(sys.argv[1])
|
||||
|
||||
def write_lines(path: pathlib.Path, count: int) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("".join(f"line {index}\n" for index in range(count)), encoding="utf-8")
|
||||
|
||||
write_lines(root / "Sources" / "Racy.swift", 40)
|
||||
(root / ".github").mkdir(parents=True, exist_ok=True)
|
||||
(root / ".github" / "test-budget.tsv").write_text("40\tSources/Racy.swift\n", encoding="utf-8")
|
||||
(root / ".gitattributes").write_text("Sources/Racy.swift merge=keepFeature\n", encoding="utf-8")
|
||||
(root / "README.md").write_text("base readme\n", encoding="utf-8")
|
||||
PY
|
||||
|
||||
git -C "$RACE_FIXTURE" init -q
|
||||
git -C "$RACE_FIXTURE" config merge.keepFeature.name 'keep feature side for race fixture'
|
||||
git -C "$RACE_FIXTURE" config merge.keepFeature.driver 'cp %B %A'
|
||||
git -C "$RACE_FIXTURE" add .
|
||||
git -C "$RACE_FIXTURE" -c user.name='cmux CI' -c user.email='[email protected]' commit -qm baseline
|
||||
git -C "$RACE_FIXTURE" branch -M main
|
||||
RACE_BASE="$(git -C "$RACE_FIXTURE" rev-parse HEAD)"
|
||||
|
||||
git -C "$RACE_FIXTURE" checkout -q -b feature
|
||||
python3 - "$RACE_FIXTURE/Sources/Racy.swift" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
path = pathlib.Path(sys.argv[1])
|
||||
path.write_text(
|
||||
"".join(f"line {index}\n" for index in range(40))
|
||||
+ "feature line 40\n"
|
||||
+ "feature line 41\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
PY
|
||||
git -C "$RACE_FIXTURE" add Sources/Racy.swift
|
||||
git -C "$RACE_FIXTURE" -c user.name='cmux CI' -c user.email='[email protected]' commit -qm 'grow racy file'
|
||||
|
||||
git -C "$RACE_FIXTURE" checkout -q main
|
||||
python3 - "$RACE_FIXTURE" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
root = pathlib.Path(sys.argv[1])
|
||||
(root / "Sources" / "Racy.swift").write_text(
|
||||
"".join(f"split line {index}\n" for index in range(6)),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(root / ".github" / "test-budget.tsv").write_text("6\tSources/Racy.swift\n", encoding="utf-8")
|
||||
(root / "README.md").write_text("main readme\n", encoding="utf-8")
|
||||
PY
|
||||
git -C "$RACE_FIXTURE" add Sources/Racy.swift .github/test-budget.tsv README.md
|
||||
git -C "$RACE_FIXTURE" -c user.name='cmux CI' -c user.email='[email protected]' commit -qm 'split racy file and lower budget'
|
||||
|
||||
git -C "$RACE_FIXTURE" checkout -q feature
|
||||
RACE_MERGE_BASE="$(git -C "$RACE_FIXTURE" merge-base main feature)"
|
||||
python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$RACE_FIXTURE" \
|
||||
--budget .github/test-budget.tsv \
|
||||
--threshold 5 \
|
||||
--base-ref "$RACE_MERGE_BASE" \
|
||||
--incidental-growth 3 \
|
||||
--hard-cap 100 >"$TMP_DIR/race-old-behavior.out"
|
||||
|
||||
if python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$RACE_FIXTURE" \
|
||||
--budget .github/test-budget.tsv \
|
||||
--threshold 5 \
|
||||
--base-ref "$RACE_MERGE_BASE" \
|
||||
--merge-ref main \
|
||||
--merge-head feature \
|
||||
--incidental-growth 3 \
|
||||
--hard-cap 100 >"$TMP_DIR/race-merge-ref.out" 2>&1; then
|
||||
echo "expected speculative merge budget check to catch racy growth" >&2
|
||||
cat "$TMP_DIR/race-merge-ref.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'Sources/Racy.swift' "$TMP_DIR/race-merge-ref.out"; then
|
||||
echo "expected speculative merge failure to name Racy.swift" >&2
|
||||
cat "$TMP_DIR/race-merge-ref.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$RACE_FIXTURE" \
|
||||
--budget .github/test-budget.tsv \
|
||||
--threshold 5 \
|
||||
--merge-ref main \
|
||||
--write-budget >"$TMP_DIR/race-write-budget.out" 2>&1; then
|
||||
echo "expected --write-budget with --merge-ref to fail" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq -- '--write-budget cannot be used with --merge-ref' "$TMP_DIR/race-write-budget.out"; then
|
||||
echo "expected --write-budget merge-ref argument error" >&2
|
||||
cat "$TMP_DIR/race-write-budget.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git -C "$RACE_FIXTURE" checkout -q -b conflict "$RACE_BASE"
|
||||
printf 'feature readme\n' >"$RACE_FIXTURE/README.md"
|
||||
git -C "$RACE_FIXTURE" add README.md
|
||||
git -C "$RACE_FIXTURE" -c user.name='cmux CI' -c user.email='[email protected]' commit -qm 'conflict on readme'
|
||||
|
||||
CONFLICT_MERGE_BASE="$(git -C "$RACE_FIXTURE" merge-base main conflict)"
|
||||
set +e
|
||||
python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$RACE_FIXTURE" \
|
||||
--budget .github/test-budget.tsv \
|
||||
--threshold 5 \
|
||||
--base-ref "$CONFLICT_MERGE_BASE" \
|
||||
--incidental-growth 3 \
|
||||
--hard-cap 100 >"$TMP_DIR/conflict-plain.out" 2>&1
|
||||
PLAIN_CONFLICT_STATUS=$?
|
||||
python3 scripts/swift_file_length_budget.py \
|
||||
--repo-root "$RACE_FIXTURE" \
|
||||
--budget .github/test-budget.tsv \
|
||||
--threshold 5 \
|
||||
--base-ref "$CONFLICT_MERGE_BASE" \
|
||||
--merge-ref main \
|
||||
--merge-head conflict \
|
||||
--incidental-growth 3 \
|
||||
--hard-cap 100 >"$TMP_DIR/conflict-merge-ref.out" 2>&1
|
||||
MERGE_REF_CONFLICT_STATUS=$?
|
||||
set -e
|
||||
|
||||
if [ "$PLAIN_CONFLICT_STATUS" -ne "$MERGE_REF_CONFLICT_STATUS" ]; then
|
||||
echo "conflict fallback should produce the same exit code as plain base-ref evaluation" >&2
|
||||
echo "plain status: $PLAIN_CONFLICT_STATUS" >&2
|
||||
echo "merge-ref status: $MERGE_REF_CONFLICT_STATUS" >&2
|
||||
cat "$TMP_DIR/conflict-merge-ref.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq 'falling back to working-tree evaluation' "$TMP_DIR/conflict-merge-ref.out"; then
|
||||
echo "expected conflict fallback notice" >&2
|
||||
cat "$TMP_DIR/conflict-merge-ref.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user