git-juggler 0.2.5__tar.gz → 0.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- git_juggler-0.2.7/PKG-INFO +141 -0
- git_juggler-0.2.7/README.md +128 -0
- git_juggler-0.2.7/resources/git-juggler-demo-1.gif +0 -0
- git_juggler-0.2.7/resources/git-juggler-demo-2.png +0 -0
- git_juggler-0.2.7/resources/git-juggler-demo-3.png +0 -0
- git_juggler-0.2.7/resources/git-juggler-demo-4.png +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/scripts/dev.sh +13 -3
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/api/ci.py +2 -1
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/ci/__init__.py +8 -7
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/ci/jenkins.py +121 -26
- git_juggler-0.2.5/src/backend/git_juggler/frontend_dist/assets/index-W_quuou6.js → git_juggler-0.2.7/src/backend/git_juggler/frontend_dist/assets/index-B-qpxlvK.js +15 -15
- git_juggler-0.2.7/src/backend/git_juggler/frontend_dist/assets/index-KUdo_LCw.css +1 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/frontend_dist/index.html +2 -2
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/main.py +11 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/schemas.py +7 -4
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_ci_poll.py +9 -9
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_jenkins.py +123 -3
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/App.css +31 -9
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/api/client.ts +2 -1
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/api/types.ts +6 -3
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/ContextMenu/CommitContextMenu.tsx +1 -1
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/ContextMenu/RepoContextMenu.tsx +14 -1
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Menu/GitHubSettings.tsx +81 -53
- git_juggler-0.2.7/src/frontend/src/components/Menu/JenkinsSettings.tsx +281 -0
- git_juggler-0.2.7/src/frontend/src/components/Search/MultiSelect.tsx +99 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Search/SearchBox.tsx +1 -1
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Tabs/TabsBar.tsx +1 -1
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/storeCore.ts +50 -6
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/tabs.ts +1 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/styles/menu.css +28 -3
- git_juggler-0.2.5/PKG-INFO +0 -102
- git_juggler-0.2.5/README.md +0 -89
- git_juggler-0.2.5/src/backend/git_juggler/frontend_dist/assets/index-DSWz5TYP.css +0 -1
- git_juggler-0.2.5/src/backend/git_juggler/frontend_dist/favicon.svg +0 -1
- git_juggler-0.2.5/src/backend/git_juggler/frontend_dist/icons.svg +0 -24
- git_juggler-0.2.5/src/frontend/public/favicon.svg +0 -1
- git_juggler-0.2.5/src/frontend/public/icons.svg +0 -24
- git_juggler-0.2.5/src/frontend/src/components/Menu/JenkinsSettings.tsx +0 -184
- git_juggler-0.2.5/src/frontend/src/components/Search/MultiSelect.tsx +0 -47
- {git_juggler-0.2.5 → git_juggler-0.2.7}/.github/workflows/build-package.yml +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/.gitignore +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/CLAUDE.md +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/LICENSE +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/notes.txt +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/pyproject.toml +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/scripts/build-package.sh +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/__init__.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/agents/__init__.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/agents/hook_events.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/agents/hooks.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/agents/tracking/__init__.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/agents/tracking/activity_models.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/agents/tracking/claude_sessions.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/agents/tracking/claude_transcripts.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/agents/tracking/git_resolver.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/agents/tracking/opencode_sessions.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/agents/tracking/paths.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/agents/tracking/text.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/api/__init__.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/api/agents.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/api/config.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/api/git.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/api/repos.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/api/terminal.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/app.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/ci/github_actions.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/ci/http.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/config.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/dev_app.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/frontend_dist/agent-logos/claude.png +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/frontend_dist/agent-logos/opencode.webp +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/frontend_dist/ci-logos/jenkins.webp +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/frontend_dist/git-juggler.png +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/frontend_dist/logo.png +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/git/__init__.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/git/command.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/git/data.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/git/detail.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/git/diff.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/git/refs.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/git/status.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/git/utils.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/repo_discovery.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/terminal.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/themes.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/ui/__init__.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/git_juggler/ui/folder_picker.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_agent_hooks.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_agent_tracking.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_ci.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_ci_poll_route.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_claude_sessions_data.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_file_diff.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_git_data.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_github_actions.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_opencode_sessions.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_pick_folder.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_preferences.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_repos.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/backend/tests/test_themes.py +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/.gitignore +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/README.md +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/index.html +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/package-lock.json +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/package.json +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/public/agent-logos/claude.png +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/public/agent-logos/opencode.webp +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/public/ci-logos/jenkins.webp +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/public/git-juggler.png +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/public/logo.png +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/App.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Agents/AgentActivityPanel.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Agents/AgentHoverCard.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Agents/AgentStatusPopover.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Agents/agentFormat.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Badges/BranchBadge.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Badges/CiRunBadge.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Badges/StashBadge.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Badges/TagBadge.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Badges/ciStatus.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Branches/DeleteBranchModal.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/CIRunPopover.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/CommandPalette/CommandPalette.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Commits/CommitDetail.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Commits/CommitList.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Commits/CommitRow.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Commits/LineCounts.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Commits/UncommittedRow.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/ContextMenu/BranchContextMenu.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/ContextMenu/CreateBranchModal.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/ContextMenu/CreateTagModal.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/ContextMenu/TagContextMenu.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Diff/FileDiffModal.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Graph/GraphPanel.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Graph/branchColor.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Graph/computeColumns.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Graph/laneWidth.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Menu/AgentQuickPicks.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Menu/AgentsSettings.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Menu/CiPollField.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Menu/DirectoryBrowserModal.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Menu/MainMenu.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Menu/RepoPathsSettings.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Menu/ThemePicker.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Menu/ThemeQuickPicks.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Pipelines/PipelinesPanel.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Pipelines/StageStrip.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Search/TriSwitch.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Sidebar/NewGroupModal.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Sidebar/RepoList.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Sidebar/Sidebar.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Tags/DeleteTagModal.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Terminal/TerminalPanel.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/Welcome/WelcomeWizard.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/icons.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/inputs/NumberField.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/inputs/TextField.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/inputs/ToggleField.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/components/inputs/styles.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/index.css +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/index.tsx +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/agentStatusPopoverData.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/appTheme.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/branchVisibility.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/bundledThemes.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/ciPoll.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/ciRunPopoverData.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/color.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/dismissOnOutsideClick.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/flip.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/formatDate.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/preferenceSender.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/scrollToCommit.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/sideBySide.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/stageWindow.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/stripAnsi.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/swallowNextClick.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/lib/vscodeTheme.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/agents.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/ci.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/graph.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/overlayStack.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/overlays.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/preferenceSync.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/preferences.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/repos.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/settings.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/store.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/terminal.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/state/themes.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/styles/ci.css +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/styles/commits.css +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/styles/graph.css +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/styles/sidebar.css +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/src/styles/terminal.css +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/tests/agentFormat.test.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/tests/agentStatusPopoverData.test.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/tests/ciPoll.test.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/tests/ciRunPopoverData.test.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/tests/computeColumns.test.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/tests/preferenceSender.test.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/tests/sideBySide.test.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/tests/stageWindow.test.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/tests/vscodeTheme.test.ts +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/tsconfig.app.json +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/tsconfig.json +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/tsconfig.node.json +0 -0
- {git_juggler-0.2.5 → git_juggler-0.2.7}/src/frontend/vite.config.ts +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: git-juggler
|
|
3
|
+
Version: 0.2.7
|
|
4
|
+
Summary: A local Git dashboard for people juggling agents and too many repos.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: fastapi>=0.110
|
|
9
|
+
Requires-Dist: gitpython>=3.1
|
|
10
|
+
Requires-Dist: pywinpty>=2.0; sys_platform == 'win32'
|
|
11
|
+
Requires-Dist: uvicorn[standard]>=0.29
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
<img src="https://github.com/franz38/git-juggler/blob/main/src/frontend/public/logo.png?raw=true" alt="git-juggler" width="400">
|
|
15
|
+
|
|
16
|
+
A local Git dashboard for people juggling agents and too many repos.
|
|
17
|
+
|
|
18
|
+
Watch agents work live, follow every repo’s git graph, and catch what changed across your workspace — all from one place, without opening a dozen editors.
|
|
19
|
+
|
|
20
|
+
<img src="https://github.com/franz38/git-juggler/blob/main/resources/git-juggler-demo-1.gif?raw=true" alt="git-juggler" width="100%">
|
|
21
|
+
|
|
22
|
+
<img src="https://github.com/franz38/git-juggler/blob/main/resources/git-juggler-demo-4.png?raw=true" alt="git-juggler" width="100%">
|
|
23
|
+
|
|
24
|
+
## What it does
|
|
25
|
+
|
|
26
|
+
- Scans the immediate children of one or more configured folders for Git repos.
|
|
27
|
+
- Lets you inspect and manage each repo's graph, branches, tags, stashes, dirty files, and file diffs.
|
|
28
|
+
- Keeps a persistent terminal per open repo and runs Git actions there so you see the real commands and output.
|
|
29
|
+
- Tracks local Claude Code and OpenCode sessions.
|
|
30
|
+
- Connects GitHub Actions and Jenkins runs to commits and active pipelines.
|
|
31
|
+
- VS Code-compatible themes.
|
|
32
|
+
|
|
33
|
+
## How to install
|
|
34
|
+
|
|
35
|
+
git-juggler is published on PyPI. The recommended way to install it is with
|
|
36
|
+
[pipx](https://pipx.pypa.io), which puts the `git-juggler` command on your
|
|
37
|
+
PATH in its own isolated environment, without touching your system Python:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
pipx install git-juggler
|
|
41
|
+
git-juggler [path-to-scan] [--host HOST] [--port PORT] [--no-open]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`<path-to-scan>` is optional and defaults to the current directory. It is used
|
|
45
|
+
to seed the initial search path; after that, search paths are managed in the app
|
|
46
|
+
from the welcome wizard or Settings.
|
|
47
|
+
|
|
48
|
+
- `--host HOST` changes the bind host from the default `127.0.0.1`.
|
|
49
|
+
- `--port PORT` changes the backend port from the default `8000`.
|
|
50
|
+
- `--no-open` disables opening the app in your browser automatically.
|
|
51
|
+
|
|
52
|
+
If you don't have pipx yet:
|
|
53
|
+
|
|
54
|
+
**macOS**
|
|
55
|
+
```
|
|
56
|
+
brew install pipx
|
|
57
|
+
pipx ensurepath
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Linux**
|
|
61
|
+
```
|
|
62
|
+
sudo apt install pipx # Debian/Ubuntu
|
|
63
|
+
sudo dnf install pipx # Fedora
|
|
64
|
+
pipx ensurepath
|
|
65
|
+
```
|
|
66
|
+
Many recent distros (like macOS with Homebrew) block a plain global
|
|
67
|
+
`pip install` outside a virtual environment (PEP 668); pipx is the
|
|
68
|
+
supported way around that.
|
|
69
|
+
|
|
70
|
+
**Windows**
|
|
71
|
+
```
|
|
72
|
+
py -m pip install --user pipx
|
|
73
|
+
py -m pipx ensurepath
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Restart your terminal after `ensurepath` so the updated PATH takes effect.
|
|
77
|
+
To upgrade later: `pipx upgrade git-juggler`.
|
|
78
|
+
|
|
79
|
+
## Dev mode
|
|
80
|
+
|
|
81
|
+
Use the dev script to start both the backend and frontend dev servers:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
./scripts/dev.sh [path-to-scan] [--port PORT] [--no-open]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- `path-to-scan` is optional. On first run, it seeds the folder whose immediate children are scanned for Git repos.
|
|
88
|
+
- `--port PORT` is optional. It changes the backend port from the default `8000`; if you use it, update `src/frontend/vite.config.ts` locally so Vite proxies `/api` and `/ws` to the same port.
|
|
89
|
+
- `--no-open` disables opening the app in your browser automatically.
|
|
90
|
+
|
|
91
|
+
Or run them manually in two terminals:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
python3 -m venv .venv
|
|
95
|
+
.venv/bin/pip install -e .
|
|
96
|
+
.venv/bin/git-juggler <path-to-scan> --reload [--no-open]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
cd src/frontend
|
|
101
|
+
npm install
|
|
102
|
+
npm run dev # http://localhost:5173 (proxies /api and /ws to :8000)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Open http://localhost:5173.
|
|
106
|
+
|
|
107
|
+
By default, `./scripts/dev.sh` opens http://localhost:5173 in your browser.
|
|
108
|
+
|
|
109
|
+
## Production (single port)
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
cd src/frontend && npm install && npm run build
|
|
113
|
+
cd ../.. && .venv/bin/git-juggler <path-to-scan> [--no-open]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The backend serves the built frontend in this mode, so only port 8000 is needed.
|
|
117
|
+
By default, it opens the app in your browser.
|
|
118
|
+
|
|
119
|
+
## Build a Python package
|
|
120
|
+
|
|
121
|
+
The Python wheel includes the built frontend assets. The generated assets are
|
|
122
|
+
not committed; `scripts/build-package.sh` builds them and copies them into the
|
|
123
|
+
backend package before creating the distributions.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
python3 -m venv .venv
|
|
127
|
+
.venv/bin/pip install build
|
|
128
|
+
./scripts/build-package.sh
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The wheel and source distribution are written to `dist/`.
|
|
132
|
+
|
|
133
|
+
Install the wheel locally with:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
pipx install dist/git_juggler-*.whl --force
|
|
137
|
+
git-juggler <path-to-scan> [--no-open]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Versions are derived from Git tags. GitHub Actions runs the same build, uploads
|
|
141
|
+
the distributions as artifacts, and publishes tagged releases to PyPI.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<img src="https://github.com/franz38/git-juggler/blob/main/src/frontend/public/logo.png?raw=true" alt="git-juggler" width="400">
|
|
2
|
+
|
|
3
|
+
A local Git dashboard for people juggling agents and too many repos.
|
|
4
|
+
|
|
5
|
+
Watch agents work live, follow every repo’s git graph, and catch what changed across your workspace — all from one place, without opening a dozen editors.
|
|
6
|
+
|
|
7
|
+
<img src="https://github.com/franz38/git-juggler/blob/main/resources/git-juggler-demo-1.gif?raw=true" alt="git-juggler" width="100%">
|
|
8
|
+
|
|
9
|
+
<img src="https://github.com/franz38/git-juggler/blob/main/resources/git-juggler-demo-4.png?raw=true" alt="git-juggler" width="100%">
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- Scans the immediate children of one or more configured folders for Git repos.
|
|
14
|
+
- Lets you inspect and manage each repo's graph, branches, tags, stashes, dirty files, and file diffs.
|
|
15
|
+
- Keeps a persistent terminal per open repo and runs Git actions there so you see the real commands and output.
|
|
16
|
+
- Tracks local Claude Code and OpenCode sessions.
|
|
17
|
+
- Connects GitHub Actions and Jenkins runs to commits and active pipelines.
|
|
18
|
+
- VS Code-compatible themes.
|
|
19
|
+
|
|
20
|
+
## How to install
|
|
21
|
+
|
|
22
|
+
git-juggler is published on PyPI. The recommended way to install it is with
|
|
23
|
+
[pipx](https://pipx.pypa.io), which puts the `git-juggler` command on your
|
|
24
|
+
PATH in its own isolated environment, without touching your system Python:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pipx install git-juggler
|
|
28
|
+
git-juggler [path-to-scan] [--host HOST] [--port PORT] [--no-open]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`<path-to-scan>` is optional and defaults to the current directory. It is used
|
|
32
|
+
to seed the initial search path; after that, search paths are managed in the app
|
|
33
|
+
from the welcome wizard or Settings.
|
|
34
|
+
|
|
35
|
+
- `--host HOST` changes the bind host from the default `127.0.0.1`.
|
|
36
|
+
- `--port PORT` changes the backend port from the default `8000`.
|
|
37
|
+
- `--no-open` disables opening the app in your browser automatically.
|
|
38
|
+
|
|
39
|
+
If you don't have pipx yet:
|
|
40
|
+
|
|
41
|
+
**macOS**
|
|
42
|
+
```
|
|
43
|
+
brew install pipx
|
|
44
|
+
pipx ensurepath
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Linux**
|
|
48
|
+
```
|
|
49
|
+
sudo apt install pipx # Debian/Ubuntu
|
|
50
|
+
sudo dnf install pipx # Fedora
|
|
51
|
+
pipx ensurepath
|
|
52
|
+
```
|
|
53
|
+
Many recent distros (like macOS with Homebrew) block a plain global
|
|
54
|
+
`pip install` outside a virtual environment (PEP 668); pipx is the
|
|
55
|
+
supported way around that.
|
|
56
|
+
|
|
57
|
+
**Windows**
|
|
58
|
+
```
|
|
59
|
+
py -m pip install --user pipx
|
|
60
|
+
py -m pipx ensurepath
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Restart your terminal after `ensurepath` so the updated PATH takes effect.
|
|
64
|
+
To upgrade later: `pipx upgrade git-juggler`.
|
|
65
|
+
|
|
66
|
+
## Dev mode
|
|
67
|
+
|
|
68
|
+
Use the dev script to start both the backend and frontend dev servers:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
./scripts/dev.sh [path-to-scan] [--port PORT] [--no-open]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `path-to-scan` is optional. On first run, it seeds the folder whose immediate children are scanned for Git repos.
|
|
75
|
+
- `--port PORT` is optional. It changes the backend port from the default `8000`; if you use it, update `src/frontend/vite.config.ts` locally so Vite proxies `/api` and `/ws` to the same port.
|
|
76
|
+
- `--no-open` disables opening the app in your browser automatically.
|
|
77
|
+
|
|
78
|
+
Or run them manually in two terminals:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
python3 -m venv .venv
|
|
82
|
+
.venv/bin/pip install -e .
|
|
83
|
+
.venv/bin/git-juggler <path-to-scan> --reload [--no-open]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
cd src/frontend
|
|
88
|
+
npm install
|
|
89
|
+
npm run dev # http://localhost:5173 (proxies /api and /ws to :8000)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Open http://localhost:5173.
|
|
93
|
+
|
|
94
|
+
By default, `./scripts/dev.sh` opens http://localhost:5173 in your browser.
|
|
95
|
+
|
|
96
|
+
## Production (single port)
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
cd src/frontend && npm install && npm run build
|
|
100
|
+
cd ../.. && .venv/bin/git-juggler <path-to-scan> [--no-open]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The backend serves the built frontend in this mode, so only port 8000 is needed.
|
|
104
|
+
By default, it opens the app in your browser.
|
|
105
|
+
|
|
106
|
+
## Build a Python package
|
|
107
|
+
|
|
108
|
+
The Python wheel includes the built frontend assets. The generated assets are
|
|
109
|
+
not committed; `scripts/build-package.sh` builds them and copies them into the
|
|
110
|
+
backend package before creating the distributions.
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
python3 -m venv .venv
|
|
114
|
+
.venv/bin/pip install build
|
|
115
|
+
./scripts/build-package.sh
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The wheel and source distribution are written to `dist/`.
|
|
119
|
+
|
|
120
|
+
Install the wheel locally with:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
pipx install dist/git_juggler-*.whl --force
|
|
124
|
+
git-juggler <path-to-scan> [--no-open]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Versions are derived from Git tags. GitHub Actions runs the same build, uploads
|
|
128
|
+
the distributions as artifacts, and publishes tagged releases to PyPI.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -5,14 +5,16 @@ set -euo pipefail
|
|
|
5
5
|
|
|
6
6
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
7
7
|
BACKEND_PORT=8000
|
|
8
|
+
OPEN_BROWSER=1
|
|
8
9
|
|
|
9
10
|
usage() {
|
|
10
|
-
echo "Usage: $0 [path-to-scan] [--port PORT]" >&2
|
|
11
|
+
echo "Usage: $0 [path-to-scan] [--port PORT] [--no-open]" >&2
|
|
11
12
|
echo " path-to-scan optional: directory whose first-level children get scanned for git repos." >&2
|
|
12
13
|
echo " Only used to seed the config on first run; scan paths are otherwise" >&2
|
|
13
14
|
echo " managed in the app (welcome wizard / Settings)." >&2
|
|
14
15
|
echo " --port PORT backend port (default: 8000). If you change this, also update" >&2
|
|
15
16
|
echo " src/frontend/vite.config.ts's proxy target -- don't commit that change." >&2
|
|
17
|
+
echo " --no-open don't open the app in a browser automatically." >&2
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
SCAN_PATH=""
|
|
@@ -26,6 +28,10 @@ while [ $# -gt 0 ]; do
|
|
|
26
28
|
BACKEND_PORT="${2:?--port requires a value}"
|
|
27
29
|
shift 2
|
|
28
30
|
;;
|
|
31
|
+
--no-open)
|
|
32
|
+
OPEN_BROWSER=0
|
|
33
|
+
shift
|
|
34
|
+
;;
|
|
29
35
|
-*)
|
|
30
36
|
echo "Unknown argument: $1" >&2
|
|
31
37
|
usage
|
|
@@ -101,10 +107,10 @@ trap cleanup EXIT INT TERM
|
|
|
101
107
|
|
|
102
108
|
if [ -n "$SCAN_PATH" ]; then
|
|
103
109
|
echo "Starting backend (--reload) on port $BACKEND_PORT, scanning $SCAN_PATH ..."
|
|
104
|
-
"$GIT_JUGGLER" "$SCAN_PATH" --reload --port "$BACKEND_PORT" &
|
|
110
|
+
"$GIT_JUGGLER" "$SCAN_PATH" --reload --port "$BACKEND_PORT" --no-open &
|
|
105
111
|
else
|
|
106
112
|
echo "Starting backend (--reload) on port $BACKEND_PORT (scan paths from config) ..."
|
|
107
|
-
"$GIT_JUGGLER" --reload --port "$BACKEND_PORT" &
|
|
113
|
+
"$GIT_JUGGLER" --reload --port "$BACKEND_PORT" --no-open &
|
|
108
114
|
fi
|
|
109
115
|
BACKEND_PID=$!
|
|
110
116
|
|
|
@@ -112,6 +118,10 @@ echo "Starting frontend dev server..."
|
|
|
112
118
|
(cd "$ROOT_DIR/src/frontend" && npm run dev) &
|
|
113
119
|
FRONTEND_PID=$!
|
|
114
120
|
|
|
121
|
+
if [ "$OPEN_BROWSER" = "1" ]; then
|
|
122
|
+
(sleep 1 && "$VENV_BIN/python" -m webbrowser http://localhost:5173) >/dev/null 2>&1 &
|
|
123
|
+
fi
|
|
124
|
+
|
|
115
125
|
echo ""
|
|
116
126
|
echo "Backend: http://127.0.0.1:$BACKEND_PORT"
|
|
117
127
|
echo "Frontend: http://localhost:5173 <- open this"
|
|
@@ -51,10 +51,11 @@ def api_ci_poll(
|
|
|
51
51
|
repo_id: str,
|
|
52
52
|
run: Annotated[list[str] | None, Query()] = None,
|
|
53
53
|
head_sha: str | None = None,
|
|
54
|
+
branch_name: str | None = None,
|
|
54
55
|
) -> list[CiRunInfo]:
|
|
55
56
|
path = _resolve_repo_path(repo_id)
|
|
56
57
|
return poll_ci_runs(
|
|
57
|
-
path, run, head_sha, config.load_github_config(), config.load_jenkins_config()
|
|
58
|
+
path, run, head_sha, branch_name, config.load_github_config(), config.load_jenkins_config()
|
|
58
59
|
)
|
|
59
60
|
|
|
60
61
|
|
|
@@ -19,7 +19,7 @@ class CiProvider(Protocol):
|
|
|
19
19
|
graph and refreshed on its own, slow, interval — never by the run poll."""
|
|
20
20
|
...
|
|
21
21
|
|
|
22
|
-
def poll_runs(self, repo_path: Path, run_ids: list[str] | None, head_sha: str | None) -> list[CiRunInfo]:
|
|
22
|
+
def poll_runs(self, repo_path: Path, run_ids: list[str] | None, head_sha: str | None, branch_name: str | None) -> list[CiRunInfo]:
|
|
23
23
|
"""`run_ids=None`: every active (queued/running) run of the repo, to
|
|
24
24
|
discover a pipeline that just started (`head_sha` narrows it to one
|
|
25
25
|
commit where the provider can filter). Otherwise exactly those runs,
|
|
@@ -36,7 +36,7 @@ class GitHubActionsProvider:
|
|
|
36
36
|
def fetch_completed_runs(self, repo_path: Path, commit_hashes: set[str]) -> dict[str, list[CiRunInfo]]:
|
|
37
37
|
return github_actions.get_github_actions_runs(repo_path, commit_hashes, self.config or {})
|
|
38
38
|
|
|
39
|
-
def poll_runs(self, repo_path: Path, run_ids: list[str] | None, head_sha: str | None) -> list[CiRunInfo]:
|
|
39
|
+
def poll_runs(self, repo_path: Path, run_ids: list[str] | None, head_sha: str | None, branch_name: str | None) -> list[CiRunInfo]:
|
|
40
40
|
return github_actions.poll_runs(repo_path, self.config, run_ids, head_sha)
|
|
41
41
|
|
|
42
42
|
|
|
@@ -48,8 +48,8 @@ class JenkinsProvider:
|
|
|
48
48
|
def fetch_completed_runs(self, repo_path: Path, commit_hashes: set[str]) -> dict[str, list[CiRunInfo]]:
|
|
49
49
|
return jenkins.get_jenkins_builds(repo_path, commit_hashes, self.config)
|
|
50
50
|
|
|
51
|
-
def poll_runs(self, repo_path: Path, run_ids: list[str] | None, head_sha: str | None) -> list[CiRunInfo]:
|
|
52
|
-
return jenkins.poll_runs(repo_path, self.config, run_ids, head_sha)
|
|
51
|
+
def poll_runs(self, repo_path: Path, run_ids: list[str] | None, head_sha: str | None, branch_name: str | None) -> list[CiRunInfo]:
|
|
52
|
+
return jenkins.poll_runs(repo_path, self.config, run_ids, head_sha, branch_name)
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def enabled_providers(github_config: dict | None, jenkins_config: dict | None) -> list[CiProvider]:
|
|
@@ -78,6 +78,7 @@ def poll_ci_runs(
|
|
|
78
78
|
repo_path: Path,
|
|
79
79
|
run_refs: list[str] | None,
|
|
80
80
|
head_sha: str | None,
|
|
81
|
+
branch_name: str | None,
|
|
81
82
|
github_config: dict | None,
|
|
82
83
|
jenkins_config: dict | None,
|
|
83
84
|
) -> list[CiRunInfo]:
|
|
@@ -86,7 +87,7 @@ def poll_ci_runs(
|
|
|
86
87
|
providers named there are asked, about just those runs."""
|
|
87
88
|
providers = enabled_providers(github_config, jenkins_config)
|
|
88
89
|
if run_refs is None:
|
|
89
|
-
return [run for provider in providers for run in provider.poll_runs(repo_path, None, head_sha)]
|
|
90
|
+
return [run for provider in providers for run in provider.poll_runs(repo_path, None, head_sha, branch_name)]
|
|
90
91
|
|
|
91
92
|
ids_by_provider: dict[str, list[str]] = {}
|
|
92
93
|
for ref in run_refs:
|
|
@@ -97,7 +98,7 @@ def poll_ci_runs(
|
|
|
97
98
|
run
|
|
98
99
|
for provider in providers
|
|
99
100
|
if provider.name in ids_by_provider
|
|
100
|
-
for run in provider.poll_runs(repo_path, ids_by_provider[provider.name], head_sha)
|
|
101
|
+
for run in provider.poll_runs(repo_path, ids_by_provider[provider.name], head_sha, branch_name)
|
|
101
102
|
]
|
|
102
103
|
|
|
103
104
|
|
|
@@ -116,7 +117,7 @@ def get_ci_run_stages(
|
|
|
116
117
|
|
|
117
118
|
|
|
118
119
|
def _active_runs_for_repo(repo: RepoSummary, github_config: dict | None, jenkins_config: dict | None) -> list[ActivePipeline]:
|
|
119
|
-
runs = poll_ci_runs(Path(repo.path), None, None, github_config, jenkins_config)
|
|
120
|
+
runs = poll_ci_runs(Path(repo.path), None, None, None, github_config, jenkins_config)
|
|
120
121
|
return [ActivePipeline(repo_id=repo.id, repo_name=repo.name, run=run) for run in runs]
|
|
121
122
|
|
|
122
123
|
|
|
@@ -2,31 +2,125 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
from datetime import datetime, timezone
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
from urllib.parse import unquote, urlencode, urlparse
|
|
8
|
+
from urllib.parse import quote, unquote, urlencode, urlparse
|
|
8
9
|
from urllib.request import urlopen
|
|
9
10
|
|
|
10
11
|
from .http import fetch_json
|
|
11
12
|
from ..schemas import CiRunInfo, CiStage
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
_TEMPLATE_RE = re.compile(r"\{([A-Za-z0-9_]+)\}")
|
|
16
|
+
_BRANCH_TEMPLATE_RE = re.compile(r"\{branch_[A-Za-z0-9_]+\}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _slug(value: str) -> str:
|
|
20
|
+
return re.sub(r"[^A-Za-z0-9._-]+", "-", value).strip("-") or value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _repo_template_values(repo_path: Path) -> dict[str, str]:
|
|
24
|
+
resolved = repo_path.expanduser().resolve()
|
|
25
|
+
repo_name = resolved.name
|
|
26
|
+
repo_slug = _slug(repo_name)
|
|
27
|
+
return {
|
|
28
|
+
"repo_path": str(resolved),
|
|
29
|
+
"repo_name": repo_name,
|
|
30
|
+
"repo_name_url": quote(repo_name, safe=""),
|
|
31
|
+
"repo_slug": repo_slug,
|
|
32
|
+
"repo_slug_url": quote(repo_slug, safe=""),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _branch_template_values(branch_name: str | None) -> dict[str, str]:
|
|
37
|
+
if not branch_name:
|
|
38
|
+
return {}
|
|
39
|
+
branch_slug = _slug(branch_name)
|
|
40
|
+
return {
|
|
41
|
+
"branch_name": branch_name,
|
|
42
|
+
"branch_name_url": quote(branch_name, safe=""),
|
|
43
|
+
"branch_slug": branch_slug,
|
|
44
|
+
"branch_slug_url": quote(branch_slug, safe=""),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _render_template(value: str, values: dict[str, str]) -> str:
|
|
49
|
+
return _TEMPLATE_RE.sub(lambda match: values.get(match.group(1), match.group(0)), value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _has_branch_template(value: str) -> bool:
|
|
53
|
+
return _BRANCH_TEMPLATE_RE.search(value) is not None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _matches_repo_path(raw_path: str, repo_path: Path, values: dict[str, str]) -> bool:
|
|
57
|
+
rendered_path = _render_template(raw_path, values).strip()
|
|
58
|
+
if rendered_path == "*":
|
|
59
|
+
return True
|
|
60
|
+
try:
|
|
61
|
+
return Path(rendered_path).expanduser().resolve() == repo_path.resolve()
|
|
62
|
+
except Exception:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _render_job_config(item: dict, repo_path: Path, values: dict[str, str], branch_name: str | None) -> dict | None:
|
|
67
|
+
job_url = item.get("job_url")
|
|
68
|
+
if not isinstance(job_url, str) or not job_url:
|
|
69
|
+
return None
|
|
70
|
+
if _has_branch_template(job_url) and not branch_name:
|
|
71
|
+
return None
|
|
72
|
+
rendered = _render_template(job_url, {**values, **_branch_template_values(branch_name)})
|
|
73
|
+
return {**item, "repo_path": str(repo_path.resolve()), "job_url": rendered}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _matching_rule_job_configs(jenkins_config: dict, repo_path: Path, values: dict[str, str], branch_name: str | None) -> list[dict]:
|
|
77
|
+
for rule in jenkins_config.get("rules", []):
|
|
78
|
+
if not isinstance(rule, dict):
|
|
18
79
|
continue
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
80
|
+
repo_paths = rule.get("repo_paths")
|
|
81
|
+
if not isinstance(repo_paths, list) or not any(
|
|
82
|
+
isinstance(raw_path, str) and _matches_repo_path(raw_path, repo_path, values) for raw_path in repo_paths
|
|
83
|
+
):
|
|
22
84
|
continue
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
85
|
+
rendered = _render_job_config({"repo_path": str(repo_path), "job_url": rule.get("job_url")}, repo_path, values, branch_name)
|
|
86
|
+
if rendered is not None:
|
|
87
|
+
return [rendered]
|
|
88
|
+
return []
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _matching_job_configs(jenkins_config: dict, repo_path: Path, branch_name: str | None = None) -> list[dict]:
|
|
93
|
+
values = _repo_template_values(repo_path)
|
|
94
|
+
return _matching_rule_job_configs(jenkins_config, repo_path, values, branch_name)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _job_url_prefixes(jenkins_config: dict, repo_path: Path) -> list[str]:
|
|
98
|
+
values = _repo_template_values(repo_path)
|
|
99
|
+
prefixes: list[str] = []
|
|
100
|
+
for job in _matching_job_configs(jenkins_config, repo_path):
|
|
101
|
+
prefixes.append(str(job["job_url"]).rstrip("/"))
|
|
102
|
+
|
|
103
|
+
items: list[dict] = []
|
|
104
|
+
for rule in jenkins_config.get("rules", []):
|
|
105
|
+
if not isinstance(rule, dict):
|
|
26
106
|
continue
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
107
|
+
repo_paths = rule.get("repo_paths")
|
|
108
|
+
if not isinstance(repo_paths, list) or not any(
|
|
109
|
+
isinstance(raw_path, str) and _matches_repo_path(raw_path, repo_path, values) for raw_path in repo_paths
|
|
110
|
+
):
|
|
111
|
+
continue
|
|
112
|
+
items.append({"job_url": rule.get("job_url")})
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
for item in items:
|
|
116
|
+
raw_url = item.get("job_url")
|
|
117
|
+
if not isinstance(raw_url, str) or not _has_branch_template(raw_url):
|
|
118
|
+
continue
|
|
119
|
+
prefix_template = _BRANCH_TEMPLATE_RE.split(raw_url, maxsplit=1)[0]
|
|
120
|
+
prefix = _render_template(prefix_template, values).rstrip("/")
|
|
121
|
+
if prefix:
|
|
122
|
+
prefixes.append(prefix)
|
|
123
|
+
return list(dict.fromkeys(prefixes))
|
|
30
124
|
|
|
31
125
|
|
|
32
126
|
def _headers(jenkins_config: dict) -> dict[str, str]:
|
|
@@ -228,14 +322,13 @@ def get_build_stages(repo_path: Path, jenkins_config: dict | None, build_url: st
|
|
|
228
322
|
followed when it lives under a job configured for this repo."""
|
|
229
323
|
if not jenkins_config:
|
|
230
324
|
return None
|
|
231
|
-
wanted = build_url.rstrip("/")
|
|
232
|
-
for
|
|
233
|
-
|
|
234
|
-
return _fetch_build_stages(build_url, _headers(jenkins_config))
|
|
325
|
+
wanted = build_url.rstrip("/")
|
|
326
|
+
if any(wanted.startswith(prefix + "/") for prefix in _job_url_prefixes(jenkins_config, repo_path)):
|
|
327
|
+
return _fetch_build_stages(build_url, _headers(jenkins_config))
|
|
235
328
|
return None
|
|
236
329
|
|
|
237
330
|
|
|
238
|
-
def get_active_builds(repo_path: Path, jenkins_config: dict | None) -> list[CiRunInfo]:
|
|
331
|
+
def get_active_builds(repo_path: Path, jenkins_config: dict | None, branch_name: str | None = None) -> list[CiRunInfo]:
|
|
239
332
|
"""Builds running right now, with their stages."""
|
|
240
333
|
if not jenkins_config:
|
|
241
334
|
return []
|
|
@@ -245,7 +338,7 @@ def get_active_builds(repo_path: Path, jenkins_config: dict | None) -> list[CiRu
|
|
|
245
338
|
"actions[lastBuiltRevision[SHA1,branch[name]],parameters[name,value]]]{0,10}"
|
|
246
339
|
)
|
|
247
340
|
result: list[CiRunInfo] = []
|
|
248
|
-
for job in _matching_job_configs(jenkins_config, repo_path):
|
|
341
|
+
for job in _matching_job_configs(jenkins_config, repo_path, branch_name):
|
|
249
342
|
job_url = str(job["job_url"])
|
|
250
343
|
data = _fetch_json(f"{job_url.rstrip('/')}/api/json?{urlencode({'tree': tree})}", headers)
|
|
251
344
|
builds = data.get("builds") if isinstance(data, dict) else None
|
|
@@ -266,7 +359,7 @@ def get_builds_by_url(repo_path: Path, jenkins_config: dict | None, build_urls:
|
|
|
266
359
|
if not jenkins_config:
|
|
267
360
|
return []
|
|
268
361
|
headers = _headers(jenkins_config)
|
|
269
|
-
job_urls =
|
|
362
|
+
job_urls = _job_url_prefixes(jenkins_config, repo_path)
|
|
270
363
|
|
|
271
364
|
result: list[CiRunInfo] = []
|
|
272
365
|
for build_url in build_urls:
|
|
@@ -289,12 +382,13 @@ def poll_runs(
|
|
|
289
382
|
jenkins_config: dict | None,
|
|
290
383
|
run_ids: list[str] | None,
|
|
291
384
|
head_sha: str | None = None,
|
|
385
|
+
branch_name: str | None = None,
|
|
292
386
|
) -> list[CiRunInfo]:
|
|
293
387
|
"""`run_ids=None`: every running build of the repo's jobs (discovery).
|
|
294
388
|
Otherwise exactly those builds, whatever their state. Jenkins can't filter
|
|
295
389
|
builds by commit, so `head_sha` is accepted for interface parity only."""
|
|
296
390
|
if run_ids is None:
|
|
297
|
-
return get_active_builds(repo_path, jenkins_config)
|
|
391
|
+
return get_active_builds(repo_path, jenkins_config, branch_name)
|
|
298
392
|
return get_builds_by_url(repo_path, jenkins_config, run_ids)
|
|
299
393
|
|
|
300
394
|
|
|
@@ -308,10 +402,11 @@ def test_connection(jenkins_config: dict | None) -> tuple[bool, str]:
|
|
|
308
402
|
if isinstance(base_url, str) and base_url.strip():
|
|
309
403
|
urls.append(f"{base_url.strip().rstrip('/')}/api/json")
|
|
310
404
|
|
|
311
|
-
for
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
405
|
+
for rule in jenkins_config.get("rules", []):
|
|
406
|
+
if isinstance(rule, dict):
|
|
407
|
+
job_url = rule.get("job_url")
|
|
408
|
+
if isinstance(job_url, str) and job_url.strip() and not _has_branch_template(job_url):
|
|
409
|
+
urls.append(f"{job_url.strip().rstrip('/')}/api/json")
|
|
315
410
|
|
|
316
411
|
if not urls:
|
|
317
412
|
return False, "Set a Jenkins base URL or at least one job URL first."
|