Rework support for scanning for SCMs in a source tree.

Review Request #12528 — Created Aug. 12, 2022 and submitted — Latest diff uploaded


When starting up a command, we scan for all available SCMs, running
through the list of registered SCMClients and seeing which match either
a remote or local repository. This work runs through a lot of checks,
attempts to handle collisions, and finally selects a SCMClient or
errors out.

That work has lived in rbtools/clients/, and had some
baked-in error handling, option validation, and sys.exit() calls, none
of which we want in rbtools.clients. Especially with upcoming work
being done on these code paths.

This change introduces a new rbtools.utils.source_tree module
containing scan_scmclients_for_path(). This performs the same logic as
the old method, but with improved logging, error handling, and better

The caller can now tell whether a match was made, which SCMClient it
matched, which local_path, and which RepositoryInfo. It also returns
other candidates that were found but rejected during the scan, and which
hit unexpected errors.

These last two bits are now used to provide better error information if
a match failed or if there were multiple matches.

Upcoming changes to dependency management for clients will build upon
this to help provide dependency-specific error output.

Performance is better with the new one. Rather than instantiating all
SCMClient classes up-front, they're now lazily-instantiated. This
is most useful when an explicit repository type is specified.

Debug logging has been improved to mark every scan-related task with a
[scan] prefix, helping separate out that information from
client-specific information. We may eventually just want to use named
loggers instead of a prefix, but for now, this helps.

scan_usable_clients() has been updated to wrap the new functionality.
In time, a version of this will be moved into rbtools.commands and the
old version deprecated.

Added comprehensive test coverage for the new functionality. All tests
pass on Python 3.7 through 3.11.

Checked all new code using mypy and pyright for Type Annotation usage.