secondshift 0.1.0b12__py3-none-any.whl
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.
- secondshift/__init__.py +1 -0
- secondshift/__main__.py +3 -0
- secondshift/cli.py +17 -0
- secondshift/config/__init__.py +0 -0
- secondshift/config/api.py +23 -0
- secondshift/config/app.py +62 -0
- secondshift/config/constants.py +279 -0
- secondshift/config/container_env.py +118 -0
- secondshift/config/first_run.py +40 -0
- secondshift/config/formatting.py +5 -0
- secondshift/config/gunicorn.py +31 -0
- secondshift/config/logging.py +40 -0
- secondshift/config/settings/__init__.py +0 -0
- secondshift/config/settings/base.py +524 -0
- secondshift/config/settings/dev.py +14 -0
- secondshift/config/settings/local.py +23 -0
- secondshift/config/settings/production.py +113 -0
- secondshift/config/settings/runner.py +140 -0
- secondshift/config/settings/test.py +47 -0
- secondshift/config/static_storage.py +45 -0
- secondshift/config/urls.py +42 -0
- secondshift/connections/__init__.py +0 -0
- secondshift/connections/access.py +27 -0
- secondshift/connections/admin.py +25 -0
- secondshift/connections/agents.py +224 -0
- secondshift/connections/api.py +747 -0
- secondshift/connections/apps.py +6 -0
- secondshift/connections/catalog.py +96 -0
- secondshift/connections/checks.py +22 -0
- secondshift/connections/dao.py +305 -0
- secondshift/connections/errors.py +13 -0
- secondshift/connections/execution.py +262 -0
- secondshift/connections/external.py +258 -0
- secondshift/connections/github.py +123 -0
- secondshift/connections/http_api/__init__.py +1 -0
- secondshift/connections/http_api/client.py +161 -0
- secondshift/connections/http_api/connections.py +187 -0
- secondshift/connections/http_api/spec.py +382 -0
- secondshift/connections/mcp/__init__.py +1 -0
- secondshift/connections/mcp/base.py +139 -0
- secondshift/connections/mcp/client.py +353 -0
- secondshift/connections/mcp/custom.py +86 -0
- secondshift/connections/mcp/github.py +256 -0
- secondshift/connections/mcp/oauth.py +352 -0
- secondshift/connections/mcp/providers.py +15 -0
- secondshift/connections/mcp/schemas.py +77 -0
- secondshift/connections/mcp/sentry.py +135 -0
- secondshift/connections/mcp/signin.py +206 -0
- secondshift/connections/migration_helpers.py +28 -0
- secondshift/connections/migrations/0001_initial.py +101 -0
- secondshift/connections/migrations/0002_alter_connection_kind.py +18 -0
- secondshift/connections/migrations/0003_require_approval.py +23 -0
- secondshift/connections/migrations/0004_new_connections_skip_approval.py +18 -0
- secondshift/connections/migrations/0005_agentconnection.py +36 -0
- secondshift/connections/migrations/0006_remove_is_default.py +23 -0
- secondshift/connections/migrations/0007_agent_last_check.py +18 -0
- secondshift/connections/migrations/0008_agentcredentialcleanup.py +25 -0
- secondshift/connections/migrations/0009_release_0_1_0b11.py +52 -0
- secondshift/connections/migrations/0010_connection_attribution.py +18 -0
- secondshift/connections/migrations/__init__.py +0 -0
- secondshift/connections/model_providers.py +441 -0
- secondshift/connections/models.py +246 -0
- secondshift/connections/outbound.py +317 -0
- secondshift/connections/repository_ref.py +19 -0
- secondshift/connections/schemas.py +313 -0
- secondshift/connections/serialization.py +100 -0
- secondshift/connections/services.py +94 -0
- secondshift/connections/tools.py +38 -0
- secondshift/connections/webhooks.py +107 -0
- secondshift/core/__init__.py +0 -0
- secondshift/core/adapters.py +38 -0
- secondshift/core/admin.py +5 -0
- secondshift/core/api.py +104 -0
- secondshift/core/apps.py +20 -0
- secondshift/core/launcher.py +311 -0
- secondshift/core/local_agents.py +90 -0
- secondshift/core/local_output.py +120 -0
- secondshift/core/management/__init__.py +0 -0
- secondshift/core/management/commands/__init__.py +0 -0
- secondshift/core/management/commands/bootstrap_dev.py +33 -0
- secondshift/core/management/commands/dev_reset_db.py +204 -0
- secondshift/core/management/commands/login.py +23 -0
- secondshift/core/management/commands/logs.py +58 -0
- secondshift/core/management/commands/status.py +46 -0
- secondshift/core/management/commands/up.py +244 -0
- secondshift/core/middleware.py +21 -0
- secondshift/core/migrations/__init__.py +0 -0
- secondshift/core/schemas.py +71 -0
- secondshift/core/signin.py +58 -0
- secondshift/core/static/core/account.css +149 -0
- secondshift/core/static/core/admin.css +135 -0
- secondshift/core/templatetags/__init__.py +0 -0
- secondshift/core/templatetags/accounts.py +20 -0
- secondshift/core/terminal.py +27 -0
- secondshift/core/views.py +10 -0
- secondshift/gateway/__init__.py +0 -0
- secondshift/gateway/apps.py +6 -0
- secondshift/gateway/dao.py +137 -0
- secondshift/gateway/metering.py +171 -0
- secondshift/gateway/migrations/0001_initial.py +54 -0
- secondshift/gateway/migrations/0002_modelusage_gateway_usage_created_at_idx.py +19 -0
- secondshift/gateway/migrations/__init__.py +0 -0
- secondshift/gateway/models.py +61 -0
- secondshift/gateway/pricing.py +39 -0
- secondshift/gateway/proxy.py +369 -0
- secondshift/gateway/tokens.py +68 -0
- secondshift/gateway/urls.py +17 -0
- secondshift/gateway/views.py +11 -0
- secondshift/github/__init__.py +6 -0
- secondshift/github/app.py +109 -0
- secondshift/github/authors.py +56 -0
- secondshift/github/credentials.py +59 -0
- secondshift/github/errors.py +2 -0
- secondshift/github/git.py +56 -0
- secondshift/github/identity.py +35 -0
- secondshift/github/installations.py +74 -0
- secondshift/github/issues.py +75 -0
- secondshift/github/permissions.py +49 -0
- secondshift/github/pulls.py +49 -0
- secondshift/github/repos.py +52 -0
- secondshift/github/rest.py +93 -0
- secondshift/github/ruff.toml +16 -0
- secondshift/github/tokens.py +52 -0
- secondshift/jobs/__init__.py +0 -0
- secondshift/jobs/access.py +12 -0
- secondshift/jobs/admin.py +83 -0
- secondshift/jobs/api.py +184 -0
- secondshift/jobs/apps.py +6 -0
- secondshift/jobs/automatic_grants.py +115 -0
- secondshift/jobs/autosync.py +78 -0
- secondshift/jobs/capabilities.py +43 -0
- secondshift/jobs/dao.py +258 -0
- secondshift/jobs/drafts.py +112 -0
- secondshift/jobs/grants.py +116 -0
- secondshift/jobs/limits.py +26 -0
- secondshift/jobs/management/__init__.py +0 -0
- secondshift/jobs/management/commands/__init__.py +0 -0
- secondshift/jobs/manifest.py +364 -0
- secondshift/jobs/migrations/0001_initial.py +87 -0
- secondshift/jobs/migrations/0002_job_history.py +18 -0
- secondshift/jobs/migrations/0003_job_skill_text.py +23 -0
- secondshift/jobs/migrations/0004_jobgrant_tool_rules.py +18 -0
- secondshift/jobs/migrations/0005_job_triggers.py +18 -0
- secondshift/jobs/migrations/0006_job_webhook.py +18 -0
- secondshift/jobs/migrations/0007_job_deliveries.py +23 -0
- secondshift/jobs/migrations/0008_job_removed_at.py +18 -0
- secondshift/jobs/migrations/0009_job_subagents.py +18 -0
- secondshift/jobs/migrations/0010_job_skill_source.py +18 -0
- secondshift/jobs/migrations/__init__.py +0 -0
- secondshift/jobs/models.py +199 -0
- secondshift/jobs/needs.py +154 -0
- secondshift/jobs/plan.py +176 -0
- secondshift/jobs/planning.py +386 -0
- secondshift/jobs/readiness.py +46 -0
- secondshift/jobs/repository_sync.py +199 -0
- secondshift/jobs/scheduling.py +26 -0
- secondshift/jobs/schemas.py +207 -0
- secondshift/jobs/serialization.py +218 -0
- secondshift/jobs/skill_needs.py +189 -0
- secondshift/jobs/sync.py +252 -0
- secondshift/jobs/tool_policy.py +82 -0
- secondshift/jobs/tool_rules.py +200 -0
- secondshift/mcp_server/__init__.py +6 -0
- secondshift/mcp_server/api.py +79 -0
- secondshift/mcp_server/apps.py +7 -0
- secondshift/mcp_server/audit.py +22 -0
- secondshift/mcp_server/core.py +12 -0
- secondshift/mcp_server/dao.py +23 -0
- secondshift/mcp_server/migrations/0001_initial.py +33 -0
- secondshift/mcp_server/migrations/__init__.py +0 -0
- secondshift/mcp_server/models.py +40 -0
- secondshift/mcp_server/schemas.py +204 -0
- secondshift/mcp_server/tools/__init__.py +9 -0
- secondshift/mcp_server/tools/jobs.py +106 -0
- secondshift/mcp_server/tools/manifests.py +127 -0
- secondshift/mcp_server/tools/repositories.py +42 -0
- secondshift/mcp_server/tools/runs.py +86 -0
- secondshift/mcp_server/tools/tasks.py +103 -0
- secondshift/mcp_server/tools_registry.py +81 -0
- secondshift/repositories/__init__.py +0 -0
- secondshift/repositories/access.py +13 -0
- secondshift/repositories/admin.py +19 -0
- secondshift/repositories/api.py +296 -0
- secondshift/repositories/apps.py +6 -0
- secondshift/repositories/dao.py +114 -0
- secondshift/repositories/external_skills.py +224 -0
- secondshift/repositories/github.py +27 -0
- secondshift/repositories/github_access.py +57 -0
- secondshift/repositories/github_check.py +279 -0
- secondshift/repositories/github_sync.py +181 -0
- secondshift/repositories/github_views.py +90 -0
- secondshift/repositories/local.py +198 -0
- secondshift/repositories/management/__init__.py +0 -0
- secondshift/repositories/management/commands/__init__.py +0 -0
- secondshift/repositories/management/commands/prune_source_cache.py +15 -0
- secondshift/repositories/migrations/0001_initial.py +45 -0
- secondshift/repositories/migrations/0002_repository_last_sync_error.py +18 -0
- secondshift/repositories/migrations/0003_repository_checked_sha.py +18 -0
- secondshift/repositories/migrations/0004_pull_request_settings.py +23 -0
- secondshift/repositories/migrations/0005_remove_repository_installation_id.py +17 -0
- secondshift/repositories/migrations/0006_allow_issues.py +18 -0
- secondshift/repositories/migrations/__init__.py +0 -0
- secondshift/repositories/mirrors.py +50 -0
- secondshift/repositories/models.py +104 -0
- secondshift/repositories/pull_requests.py +199 -0
- secondshift/repositories/schemas.py +102 -0
- secondshift/repositories/serialization.py +133 -0
- secondshift/repositories/skill_source.py +64 -0
- secondshift/repositories/snapshots.py +238 -0
- secondshift/repositories/sources.py +256 -0
- secondshift/repositories/status.py +67 -0
- secondshift/runner/README.md +65 -0
- secondshift/runner/__init__.py +1 -0
- secondshift/runner/agent_credentials.py +89 -0
- secondshift/runner/agent_image/Dockerfile +16 -0
- secondshift/runner/agent_image/agent.py +132 -0
- secondshift/runner/agent_image/claude.py +254 -0
- secondshift/runner/agent_image/codex.py +84 -0
- secondshift/runner/agent_image/outcomes.py +120 -0
- secondshift/runner/agent_image/subagent_hook.py +71 -0
- secondshift/runner/agent_protocol.py +41 -0
- secondshift/runner/agents.py +254 -0
- secondshift/runner/api.py +123 -0
- secondshift/runner/apps.py +6 -0
- secondshift/runner/client.py +168 -0
- secondshift/runner/collection.py +276 -0
- secondshift/runner/dao.py +64 -0
- secondshift/runner/dev.py +19 -0
- secondshift/runner/images.py +57 -0
- secondshift/runner/migrations/0001_initial.py +48 -0
- secondshift/runner/migrations/0002_session_source_sha256.py +18 -0
- secondshift/runner/migrations/0003_session_collect_changes.py +18 -0
- secondshift/runner/migrations/0004_session_history_sha256.py +18 -0
- secondshift/runner/migrations/0005_require_source_sha256.py +18 -0
- secondshift/runner/migrations/0006_session_skill_sha256.py +18 -0
- secondshift/runner/migrations/__init__.py +0 -0
- secondshift/runner/models.py +53 -0
- secondshift/runner/protocol.py +97 -0
- secondshift/runner/schemas.py +36 -0
- secondshift/runner/services.py +465 -0
- secondshift/runner/sources.py +130 -0
- secondshift/runner/urls.py +7 -0
- secondshift/runs/__init__.py +0 -0
- secondshift/runs/access.py +13 -0
- secondshift/runs/admin.py +89 -0
- secondshift/runs/api.py +211 -0
- secondshift/runs/apps.py +11 -0
- secondshift/runs/artifacts.py +261 -0
- secondshift/runs/dao.py +640 -0
- secondshift/runs/deliveries.py +80 -0
- secondshift/runs/execution/__init__.py +0 -0
- secondshift/runs/execution/access_needs.py +89 -0
- secondshift/runs/execution/agent.py +91 -0
- secondshift/runs/execution/agent_broker.py +198 -0
- secondshift/runs/execution/budgets.py +177 -0
- secondshift/runs/execution/cli_agent.py +167 -0
- secondshift/runs/execution/consent.py +58 -0
- secondshift/runs/execution/deadlines.py +47 -0
- secondshift/runs/execution/external_tools.py +84 -0
- secondshift/runs/execution/issues.py +99 -0
- secondshift/runs/execution/model_gateway.py +90 -0
- secondshift/runs/execution/payload.py +63 -0
- secondshift/runs/execution/result.py +59 -0
- secondshift/runs/execution/run.py +345 -0
- secondshift/runs/execution/subagents.py +246 -0
- secondshift/runs/execution/tool_broker.py +123 -0
- secondshift/runs/execution/workspace_tools.py +248 -0
- secondshift/runs/history.py +182 -0
- secondshift/runs/inputs.py +49 -0
- secondshift/runs/issue_delivery.py +144 -0
- secondshift/runs/management/__init__.py +0 -0
- secondshift/runs/management/commands/__init__.py +0 -0
- secondshift/runs/management/commands/prune_runs.py +12 -0
- secondshift/runs/management/commands/run_dbos_worker.py +129 -0
- secondshift/runs/migration_helpers.py +313 -0
- secondshift/runs/migrations/0001_initial.py +69 -0
- secondshift/runs/migrations/0002_run_started_by.py +18 -0
- secondshift/runs/migrations/0003_agentlease.py +25 -0
- secondshift/runs/migrations/0004_task_assignment.py +39 -0
- secondshift/runs/migrations/0005_backfill_tasks.py +19 -0
- secondshift/runs/migrations/0006_standalone_tasks.py +50 -0
- secondshift/runs/migrations/0007_run_delivery.py +35 -0
- secondshift/runs/migrations/0008_run_outputs.py +18 -0
- secondshift/runs/migrations/0009_pull_request_delivery.py +28 -0
- secondshift/runs/migrations/0010_run_issue.py +32 -0
- secondshift/runs/migrations/0011_issues_delivery.py +23 -0
- secondshift/runs/migrations/0012_alter_rundelivery_status.py +18 -0
- secondshift/runs/migrations/0013_release_0_1_0b11.py +113 -0
- secondshift/runs/migrations/0014_run_access_need.py +30 -0
- secondshift/runs/migrations/__init__.py +0 -0
- secondshift/runs/models.py +340 -0
- secondshift/runs/orchestration/__init__.py +0 -0
- secondshift/runs/orchestration/admission.py +15 -0
- secondshift/runs/orchestration/dbos_runtime.py +43 -0
- secondshift/runs/orchestration/dispatch.py +130 -0
- secondshift/runs/orchestration/periodic.py +39 -0
- secondshift/runs/orchestration/reconciliation.py +98 -0
- secondshift/runs/orchestration/schedules.py +51 -0
- secondshift/runs/orchestration/workflows.py +480 -0
- secondshift/runs/outputs.py +52 -0
- secondshift/runs/publishing.py +162 -0
- secondshift/runs/queue.py +26 -0
- secondshift/runs/reporting/__init__.py +0 -0
- secondshift/runs/reporting/collection.py +26 -0
- secondshift/runs/reporting/connection_usage.py +45 -0
- secondshift/runs/reporting/failures.py +150 -0
- secondshift/runs/reporting/health.py +158 -0
- secondshift/runs/reporting/notifications.py +258 -0
- secondshift/runs/reporting/queries.py +60 -0
- secondshift/runs/retention.py +35 -0
- secondshift/runs/schemas.py +192 -0
- secondshift/runs/serialization.py +245 -0
- secondshift/runs/services.py +176 -0
- secondshift/runs/signals.py +25 -0
- secondshift/runs/submissions.py +236 -0
- secondshift/runs/transitions.py +84 -0
- secondshift/templates/account/password_reset_help_text.html +1 -0
- secondshift/templates/admin/base_site.html +24 -0
- secondshift/templates/allauth/elements/provider.html +9 -0
- secondshift/templates/allauth/layouts/base.html +67 -0
- secondshift/templates/socialaccount/snippets/login.html +9 -0
- secondshift/templates/workspaces/invite_unusable.html +10 -0
- secondshift/triggers/__init__.py +0 -0
- secondshift/triggers/admin.py +19 -0
- secondshift/triggers/api.py +116 -0
- secondshift/triggers/apps.py +6 -0
- secondshift/triggers/dao.py +66 -0
- secondshift/triggers/filters.py +48 -0
- secondshift/triggers/migrations/0001_initial.py +55 -0
- secondshift/triggers/migrations/0002_ignored_events_and_hourly_limit.py +23 -0
- secondshift/triggers/migrations/__init__.py +0 -0
- secondshift/triggers/models.py +83 -0
- secondshift/triggers/schemas.py +58 -0
- secondshift/triggers/serialization.py +91 -0
- secondshift/triggers/services.py +246 -0
- secondshift/triggers/verification.py +84 -0
- secondshift/web/assets/index-Bwy0hvk_.js +2419 -0
- secondshift/web/assets/index-D27gB9_L.css +1 -0
- secondshift/web/favicon.svg +19 -0
- secondshift/web/index.html +16 -0
- secondshift/web/secondshift-mark.svg +16 -0
- secondshift/workspaces/__init__.py +0 -0
- secondshift/workspaces/access.py +181 -0
- secondshift/workspaces/admin.py +37 -0
- secondshift/workspaces/api.py +264 -0
- secondshift/workspaces/apps.py +6 -0
- secondshift/workspaces/context_processors.py +11 -0
- secondshift/workspaces/dao.py +281 -0
- secondshift/workspaces/invites.py +150 -0
- secondshift/workspaces/limits.py +197 -0
- secondshift/workspaces/management/__init__.py +0 -0
- secondshift/workspaces/management/commands/__init__.py +0 -0
- secondshift/workspaces/management/commands/create_workspace.py +39 -0
- secondshift/workspaces/migrations/0001_initial.py +67 -0
- secondshift/workspaces/migrations/0002_api_token.py +34 -0
- secondshift/workspaces/migrations/0003_execution_default.py +29 -0
- secondshift/workspaces/migrations/0004_copy_default_model_key.py +26 -0
- secondshift/workspaces/migrations/__init__.py +0 -0
- secondshift/workspaces/models.py +169 -0
- secondshift/workspaces/owners.py +58 -0
- secondshift/workspaces/schemas.py +149 -0
- secondshift/workspaces/serialization.py +138 -0
- secondshift/workspaces/server_settings.py +140 -0
- secondshift/workspaces/services.py +94 -0
- secondshift/workspaces/tokens.py +108 -0
- secondshift/workspaces/views.py +32 -0
- secondshift-0.1.0b12.dist-info/METADATA +270 -0
- secondshift-0.1.0b12.dist-info/RECORD +371 -0
- secondshift-0.1.0b12.dist-info/WHEEL +4 -0
- secondshift-0.1.0b12.dist-info/entry_points.txt +2 -0
- secondshift-0.1.0b12.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Fetch GitHub source into the control plane's mirror. Tokens never leave it."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from secondshift.connections.dao import connected_github_sources
|
|
9
|
+
from secondshift.connections.github import connection_source, usable_source
|
|
10
|
+
from secondshift.connections.models import Connection
|
|
11
|
+
from secondshift.github import credentials, repos
|
|
12
|
+
from secondshift.github.app import configured, installation_repositories
|
|
13
|
+
from secondshift.github.errors import GitHubError
|
|
14
|
+
from secondshift.github.git import authenticated_git
|
|
15
|
+
from secondshift.github.identity import clone_url
|
|
16
|
+
from secondshift.repositories.dao import (
|
|
17
|
+
lock_or_create_github_repository,
|
|
18
|
+
lock_repository,
|
|
19
|
+
)
|
|
20
|
+
from secondshift.repositories.github_access import (
|
|
21
|
+
GitHubAccess,
|
|
22
|
+
connection_access,
|
|
23
|
+
github_access,
|
|
24
|
+
)
|
|
25
|
+
from secondshift.repositories.mirrors import SYNCED_REF, locked_mirror
|
|
26
|
+
from secondshift.repositories.models import Repository
|
|
27
|
+
from secondshift.workspaces.models import Workspace
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class FetchedBranch:
|
|
32
|
+
"""The branch's newest commit, fetched into the mirror."""
|
|
33
|
+
|
|
34
|
+
mirror: Path
|
|
35
|
+
revision: str
|
|
36
|
+
token: str
|
|
37
|
+
|
|
38
|
+
def mark_synced(self) -> None:
|
|
39
|
+
"""Keep this commit's objects in the mirror."""
|
|
40
|
+
authenticated_git(
|
|
41
|
+
self.mirror, self.token, "update-ref", SYNCED_REF, self.revision
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@contextmanager
|
|
46
|
+
def fetched_branch(repository: Repository) -> Iterator[FetchedBranch]:
|
|
47
|
+
"""Fetch the branch and hold the mirror's lock until the caller is done.
|
|
48
|
+
|
|
49
|
+
The fetch runs outside any database transaction.
|
|
50
|
+
"""
|
|
51
|
+
access = github_access(repository)
|
|
52
|
+
with locked_mirror(repository.pk) as mirror:
|
|
53
|
+
authenticated_git(
|
|
54
|
+
mirror,
|
|
55
|
+
access.token,
|
|
56
|
+
"fetch",
|
|
57
|
+
"--no-tags",
|
|
58
|
+
"--",
|
|
59
|
+
access.clone_url,
|
|
60
|
+
f"refs/heads/{repository.default_branch}",
|
|
61
|
+
)
|
|
62
|
+
revision = authenticated_git(mirror, access.token, "rev-parse", "FETCH_HEAD")
|
|
63
|
+
yield FetchedBranch(mirror, revision, access.token)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def record_synced_revision(repository_id: int, revision: str) -> Repository:
|
|
67
|
+
"""Save a good sync of this commit. Call inside a transaction."""
|
|
68
|
+
locked = lock_repository(repository_id)
|
|
69
|
+
locked.config_sha = revision
|
|
70
|
+
locked.checked_sha = revision
|
|
71
|
+
locked.last_sync_error = ""
|
|
72
|
+
locked.save(
|
|
73
|
+
update_fields=["config_sha", "checked_sha", "last_sync_error", "updated_at"]
|
|
74
|
+
)
|
|
75
|
+
return locked
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def fetch_github_commit(access: GitHubAccess, commit_sha: str, mirror: Path) -> None:
|
|
79
|
+
"""Fetch one commit by its SHA into the repository's mirror."""
|
|
80
|
+
try:
|
|
81
|
+
authenticated_git(
|
|
82
|
+
mirror,
|
|
83
|
+
access.token,
|
|
84
|
+
"fetch",
|
|
85
|
+
"--no-tags",
|
|
86
|
+
"--",
|
|
87
|
+
access.clone_url,
|
|
88
|
+
commit_sha,
|
|
89
|
+
)
|
|
90
|
+
except GitHubError as error:
|
|
91
|
+
raise GitHubError(
|
|
92
|
+
f"The commit {commit_sha[:12]} is no longer available from GitHub."
|
|
93
|
+
) from error
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class GitHubRepositoryExistsError(GitHubError):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def source_repositories(connection: Connection) -> list[dict]:
|
|
101
|
+
"""What a source connection can import, as GitHub lists it."""
|
|
102
|
+
source = connection_source(connection)
|
|
103
|
+
if isinstance(source, credentials.AppInstallation):
|
|
104
|
+
return installation_repositories(source.installation_id)
|
|
105
|
+
return repos.token_repositories(source.token)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def available_github_sources(
|
|
109
|
+
workspace: Workspace,
|
|
110
|
+
) -> tuple[list[dict[str, object]], list[str]]:
|
|
111
|
+
available = []
|
|
112
|
+
errors = []
|
|
113
|
+
for connection in connected_github_sources(workspace.pk):
|
|
114
|
+
if not usable_source(connection) or (
|
|
115
|
+
connection.auth_strategy == Connection.AuthStrategy.MANAGED
|
|
116
|
+
and not configured()
|
|
117
|
+
):
|
|
118
|
+
continue
|
|
119
|
+
try:
|
|
120
|
+
repositories = source_repositories(connection)
|
|
121
|
+
except GitHubError as error:
|
|
122
|
+
errors.append(f"{connection.display_name}: {error}")
|
|
123
|
+
continue
|
|
124
|
+
for item in repositories:
|
|
125
|
+
available.append(
|
|
126
|
+
{
|
|
127
|
+
"connection_id": connection.pk,
|
|
128
|
+
"repository_id": item["id"],
|
|
129
|
+
"full_name": item["full_name"],
|
|
130
|
+
"default_branch": item["default_branch"],
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
return available, errors
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# A removed repository that is added again gets these back if its sync fails.
|
|
137
|
+
RESTORED_FIELDS = (
|
|
138
|
+
"removed_at",
|
|
139
|
+
"github_connection",
|
|
140
|
+
"default_branch",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def save_attached_repository(
|
|
145
|
+
workspace: Workspace,
|
|
146
|
+
connection: Connection,
|
|
147
|
+
repository_id: int,
|
|
148
|
+
branch: str,
|
|
149
|
+
metadata: dict,
|
|
150
|
+
) -> tuple[Repository, dict | None]:
|
|
151
|
+
"""The new or restored row, and a restored row's earlier values."""
|
|
152
|
+
repository, created = lock_or_create_github_repository(
|
|
153
|
+
workspace,
|
|
154
|
+
metadata["full_name"],
|
|
155
|
+
defaults={
|
|
156
|
+
"source_type": Repository.SourceType.GITHUB,
|
|
157
|
+
"github_connection": connection,
|
|
158
|
+
"external_id": str(repository_id),
|
|
159
|
+
"clone_url": clone_url(metadata["full_name"]),
|
|
160
|
+
"default_branch": branch or metadata["default_branch"],
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
if created:
|
|
164
|
+
return repository, None
|
|
165
|
+
if (
|
|
166
|
+
repository.removed_at is None
|
|
167
|
+
or repository.source_type != Repository.SourceType.GITHUB
|
|
168
|
+
or repository.external_id != str(repository_id)
|
|
169
|
+
):
|
|
170
|
+
raise GitHubRepositoryExistsError("This repository is already connected.")
|
|
171
|
+
previous = {field: getattr(repository, field) for field in RESTORED_FIELDS}
|
|
172
|
+
repository.removed_at = None
|
|
173
|
+
repository.github_connection = connection
|
|
174
|
+
repository.default_branch = branch or metadata["default_branch"]
|
|
175
|
+
repository.save()
|
|
176
|
+
return repository, previous
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def repository_branches(connection: Connection, repository_id: int) -> list[str]:
|
|
180
|
+
access = connection_access(connection, repository_id)
|
|
181
|
+
return repos.branches(access.token, access.full_name)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Browser authorization verifies installations with the authorizing user's token."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.contrib.auth.decorators import login_required
|
|
8
|
+
from django.core.exceptions import PermissionDenied
|
|
9
|
+
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
|
|
10
|
+
from django.shortcuts import redirect
|
|
11
|
+
from django.urls import reverse
|
|
12
|
+
from django.views.decorators.http import require_GET
|
|
13
|
+
|
|
14
|
+
from secondshift.connections.github import link_installations
|
|
15
|
+
from secondshift.github import installations
|
|
16
|
+
from secondshift.github.app import configured
|
|
17
|
+
from secondshift.github.errors import GitHubError
|
|
18
|
+
from secondshift.workspaces.access import AccessDenied, Action, require
|
|
19
|
+
|
|
20
|
+
STATE_KEY = "secondshift.github_installation"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@login_required
|
|
24
|
+
@require_GET
|
|
25
|
+
def github_start(request: HttpRequest) -> HttpResponse:
|
|
26
|
+
try:
|
|
27
|
+
workspace = require(request, Action.ADD_GITHUB_REPOSITORIES)
|
|
28
|
+
except AccessDenied as error:
|
|
29
|
+
raise PermissionDenied(str(error)) from error
|
|
30
|
+
if not configured():
|
|
31
|
+
return redirect("/repositories/new?source=github")
|
|
32
|
+
state = secrets.token_urlsafe(32)
|
|
33
|
+
request.session[STATE_KEY] = {
|
|
34
|
+
"state": state,
|
|
35
|
+
"workspace": workspace.pk,
|
|
36
|
+
"time": time.time(),
|
|
37
|
+
}
|
|
38
|
+
if request.GET.get("install"):
|
|
39
|
+
return redirect(installations.installation_url(state))
|
|
40
|
+
return redirect(installations.authorization_url(state, callback_url()))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@login_required
|
|
44
|
+
@require_GET
|
|
45
|
+
def github_callback(request: HttpRequest) -> HttpResponse:
|
|
46
|
+
try:
|
|
47
|
+
workspace = require(request, Action.ADD_GITHUB_REPOSITORIES)
|
|
48
|
+
except AccessDenied as error:
|
|
49
|
+
raise PermissionDenied(str(error)) from error
|
|
50
|
+
pending = request.session.get(STATE_KEY, {})
|
|
51
|
+
state = request.GET.get("state", "")
|
|
52
|
+
if (
|
|
53
|
+
not state
|
|
54
|
+
or not secrets.compare_digest(state, pending.get("state", ""))
|
|
55
|
+
or pending.get("workspace") != workspace.pk
|
|
56
|
+
or time.time() - pending.get("time", 0) > 600
|
|
57
|
+
):
|
|
58
|
+
return HttpResponseBadRequest(
|
|
59
|
+
"GitHub authorization expired or does not match this workspace."
|
|
60
|
+
)
|
|
61
|
+
if request.GET.get("setup_action") == "request":
|
|
62
|
+
request.session.pop(STATE_KEY, None)
|
|
63
|
+
return HttpResponseBadRequest(
|
|
64
|
+
"Your organization must approve the GitHub App installation. Connect again after approval."
|
|
65
|
+
)
|
|
66
|
+
if request.GET.get("error"):
|
|
67
|
+
request.session.pop(STATE_KEY, None)
|
|
68
|
+
return HttpResponseBadRequest("GitHub authorization was cancelled.")
|
|
69
|
+
code = request.GET.get("code", "")
|
|
70
|
+
if not code:
|
|
71
|
+
# Installations that already exist still require user authorization.
|
|
72
|
+
return redirect(installations.authorization_url(state, callback_url()))
|
|
73
|
+
request.session.pop(STATE_KEY, None)
|
|
74
|
+
try:
|
|
75
|
+
token = installations.user_token(code, callback_url())
|
|
76
|
+
if not link_installations(workspace, installations.user_installations(token)):
|
|
77
|
+
state = secrets.token_urlsafe(32)
|
|
78
|
+
request.session[STATE_KEY] = {
|
|
79
|
+
"state": state,
|
|
80
|
+
"workspace": workspace.pk,
|
|
81
|
+
"time": time.time(),
|
|
82
|
+
}
|
|
83
|
+
return redirect(installations.installation_url(state))
|
|
84
|
+
except GitHubError as error:
|
|
85
|
+
return HttpResponseBadRequest(str(error))
|
|
86
|
+
return redirect("/repositories/new?source=github")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def callback_url() -> str:
|
|
90
|
+
return settings.FRONTEND_URL.rstrip("/") + reverse("github-callback")
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
import posixpath
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from secondshift.config.constants import GITHUB_BASE_URL, MANIFEST_FILE
|
|
9
|
+
from secondshift.repositories.dao import local_repository, save_local_repository
|
|
10
|
+
from secondshift.repositories.github import github_remote_name
|
|
11
|
+
from secondshift.repositories.models import SYNC_ERROR_MAX_CHARS, Repository
|
|
12
|
+
from secondshift.workspaces.models import Workspace
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LocalRepositoryError(ValueError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class LocalRepositoryLocation:
|
|
23
|
+
root: Path
|
|
24
|
+
# The branch SecondShift follows, and its newest commit.
|
|
25
|
+
branch: str
|
|
26
|
+
revision: str
|
|
27
|
+
repository_name: str
|
|
28
|
+
clone_url: str = ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Git file modes for a regular and an executable file. Symbolic links and
|
|
32
|
+
# submodules are refused so a path cannot point outside the repository.
|
|
33
|
+
FILE_MODES = {"100644", "100755"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def git(root: Path, *arguments: str) -> str:
|
|
37
|
+
try:
|
|
38
|
+
completed = subprocess.run(
|
|
39
|
+
["git", "-C", str(root), *arguments],
|
|
40
|
+
capture_output=True,
|
|
41
|
+
check=False,
|
|
42
|
+
text=True,
|
|
43
|
+
timeout=10,
|
|
44
|
+
)
|
|
45
|
+
except subprocess.TimeoutExpired as error:
|
|
46
|
+
raise LocalRepositoryError("Git took too long to answer.") from error
|
|
47
|
+
if completed.returncode:
|
|
48
|
+
# Git's own output can name folders on this machine, so it stays in
|
|
49
|
+
# the server log. Only owners may see local paths.
|
|
50
|
+
logger.info(
|
|
51
|
+
"git %s failed: %s",
|
|
52
|
+
arguments[0] if arguments else "",
|
|
53
|
+
completed.stderr.strip() or completed.stdout.strip(),
|
|
54
|
+
)
|
|
55
|
+
raise LocalRepositoryError("Git could not inspect the repository.")
|
|
56
|
+
return completed.stdout.strip()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def committed_file(root: Path, revision: str, path: str) -> str:
|
|
60
|
+
"""A file's text at a commit. Only regular files inside the repository."""
|
|
61
|
+
normalized = posixpath.normpath(path)
|
|
62
|
+
if normalized.startswith(("../", "/")) or normalized in ("..", "."):
|
|
63
|
+
raise LocalRepositoryError(f"{path} must be a file inside the repository.")
|
|
64
|
+
try:
|
|
65
|
+
entry = git(root, "ls-tree", revision, "--", normalized)
|
|
66
|
+
except LocalRepositoryError as error:
|
|
67
|
+
raise LocalRepositoryError(f"Git could not read {path}.") from error
|
|
68
|
+
if not entry:
|
|
69
|
+
raise LocalRepositoryError(f"{path} is not committed on the branch.")
|
|
70
|
+
mode = entry.split(maxsplit=1)[0]
|
|
71
|
+
if mode not in FILE_MODES:
|
|
72
|
+
raise LocalRepositoryError(f"{path} must be a regular file, not a link.")
|
|
73
|
+
try:
|
|
74
|
+
return git(root, "show", f"{revision}:{normalized}")
|
|
75
|
+
except LocalRepositoryError as error:
|
|
76
|
+
raise LocalRepositoryError(f"Git could not read {path}.") from error
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def current_branch(root: Path) -> str:
|
|
80
|
+
try:
|
|
81
|
+
return git(root, "symbolic-ref", "--short", "HEAD")
|
|
82
|
+
except LocalRepositoryError as error:
|
|
83
|
+
raise LocalRepositoryError(
|
|
84
|
+
"Check out the branch SecondShift should follow, then add the repository."
|
|
85
|
+
) from error
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def branch_revision(root: Path, branch: str) -> str:
|
|
89
|
+
"""The newest commit on a branch."""
|
|
90
|
+
try:
|
|
91
|
+
return git(root, "rev-parse", "--verify", f"refs/heads/{branch}^{{commit}}")
|
|
92
|
+
except LocalRepositoryError as error:
|
|
93
|
+
raise LocalRepositoryError(f"The branch {branch} does not exist.") from error
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def locate_local_repository(
|
|
97
|
+
raw_path: str, branch: str | None = None
|
|
98
|
+
) -> LocalRepositoryLocation:
|
|
99
|
+
"""Find the repository and the newest commit on a branch.
|
|
100
|
+
|
|
101
|
+
Without a branch it follows the one checked out now.
|
|
102
|
+
"""
|
|
103
|
+
candidate = Path(raw_path).expanduser().resolve()
|
|
104
|
+
if not candidate.is_dir():
|
|
105
|
+
raise LocalRepositoryError("The selected path is not a directory.")
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
root = Path(git(candidate, "rev-parse", "--show-toplevel")).resolve()
|
|
109
|
+
except LocalRepositoryError as error:
|
|
110
|
+
raise LocalRepositoryError(
|
|
111
|
+
"The selected path is not a Git repository."
|
|
112
|
+
) from error
|
|
113
|
+
|
|
114
|
+
branch = branch or current_branch(root)
|
|
115
|
+
revision = branch_revision(root, branch)
|
|
116
|
+
try:
|
|
117
|
+
github_name = github_remote_name(git(root, "remote", "get-url", "origin"))
|
|
118
|
+
except LocalRepositoryError:
|
|
119
|
+
github_name = None
|
|
120
|
+
return LocalRepositoryLocation(
|
|
121
|
+
root=root,
|
|
122
|
+
branch=branch,
|
|
123
|
+
revision=revision,
|
|
124
|
+
repository_name=local_repository_name(root),
|
|
125
|
+
clone_url=f"{GITHUB_BASE_URL}/{github_name}" if github_name else "",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def local_repository_name(root: Path) -> str:
|
|
130
|
+
digest = hashlib.sha256(str(root).encode()).hexdigest()[:8]
|
|
131
|
+
return f"local/{root.name}-{digest}"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def save_local_location(
|
|
135
|
+
workspace: Workspace,
|
|
136
|
+
location: LocalRepositoryLocation,
|
|
137
|
+
config_error: str,
|
|
138
|
+
config_read: bool,
|
|
139
|
+
) -> tuple[Repository, bool]:
|
|
140
|
+
"""Save what was found in the folder.
|
|
141
|
+
|
|
142
|
+
Returns the repository and whether its GitHub origin changed.
|
|
143
|
+
"""
|
|
144
|
+
previous = local_repository(workspace, str(location.root))
|
|
145
|
+
origin_changed = previous is not None and previous.clone_url != location.clone_url
|
|
146
|
+
synced = {"config_sha": location.revision} if config_read else {}
|
|
147
|
+
# Only a new or a removed repository is switched on. A resync keeps the
|
|
148
|
+
# owner's choice, as GitHub syncs do.
|
|
149
|
+
added = (
|
|
150
|
+
{"enabled": True, "removed_at": None}
|
|
151
|
+
if previous is None or previous.removed_at is not None
|
|
152
|
+
else {}
|
|
153
|
+
)
|
|
154
|
+
repository = save_local_repository(
|
|
155
|
+
workspace,
|
|
156
|
+
str(location.root),
|
|
157
|
+
{
|
|
158
|
+
"source_type": Repository.SourceType.LOCAL,
|
|
159
|
+
"full_name": location.repository_name,
|
|
160
|
+
"clone_url": location.clone_url,
|
|
161
|
+
"default_branch": location.branch,
|
|
162
|
+
"config_path": MANIFEST_FILE,
|
|
163
|
+
"checked_sha": location.revision,
|
|
164
|
+
"last_sync_error": config_error[:SYNC_ERROR_MAX_CHARS],
|
|
165
|
+
**added,
|
|
166
|
+
**synced,
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
return repository, origin_changed
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def local_directories(raw_path: str) -> tuple[Path, Path | None, list[Path]]:
|
|
173
|
+
requested = raw_path if raw_path.strip() else ""
|
|
174
|
+
try:
|
|
175
|
+
candidate = (
|
|
176
|
+
Path(requested).expanduser().resolve()
|
|
177
|
+
if requested
|
|
178
|
+
else Path.home().resolve()
|
|
179
|
+
)
|
|
180
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
181
|
+
target = "selected path" if requested else "home directory"
|
|
182
|
+
raise LocalRepositoryError(f"Unable to open the {target}.") from error
|
|
183
|
+
try:
|
|
184
|
+
is_directory = candidate.is_dir()
|
|
185
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
186
|
+
raise LocalRepositoryError(f"Unable to access {candidate}.") from error
|
|
187
|
+
if not is_directory:
|
|
188
|
+
raise LocalRepositoryError(f"{candidate} is not a directory.")
|
|
189
|
+
try:
|
|
190
|
+
entries = list(candidate.iterdir())
|
|
191
|
+
directories = sorted(
|
|
192
|
+
(item for item in entries if item.is_dir()),
|
|
193
|
+
key=lambda item: item.name.lower(),
|
|
194
|
+
)
|
|
195
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
196
|
+
raise LocalRepositoryError(f"Unable to read {candidate}.") from error
|
|
197
|
+
parent = None if candidate.parent == candidate else candidate.parent
|
|
198
|
+
return candidate, parent, directories
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import djclick as click
|
|
2
|
+
|
|
3
|
+
from secondshift.repositories.sources import prune_source_cache
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@click.command()
|
|
7
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be removed.")
|
|
8
|
+
def command(dry_run: bool) -> None:
|
|
9
|
+
"""Remove cached repository sources no repository needs, and trim to size."""
|
|
10
|
+
pruned = prune_source_cache(dry_run=dry_run)
|
|
11
|
+
verb = "Would remove" if dry_run else "Removed"
|
|
12
|
+
click.echo(
|
|
13
|
+
f"{verb} {pruned.snapshots} snapshot(s) and {pruned.mirrors} mirror(s), "
|
|
14
|
+
f"{pruned.bytes / (1024 * 1024):.1f} MiB"
|
|
15
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-10-26 14:22
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
('connections', '0001_initial'),
|
|
13
|
+
('workspaces', '0001_initial'),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name='Repository',
|
|
19
|
+
fields=[
|
|
20
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
21
|
+
('source_type', models.CharField(choices=[('github', 'GitHub'), ('local', 'Local')], default='github', max_length=16)),
|
|
22
|
+
('local_path', models.CharField(blank=True, max_length=1000)),
|
|
23
|
+
('external_id', models.CharField(blank=True, max_length=64)),
|
|
24
|
+
('full_name', models.CharField(max_length=255)),
|
|
25
|
+
('clone_url', models.URLField(blank=True)),
|
|
26
|
+
('default_branch', models.CharField(default='main', max_length=255)),
|
|
27
|
+
('installation_id', models.CharField(blank=True, max_length=64)),
|
|
28
|
+
('enabled', models.BooleanField(default=False)),
|
|
29
|
+
('auto_sync', models.BooleanField(default=True)),
|
|
30
|
+
('config_path', models.CharField(default='.secondshift.yaml', max_length=255)),
|
|
31
|
+
('config_sha', models.CharField(blank=True, max_length=64)),
|
|
32
|
+
('last_synced_at', models.DateTimeField(blank=True, null=True)),
|
|
33
|
+
('removed_at', models.DateTimeField(blank=True, null=True)),
|
|
34
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
35
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
36
|
+
('github_connection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='repositories', to='connections.connection')),
|
|
37
|
+
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='repositories', to='workspaces.workspace')),
|
|
38
|
+
],
|
|
39
|
+
options={
|
|
40
|
+
'verbose_name_plural': 'repositories',
|
|
41
|
+
'ordering': ['full_name'],
|
|
42
|
+
'constraints': [models.UniqueConstraint(fields=('workspace', 'full_name'), name='unique_workspace_repository'), models.UniqueConstraint(condition=models.Q(('local_path', ''), _negated=True), fields=('workspace', 'local_path'), name='unique_workspace_local_repository')],
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-10-26 15:22
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('repositories', '0001_initial'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='repository',
|
|
15
|
+
name='last_sync_error',
|
|
16
|
+
field=models.CharField(blank=True, max_length=500),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-11-03 13:46
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('repositories', '0002_repository_last_sync_error'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='repository',
|
|
15
|
+
name='checked_sha',
|
|
16
|
+
field=models.CharField(blank=True, max_length=64),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-11-03 20:32
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('repositories', '0003_repository_checked_sha'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='repository',
|
|
15
|
+
name='allow_pull_requests',
|
|
16
|
+
field=models.BooleanField(default=False),
|
|
17
|
+
),
|
|
18
|
+
migrations.AddField(
|
|
19
|
+
model_name='repository',
|
|
20
|
+
name='pull_request_ci',
|
|
21
|
+
field=models.BooleanField(default=False),
|
|
22
|
+
),
|
|
23
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-11-03 23:46
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('repositories', '0004_pull_request_settings'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.RemoveField(
|
|
14
|
+
model_name='repository',
|
|
15
|
+
name='installation_id',
|
|
16
|
+
),
|
|
17
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-11-04 09:19
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('repositories', '0005_remove_repository_installation_id'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='repository',
|
|
15
|
+
name='allow_issues',
|
|
16
|
+
field=models.BooleanField(default=False),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
File without changes
|