[FIX] odoobin: read the repository name from its git remote - #179
[FIX] odoobin: read the repository name from its git remote#179brinkflew wants to merge 10 commits into
Conversation
`odev run` inside a git repository linked it to the database under a name built
from directory names — `f"{path.parent.name}/{path.name}"` — even though it had
the real path in hand and had just checked it was a git repository. For a clone
that does not follow the `<repositories>/<organization>/<repository>` layout,
that name is wrong, and `GitConnector.path` expands it back to a directory that
does not exist:
repositories root: ~/odoo/dev
clone: ~/odoo/dev/tutorials
stored as: dev/tutorials
resolved to: ~/odoo/dev/dev/tutorials
Pass the path to `GitConnector` so the name is read from the git remote, which
is the only reliable source of truth, and keep the directory names as a fallback
for repositories that have no remote.
Two supporting changes:
- `GitConnector` now resolves its name in `_name_from_remote` / `_name_from_string`.
Reading the remote no longer raises when a repository has none, when its URL has
fewer than two segments, or when the only remote is not named `origin`; all three
fall back to the name passed to the connector.
- `OdoobinCommand._guess_addons_paths` no longer pins a repository whose directory
is missing. It used to prefer the stored repository over the current directory
unconditionally, so a database already linked to a wrong name could never be
re-detected and silently ran without its custom addons. It now warns and falls
back to the current directory, which lets the link repair itself on the next run.
Closes #92
Writing unit tests for helpers that had none surfaced five defects, all on code paths odev actually walks: - `PostgresConnector.columns_exist` returned an empty list when none of the requested columns existed, which is indistinguishable from all of them being present. A table created from an older definition therefore kept none of its new columns, since `CREATE TABLE IF NOT EXISTS` leaves an existing table alone and the missing-columns pass is the only thing that can migrate it. - `PostgresDatabase.tables` was a class attribute, so every database instance shared a single registry and tables from different databases collided on their name alone. - `string.quote` selected its delimiter with `max()` over the offsets of both quote characters, which picks the last one rather than the first and mis-quoted any string mixing them. The helper never escapes, so its docstring now says so. - `OdooVersion.__bool__` was always true: `module` is padded to `MIN_VERSION_LENGTH` and is therefore never an empty tuple. - `string.min_indent` raised on a text without any non-blank line, which `odev help` reaches through `dedent`. `float_to_hours` and `strip_styles` are broken too but are called nowhere in odev nor in the plugins; they are left alone and documented in the tests instead. Claude-Session: https://claude.ai/code/session_012tWyDuUYenE92nsBtG19Du
The suite sat at 64% with `.coveragerc` gating at 60%, and the gap was widest on pure logic the rest of the framework leans on. `odev/common/string.py` had no test module at all despite backing `odev help`, `odev history` and the database listing query. `GitWorktree` turns `git worktree list --porcelain` into the objects the whole fetch/pull/worktree family works with, and nothing exercised it. `OdooVersion` was tested for parsing only, while ordering is what actually picks a revision at runtime. Add test modules for the string helpers, the worktree parser and the datastore table preparation, plus ordering tests for versions. None of them need a network or a real repository. String and version helpers reach 100%, `common/postgres.py` 81% to 93% and the git connector 34% to 40%. Correct four things in the existing suite along the way: - The sudo tests shelled out to a real `sudo cat >> /etc/shadow`. The premise that the command fails only holds for an unprivileged user whose shell cannot open the redirection; a machine granting passwordless sudo runs it for real. Simulate the subprocess and the effective user instead, which also lets the elevation path be asserted rather than inferred. - `test_odev` left a command line behind in `sys.argv` for whichever test ran next. - `_patches` was a list defined on `OdevTestCase`, shared by every subclass through `cls._patches.append`, so each class tore down the patches of all the classes before it. - `PostgresTable` preparation is asserted against a mocked database: driving it against the live datastore made it depend on the connector's query cache, which DDL does not invalidate. Claude-Session: https://claude.ai/code/session_012tWyDuUYenE92nsBtG19Du
`is_odoo` checks that a database exists and then connects to it, and any process can drop it in between: `odev list` inspects every database in turn and would fail outright because one went away. A database that is gone is not an Odoo database, while anything else stays an error. The second check bypasses the query cache, since the cache is what claimed the database was still there. Claude-Session: https://claude.ai/code/session_012tWyDuUYenE92nsBtG19Du
`Odev` derived its name from the test mode alone, and everything it owns is named after it: the configuration file and the datastore database were fixed paths that any two instances had to share. It can now be given a name of its own. The setup script computed the destination of the `odev` symlink halfway through creating it, leaving no way to point it elsewhere; the decision moves to `link_path`. Both make the test suite able to run against resources of its own rather than against those of the user. Claude-Session: https://claude.ai/code/session_012tWyDuUYenE92nsBtG19Du
Everything the suite touched was named after a constant `odev-test`: the datastore database, the configuration file, the run directories and the databases created by the command tests. Two suites running at once shared all of it and destroyed each other's work — `odev delete --expression` removed the databases of the other run, dropping the datastore terminated its connections, and both wrote the same configuration file at once. A run now claims a sandbox named after itself and holds an exclusive lock on it for its whole life. The sandbox is removed when the session ends, including on `Ctrl+C` and on `SIGTERM`; the kernel drops the lock however the process dies, so a lock that can be taken marks leftovers the next run sweeps away. Nothing is written outside of it anymore: the configuration directory, the repositories and dumps directories, and the two symlinks the setup scripts create all point inside the sandbox, which also keeps the suite from repointing the `odev` command of the developer at whichever checkout it happens to run from. odev captures `SIGINT` around every query to cancel it rather than let it through, so an interrupt is recorded and acted upon at the next test boundary: pressing `Ctrl+C` stops the run without abandoning a connection mid-statement. Claude-Session: https://claude.ai/code/session_012tWyDuUYenE92nsBtG19Du
Both database context managers built a second, unconnected connector to close instead of the one they had connected, so `disconnect` did nothing and the connection stayed open until the garbage collector got to it. `ensure_connected` runs every database method inside its own block and those blocks nest — `is_odoo` opens one and then calls `table_exists`, which opens another — so this meant a fresh backend per call. Closing the right connector is not enough on its own: an inner block would close the connection the enclosing one is still using. The blocks are now reentrant and share a single connector, counted in the mixin so both classes get the same behaviour. The datastore keeps its connection instead of reopening it for each read: every command reads it and it lives as long as the process, which is not true of the databases odev walks through for `list` or `delete`. Over a full test suite run, the backends held at once drop from 42 to 3, and the suite goes from 55s to 34s. Claude-Session: https://claude.ai/code/session_012tWyDuUYenE92nsBtG19Du
Pin the behaviour a connection block is expected to have, since nothing failed loudly when it did not have it: leaving a block closes what it opened, a nested block joins the connection of the enclosing one rather than opening its own, repeated calls to decorated methods do not leave backends behind, and the datastore holds a single one throughout. The count is read from `pg_stat_activity`, which is what makes the third one a regression guard rather than a restatement of the code. Claude-Session: https://claude.ai/code/session_012tWyDuUYenE92nsBtG19Du
odev.common.logging configures logging on import, but logging.basicConfig is a no-op once the root logger has handlers: whether odev's handler gets installed depends on whether that import happens before or after pytest sets its own up, and importing the sandbox from conftest tips it. Combined with the capture added by #176 every record then reached the output twice, and a plain logger.info became a console.print that #177's run_hook test asserts on. The handler is dropped in pytest_configure so the suite no longer depends on import order. Also resolves the version command test, which this branch made namespace-agnostic while #175 rewrote it, and bumps the version to 4.31.2, one increment above the base branch.
…e ones #174 added a TestGithubConnectorRepositories class to this module, and both branches narrowed its imports to what they needed. Both sets of classes are kept and the imports are the union of the two. Also bumps the version to 4.31.3, one increment above the base branch.
Merge order: 4th of 4 — lastImportant Merge #174, #175 and #178, in that order, before this one. The base of this PR is now
What the merge commit resolves#174 added a Both sets of classes are kept, and the imports are the union of the two — Queue
Once this lands, Warning GitHub Actions has not run in this repository since 2026-07-29, so the checks here are stale. The full suite was run locally against the exact merged tree of all four: 357 passed, pre-commit clean. |
The merge-base changed after approval.
|
Merged |
## Description
`odev run` inside a git clone links it to the database under a name built from directory names —
`f"{path.parent.name}/{path.name}"` — even though it already holds the real path and has just
checked that it is a git repository. For a clone that does not follow the
`<repositories>/<organization>/<repository>` layout that name is wrong, and `GitConnector.path`
expands it back to a directory that does not exist:
```
repositories root: ~/odoo/dev
clone: ~/odoo/dev/tutorials
stored as: dev/tutorials
resolved back to: ~/odoo/dev/dev/tutorials
```
The wrong name is persisted, so every later command resolving the link looks in the duplicated
directory.
- `OdoobinProcess.additional_repositories` passes the path to `GitConnector`, so the name is read
from the git remote — the only reliable source of truth — and the directory names become a
fallback rather than the value.
- `GitConnector` resolves its name in `_name_from_remote` / `_name_from_string` / `_fallback_name`.
Reading the remote used to raise for a repository that has none and for a URL with fewer than two
segments, and it ignored remotes not named `origin`. All three now fall back to the name passed to
the connector instead of raising.
- `OdoobinCommand._guess_addons_paths` no longer pins a repository whose directory is missing. It
preferred the stored repository over the current directory unconditionally, so a database already
linked under a wrong name could never be re-detected and silently ran without its custom addons.
It now warns, names the missing path and falls back to the current directory — which is what lets
an affected link repair itself on the next run.
### Note for reviewers
Databases stored under a wrong name repair themselves the next time `odev run` is used from the
clone, but `save_database_repository` asks once whether to relink ("already linked to another
repository"). That prompt is the migration path. There is deliberately no upgrade script:
`__validate_upgrade_script` keys off the directory name, so it would need renaming on every rebase,
and the state is self-healing anyway.
`GitConnector.path` keeps expanding `<repositories>/<name>` with no flat-layout fallback. It also
feeds `clone()`, `fix_corrupted()` (which `rmtree`s it), `remove`, `worktrees` and
`requirements_path`, and a name-only match is ambiguous across organizations.
### Tests
`tests/tests/common/test_git_connector.py` grows from name parsing only to real repositories built
with `Repo.init` + `create_remote` in a temporary directory: the name read from the remote overrides
the directory names (the regression guard for this issue), HTTPS and non-`origin` remotes, no remote
at all, an unparseable remote, an absolute path that is not a git repository, and the `path`
argument taking precedence over the conventional location. A second class covers the process end to
end — a flat clone directly under the repositories root now resolves to its own directory. Full test
suite passes.
## Linked Issues
- closes #92
## Compliance
- [x] I have read the [contribution guide](../docs/CONTRIBUTING.md)
- [x] I made sure the documentation is up-to-date both in doctrings and the `docs` directory
- [x] I have added or modified unit tests where necessary
- [x] I have added new libraries to the `requirements.txt` file, if any
- [x] I have incremented the version number according the [versioning guide](../../docs/contributing/versioning.md)
- [x] The PR contains **my changes only** and **no other external commit**
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01K8csZBrrBYp8oqH5paxTAm
Description
odev runinside a git clone links it to the database under a name built from directory names —f"{path.parent.name}/{path.name}"— even though it already holds the real path and has justchecked that it is a git repository. For a clone that does not follow the
<repositories>/<organization>/<repository>layout that name is wrong, andGitConnector.pathexpands it back to a directory that does not exist:
The wrong name is persisted, so every later command resolving the link looks in the duplicated
directory.
OdoobinProcess.additional_repositoriespasses the path toGitConnector, so the name is readfrom the git remote — the only reliable source of truth — and the directory names become a
fallback rather than the value.
GitConnectorresolves its name in_name_from_remote/_name_from_string/_fallback_name.Reading the remote used to raise for a repository that has none and for a URL with fewer than two
segments, and it ignored remotes not named
origin. All three now fall back to the name passed tothe connector instead of raising.
OdoobinCommand._guess_addons_pathsno longer pins a repository whose directory is missing. Itpreferred the stored repository over the current directory unconditionally, so a database already
linked under a wrong name could never be re-detected and silently ran without its custom addons.
It now warns, names the missing path and falls back to the current directory — which is what lets
an affected link repair itself on the next run.
Note for reviewers
Databases stored under a wrong name repair themselves the next time
odev runis used from theclone, but
save_database_repositoryasks once whether to relink ("already linked to anotherrepository"). That prompt is the migration path. There is deliberately no upgrade script:
__validate_upgrade_scriptkeys off the directory name, so it would need renaming on every rebase,and the state is self-healing anyway.
GitConnector.pathkeeps expanding<repositories>/<name>with no flat-layout fallback. It alsofeeds
clone(),fix_corrupted()(whichrmtrees it),remove,worktreesandrequirements_path, and a name-only match is ambiguous across organizations.Tests
tests/tests/common/test_git_connector.pygrows from name parsing only to real repositories builtwith
Repo.init+create_remotein a temporary directory: the name read from the remote overridesthe directory names (the regression guard for this issue), HTTPS and non-
originremotes, no remoteat all, an unparseable remote, an absolute path that is not a git repository, and the
pathargument taking precedence over the conventional location. A second class covers the process end to
end — a flat clone directly under the repositories root now resolves to its own directory. Full test
suite passes.
Linked Issues
Compliance
docsdirectoryrequirements.txtfile, if any🤖 Generated with Claude Code
https://claude.ai/code/session_01K8csZBrrBYp8oqH5paxTAm