db-git 0.1.1__tar.gz → 0.2.0__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.
- db_git-0.2.0/.github/workflows/release.yml +79 -0
- {db_git-0.1.1 → db_git-0.2.0}/.github/workflows/test.yml +29 -28
- {db_git-0.1.1 → db_git-0.2.0}/.pre-commit-config.yaml +1 -1
- {db_git-0.1.1 → db_git-0.2.0}/PKG-INFO +10 -1
- {db_git-0.1.1 → db_git-0.2.0}/README.md +9 -0
- {db_git-0.1.1 → db_git-0.2.0}/pyproject.toml +12 -1
- db_git-0.2.0/src/db_git/__init__.py +3 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/inspect.py +51 -1
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/config.py +5 -1
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/db.py +12 -1
- {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/test_per_branch_pgdump_workflow.py +32 -1
- {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/test_per_branch_template_workflow.py +32 -1
- {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/test_shared_pgdump_workflow.py +9 -1
- {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/test_shared_template_workflow.py +9 -1
- {db_git-0.1.1 → db_git-0.2.0}/tests/unit/test_cli.py +65 -0
- db_git-0.2.0/tests/unit/test_db.py +41 -0
- {db_git-0.1.1 → db_git-0.2.0}/uv.lock +1 -1
- db_git-0.1.1/.claude/settings.local.json +0 -29
- db_git-0.1.1/src/db_git/__init__.py +0 -1
- db_git-0.1.1/tests/unit/test_db.py +0 -23
- {db_git-0.1.1 → db_git-0.2.0}/.github/dependabot.yml +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/.gitignore +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/CHANGELOG.md +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/LICENSE +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/noxfile.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/__init__.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/postgresql/__init__.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/postgresql/backend.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/postgresql/branch_db.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/postgresql/connections.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/postgresql/pgdump.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/postgresql/template.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/__init__.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/_common.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/_console.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/_format.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/_prompts.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/branch.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/hook.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/init.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/snapshot.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/errors.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/git.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/hook_script.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/state.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/src/db_git/storage.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/__init__.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/_pg_helpers.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/conftest.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/__init__.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/_helpers.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/conftest.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/integration/__init__.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/integration/conftest.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/integration/test_branch_db.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/integration/test_connections.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/integration/test_pgdump_strategy.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/integration/test_template_strategy.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/unit/__init__.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/unit/test_config.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/unit/test_git.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/unit/test_state.py +0 -0
- {db_git-0.1.1 → db_git-0.2.0}/tests/unit/test_storage.py +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_run:
|
|
5
|
+
workflows: [test]
|
|
6
|
+
branches: [main]
|
|
7
|
+
types: [completed]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
|
11
|
+
cancel-in-progress: false
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
release:
|
|
18
|
+
name: semantic release
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
if: github.event.workflow_run.conclusion == 'success'
|
|
21
|
+
permissions:
|
|
22
|
+
contents: write
|
|
23
|
+
id-token: write
|
|
24
|
+
outputs:
|
|
25
|
+
released: ${{ steps.release.outputs.released || 'false' }}
|
|
26
|
+
steps:
|
|
27
|
+
- name: Checkout release branch
|
|
28
|
+
uses: actions/checkout@v6
|
|
29
|
+
with:
|
|
30
|
+
ref: ${{ github.event.workflow_run.head_branch }}
|
|
31
|
+
fetch-depth: 0
|
|
32
|
+
|
|
33
|
+
- name: Reset to tested commit
|
|
34
|
+
run: git reset --hard "${{ github.event.workflow_run.head_sha }}"
|
|
35
|
+
|
|
36
|
+
- name: Semantic version release
|
|
37
|
+
id: release
|
|
38
|
+
uses: python-semantic-release/python-semantic-release@v10.5.3
|
|
39
|
+
with:
|
|
40
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
41
|
+
git_committer_name: "github-actions"
|
|
42
|
+
git_committer_email: "actions@users.noreply.github.com"
|
|
43
|
+
|
|
44
|
+
- name: Upload distributions to GitHub Release
|
|
45
|
+
if: steps.release.outputs.released == 'true'
|
|
46
|
+
uses: python-semantic-release/publish-action@v10.5.3
|
|
47
|
+
with:
|
|
48
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
49
|
+
tag: ${{ steps.release.outputs.tag }}
|
|
50
|
+
|
|
51
|
+
- name: Upload distributions artifact
|
|
52
|
+
if: steps.release.outputs.released == 'true'
|
|
53
|
+
uses: actions/upload-artifact@v7
|
|
54
|
+
with:
|
|
55
|
+
name: distribution-artifacts
|
|
56
|
+
path: dist
|
|
57
|
+
if-no-files-found: error
|
|
58
|
+
|
|
59
|
+
publish:
|
|
60
|
+
name: publish to PyPI
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
needs: release
|
|
63
|
+
if: needs.release.outputs.released == 'true'
|
|
64
|
+
permissions:
|
|
65
|
+
actions: read
|
|
66
|
+
contents: read
|
|
67
|
+
id-token: write
|
|
68
|
+
steps:
|
|
69
|
+
- name: Download distributions artifact
|
|
70
|
+
uses: actions/download-artifact@v8
|
|
71
|
+
with:
|
|
72
|
+
name: distribution-artifacts
|
|
73
|
+
path: dist
|
|
74
|
+
|
|
75
|
+
- name: Publish package distributions to PyPI
|
|
76
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
77
|
+
with:
|
|
78
|
+
packages-dir: dist
|
|
79
|
+
print-hash: true
|
|
@@ -10,6 +10,34 @@ concurrency:
|
|
|
10
10
|
cancel-in-progress: true
|
|
11
11
|
|
|
12
12
|
jobs:
|
|
13
|
+
lint:
|
|
14
|
+
name: ruff
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
- uses: astral-sh/setup-uv@v7
|
|
19
|
+
with:
|
|
20
|
+
enable-cache: true
|
|
21
|
+
- uses: actions/setup-python@v6
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.12"
|
|
24
|
+
- run: pipx install nox
|
|
25
|
+
- run: nox -s lint
|
|
26
|
+
|
|
27
|
+
types:
|
|
28
|
+
name: mypy
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v6
|
|
32
|
+
- uses: astral-sh/setup-uv@v7
|
|
33
|
+
with:
|
|
34
|
+
enable-cache: true
|
|
35
|
+
- uses: actions/setup-python@v6
|
|
36
|
+
with:
|
|
37
|
+
python-version: "3.12"
|
|
38
|
+
- run: pipx install nox
|
|
39
|
+
- run: nox -s types
|
|
40
|
+
|
|
13
41
|
unit:
|
|
14
42
|
name: unit (${{ matrix.python }})
|
|
15
43
|
runs-on: ubuntu-latest
|
|
@@ -30,6 +58,7 @@ jobs:
|
|
|
30
58
|
integration:
|
|
31
59
|
name: integration (py${{ matrix.python }}, pg${{ matrix.postgres }})
|
|
32
60
|
runs-on: ubuntu-latest
|
|
61
|
+
needs: [lint, types, unit]
|
|
33
62
|
strategy:
|
|
34
63
|
fail-fast: false
|
|
35
64
|
matrix:
|
|
@@ -51,31 +80,3 @@ jobs:
|
|
|
51
80
|
sudo apt-get install -y postgresql-client-${{ matrix.postgres }}
|
|
52
81
|
- run: pipx install nox
|
|
53
82
|
- run: nox -s "integration-${{ matrix.python }}(pg_image='postgres:${{ matrix.postgres }}')"
|
|
54
|
-
|
|
55
|
-
types:
|
|
56
|
-
name: mypy
|
|
57
|
-
runs-on: ubuntu-latest
|
|
58
|
-
steps:
|
|
59
|
-
- uses: actions/checkout@v6
|
|
60
|
-
- uses: astral-sh/setup-uv@v7
|
|
61
|
-
with:
|
|
62
|
-
enable-cache: true
|
|
63
|
-
- uses: actions/setup-python@v6
|
|
64
|
-
with:
|
|
65
|
-
python-version: "3.12"
|
|
66
|
-
- run: pipx install nox
|
|
67
|
-
- run: nox -s types
|
|
68
|
-
|
|
69
|
-
lint:
|
|
70
|
-
name: ruff
|
|
71
|
-
runs-on: ubuntu-latest
|
|
72
|
-
steps:
|
|
73
|
-
- uses: actions/checkout@v6
|
|
74
|
-
- uses: astral-sh/setup-uv@v7
|
|
75
|
-
with:
|
|
76
|
-
enable-cache: true
|
|
77
|
-
- uses: actions/setup-python@v6
|
|
78
|
-
with:
|
|
79
|
-
python-version: "3.12"
|
|
80
|
-
- run: pipx install nox
|
|
81
|
-
- run: nox -s lint
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: db-git
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Keep your database in sync with your git branches.
|
|
5
5
|
Project-URL: Homepage, https://github.com/earthcomfy/db-git
|
|
6
6
|
Project-URL: Repository, https://github.com/earthcomfy/db-git
|
|
@@ -99,6 +99,14 @@ Because the database name changes per branch, your application server also
|
|
|
99
99
|
needs to connect to the branch database. For example, when working on
|
|
100
100
|
`feature/auth`, point your app's `DATABASE_URL` at `myapp__feature__auth`.
|
|
101
101
|
|
|
102
|
+
`db-git url` prints the full connection URL for the current branch, so you can
|
|
103
|
+
hand it straight to your app or client:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
export DATABASE_URL=$(db-git url)
|
|
107
|
+
psql "$(db-git url)"
|
|
108
|
+
```
|
|
109
|
+
|
|
102
110
|
## Choosing a Mode
|
|
103
111
|
|
|
104
112
|
### Shared Mode
|
|
@@ -172,6 +180,7 @@ db-git init --database-url postgresql://localhost/myapp --no-hook
|
|
|
172
180
|
```bash
|
|
173
181
|
db-git status
|
|
174
182
|
db-git list
|
|
183
|
+
db-git url
|
|
175
184
|
```
|
|
176
185
|
|
|
177
186
|
### Shared Mode Commands
|
|
@@ -68,6 +68,14 @@ Because the database name changes per branch, your application server also
|
|
|
68
68
|
needs to connect to the branch database. For example, when working on
|
|
69
69
|
`feature/auth`, point your app's `DATABASE_URL` at `myapp__feature__auth`.
|
|
70
70
|
|
|
71
|
+
`db-git url` prints the full connection URL for the current branch, so you can
|
|
72
|
+
hand it straight to your app or client:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
export DATABASE_URL=$(db-git url)
|
|
76
|
+
psql "$(db-git url)"
|
|
77
|
+
```
|
|
78
|
+
|
|
71
79
|
## Choosing a Mode
|
|
72
80
|
|
|
73
81
|
### Shared Mode
|
|
@@ -141,6 +149,7 @@ db-git init --database-url postgresql://localhost/myapp --no-hook
|
|
|
141
149
|
```bash
|
|
142
150
|
db-git status
|
|
143
151
|
db-git list
|
|
152
|
+
db-git url
|
|
144
153
|
```
|
|
145
154
|
|
|
146
155
|
### Shared Mode Commands
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "db-git"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Keep your database in sync with your git branches."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -64,6 +64,17 @@ build-backend = "hatchling.build"
|
|
|
64
64
|
[tool.hatch.build.targets.wheel]
|
|
65
65
|
packages = ["src/db_git"]
|
|
66
66
|
|
|
67
|
+
[tool.semantic_release]
|
|
68
|
+
allow_zero_version = true
|
|
69
|
+
build_command = "python -m pip install build && python -m build --sdist --wheel ."
|
|
70
|
+
commit_message = "chore: release v{version}"
|
|
71
|
+
commit_parser = "conventional"
|
|
72
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
73
|
+
|
|
74
|
+
[tool.semantic_release.changelog.default_templates]
|
|
75
|
+
changelog_file = "CHANGELOG.md"
|
|
76
|
+
output_format = "md"
|
|
77
|
+
|
|
67
78
|
[tool.pytest.ini_options]
|
|
68
79
|
markers = ["integration: integration tests", "e2e: end-to-end tests"]
|
|
69
80
|
|
|
@@ -8,7 +8,7 @@ from rich.table import Table
|
|
|
8
8
|
|
|
9
9
|
from db_git.backends import DatabaseBackend, SnapshotStrategy, get_backend
|
|
10
10
|
from db_git.config import DbGitConfig, load_config
|
|
11
|
-
from db_git.db import parse_database_url
|
|
11
|
+
from db_git.db import parse_database_url, with_database_name
|
|
12
12
|
from db_git.errors import DbGitError
|
|
13
13
|
from db_git.git import get_current_branch, get_git_dir, list_branches
|
|
14
14
|
from db_git.state import load_state
|
|
@@ -200,6 +200,56 @@ def status(
|
|
|
200
200
|
raise typer.Exit(1) from e
|
|
201
201
|
|
|
202
202
|
|
|
203
|
+
@app.command()
|
|
204
|
+
def url(
|
|
205
|
+
branch: Annotated[str | None, typer.Argument()] = None,
|
|
206
|
+
database_url: Annotated[
|
|
207
|
+
str | None, typer.Option("--database-url", envvar="DATABASE_URL")
|
|
208
|
+
] = None,
|
|
209
|
+
) -> None:
|
|
210
|
+
"""
|
|
211
|
+
Print the connectable database URL for the current (or given) branch.
|
|
212
|
+
"""
|
|
213
|
+
require_init()
|
|
214
|
+
try:
|
|
215
|
+
config = load_config(cli_overrides={"database_url": database_url})
|
|
216
|
+
|
|
217
|
+
if config.mode != "per-branch":
|
|
218
|
+
# Shared mode: one fixed database, so emit the configured URL as-is.
|
|
219
|
+
typer.echo(config.database_url)
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
if branch is None:
|
|
223
|
+
branch = get_current_branch()
|
|
224
|
+
if branch is None:
|
|
225
|
+
console.print("[red]Error:[/] HEAD is detached. Specify a branch name.")
|
|
226
|
+
raise typer.Exit(1)
|
|
227
|
+
|
|
228
|
+
backend = get_backend(config.database_url)
|
|
229
|
+
params = backend.apply_url_defaults(parse_database_url(config.database_url))
|
|
230
|
+
dbname = str(params["dbname"])
|
|
231
|
+
target_db = branch_db_name(
|
|
232
|
+
branch,
|
|
233
|
+
dbname,
|
|
234
|
+
config.default_branch,
|
|
235
|
+
backend.max_identifier_length,
|
|
236
|
+
)
|
|
237
|
+
typer.echo(with_database_name(config.database_url, target_db))
|
|
238
|
+
except DbGitError as e:
|
|
239
|
+
console.print(f"[red]Error:[/] {e}")
|
|
240
|
+
raise typer.Exit(1) from e
|
|
241
|
+
except typer.Exit:
|
|
242
|
+
raise
|
|
243
|
+
except Exception as e:
|
|
244
|
+
if debug_enabled():
|
|
245
|
+
raise
|
|
246
|
+
console.print(
|
|
247
|
+
f"[red]Error:[/] Unexpected error: {e}\n"
|
|
248
|
+
"[dim]Set DB_GIT_DEBUG=1 to see the full traceback.[/]"
|
|
249
|
+
)
|
|
250
|
+
raise typer.Exit(1) from e
|
|
251
|
+
|
|
252
|
+
|
|
203
253
|
def _list_per_branch(config: DbGitConfig) -> None:
|
|
204
254
|
"""
|
|
205
255
|
List branch databases in per-branch mode.
|
|
@@ -14,7 +14,11 @@ VALID_MODES = {"shared", "per-branch"}
|
|
|
14
14
|
VALID_STRATEGIES = {"template", "pgdump"}
|
|
15
15
|
|
|
16
16
|
_CONFIG_COMMENTS: dict[str, str] = {
|
|
17
|
-
"database_url":
|
|
17
|
+
"database_url": (
|
|
18
|
+
"Base database connection URL: credentials, host/port, and the database for\n"
|
|
19
|
+
"# your default branch. In per-branch mode each branch's database is copied\n"
|
|
20
|
+
"# from this one."
|
|
21
|
+
),
|
|
18
22
|
"mode": (
|
|
19
23
|
"How db-git manages databases across branches.\n"
|
|
20
24
|
'# "shared": one database, snapshot/restore on switch\n'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from urllib.parse import unquote, urlparse
|
|
3
|
+
from urllib.parse import unquote, urlparse, urlunparse
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def parse_database_url(url: str) -> dict[str, str | int | None]:
|
|
@@ -15,3 +15,14 @@ def parse_database_url(url: str) -> dict[str, str | int | None]:
|
|
|
15
15
|
"port": parsed.port,
|
|
16
16
|
"dbname": parsed.path.lstrip("/") or None,
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def with_database_name(url: str, dbname: str) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Return url with its database name replaced by dbname.
|
|
23
|
+
|
|
24
|
+
Only the database name is swapped; credentials, host, port, and query
|
|
25
|
+
string are preserved as-is.
|
|
26
|
+
"""
|
|
27
|
+
parsed = urlparse(url)
|
|
28
|
+
return urlunparse(parsed._replace(path=f"/{dbname}"))
|
|
@@ -689,7 +689,7 @@ class TestPerBranchPgdumpWorkflow:
|
|
|
689
689
|
assert result.returncode != 0
|
|
690
690
|
|
|
691
691
|
# -----------------------------------------------------------------------
|
|
692
|
-
# list / status
|
|
692
|
+
# list / status / url
|
|
693
693
|
# -----------------------------------------------------------------------
|
|
694
694
|
|
|
695
695
|
def test_list_shows_branch_databases(self, initialized: dict) -> None:
|
|
@@ -712,6 +712,37 @@ class TestPerBranchPgdumpWorkflow:
|
|
|
712
712
|
assert "per-branch" in out
|
|
713
713
|
assert "pgdump" in out
|
|
714
714
|
|
|
715
|
+
def test_url_emits_connectable_branch_url(self, initialized: dict) -> None:
|
|
716
|
+
pg_info = initialized["pg_info"]
|
|
717
|
+
seed = pg_info["dbname"]
|
|
718
|
+
default = get_default_branch(initialized["repo"])
|
|
719
|
+
repo = initialized["repo"]
|
|
720
|
+
env = initialized["subprocess_env"]
|
|
721
|
+
|
|
722
|
+
seed_users(initialized["db_url"])
|
|
723
|
+
make_branch(initialized, "feature")
|
|
724
|
+
feature_db = branch_db_name("feature", seed, default)
|
|
725
|
+
|
|
726
|
+
result = run_db_git("url", cwd=repo, env=env)
|
|
727
|
+
printed = result.stdout.strip()
|
|
728
|
+
assert printed == build_url(pg_info, feature_db)
|
|
729
|
+
|
|
730
|
+
conn = reconnect(printed)
|
|
731
|
+
try:
|
|
732
|
+
cur = conn.execute("SELECT name FROM users ORDER BY name")
|
|
733
|
+
assert [r[0] for r in cur.fetchall()] == ["Alice", "Bob", "Charlie"]
|
|
734
|
+
finally:
|
|
735
|
+
conn.close()
|
|
736
|
+
|
|
737
|
+
def test_url_for_default_branch_returns_seed_url(self, initialized: dict) -> None:
|
|
738
|
+
result = run_db_git(
|
|
739
|
+
"url",
|
|
740
|
+
get_default_branch(initialized["repo"]),
|
|
741
|
+
cwd=initialized["repo"],
|
|
742
|
+
env=initialized["subprocess_env"],
|
|
743
|
+
)
|
|
744
|
+
assert result.stdout.strip() == build_url(initialized["pg_info"])
|
|
745
|
+
|
|
715
746
|
# -----------------------------------------------------------------------
|
|
716
747
|
# Shared-mode-only commands
|
|
717
748
|
# -----------------------------------------------------------------------
|
|
@@ -733,7 +733,7 @@ class TestPerBranchTemplateWorkflow:
|
|
|
733
733
|
assert result.returncode != 0
|
|
734
734
|
|
|
735
735
|
# -----------------------------------------------------------------------
|
|
736
|
-
# list / status
|
|
736
|
+
# list / status / url
|
|
737
737
|
# -----------------------------------------------------------------------
|
|
738
738
|
|
|
739
739
|
def test_list_shows_branch_databases(self, initialized: dict) -> None:
|
|
@@ -757,6 +757,37 @@ class TestPerBranchTemplateWorkflow:
|
|
|
757
757
|
assert "per-branch" in out
|
|
758
758
|
assert "template" in out
|
|
759
759
|
|
|
760
|
+
def test_url_emits_connectable_branch_url(self, initialized: dict) -> None:
|
|
761
|
+
pg_info = initialized["pg_info"]
|
|
762
|
+
seed = pg_info["dbname"]
|
|
763
|
+
default = get_default_branch(initialized["repo"])
|
|
764
|
+
repo = initialized["repo"]
|
|
765
|
+
env = initialized["subprocess_env"]
|
|
766
|
+
|
|
767
|
+
seed_users(initialized["db_url"])
|
|
768
|
+
make_branch(initialized, "feature")
|
|
769
|
+
feature_db = branch_db_name("feature", seed, default)
|
|
770
|
+
|
|
771
|
+
result = run_db_git("url", cwd=repo, env=env)
|
|
772
|
+
printed = result.stdout.strip()
|
|
773
|
+
assert printed == build_url(pg_info, feature_db)
|
|
774
|
+
|
|
775
|
+
conn = reconnect(printed)
|
|
776
|
+
try:
|
|
777
|
+
cur = conn.execute("SELECT name FROM users ORDER BY name")
|
|
778
|
+
assert [r[0] for r in cur.fetchall()] == ["Alice", "Bob", "Charlie"]
|
|
779
|
+
finally:
|
|
780
|
+
conn.close()
|
|
781
|
+
|
|
782
|
+
def test_url_for_default_branch_returns_seed_url(self, initialized: dict) -> None:
|
|
783
|
+
result = run_db_git(
|
|
784
|
+
"url",
|
|
785
|
+
get_default_branch(initialized["repo"]),
|
|
786
|
+
cwd=initialized["repo"],
|
|
787
|
+
env=initialized["subprocess_env"],
|
|
788
|
+
)
|
|
789
|
+
assert result.stdout.strip() == build_url(initialized["pg_info"])
|
|
790
|
+
|
|
760
791
|
# -----------------------------------------------------------------------
|
|
761
792
|
# Shared-mode-only commands
|
|
762
793
|
# -----------------------------------------------------------------------
|
|
@@ -681,7 +681,7 @@ class TestSharedPgdumpWorkflow:
|
|
|
681
681
|
assert len(_meta_files(repo)) <= 1
|
|
682
682
|
|
|
683
683
|
# -----------------------------------------------------------------------
|
|
684
|
-
# list / status
|
|
684
|
+
# list / status / url
|
|
685
685
|
# -----------------------------------------------------------------------
|
|
686
686
|
|
|
687
687
|
def test_list_shows_saved_snapshots(self, initialized: dict) -> None:
|
|
@@ -711,3 +711,11 @@ class TestSharedPgdumpWorkflow:
|
|
|
711
711
|
run_db_git("disable", cwd=repo, env=env)
|
|
712
712
|
r2 = run_db_git("status", cwd=repo, env=env)
|
|
713
713
|
assert "no" in (r2.stdout + r2.stderr).lower()
|
|
714
|
+
|
|
715
|
+
def test_url_emits_configured_url(self, initialized: dict) -> None:
|
|
716
|
+
result = run_db_git(
|
|
717
|
+
"url",
|
|
718
|
+
cwd=initialized["repo"],
|
|
719
|
+
env=initialized["subprocess_env"],
|
|
720
|
+
)
|
|
721
|
+
assert result.stdout.strip() == initialized["db_url"]
|
|
@@ -695,7 +695,7 @@ class TestSharedTemplateWorkflow:
|
|
|
695
695
|
assert len(_meta_files(repo)) <= 1
|
|
696
696
|
|
|
697
697
|
# -----------------------------------------------------------------------
|
|
698
|
-
# list / status
|
|
698
|
+
# list / status / url
|
|
699
699
|
# -----------------------------------------------------------------------
|
|
700
700
|
|
|
701
701
|
def test_list_shows_saved_snapshots(self, initialized: dict) -> None:
|
|
@@ -728,3 +728,11 @@ class TestSharedTemplateWorkflow:
|
|
|
728
728
|
r2 = run_db_git("status", cwd=repo, env=env)
|
|
729
729
|
out2 = r2.stdout + r2.stderr
|
|
730
730
|
assert "no" in out2.lower()
|
|
731
|
+
|
|
732
|
+
def test_url_emits_configured_url(self, initialized: dict) -> None:
|
|
733
|
+
result = run_db_git(
|
|
734
|
+
"url",
|
|
735
|
+
cwd=initialized["repo"],
|
|
736
|
+
env=initialized["subprocess_env"],
|
|
737
|
+
)
|
|
738
|
+
assert result.stdout.strip() == initialized["db_url"]
|
|
@@ -481,6 +481,71 @@ class TestStatus:
|
|
|
481
481
|
os.chdir(old_cwd)
|
|
482
482
|
|
|
483
483
|
|
|
484
|
+
class TestUrl:
|
|
485
|
+
def test_per_branch_explicit_branch(self, git_repo, setup_config):
|
|
486
|
+
setup_config(git_repo, mode="per-branch")
|
|
487
|
+
old_cwd = os.getcwd()
|
|
488
|
+
os.chdir(git_repo)
|
|
489
|
+
try:
|
|
490
|
+
result = runner.invoke(app, ["url", "feature/auth"])
|
|
491
|
+
assert result.exit_code == 0
|
|
492
|
+
assert (
|
|
493
|
+
"postgresql://postgres:postgres@localhost:5432/testdb__feature__auth"
|
|
494
|
+
in result.output
|
|
495
|
+
)
|
|
496
|
+
finally:
|
|
497
|
+
os.chdir(old_cwd)
|
|
498
|
+
|
|
499
|
+
def test_per_branch_default_branch_uses_seed(self, git_repo, setup_config):
|
|
500
|
+
setup_config(git_repo, mode="per-branch")
|
|
501
|
+
old_cwd = os.getcwd()
|
|
502
|
+
os.chdir(git_repo)
|
|
503
|
+
try:
|
|
504
|
+
result = runner.invoke(app, ["url", "main"])
|
|
505
|
+
assert result.exit_code == 0
|
|
506
|
+
assert (
|
|
507
|
+
"postgresql://postgres:postgres@localhost:5432/testdb" in result.output
|
|
508
|
+
)
|
|
509
|
+
assert "testdb__" not in result.output
|
|
510
|
+
finally:
|
|
511
|
+
os.chdir(old_cwd)
|
|
512
|
+
|
|
513
|
+
def test_shared_mode_emits_configured_url(self, git_repo, setup_config):
|
|
514
|
+
setup_config(git_repo)
|
|
515
|
+
old_cwd = os.getcwd()
|
|
516
|
+
os.chdir(git_repo)
|
|
517
|
+
try:
|
|
518
|
+
result = runner.invoke(app, ["url"])
|
|
519
|
+
assert result.exit_code == 0
|
|
520
|
+
assert (
|
|
521
|
+
"postgresql://postgres:postgres@localhost:5432/testdb" in result.output
|
|
522
|
+
)
|
|
523
|
+
finally:
|
|
524
|
+
os.chdir(old_cwd)
|
|
525
|
+
|
|
526
|
+
def test_detached_head_without_branch_fails(self, git_repo, setup_config):
|
|
527
|
+
setup_config(git_repo, mode="per-branch")
|
|
528
|
+
old_cwd = os.getcwd()
|
|
529
|
+
os.chdir(git_repo)
|
|
530
|
+
try:
|
|
531
|
+
with patch("db_git.cli.inspect.get_current_branch", return_value=None):
|
|
532
|
+
result = runner.invoke(app, ["url"])
|
|
533
|
+
assert result.exit_code == 1
|
|
534
|
+
assert "detached" in result.output
|
|
535
|
+
finally:
|
|
536
|
+
os.chdir(old_cwd)
|
|
537
|
+
|
|
538
|
+
def test_fails_without_init(self, git_repo):
|
|
539
|
+
old_cwd = os.getcwd()
|
|
540
|
+
os.chdir(git_repo)
|
|
541
|
+
try:
|
|
542
|
+
result = runner.invoke(app, ["url"])
|
|
543
|
+
assert result.exit_code == 1
|
|
544
|
+
assert "not initialized" in result.output
|
|
545
|
+
finally:
|
|
546
|
+
os.chdir(old_cwd)
|
|
547
|
+
|
|
548
|
+
|
|
484
549
|
class TestPrune:
|
|
485
550
|
def test_dry_run(self, git_repo, setup_with_snapshots):
|
|
486
551
|
setup_with_snapshots(git_repo, ["main", "stale-branch"])
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from db_git.db import parse_database_url, with_database_name
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestParseDatabaseUrl:
|
|
7
|
+
def test_standard_url(self):
|
|
8
|
+
result = parse_database_url("postgresql://user:pass@host:5433/mydb")
|
|
9
|
+
assert result == {
|
|
10
|
+
"user": "user",
|
|
11
|
+
"password": "pass",
|
|
12
|
+
"host": "host",
|
|
13
|
+
"port": 5433,
|
|
14
|
+
"dbname": "mydb",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def test_missing_fields_return_none(self):
|
|
18
|
+
result = parse_database_url("postgresql:///mydb")
|
|
19
|
+
assert result["user"] is None
|
|
20
|
+
assert result["password"] is None
|
|
21
|
+
assert result["host"] is None
|
|
22
|
+
assert result["port"] is None
|
|
23
|
+
assert result["dbname"] == "mydb"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestWithDatabaseName:
|
|
27
|
+
def test_swaps_name_preserving_credentials(self):
|
|
28
|
+
result = with_database_name(
|
|
29
|
+
"postgresql://user:pass@host:5433/myapp", "myapp__feature__auth"
|
|
30
|
+
)
|
|
31
|
+
assert result == "postgresql://user:pass@host:5433/myapp__feature__auth"
|
|
32
|
+
|
|
33
|
+
def test_preserves_query_string(self):
|
|
34
|
+
result = with_database_name(
|
|
35
|
+
"postgresql://user:pass@host:5432/myapp?sslmode=require", "myapp__wip"
|
|
36
|
+
)
|
|
37
|
+
assert result == "postgresql://user:pass@host:5432/myapp__wip?sslmode=require"
|
|
38
|
+
|
|
39
|
+
def test_bare_url_without_credentials(self):
|
|
40
|
+
result = with_database_name("postgresql://localhost/myapp", "myapp__wip")
|
|
41
|
+
assert result == "postgresql://localhost/myapp__wip"
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"WebSearch",
|
|
5
|
-
"mcp__context7__query-docs",
|
|
6
|
-
"Read(//private/tmp/git-hook-test/**)",
|
|
7
|
-
"Bash(chmod +x .git/hooks/post-checkout)",
|
|
8
|
-
"mcp__context7__resolve-library-id",
|
|
9
|
-
"Bash(uv run:*)",
|
|
10
|
-
"WebFetch(domain:raw.githubusercontent.com)",
|
|
11
|
-
"Bash(gh search:*)",
|
|
12
|
-
"WebFetch(domain:pytest-postgresql.readthedocs.io)",
|
|
13
|
-
"WebFetch(domain:github.com)",
|
|
14
|
-
"Bash(gh api *)",
|
|
15
|
-
"Bash(curl -s \"https://api.github.com/repos/dbt-labs/dbt-core/contents/tests/functional\")",
|
|
16
|
-
"Bash(python3 -c \"import json,sys; [print\\(x['name']\\) for x in json.load\\(sys.stdin\\)]\")",
|
|
17
|
-
"Bash(curl -s \"https://api.github.com/repos/dolthub/dolt/contents/integration-tests/bats\")",
|
|
18
|
-
"Bash(python3 -c \"import json,sys; [print\\(x['name']\\) for x in json.load\\(sys.stdin\\) if 'branch' in x.get\\('name',''\\).lower\\(\\) or 'checkout' in x.get\\('name',''\\).lower\\(\\)]\")",
|
|
19
|
-
"Bash(curl -s \"https://api.github.com/repos/dbt-labs/dbt-adapters/contents/dbt-tests-adapter/src/dbt/tests/fixtures\")",
|
|
20
|
-
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); [print\\(x['name']\\) for x in d] if isinstance\\(d,list\\) else print\\(d\\)\")",
|
|
21
|
-
"Bash(curl -s \"https://api.github.com/repos/dbt-labs/dbt-adapters/git/trees/main?recursive=1\")",
|
|
22
|
-
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); [print\\(x['path']\\) for x in d.get\\('tree',[]\\) if 'fixtures' in x['path'] and x['path'].endswith\\('.py'\\)]\")",
|
|
23
|
-
"Bash(python -c \"import typer, click; print\\('typer.Exit MRO:', [c.__name__ for c in typer.Exit.__mro__]\\); print\\('is Exception subclass:', issubclass\\(typer.Exit, Exception\\)\\)\")",
|
|
24
|
-
"Bash(rtk grep *)",
|
|
25
|
-
"Bash(rtk read *)",
|
|
26
|
-
"Bash(rtk git *)"
|
|
27
|
-
]
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from db_git.db import parse_database_url
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class TestParseDatabaseUrl:
|
|
7
|
-
def test_standard_url(self):
|
|
8
|
-
result = parse_database_url("postgresql://user:pass@host:5433/mydb")
|
|
9
|
-
assert result == {
|
|
10
|
-
"user": "user",
|
|
11
|
-
"password": "pass",
|
|
12
|
-
"host": "host",
|
|
13
|
-
"port": 5433,
|
|
14
|
-
"dbname": "mydb",
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
def test_missing_fields_return_none(self):
|
|
18
|
-
result = parse_database_url("postgresql:///mydb")
|
|
19
|
-
assert result["user"] is None
|
|
20
|
-
assert result["password"] is None
|
|
21
|
-
assert result["host"] is None
|
|
22
|
-
assert result["port"] is None
|
|
23
|
-
assert result["dbname"] == "mydb"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|