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.
Files changed (63) hide show
  1. db_git-0.2.0/.github/workflows/release.yml +79 -0
  2. {db_git-0.1.1 → db_git-0.2.0}/.github/workflows/test.yml +29 -28
  3. {db_git-0.1.1 → db_git-0.2.0}/.pre-commit-config.yaml +1 -1
  4. {db_git-0.1.1 → db_git-0.2.0}/PKG-INFO +10 -1
  5. {db_git-0.1.1 → db_git-0.2.0}/README.md +9 -0
  6. {db_git-0.1.1 → db_git-0.2.0}/pyproject.toml +12 -1
  7. db_git-0.2.0/src/db_git/__init__.py +3 -0
  8. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/inspect.py +51 -1
  9. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/config.py +5 -1
  10. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/db.py +12 -1
  11. {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/test_per_branch_pgdump_workflow.py +32 -1
  12. {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/test_per_branch_template_workflow.py +32 -1
  13. {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/test_shared_pgdump_workflow.py +9 -1
  14. {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/test_shared_template_workflow.py +9 -1
  15. {db_git-0.1.1 → db_git-0.2.0}/tests/unit/test_cli.py +65 -0
  16. db_git-0.2.0/tests/unit/test_db.py +41 -0
  17. {db_git-0.1.1 → db_git-0.2.0}/uv.lock +1 -1
  18. db_git-0.1.1/.claude/settings.local.json +0 -29
  19. db_git-0.1.1/src/db_git/__init__.py +0 -1
  20. db_git-0.1.1/tests/unit/test_db.py +0 -23
  21. {db_git-0.1.1 → db_git-0.2.0}/.github/dependabot.yml +0 -0
  22. {db_git-0.1.1 → db_git-0.2.0}/.gitignore +0 -0
  23. {db_git-0.1.1 → db_git-0.2.0}/CHANGELOG.md +0 -0
  24. {db_git-0.1.1 → db_git-0.2.0}/LICENSE +0 -0
  25. {db_git-0.1.1 → db_git-0.2.0}/noxfile.py +0 -0
  26. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/__init__.py +0 -0
  27. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/postgresql/__init__.py +0 -0
  28. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/postgresql/backend.py +0 -0
  29. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/postgresql/branch_db.py +0 -0
  30. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/postgresql/connections.py +0 -0
  31. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/postgresql/pgdump.py +0 -0
  32. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/backends/postgresql/template.py +0 -0
  33. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/__init__.py +0 -0
  34. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/_common.py +0 -0
  35. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/_console.py +0 -0
  36. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/_format.py +0 -0
  37. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/_prompts.py +0 -0
  38. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/branch.py +0 -0
  39. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/hook.py +0 -0
  40. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/init.py +0 -0
  41. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/cli/snapshot.py +0 -0
  42. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/errors.py +0 -0
  43. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/git.py +0 -0
  44. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/hook_script.py +0 -0
  45. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/state.py +0 -0
  46. {db_git-0.1.1 → db_git-0.2.0}/src/db_git/storage.py +0 -0
  47. {db_git-0.1.1 → db_git-0.2.0}/tests/__init__.py +0 -0
  48. {db_git-0.1.1 → db_git-0.2.0}/tests/_pg_helpers.py +0 -0
  49. {db_git-0.1.1 → db_git-0.2.0}/tests/conftest.py +0 -0
  50. {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/__init__.py +0 -0
  51. {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/_helpers.py +0 -0
  52. {db_git-0.1.1 → db_git-0.2.0}/tests/e2e/conftest.py +0 -0
  53. {db_git-0.1.1 → db_git-0.2.0}/tests/integration/__init__.py +0 -0
  54. {db_git-0.1.1 → db_git-0.2.0}/tests/integration/conftest.py +0 -0
  55. {db_git-0.1.1 → db_git-0.2.0}/tests/integration/test_branch_db.py +0 -0
  56. {db_git-0.1.1 → db_git-0.2.0}/tests/integration/test_connections.py +0 -0
  57. {db_git-0.1.1 → db_git-0.2.0}/tests/integration/test_pgdump_strategy.py +0 -0
  58. {db_git-0.1.1 → db_git-0.2.0}/tests/integration/test_template_strategy.py +0 -0
  59. {db_git-0.1.1 → db_git-0.2.0}/tests/unit/__init__.py +0 -0
  60. {db_git-0.1.1 → db_git-0.2.0}/tests/unit/test_config.py +0 -0
  61. {db_git-0.1.1 → db_git-0.2.0}/tests/unit/test_git.py +0 -0
  62. {db_git-0.1.1 → db_git-0.2.0}/tests/unit/test_state.py +0 -0
  63. {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
@@ -13,7 +13,7 @@ repos:
13
13
  - id: check-added-large-files
14
14
 
15
15
  - repo: https://github.com/astral-sh/ruff-pre-commit
16
- rev: v0.15.15
16
+ rev: v0.15.16
17
17
  hooks:
18
18
  - id: ruff-check
19
19
  args: [--fix]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: db-git
3
- Version: 0.1.1
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.1.1"
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
 
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("db-git")
@@ -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": "Database connection 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"
@@ -155,7 +155,7 @@ wheels = [
155
155
 
156
156
  [[package]]
157
157
  name = "db-git"
158
- version = "0.1.1"
158
+ version = "0.1.0"
159
159
  source = { editable = "." }
160
160
  dependencies = [
161
161
  { name = "psycopg", extra = ["binary"] },
@@ -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