generated from McMassiveNZ/blank-slate
Implement bounded repository navigation with repo-lens
CI / build (debug, map[compiler:appleclang os:macos-latest]) (push) Canceled after 0s
CI / build (debug, map[compiler:clang os:ubuntu-latest]) (push) Canceled after 0s
CI / build (debug, map[compiler:gcc os:ubuntu-latest]) (push) Canceled after 0s
CI / build (debug, map[compiler:msvc os:windows-latest]) (push) Canceled after 0s
CI / build (release, map[compiler:appleclang os:macos-latest]) (push) Canceled after 0s
CI / build (release, map[compiler:clang os:ubuntu-latest]) (push) Canceled after 0s
CI / build (release, map[compiler:gcc os:ubuntu-latest]) (push) Canceled after 0s
CI / build (release, map[compiler:msvc os:windows-latest]) (push) Canceled after 0s
CI / build (sanitizers, map[compiler:appleclang os:macos-latest]) (push) Canceled after 0s
CI / build (sanitizers, map[compiler:clang os:ubuntu-latest]) (push) Canceled after 0s
CI / build (sanitizers, map[compiler:gcc os:ubuntu-latest]) (push) Canceled after 0s
CI / build (sanitizers, map[compiler:msvc os:windows-latest]) (push) Canceled after 0s
CI / build (debug, map[compiler:appleclang os:macos-latest]) (push) Canceled after 0s
CI / build (debug, map[compiler:clang os:ubuntu-latest]) (push) Canceled after 0s
CI / build (debug, map[compiler:gcc os:ubuntu-latest]) (push) Canceled after 0s
CI / build (debug, map[compiler:msvc os:windows-latest]) (push) Canceled after 0s
CI / build (release, map[compiler:appleclang os:macos-latest]) (push) Canceled after 0s
CI / build (release, map[compiler:clang os:ubuntu-latest]) (push) Canceled after 0s
CI / build (release, map[compiler:gcc os:ubuntu-latest]) (push) Canceled after 0s
CI / build (release, map[compiler:msvc os:windows-latest]) (push) Canceled after 0s
CI / build (sanitizers, map[compiler:appleclang os:macos-latest]) (push) Canceled after 0s
CI / build (sanitizers, map[compiler:clang os:ubuntu-latest]) (push) Canceled after 0s
CI / build (sanitizers, map[compiler:gcc os:ubuntu-latest]) (push) Canceled after 0s
CI / build (sanitizers, map[compiler:msvc os:windows-latest]) (push) Canceled after 0s
- Add map, find, and around commands with repository root discovery - Stream literal ripgrep searches with match counts and result limits - Add direct subprocess execution and explicit error returns - Disable C++ exceptions and add navigation tests - Support CMake installation and document builds and deployment - Add a reusable agent skill and document skill discovery
This commit is contained in:
1 parent
70c43b6a68
commit
a0fbb9f50e
22 files changed
+1185
-112
No files matched your search
@@ -3,7 +3,7 @@
|
||||
- Keep engineering simple: prefer deterministic local queries, bounded output, composable commands, and explicit context limits. Orchestrate proven tools before reimplementing them; semantic tooling must remain optional.
|
||||
- Avoid speculative infrastructure, dependencies, and abstractions. Introduce complexity only for a demonstrated need; source and Git are canonical.
|
||||
- Write clear, maintainable native C++. Follow surrounding code and existing formatting/tool configuration. Do not invent detailed conventions; document project conventions as they actually emerge.
|
||||
- Use trailing return types where applicable. Declare `execute` overrides `[[nodiscard]]` and `noexcept`, matching the interface. Keep `noexcept` contracts honest: recoverable execution failures must be returned as exit codes rather than escaping exceptions.
|
||||
- Use trailing return types where applicable. Declare `execute` overrides `[[nodiscard]]` and `noexcept`, matching the interface. Keep `noexcept` contracts honest: exceptions are disabled; use explicit status returns and non-throwing filesystem overloads for recoverable failures. Do not use `try`, `catch`, or `throw`.
|
||||
- Start with `docs/PROJECTMAP.md`. Prefer available symbol lookup for known symbols, textual search for known text, and sparse semantic bookmarks for architectural concepts without suitable symbols. Search, narrow, inspect a bounded range or containing symbol, then expand only as needed. Justify whole-file reads.
|
||||
- Keep context small: summarize large result sets, report limits and omitted results, and inspect callers, references, tests, or related code only when relevant. Never claim semantic classifications that the underlying evidence cannot establish reliably.
|
||||
- Inspect the working tree before editing; preserve existing human changes. Keep changes focused and avoid unrelated cleanup.
|
||||
|
||||
@@ -5,6 +5,15 @@ project(repo-lens VERSION 1.0.0 LANGUAGES CXX)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED true)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# Recoverable failures use explicit status returns throughout the project.
|
||||
if(MSVC)
|
||||
string(REGEX REPLACE "/EH[a-zA-Z+-]+" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
|
||||
add_compile_options(/EHs-c-)
|
||||
add_compile_definitions(_HAS_EXCEPTIONS=0)
|
||||
else()
|
||||
add_compile_options(-fno-exceptions)
|
||||
endif()
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE BOOL "Generate compile_commands.json with supported generators")
|
||||
|
||||
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
# repo-lens
|
||||
|
||||
repo-lens is a planned lightweight, agent-agnostic repository navigation and context tool. It is infrastructure that coding agents can use, not an autonomous coding agent.
|
||||
repo-lens is a lightweight, agent-agnostic repository navigation and context tool. It is infrastructure that coding agents can use, not an autonomous coding agent.
|
||||
|
||||
Coding agents often spend expensive model context rediscovering where code lives and reading irrelevant source. repo-lens aims to perform cheap, deterministic repository queries locally and return compact, bounded results, leaving the model more room to reason about the requested change.
|
||||
|
||||
**Status:** early bootstrap stage. The repository contains a native C++ CLI skeleton and inherited build/test scaffolding, but navigation functionality is not yet implemented. The command-line executable is named `repo`.
|
||||
**Status:** first functional version. The native C++20 executable is named `repo` and implements three bounded navigation commands:
|
||||
|
||||
The skeleton defines `find`, `map`, and `around` tool types. Argument parsing, usage output, and tool implementations remain TODOs; `around` is not dispatched yet. The existing test is a sample assertion, not a navigation test. C++ functions use trailing return types, and the tool interface requires checked `[[nodiscard]]` results and `noexcept` execution.
|
||||
```text
|
||||
repo map process
|
||||
repo find RepoLensToolFind
|
||||
repo around src/main.cpp:30
|
||||
repo around src/main.cpp:30 --before 10 --after 10
|
||||
```
|
||||
|
||||
## Building the skeleton
|
||||
Run from anywhere inside a Git repository; the nearest ancestor containing `.git` is the root. Relative source paths are resolved from that root; absolute paths (including Windows drive letters) also work. `repo` and `repo --help` show usage.
|
||||
|
||||
Use CMake 4.0 or newer, Ninja, and a C++20 compiler. Presets use separate build directories under `build/<preset>` and provide matching configure, build, and test commands:
|
||||
- `map` queries `docs/PROJECTMAP.md` case-insensitively, preferring heading matches over body matches. Sections end at the next heading. Output is limited to 3 sections, 120 lines, and 16 KiB, with an explicit truncation notice.
|
||||
- `find` requires ripgrep (`rg`) on `PATH`. It searches a non-empty, single-line literal, respects normal ignore rules, sorts paths, displays up to 20 occurrence locations, and counts all occurrences (including repeated matches on one line). It streams results without retaining the full search output. No semantic labels are inferred.
|
||||
- `around` shows 30 lines before and after the requested line, clamped to the file. Optional counts range from 0 to 1000. Output identifies the file, target, and actual range; malformed locations, missing files, and out-of-range lines fail clearly.
|
||||
|
||||
No-result searches succeed. Usage/file/process errors return non-zero; ripgrep failures preserve its exit code. Subprocesses execute directly without a shell, streaming stdout and reporting up to 4096 bytes of stderr (spooled to a temporary file).
|
||||
|
||||
## Building
|
||||
|
||||
Use CMake 4.0 or newer, Ninja, and a C++20 compiler. C++ exceptions are disabled; recoverable failures use explicit status returns. Presets use separate build directories under `.build/<preset>` and provide matching configure, build, and test commands:
|
||||
|
||||
```text
|
||||
cmake --list-presets
|
||||
@@ -21,7 +34,7 @@ ctest --preset msvc-debug
|
||||
|
||||
After editing code, repeat the build and test steps; reconfigure when changing options or presets. To work in Visual Studio, open the repository folder and select a preset. Ninja does not generate a solution for `cmake --open`. Manual configuration with a Visual Studio generator still supports that command, using a separate build directory.
|
||||
|
||||
Configuration generates `compile_commands.json` in the build directory, for example `build/msvc-debug/compile_commands.json`. Export is enabled by default for manual builds too. CMake supports this with Ninja and Makefile generators; Visual Studio solution generators do not produce it. In Visual Studio, use Open Folder with a Ninja preset when you need the compilation database.
|
||||
Configuration generates `compile_commands.json` in the build directory, for example `.build/msvc-debug/compile_commands.json`. Export is enabled by default for manual builds too. CMake supports this with Ninja and Makefile generators; Visual Studio solution generators do not produce it. In Visual Studio, use Open Folder with a Ninja preset when you need the compilation database.
|
||||
|
||||
Choose a preset for your host and compiler:
|
||||
|
||||
@@ -38,12 +51,52 @@ Warnings remain local to project targets. `ENABLE_WARNINGS_AS_ERRORS=OFF` disabl
|
||||
|
||||
On Windows, run these commands from an x64 Native Tools Command Prompt or Developer PowerShell for Visual Studio, with Ninja on `PATH`. On Linux, install the chosen compiler and Ninja; on macOS, install Xcode Command Line Tools and Ninja. Toolchain files in `cmake/toolchains/` select native compilers; these presets do not cross-compile to another operating system. Local paths and overrides belong in an untracked `CMakeUserPresets.json` or configure arguments.
|
||||
|
||||
The default build includes the existing GoogleTest sample test and may download its build dependencies. For an application-only build without downloads, configure with `-DENABLE_TESTS=OFF` and skip the test step. Sanitizers are opt-in with `-DENABLE_SANITIZERS=ON`; static analysis and warnings retain their existing configuration options. Building successfully does not make the stub commands functional. Manual configuration with `cmake -S . -B build/manual` remains supported; set `-DCMAKE_BUILD_TYPE=Debug` for single-configuration generators.
|
||||
The default build includes navigation unit and integration tests and may download its build dependencies. For an application-only build without downloads, configure with `-DENABLE_TESTS=OFF` and skip the test step. Sanitizers are opt-in with `-DENABLE_SANITIZERS=ON`; static analysis and warnings retain their existing configuration options. Manual configuration with `cmake -S . -B build/manual` remains supported; set `-DCMAKE_BUILD_TYPE=Debug` for single-configuration generators.
|
||||
|
||||
GoogleTest is pinned to v1.18.0 and managed by CMake's built-in FetchContent. To reuse a local checkout without downloading it, configure with `-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST=/absolute/path/to/googletest`.
|
||||
|
||||
CI installs CMake 4.x and runs Debug, Release, and sanitizer presets for Linux GCC/Clang, macOS AppleClang, and Windows MSVC. The inherited static-analysis helper configures Visual Studio generators; the Ninja presets do not currently run clang-tidy or Microsoft code analysis.
|
||||
|
||||
## Installing
|
||||
|
||||
Build Release, then install into a directory you own (Developer PowerShell on Windows):
|
||||
|
||||
```powershell
|
||||
cmake --preset msvc-release
|
||||
cmake --build --preset msvc-release
|
||||
ctest --preset msvc-release
|
||||
cmake --install .build/msvc-release --prefix "$env:LOCALAPPDATA/repo-lens"
|
||||
```
|
||||
|
||||
This installs `bin/repo.exe`. Add `%LOCALAPPDATA%\repo-lens\bin` to your user `PATH`, or call the installed executable by its full path. The install does not bundle ripgrep or modify `PATH`; `find` still needs `rg` available.
|
||||
|
||||
For a Visual Studio solution build, use `cmake --build build/vs --config Release` followed by `cmake --install build/vs --config Release --prefix "$env:LOCALAPPDATA/repo-lens"`. On Linux/macOS, use the matching Release preset and install its build directory with a suitable `--prefix`, for example `"$HOME/.local"`.
|
||||
|
||||
## Agent skill setup
|
||||
|
||||
The reusable [repo-lens skill](skills/repo-lens/SKILL.md) teaches agents the `map -> find -> around` workflow. Copy it into a target repository using this layout:
|
||||
|
||||
```text
|
||||
your-repository/
|
||||
├── AGENTS.md
|
||||
└── .agents/
|
||||
└── skills/
|
||||
└── repo-lens/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
Codex automatically discovers `.agents/skills/` directories from its current working directory up to the repository root. Personal skills can live in `~/.agents/skills/`. An `AGENTS.md` pointer is not required for Codex discovery. See the [official skill discovery documentation](https://learn.chatgpt.com/docs/build-skills#where-codex-loads-local-skills).
|
||||
|
||||
There is no universal discovery folder across all agents. The shared skill format is a named folder containing `SKILL.md` with `name` and `description` frontmatter; discovery paths depend on the agent. Claude uses `.claude/skills/<skill-name>/SKILL.md`; see the [migration documentation](https://developers.openai.com/cookbook/examples/agents_sdk/migrate-from-claude-agent-sdk/readme).
|
||||
|
||||
For agents that follow `AGENTS.md` but do not scan `.agents/skills/`, add an explicit pointer:
|
||||
|
||||
```markdown
|
||||
- Before navigating source, read and follow `.agents/skills/repo-lens/SKILL.md`.
|
||||
```
|
||||
|
||||
Alternatively, keep the file at `docs/repolens/SKILL.md` and point to that path in `AGENTS.md`; this provides repository guidance rather than automatic skill discovery. The skill supplies instructions only: `repo` and `rg` still need to be available on `PATH`. CMake installation currently installs the executable, not the skill.
|
||||
|
||||
## Navigation philosophy
|
||||
|
||||
`search -> narrow -> inspect -> expand only when necessary`
|
||||
@@ -58,20 +111,9 @@ Semantic bookmarks may identify stable architectural locations in source that or
|
||||
|
||||
Two `# @ai-bookmark` comments currently mark build invariants: the optional test dependency boundary and MSVC sanitizer annotation alignment. Their paths are listed in [the project map](docs/PROJECTMAP.md). Locate them with ripgrep for now; bookmark commands remain directional.
|
||||
|
||||
## Directional CLI
|
||||
## Future direction
|
||||
|
||||
These examples illustrate possible workflows. They are neither implemented nor a final command set:
|
||||
|
||||
```text
|
||||
repo map cloth
|
||||
repo find ClothWorld
|
||||
repo bookmarks
|
||||
repo bookmark CLOTH_SOLVE
|
||||
repo around src/Physics/Cloth/ClothWorld.cpp:417
|
||||
repo history ClothWorld.cpp
|
||||
repo diff
|
||||
repo changed
|
||||
```
|
||||
Bookmarks, Git history/diff queries, symbol lookup, AST/clangd/LSP integration, telemetry, configuration files, and semantic search remain deferred. Only `map`, `find`, and `around` are implemented. The milestone measures whether `route -> narrow -> inspect` reduces context use; it adds no measurement infrastructure.
|
||||
|
||||
## Scope and development
|
||||
|
||||
|
||||
@@ -18,3 +18,10 @@ set_property(TARGET
|
||||
gmock
|
||||
gmock_main
|
||||
PROPERTY FOLDER third_party/GoogleTest)
|
||||
|
||||
# GoogleTest adds exception flags of its own; keep it aligned with project targets.
|
||||
foreach(_test_dependency IN ITEMS gtest gtest_main gmock gmock_main)
|
||||
get_target_property(_test_compile_flags ${_test_dependency} COMPILE_FLAGS)
|
||||
string(REGEX REPLACE "[-/]EHsc|[-/]D_HAS_EXCEPTIONS=1|-fexceptions" "" _test_compile_flags "${_test_compile_flags}")
|
||||
set_property(TARGET ${_test_dependency} PROPERTY COMPILE_FLAGS "${_test_compile_flags}")
|
||||
endforeach()
|
||||
+16
-9
@@ -2,28 +2,35 @@
|
||||
|
||||
This is a routing table: **where should an agent look?** It is not an architecture manual. Start here, then inspect only the source or documentation relevant to the task.
|
||||
|
||||
The repository contains a C++ CLI skeleton, native compiler presets, and a sample test. Navigation implementations remain TODOs. The bookmarks below route to existing build invariants, not implemented navigation features.
|
||||
The repository contains a functional C++ CLI with bounded `map`, `find`, and `around` commands, native compiler presets, and navigation tests. The bookmarks below route to existing build invariants, not implemented navigation features.
|
||||
|
||||
## Current routes
|
||||
|
||||
### Purpose and direction
|
||||
Path: `README.md`
|
||||
Entry: project purpose, scope, illustrative CLI, and measurement goals.
|
||||
Entry: project purpose, scope, implemented CLI, and measurement goals.
|
||||
Related: `AGENTS.md` for agent operating instructions.
|
||||
|
||||
### CLI parsing and dispatch skeleton
|
||||
### CLI parsing and dispatch
|
||||
Path: `src/main.cpp`
|
||||
Entry: `main`
|
||||
Important symbols: `print_usage`, `extract_tool_name`, `extract_tool_arguments`
|
||||
Flow: placeholder parsing -> `find`/`map` dispatch -> tool `execute`; parsing and usage remain TODOs.
|
||||
Related: `src/CMakeLists.txt` lists all executable sources. `around` has a stub but is not dispatched yet.
|
||||
Important symbols: `print_usage`
|
||||
Flow: argv views -> `find`/`map`/`around` dispatch -> tool `execute`.
|
||||
Related: `src/CMakeLists.txt` lists all executable sources.
|
||||
|
||||
### Tool interface and implementations
|
||||
Path: `include/repo_tool_*.hpp`, `src/repo_tool_*.cpp`
|
||||
Entry: `include/repo_tool_interface.hpp`
|
||||
Important symbols: `rl::IRepoLensTool`, `rl::exit_code`, `rl::RepoLensToolFind`, `rl::RepoLensToolMap`, `rl::RepoLensToolAround`
|
||||
Related: `AGENTS.md` for trailing return types and the `[[nodiscard]]`/`noexcept` execution contract. Tool bodies remain TODOs.
|
||||
Tests: no navigation behavior tests yet.
|
||||
Related: `AGENTS.md` for trailing return types and the `[[nodiscard]]`/`noexcept` execution contract.
|
||||
Tests: `test/navigation_test.cpp` covers parsing, ranges, map ranking/limits, and subprocess/search behavior.
|
||||
|
||||
### Repository navigation and subprocess support
|
||||
Path: `include/navigation.hpp`, `src/navigation.cpp`, `include/process.hpp`, `src/process.cpp`
|
||||
Entry: `rl::repository_root`, `rl::parse_location`, `rl::map_sections`, `rl::run_process`
|
||||
Flow: discover nearest `.git` ancestor -> query root-relative map/source or stream ripgrep output.
|
||||
Related: `src/repo_tool_find.cpp` limits occurrence locations; `src/repo_tool_around.cpp` reads the requested window; `src/repo_tool_map.cpp` queries map sections.
|
||||
Tests: `test/navigation_test.cpp` uses temporary repositories and a subprocess fixture in `test/test_main.cpp`.
|
||||
|
||||
### Native build and compiler settings
|
||||
Path: `CMakeLists.txt`, `CMakePresets.json`, `cmake/`, `src/CMakeLists.txt`
|
||||
@@ -35,7 +42,7 @@ Ignore normally: generated `build/` and `.build/` trees; build helpers unless th
|
||||
|
||||
### Test dependencies and validation
|
||||
Path: `test/`, `cmake/gtest.cmake`
|
||||
Entry: `test/CMakeLists.txt`; `test/test_main.cpp` is only a sample assertion.
|
||||
Entry: `test/CMakeLists.txt`; `test/navigation_test.cpp` contains navigation tests; `test/test_main.cpp` supplies the test runner and subprocess fixture.
|
||||
Flow: `ENABLE_TESTS` -> FetchContent GoogleTest -> `repo-lens-test` -> CTest discovery.
|
||||
Bookmarks: `MSVC_SANITIZER_ANNOTATION_ALIGNMENT` in `test/CMakeLists.txt` keeps STL annotations consistent across the instrumented test and its dependencies.
|
||||
Related: `<compiler>-sanitizers` presets; `README.md` for local dependency overrides and build instructions.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace rl
|
||||
{
|
||||
[[nodiscard]] auto repository_root(std::filesystem::path& root, std::string& error) noexcept -> bool;
|
||||
[[nodiscard]] auto number(std::string_view text) noexcept -> std::optional<std::size_t>;
|
||||
struct source_location
|
||||
{
|
||||
std::filesystem::path file;
|
||||
std::size_t line;
|
||||
};
|
||||
[[nodiscard]] auto parse_location(std::string_view text) noexcept -> std::optional<source_location>;
|
||||
[[nodiscard]] auto read_lines(const std::filesystem::path& file, std::vector<std::string>& lines, std::string& error) noexcept -> bool;
|
||||
auto map_sections(const std::vector<std::string>& lines, std::string_view term) -> std::string;
|
||||
} // namespace rl
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace rl
|
||||
{
|
||||
struct process_result
|
||||
{
|
||||
int exit_code;
|
||||
std::string error;
|
||||
};
|
||||
// Streams stdout; stderr is retained up to 4096 bytes for diagnostics.
|
||||
[[nodiscard]] auto run_process(const std::vector<std::string>& args, const std::filesystem::path& directory,
|
||||
const std::function<void(std::string_view)>& consume) noexcept -> process_result;
|
||||
} // namespace rl
|
||||
@@ -10,8 +10,8 @@ namespace rl
|
||||
class RepoLensToolAround final : public IRepoLensTool
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] auto execute(std::string_view args) noexcept -> exit_code override;
|
||||
[[nodiscard]] auto execute(arguments args) noexcept -> exit_code override;
|
||||
};
|
||||
}
|
||||
} // namespace rl
|
||||
|
||||
#endif //REPO_LENSE_TOOL_AROUND_HPP_INCLUDED
|
||||
#endif // REPO_LENSE_TOOL_AROUND_HPP_INCLUDED
|
||||
@@ -10,8 +10,8 @@ namespace rl
|
||||
class RepoLensToolFind final : public IRepoLensTool
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] auto execute(std::string_view args) noexcept -> exit_code override;
|
||||
[[nodiscard]] auto execute(arguments args) noexcept -> exit_code override;
|
||||
};
|
||||
}
|
||||
} // namespace rl
|
||||
|
||||
#endif //REPO_LENSE_TOOL_FIND_HPP_INCLUDED
|
||||
#endif // REPO_LENSE_TOOL_FIND_HPP_INCLUDED
|
||||
@@ -2,6 +2,7 @@
|
||||
#define REPO_LENSE_TOOL_INTERFACE_HPP_INCLUDED
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
|
||||
#pragma once
|
||||
@@ -10,20 +11,21 @@ namespace rl
|
||||
{
|
||||
|
||||
using exit_code = std::int32_t;
|
||||
using arguments = std::span<const std::string_view>;
|
||||
|
||||
class IRepoLensTool
|
||||
{
|
||||
public:
|
||||
IRepoLensTool() noexcept = default;
|
||||
virtual ~IRepoLensTool() noexcept = default;
|
||||
IRepoLensTool(const IRepoLensTool&) noexcept = delete;
|
||||
IRepoLensTool(IRepoLensTool&&) noexcept = delete;
|
||||
auto operator=(const IRepoLensTool&) noexcept -> IRepoLensTool& = delete;
|
||||
auto operator=(IRepoLensTool&&) noexcept -> IRepoLensTool& = delete;
|
||||
public:
|
||||
IRepoLensTool() noexcept = default;
|
||||
virtual ~IRepoLensTool() noexcept = default;
|
||||
IRepoLensTool(const IRepoLensTool&) noexcept = delete;
|
||||
IRepoLensTool(IRepoLensTool&&) noexcept = delete;
|
||||
auto operator=(const IRepoLensTool&) noexcept -> IRepoLensTool& = delete;
|
||||
auto operator=(IRepoLensTool&&) noexcept -> IRepoLensTool& = delete;
|
||||
|
||||
[[nodiscard]] virtual auto execute(std::string_view arguments) noexcept -> exit_code = 0;
|
||||
[[nodiscard]] virtual auto execute(arguments args) noexcept -> exit_code = 0;
|
||||
};
|
||||
|
||||
}
|
||||
} // namespace rl
|
||||
|
||||
#endif //REPO_LENSE_TOOL_INTERFACE_HPP_INCLUDED
|
||||
#endif // REPO_LENSE_TOOL_INTERFACE_HPP_INCLUDED
|
||||
@@ -10,8 +10,8 @@ namespace rl
|
||||
class RepoLensToolMap final : public IRepoLensTool
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] auto execute(std::string_view args) noexcept -> exit_code override;
|
||||
[[nodiscard]] auto execute(arguments args) noexcept -> exit_code override;
|
||||
};
|
||||
}
|
||||
} // namespace rl
|
||||
|
||||
#endif //REPO_LENSE_TOOL_MAP_HPP_INCLUDED
|
||||
#endif // REPO_LENSE_TOOL_MAP_HPP_INCLUDED
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: repo-lens
|
||||
description: Navigate a Git repository with the repo-lens CLI using bounded project-map queries, literal occurrence searches, and numbered source windows. Use when locating code or inspecting a known source location while keeping context small.
|
||||
---
|
||||
|
||||
# Repository navigation with repo-lens
|
||||
|
||||
Use the executable `repo` to route, narrow, and inspect source. These commands are read-only. Choose the command that fits the information already available; a known file and line can go straight to `around`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Run from inside the target Git repository. `repo` discovers the nearest ancestor containing `.git`; searches and relative source paths use that root, even from a nested directory.
|
||||
|
||||
The executable must be on `PATH`, or invoked by its full path. Run `repo --help` to check availability. `find` also requires ripgrep (`rg`) on `PATH`. `map` requires the target repository's `docs/PROJECTMAP.md`.
|
||||
|
||||
If unavailable, report the missing prerequisite and use existing repository tools within the task's scope. Building or installing repo-lens is not required just to navigate code.
|
||||
|
||||
## Route: repo map <term>
|
||||
|
||||
Query a subsystem or architectural concept before reading the whole project map:
|
||||
|
||||
```text
|
||||
repo map process
|
||||
```
|
||||
|
||||
Matching is case-insensitive. Heading matches rank before body-only matches; each section ends at the next Markdown heading. Output preserves the original Markdown and is capped at 3 sections, 120 lines, and 16 KiB, with `TRUNCATED` when limited. Use returned paths and entry symbols to choose the next search. A heading match does not automatically include its child sections.
|
||||
|
||||
If no section matches, try another concrete term or proceed with known text. Do not invent routes that are absent from the map.
|
||||
|
||||
## Narrow: repo find <term>
|
||||
|
||||
Search for a known identifier or literal text string:
|
||||
|
||||
```text
|
||||
repo find RepoLensToolFind
|
||||
repo find "Cannot open file"
|
||||
```
|
||||
|
||||
Quote terms containing spaces as one shell argument. Terms must be non-empty and single-line. This is a case-sensitive literal search, not a regular expression or symbol query. Ripgrep's normal ignore rules apply; paths are sorted.
|
||||
|
||||
Read the `file:line` locations and the summary:
|
||||
|
||||
- `MATCHES` counts all occurrences, including repeated occurrences on the same source line.
|
||||
- `SHOWN` is at most 20; locations can repeat when a line contains multiple occurrences.
|
||||
- `TRUNCATED` means additional occurrences were omitted. The first 20 are sorted results, not relevance-ranked results.
|
||||
|
||||
When truncated, use a more specific literal or inspect a promising location. There is no pagination or path-filter option. If a task requires a scoped search or ignored files, use ripgrep directly with an explicit path and bounded output.
|
||||
|
||||
Text matches do not establish definitions, declarations, calls, tests, or references. Inspect source before drawing those conclusions.
|
||||
|
||||
## Inspect: repo around <file>:<line>
|
||||
|
||||
Pass a returned location to inspect a numbered source window:
|
||||
|
||||
```text
|
||||
repo around src/main.cpp:30
|
||||
repo around src/main.cpp:30 --before 10 --after 10
|
||||
repo around "C:\src\repo\file with spaces.cpp:417" --before 5 --after 15
|
||||
```
|
||||
|
||||
The default is 30 lines before and 30 after the target, clamped at the file boundaries. Optional counts accept integers from 0 to 1000. Source line numbers start at 1; the requested line must exist. Absolute Windows paths are supported: the final colon separates the path and line number.
|
||||
|
||||
`FILE`, `LINES`, and `TARGET` identify what was returned. Each source line has its original line number. Start with a small window; expand only when surrounding control flow, declarations, or callers are needed. The window is textual and may cut across function boundaries; it is not a containing-symbol query.
|
||||
|
||||
## Interpret outcomes
|
||||
|
||||
No search matches or no relevant map section is a successful no-result outcome. Non-zero exit codes indicate usage, file, repository-discovery, or subprocess failures; inspect stderr before continuing. Missing map files are errors, not proof that a subsystem does not exist.
|
||||
|
||||
Report relevant paths and line numbers, plus any limits that affect your conclusion. Only `map`, `find`, and `around` are implemented; do not invoke bookmark, Git-history, symbol, or semantic commands.
|
||||
@@ -3,6 +3,8 @@ set(current_target repo)
|
||||
add_executable(
|
||||
${current_target}
|
||||
main.cpp
|
||||
navigation.cpp
|
||||
process.cpp
|
||||
repo_tool_around.cpp
|
||||
repo_tool_find.cpp
|
||||
repo_tool_map.cpp
|
||||
@@ -10,6 +12,9 @@ add_executable(
|
||||
|
||||
target_include_directories(${current_target} PRIVATE "${PROJECT_SOURCE_DIR}/include")
|
||||
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS ${current_target} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
|
||||
if( ENABLE_ALL_REASONABLE_WARNINGS )
|
||||
MESSAGE("-- Additional Warnings Enabled")
|
||||
target_enable_warnings(${current_target})
|
||||
|
||||
+36
-43
@@ -1,50 +1,43 @@
|
||||
#include <cinttypes>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "repo_tool_find.hpp"
|
||||
#include "repo_tool_map.hpp"
|
||||
|
||||
#include "repo_tool_around.hpp"
|
||||
#include "repo_tool_find.hpp"
|
||||
#include "repo_tool_map.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
auto print_usage() noexcept -> int32_t
|
||||
{
|
||||
//@AGENT TODO: print to stdout the usage parameters
|
||||
return -1;
|
||||
}
|
||||
auto print_usage() -> int
|
||||
{
|
||||
std::cout << "repo-lens\n\nUsage:\n repo find <term>\n repo around <file>:<line> [--before N] [--after N]\n repo map <term>\n";
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
auto extract_tool_name([[maybe_unused]] std::string_view cmdline) noexcept -> std::string_view
|
||||
auto main(int argc, char* argv[]) -> int
|
||||
{
|
||||
if (argc == 1 || (argc == 2 && std::string_view(argv[1]) == "--help"))
|
||||
{
|
||||
//@AGENT TODO: cmdline should be the entire contents of argv, extract the tool name and return it.
|
||||
return {};
|
||||
return print_usage();
|
||||
}
|
||||
|
||||
auto extract_tool_arguments([[maybe_unused]] std::string_view cmdline) noexcept -> std::vector<std::string_view>
|
||||
std::vector<std::string_view> args;
|
||||
for (int i = 2; i < argc; ++i)
|
||||
{
|
||||
//@AGENT TODO: cmdline should be the entire contents of argv, extract the tool arguments and return them
|
||||
return {};
|
||||
args.emplace_back(argv[i]);
|
||||
}
|
||||
}
|
||||
|
||||
auto main(int argc, char* argv[]) noexcept -> int
|
||||
{
|
||||
if (argc < 2)
|
||||
{
|
||||
return print_usage();
|
||||
}
|
||||
|
||||
const std::string_view tool_name = extract_tool_name(argv[1]);
|
||||
const std::string_view tool_args = extract_tool_name(argv[1]);
|
||||
|
||||
if( tool_name == "find" )
|
||||
{
|
||||
rl::RepoLensToolFind tool;
|
||||
return tool.execute(tool_args);
|
||||
}
|
||||
|
||||
else if (tool_name == "map")
|
||||
{
|
||||
rl::RepoLensToolMap tool;
|
||||
return tool.execute(tool_args);
|
||||
}
|
||||
}
|
||||
const std::string_view command(argv[1]);
|
||||
if (command == "find")
|
||||
{
|
||||
return rl::RepoLensToolFind{}.execute(args);
|
||||
}
|
||||
if (command == "around")
|
||||
{
|
||||
return rl::RepoLensToolAround{}.execute(args);
|
||||
}
|
||||
if (command == "map")
|
||||
{
|
||||
return rl::RepoLensToolMap{}.execute(args);
|
||||
}
|
||||
std::cerr << "Unknown command: " << command << '\n';
|
||||
return 2;
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
#include "navigation.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <system_error>
|
||||
|
||||
namespace rl
|
||||
{
|
||||
auto repository_root(std::filesystem::path& root, std::string& error) noexcept -> bool
|
||||
{
|
||||
std::error_code code;
|
||||
auto path = std::filesystem::current_path(code);
|
||||
if (code)
|
||||
{
|
||||
error = "Cannot determine current directory: " + code.message();
|
||||
return false;
|
||||
}
|
||||
for (;;)
|
||||
{
|
||||
const bool found = std::filesystem::exists(path / ".git", code);
|
||||
if (code)
|
||||
{
|
||||
error = "Cannot inspect repository root: " + code.message();
|
||||
return false;
|
||||
}
|
||||
if (found)
|
||||
{
|
||||
root = std::move(path);
|
||||
return true;
|
||||
}
|
||||
const auto parent = path.parent_path();
|
||||
if (parent == path)
|
||||
{
|
||||
error = "No repository root (.git) found above the current directory";
|
||||
return false;
|
||||
}
|
||||
path = parent;
|
||||
}
|
||||
}
|
||||
|
||||
auto number(std::string_view text) noexcept -> std::optional<std::size_t>
|
||||
{
|
||||
if (text.empty())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
std::size_t value = 0;
|
||||
const auto result = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
if (result.ec != std::errc{} || result.ptr != text.data() + text.size())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
auto parse_location(std::string_view text) noexcept -> std::optional<source_location>
|
||||
{
|
||||
const auto colon = text.rfind(':');
|
||||
if (colon == std::string_view::npos || colon == 0)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto line = number(text.substr(colon + 1));
|
||||
if (!line || *line == 0)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return source_location{std::filesystem::path(text.substr(0, colon)), *line};
|
||||
}
|
||||
|
||||
auto read_lines(const std::filesystem::path& file, std::vector<std::string>& lines, std::string& error) noexcept -> bool
|
||||
{
|
||||
std::ifstream input(file);
|
||||
if (!input)
|
||||
{
|
||||
error = "Cannot open file: " + file.string();
|
||||
return false;
|
||||
}
|
||||
lines.clear();
|
||||
for (std::string line; std::getline(input, line);)
|
||||
{
|
||||
lines.push_back(std::move(line));
|
||||
}
|
||||
if (input.bad())
|
||||
{
|
||||
error = "Cannot read file: " + file.string();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
auto lower(std::string_view text) -> std::string
|
||||
{
|
||||
std::string result(text);
|
||||
std::transform(result.begin(), result.end(), result.begin(), [](unsigned char ch)
|
||||
{
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
auto heading(std::string_view line) -> bool
|
||||
{
|
||||
const auto end = line.find_first_not_of('#');
|
||||
return end != std::string_view::npos && end > 0 && end <= 6 && line[end] == ' ';
|
||||
}
|
||||
} // namespace
|
||||
|
||||
auto map_sections(const std::vector<std::string>& lines, std::string_view term) -> std::string
|
||||
{
|
||||
struct section
|
||||
{
|
||||
std::size_t begin;
|
||||
std::size_t end;
|
||||
bool title_match;
|
||||
};
|
||||
std::vector<section> matches;
|
||||
const auto query = lower(term);
|
||||
bool fenced = false;
|
||||
std::vector<std::size_t> boundaries{0};
|
||||
for (std::size_t i = 0; i < lines.size(); ++i)
|
||||
{
|
||||
if (lines[i].starts_with("```") || lines[i].starts_with("~~~"))
|
||||
{
|
||||
fenced = !fenced;
|
||||
}
|
||||
if (!fenced && heading(lines[i]) && i != 0)
|
||||
{
|
||||
boundaries.push_back(i);
|
||||
}
|
||||
}
|
||||
boundaries.push_back(lines.size());
|
||||
for (std::size_t i = 1; i < boundaries.size(); ++i)
|
||||
{
|
||||
const auto begin = boundaries[i - 1];
|
||||
const auto end = boundaries[i];
|
||||
if (begin == end)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const bool title_match = heading(lines[begin]) && lower(lines[begin]).find(query) != std::string::npos;
|
||||
bool body_match = false;
|
||||
for (auto j = begin; j < end && !body_match; ++j)
|
||||
{
|
||||
body_match = lower(lines[j]).find(query) != std::string::npos;
|
||||
}
|
||||
if (body_match)
|
||||
{
|
||||
matches.push_back({begin, end, title_match});
|
||||
}
|
||||
}
|
||||
std::stable_sort(matches.begin(), matches.end(), [](const section& a, const section& b)
|
||||
{
|
||||
return a.title_match > b.title_match;
|
||||
});
|
||||
std::string output;
|
||||
std::size_t shown_lines = 0;
|
||||
std::size_t shown_sections = 0;
|
||||
bool truncated = false;
|
||||
for (const auto& match : matches)
|
||||
{
|
||||
if (shown_sections == 3)
|
||||
{
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
++shown_sections;
|
||||
for (auto i = match.begin; i < match.end; ++i)
|
||||
{
|
||||
if (shown_lines == 120 || output.size() + lines[i].size() + 1 > 16384)
|
||||
{
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
output += lines[i] + '\n';
|
||||
++shown_lines;
|
||||
}
|
||||
if (truncated)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matches.empty())
|
||||
{
|
||||
return "No relevant project-map section found.\n";
|
||||
}
|
||||
if (truncated)
|
||||
{
|
||||
output += "TRUNCATED (limit: 3 sections, 120 lines, 16384 bytes)\n";
|
||||
}
|
||||
return output;
|
||||
}
|
||||
} // namespace rl
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
#include "process.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <system_error>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace rl
|
||||
{
|
||||
namespace
|
||||
{
|
||||
#ifdef _WIN32
|
||||
struct handle
|
||||
{
|
||||
HANDLE value = nullptr;
|
||||
~handle()
|
||||
{
|
||||
if (value && value != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
CloseHandle(value);
|
||||
}
|
||||
}
|
||||
handle() = default;
|
||||
handle(const handle&) = delete;
|
||||
auto operator=(const handle&) -> handle& = delete;
|
||||
};
|
||||
auto windows_error(const char* operation) -> process_result
|
||||
{
|
||||
return {2, std::string(operation) + ": " + std::error_code(static_cast<int>(GetLastError()), std::system_category()).message()};
|
||||
}
|
||||
auto wide(std::string_view text, std::wstring& result) -> bool
|
||||
{
|
||||
const auto length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast<int>(text.size()), nullptr, 0);
|
||||
if (length == 0 && !text.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
result.assign(static_cast<std::size_t>(length), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast<int>(text.size()), result.data(), length);
|
||||
return true;
|
||||
}
|
||||
// Encode each argument using the Windows C runtime's backslash/quote rules.
|
||||
auto quote(std::wstring_view argument) -> std::wstring
|
||||
{
|
||||
std::wstring result = L"\"";
|
||||
std::size_t slashes = 0;
|
||||
for (const auto ch : argument)
|
||||
{
|
||||
if (ch == L'\\')
|
||||
{
|
||||
++slashes;
|
||||
continue;
|
||||
}
|
||||
result.append(ch == L'"' ? 2 * slashes + 1 : slashes, L'\\');
|
||||
slashes = 0;
|
||||
result += ch;
|
||||
}
|
||||
result.append(2 * slashes, L'\\');
|
||||
result += L'"';
|
||||
return result;
|
||||
}
|
||||
#else
|
||||
struct descriptor
|
||||
{
|
||||
int value = -1;
|
||||
~descriptor()
|
||||
{
|
||||
if (value >= 0)
|
||||
{
|
||||
close(value);
|
||||
}
|
||||
}
|
||||
explicit descriptor(int fd) : value(fd) {}
|
||||
descriptor(const descriptor&) = delete;
|
||||
auto operator=(const descriptor&) -> descriptor& = delete;
|
||||
};
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
auto run_process(const std::vector<std::string>& args, const std::filesystem::path& directory,
|
||||
const std::function<void(std::string_view)>& consume) noexcept -> process_result
|
||||
{
|
||||
if (args.empty() || !consume)
|
||||
{
|
||||
return {2, "Missing process executable"};
|
||||
}
|
||||
FILE* error_file = nullptr;
|
||||
#ifdef _WIN32
|
||||
if (tmpfile_s(&error_file) != 0)
|
||||
{
|
||||
return {2, "Cannot create subprocess stderr file"};
|
||||
}
|
||||
#else
|
||||
error_file = std::tmpfile();
|
||||
#endif
|
||||
const std::unique_ptr<FILE, decltype(&std::fclose)> error_owner(error_file, &std::fclose);
|
||||
if (!error_file)
|
||||
{
|
||||
return {2, "Cannot create subprocess stderr file"};
|
||||
}
|
||||
std::array<char, 4096> buffer{};
|
||||
const auto deliver = [&](std::size_t size)
|
||||
{
|
||||
consume(std::string_view(buffer.data(), size));
|
||||
};
|
||||
int status = 0;
|
||||
#ifdef _WIN32
|
||||
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
|
||||
handle output_read;
|
||||
handle output_write;
|
||||
if (!CreatePipe(&output_read.value, &output_write.value, &security, 0))
|
||||
{
|
||||
return windows_error("CreatePipe");
|
||||
}
|
||||
if (!SetHandleInformation(output_read.value, HANDLE_FLAG_INHERIT, 0))
|
||||
{
|
||||
return windows_error("SetHandleInformation");
|
||||
}
|
||||
handle error_write;
|
||||
const auto raw_error = reinterpret_cast<HANDLE>(_get_osfhandle(_fileno(error_file)));
|
||||
if (!DuplicateHandle(GetCurrentProcess(), raw_error, GetCurrentProcess(), &error_write.value, 0, TRUE, DUPLICATE_SAME_ACCESS))
|
||||
{
|
||||
return windows_error("DuplicateHandle");
|
||||
}
|
||||
handle input;
|
||||
input.value = CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, &security, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (input.value == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
return windows_error("Open NUL");
|
||||
}
|
||||
STARTUPINFOW startup{};
|
||||
startup.cb = sizeof(startup);
|
||||
startup.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.hStdOutput = output_write.value;
|
||||
startup.hStdError = error_write.value;
|
||||
startup.hStdInput = input.value;
|
||||
std::wstring command;
|
||||
for (const auto& arg : args)
|
||||
{
|
||||
std::wstring argument;
|
||||
if (!wide(arg, argument))
|
||||
{
|
||||
return windows_error("Invalid UTF-8 argument");
|
||||
}
|
||||
command += quote(argument) + L' ';
|
||||
}
|
||||
PROCESS_INFORMATION process{};
|
||||
if (!CreateProcessW(nullptr, command.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, directory.c_str(), &startup, &process))
|
||||
{
|
||||
return windows_error("Cannot start subprocess");
|
||||
}
|
||||
handle child;
|
||||
child.value = process.hProcess;
|
||||
handle thread;
|
||||
thread.value = process.hThread;
|
||||
CloseHandle(output_write.value);
|
||||
output_write.value = nullptr;
|
||||
DWORD read_error = ERROR_SUCCESS;
|
||||
for (;;)
|
||||
{
|
||||
DWORD count = 0;
|
||||
if (!ReadFile(output_read.value, buffer.data(), static_cast<DWORD>(buffer.size()), &count, nullptr))
|
||||
{
|
||||
const auto code = GetLastError();
|
||||
if (code != ERROR_BROKEN_PIPE)
|
||||
{
|
||||
read_error = code;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
deliver(count);
|
||||
}
|
||||
if (WaitForSingleObject(child.value, INFINITE) != WAIT_OBJECT_0)
|
||||
{
|
||||
return windows_error("WaitForSingleObject");
|
||||
}
|
||||
DWORD code = 0;
|
||||
if (!GetExitCodeProcess(child.value, &code))
|
||||
{
|
||||
return windows_error("GetExitCodeProcess");
|
||||
}
|
||||
status = static_cast<int>(code);
|
||||
if (read_error != ERROR_SUCCESS)
|
||||
{
|
||||
return {2, std::string("Read subprocess stdout: ") + std::error_code(static_cast<int>(read_error), std::system_category()).message()};
|
||||
}
|
||||
#else
|
||||
std::vector<char*> argv;
|
||||
for (const auto& arg : args)
|
||||
{
|
||||
argv.push_back(const_cast<char*>(arg.c_str()));
|
||||
}
|
||||
argv.push_back(nullptr);
|
||||
int pipe_fds[2];
|
||||
if (pipe(pipe_fds) != 0)
|
||||
{
|
||||
return {2, std::string("pipe: ") + std::error_code(errno, std::generic_category()).message()};
|
||||
}
|
||||
descriptor output_read(pipe_fds[0]);
|
||||
descriptor output_write(pipe_fds[1]);
|
||||
const auto pid = fork();
|
||||
if (pid < 0)
|
||||
{
|
||||
return {2, std::string("fork: ") + std::error_code(errno, std::generic_category()).message()};
|
||||
}
|
||||
if (pid == 0)
|
||||
{
|
||||
if (chdir(directory.c_str()) != 0 || dup2(output_write.value, STDOUT_FILENO) < 0 || dup2(fileno(error_file), STDERR_FILENO) < 0)
|
||||
{
|
||||
_exit(126);
|
||||
}
|
||||
close(output_read.value);
|
||||
close(output_write.value);
|
||||
execvp(argv[0], argv.data());
|
||||
const char message[] = "Cannot execute subprocess (check executable PATH)\n";
|
||||
const auto ignored = write(STDERR_FILENO, message, sizeof(message) - 1);
|
||||
static_cast<void>(ignored);
|
||||
_exit(127);
|
||||
}
|
||||
close(output_write.value);
|
||||
output_write.value = -1;
|
||||
int read_error = 0;
|
||||
for (;;)
|
||||
{
|
||||
const auto count = read(output_read.value, buffer.data(), buffer.size());
|
||||
if (count < 0 && errno == EINTR)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (count < 0)
|
||||
{
|
||||
read_error = errno;
|
||||
break;
|
||||
}
|
||||
if (count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
deliver(static_cast<std::size_t>(count));
|
||||
}
|
||||
int child_status = 0;
|
||||
while (waitpid(pid, &child_status, 0) < 0)
|
||||
{
|
||||
if (errno != EINTR)
|
||||
{
|
||||
return {2, std::string("waitpid: ") + std::error_code(errno, std::generic_category()).message()};
|
||||
}
|
||||
}
|
||||
status = WIFEXITED(child_status) ? WEXITSTATUS(child_status) : 128 + WTERMSIG(child_status);
|
||||
if (read_error != 0)
|
||||
{
|
||||
return {2, std::string("Read subprocess stdout: ") + std::error_code(read_error, std::generic_category()).message()};
|
||||
}
|
||||
#endif
|
||||
std::rewind(error_file);
|
||||
const auto count = std::fread(buffer.data(), 1, buffer.size(), error_file);
|
||||
std::string error(buffer.data(), count);
|
||||
if (count == buffer.size())
|
||||
{
|
||||
error += "\n[stderr limited to 4096 bytes]\n";
|
||||
}
|
||||
return {status, std::move(error)};
|
||||
}
|
||||
} // namespace rl
|
||||
@@ -1,10 +1,94 @@
|
||||
#include "navigation.hpp"
|
||||
#include "repo_tool_around.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
namespace rl
|
||||
{
|
||||
auto RepoLensToolAround::execute([[maybe_unused]] std::string_view args) noexcept -> exit_code
|
||||
{
|
||||
//@AGENT_TODO: Around tool implementation goes here
|
||||
return 0;
|
||||
}
|
||||
auto RepoLensToolAround::execute(arguments args) noexcept -> exit_code
|
||||
{
|
||||
if (args.empty() || args.size() % 2 != 1)
|
||||
{
|
||||
std::cerr << "around: Usage: repo around <file>:<line> [--before N] [--after N]" << '\n';
|
||||
return 2;
|
||||
}
|
||||
const auto location = parse_location(args[0]);
|
||||
if (!location)
|
||||
{
|
||||
std::cerr << "around: Expected <file>:<line> with a positive source line\n";
|
||||
return 2;
|
||||
}
|
||||
std::size_t before = 30;
|
||||
std::size_t after = 30;
|
||||
bool seen_before = false;
|
||||
bool seen_after = false;
|
||||
for (std::size_t i = 1; i < args.size(); i += 2)
|
||||
{
|
||||
const auto count = number(args[i + 1]);
|
||||
if (!count || *count > 1000)
|
||||
{
|
||||
std::cerr << "around: Context counts must be at most 1000" << '\n';
|
||||
return 2;
|
||||
}
|
||||
if (args[i] == "--before" && !seen_before)
|
||||
{
|
||||
before = *count;
|
||||
seen_before = true;
|
||||
}
|
||||
else if (args[i] == "--after" && !seen_after)
|
||||
{
|
||||
after = *count;
|
||||
seen_after = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "around: Unknown or repeated around option: " + std::string(args[i]) << '\n';
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
std::filesystem::path root;
|
||||
std::string error;
|
||||
if (!repository_root(root, error))
|
||||
{
|
||||
std::cerr << "around: " << error << '\n';
|
||||
return 2;
|
||||
}
|
||||
const auto file = location->file.is_absolute() ? location->file : root / location->file;
|
||||
std::ifstream input(file);
|
||||
if (!input)
|
||||
{
|
||||
std::cerr << "around: Cannot open file: " + file.string() << '\n';
|
||||
return 2;
|
||||
}
|
||||
const auto first = location->line > before ? location->line - before : 1;
|
||||
const auto last = location->line + std::min(after, SIZE_MAX - location->line);
|
||||
std::vector<std::string> window;
|
||||
std::size_t current = 0;
|
||||
for (std::string line; current < last && std::getline(input, line);)
|
||||
{
|
||||
++current;
|
||||
if (current >= first)
|
||||
{
|
||||
window.push_back(std::move(line));
|
||||
}
|
||||
}
|
||||
if (input.bad())
|
||||
{
|
||||
std::cerr << "around: Cannot read file: " + file.string() << '\n';
|
||||
return 2;
|
||||
}
|
||||
if (current < location->line)
|
||||
{
|
||||
std::cerr << "around: Requested line is outside the file" << '\n';
|
||||
return 2;
|
||||
}
|
||||
std::cout << "FILE " << location->file.generic_string() << "\nLINES " << first << '-' << current << "\nTARGET " << location->line << '\n';
|
||||
for (std::size_t i = 0; i < window.size(); ++i)
|
||||
{
|
||||
std::cout << first + i << " | " << window[i] << '\n';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace rl
|
||||
+100
-6
@@ -1,13 +1,107 @@
|
||||
#include "navigation.hpp"
|
||||
#include "repo_tool_find.hpp"
|
||||
#include "process.hpp"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
namespace rl
|
||||
{
|
||||
auto RepoLensToolFind::execute([[maybe_unused]] std::string_view args) noexcept -> exit_code
|
||||
{
|
||||
//@AGENT_TODO: find tool implementation goes here
|
||||
std::cout << "Repo Lense Find Tool Executed\n";
|
||||
return 0;
|
||||
}
|
||||
auto RepoLensToolFind::execute(arguments args) noexcept -> exit_code
|
||||
{
|
||||
if (args.size() != 1 || args[0].empty() || args[0].find_first_of("\r\n") != std::string_view::npos)
|
||||
{
|
||||
std::cerr << "find: Usage: repo find <term> (non-empty, single-line literal)" << '\n';
|
||||
return 2;
|
||||
}
|
||||
std::filesystem::path root;
|
||||
std::string error;
|
||||
if (!repository_root(root, error))
|
||||
{
|
||||
std::cerr << "find: " << error << '\n';
|
||||
return 2;
|
||||
}
|
||||
std::vector<std::string> shown;
|
||||
std::size_t total = 0;
|
||||
std::string file;
|
||||
std::string line;
|
||||
enum class field
|
||||
{
|
||||
file,
|
||||
line,
|
||||
text
|
||||
};
|
||||
auto state = field::file;
|
||||
const auto result = run_process({"rg", "--no-config", "--color", "never", "--sort", "path", "--fixed-strings",
|
||||
"--only-matching", "--line-number", "--with-filename", "--no-heading", "--null", "--", std::string(args[0]), "."},
|
||||
root,
|
||||
[&](std::string_view chunk)
|
||||
{
|
||||
for (const auto ch : chunk)
|
||||
{
|
||||
if (state == field::file)
|
||||
{
|
||||
if (ch == '\0')
|
||||
{
|
||||
state = field::line;
|
||||
}
|
||||
else
|
||||
{
|
||||
file += ch;
|
||||
}
|
||||
}
|
||||
else if (state == field::line)
|
||||
{
|
||||
if (ch == ':')
|
||||
{
|
||||
state = field::text;
|
||||
}
|
||||
else
|
||||
{
|
||||
line += ch;
|
||||
}
|
||||
}
|
||||
else if (ch == '\n')
|
||||
{
|
||||
++total;
|
||||
if (shown.size() < 20)
|
||||
{
|
||||
auto path = std::filesystem::path(file).generic_string();
|
||||
if (path.starts_with("./"))
|
||||
{
|
||||
path.erase(0, 2);
|
||||
}
|
||||
shown.push_back(path + ':' + line);
|
||||
}
|
||||
file.clear();
|
||||
line.clear();
|
||||
state = field::file;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!result.error.empty())
|
||||
{
|
||||
std::cerr << result.error;
|
||||
}
|
||||
|
||||
if (result.exit_code != 0 && result.exit_code != 1)
|
||||
{
|
||||
std::cerr << "find: rg failed with exit code " << result.exit_code << '\n';
|
||||
return result.exit_code;
|
||||
}
|
||||
|
||||
std::cout << "QUERY " << args[0] << "\n\n";
|
||||
for (const auto& match : shown)
|
||||
{
|
||||
std::cout << match << '\n';
|
||||
}
|
||||
|
||||
std::cout << "\nMATCHES " << total << "\nSHOWN " << shown.size() << '\n';
|
||||
if (total > shown.size())
|
||||
{
|
||||
std::cout << "TRUNCATED\n";
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace rl
|
||||
+21
-5
@@ -1,10 +1,26 @@
|
||||
#include "navigation.hpp"
|
||||
#include "repo_tool_map.hpp"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
namespace rl
|
||||
{
|
||||
auto RepoLensToolMap::execute([[maybe_unused]] std::string_view args) noexcept -> exit_code
|
||||
{
|
||||
//@AGENT_TODO: Map tool implementation goes here
|
||||
return 0;
|
||||
}
|
||||
auto RepoLensToolMap::execute(arguments args) noexcept -> exit_code
|
||||
{
|
||||
if (args.size() != 1 || args[0].empty())
|
||||
{
|
||||
std::cerr << "map: Usage: repo map <term> (non-empty term)" << '\n';
|
||||
return 2;
|
||||
}
|
||||
std::filesystem::path root;
|
||||
std::string error;
|
||||
std::vector<std::string> lines;
|
||||
if (!repository_root(root, error) || !read_lines(root / "docs" / "PROJECTMAP.md", lines, error))
|
||||
{
|
||||
std::cerr << "map: " << error << '\n';
|
||||
return 2;
|
||||
}
|
||||
std::cout << map_sections(lines, args[0]);
|
||||
return 0;
|
||||
}
|
||||
} // namespace rl
|
||||
+10
-1
@@ -3,11 +3,17 @@ include(gtest)
|
||||
add_executable(
|
||||
repo-lens-test
|
||||
test_main.cpp
|
||||
navigation_test.cpp
|
||||
../src/navigation.cpp
|
||||
../src/process.cpp
|
||||
../src/repo_tool_around.cpp
|
||||
../src/repo_tool_find.cpp
|
||||
../src/repo_tool_map.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
repo-lens-test
|
||||
gtest_main
|
||||
gtest
|
||||
)
|
||||
|
||||
if(ENABLE_ALL_REASONABLE_WARNINGS)
|
||||
@@ -29,3 +35,6 @@ endif()
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(repo-lens-test)
|
||||
|
||||
target_include_directories(repo-lens-test PRIVATE "${PROJECT_SOURCE_DIR}/include")
|
||||
target_compile_definitions(repo-lens-test PRIVATE REPO_TEST_EXECUTABLE="$<TARGET_FILE:repo-lens-test>")
|
||||
@@ -0,0 +1,213 @@
|
||||
#include "navigation.hpp"
|
||||
#include "process.hpp"
|
||||
#include "repo_tool_around.hpp"
|
||||
#include "repo_tool_find.hpp"
|
||||
#include "repo_tool_map.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
TEST(Location, FinalColonAndNumericValidation)
|
||||
{
|
||||
const auto windows = rl::parse_location("C:\\src\\repo\\file.cpp:417");
|
||||
ASSERT_TRUE(windows.has_value());
|
||||
EXPECT_EQ(windows->file.string(), "C:\\src\\repo\\file.cpp");
|
||||
EXPECT_EQ(windows->line, 417U);
|
||||
EXPECT_EQ(rl::parse_location("src/file.cpp:1")->line, 1U);
|
||||
for (const auto value : {"file", ":1", "file:0", "file:-1", "file:12x", "file:", "file:999999999999999999999999"})
|
||||
{
|
||||
EXPECT_FALSE(rl::parse_location(value).has_value()) << value;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Map, HeadingsRankBeforeBodyAndBoundariesAreRespected)
|
||||
{
|
||||
const std::vector<std::string> lines{"# Map", "## Other", "cloth mentioned here", "### CLOTH", "preferred route", "### Next", "unrelated route"};
|
||||
const auto output = rl::map_sections(lines, "cloth");
|
||||
EXPECT_TRUE(output.starts_with("### CLOTH\npreferred route\n"));
|
||||
EXPECT_NE(output.find("cloth mentioned here"), std::string::npos);
|
||||
EXPECT_EQ(output.find("unrelated route"), std::string::npos);
|
||||
EXPECT_EQ(rl::map_sections(lines, "missing"), "No relevant project-map section found.\n");
|
||||
}
|
||||
|
||||
TEST(Map, LimitsSectionsLinesAndBytes)
|
||||
{
|
||||
EXPECT_NE(rl::map_sections({"## route 1", "## route 2", "## route 3", "## route 4"}, "route").find("TRUNCATED"), std::string::npos);
|
||||
std::vector<std::string> lines(200, "route");
|
||||
lines[0] = "## route";
|
||||
const auto output = rl::map_sections(lines, "route");
|
||||
EXPECT_LT(output.size(), 1000U);
|
||||
EXPECT_NE(output.find("TRUNCATED"), std::string::npos);
|
||||
EXPECT_LT(rl::map_sections({"## route", std::string(20000, 'x')}, "route").size(), 16460U);
|
||||
}
|
||||
|
||||
TEST(Map, FencedHeadingsAreNotSectionBoundaries)
|
||||
{
|
||||
const auto output = rl::map_sections({"## route", "```markdown", "## example", "```", "tail", "## other", "omit"}, "route");
|
||||
EXPECT_NE(output.find("tail"), std::string::npos);
|
||||
EXPECT_EQ(output.find("omit"), std::string::npos);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
class Repository : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
std::filesystem::path previous;
|
||||
std::filesystem::path root;
|
||||
std::ostringstream output;
|
||||
std::ostringstream errors;
|
||||
std::streambuf* old_output = nullptr;
|
||||
std::streambuf* old_errors = nullptr;
|
||||
void SetUp() override
|
||||
{
|
||||
std::error_code code;
|
||||
previous = std::filesystem::current_path(code);
|
||||
ASSERT_FALSE(code) << code.message();
|
||||
const auto temporary = std::filesystem::temp_directory_path(code);
|
||||
ASSERT_FALSE(code) << code.message();
|
||||
root = temporary / ("repo-lens-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
std::filesystem::create_directories(root / ".git", code);
|
||||
ASSERT_FALSE(code) << code.message();
|
||||
std::filesystem::create_directories(root / "nested", code);
|
||||
ASSERT_FALSE(code) << code.message();
|
||||
std::filesystem::create_directories(root / "docs", code);
|
||||
ASSERT_FALSE(code) << code.message();
|
||||
std::ofstream(root / "docs" / "PROJECTMAP.md") << "## route\nPath: src/\n";
|
||||
std::ofstream file(root / "file with spaces.txt");
|
||||
for (int i = 1; i <= 80; ++i)
|
||||
{
|
||||
file << "source " << i << '\n';
|
||||
}
|
||||
std::filesystem::current_path(root / "nested", code);
|
||||
ASSERT_FALSE(code) << code.message();
|
||||
old_output = std::cout.rdbuf(output.rdbuf());
|
||||
old_errors = std::cerr.rdbuf(errors.rdbuf());
|
||||
}
|
||||
void TearDown() override
|
||||
{
|
||||
if (old_output)
|
||||
{
|
||||
std::cout.rdbuf(old_output);
|
||||
}
|
||||
if (old_errors)
|
||||
{
|
||||
std::cerr.rdbuf(old_errors);
|
||||
}
|
||||
std::error_code code;
|
||||
std::filesystem::current_path(previous, code);
|
||||
EXPECT_FALSE(code) << code.message();
|
||||
std::filesystem::remove_all(root, code);
|
||||
EXPECT_FALSE(code) << code.message();
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(Repository, RootDiscoveryAndDefaultSourceWindow)
|
||||
{
|
||||
std::filesystem::path found;
|
||||
std::string error;
|
||||
ASSERT_TRUE(rl::repository_root(found, error)) << error;
|
||||
EXPECT_EQ(found, root);
|
||||
const std::vector<std::string_view> args{"file with spaces.txt:40"};
|
||||
EXPECT_EQ(rl::RepoLensToolAround{}.execute(args), 0);
|
||||
EXPECT_TRUE(output.str().starts_with("FILE file with spaces.txt\nLINES 10-70\nTARGET 40\n10 | source 10\n"));
|
||||
EXPECT_TRUE(output.str().ends_with("70 | source 70\n"));
|
||||
}
|
||||
|
||||
TEST_F(Repository, SourceWindowClampsAtBothEnds)
|
||||
{
|
||||
const std::vector<std::string_view> start{"file with spaces.txt:1"};
|
||||
EXPECT_EQ(rl::RepoLensToolAround{}.execute(start), 0);
|
||||
EXPECT_NE(output.str().find("LINES 1-31"), std::string::npos);
|
||||
output.str("");
|
||||
const auto absolute = (root / "file with spaces.txt").string() + ":80";
|
||||
const std::vector<std::string_view> end{absolute, "--before", "0", "--after", "30"};
|
||||
EXPECT_EQ(rl::RepoLensToolAround{}.execute(end), 0);
|
||||
EXPECT_NE(output.str().find("LINES 80-80"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(Repository, InvalidAroundRequestsFail)
|
||||
{
|
||||
for (const auto location : {"missing:1", "file with spaces.txt:81", "file with spaces.txt:0"})
|
||||
{
|
||||
const std::vector<std::string_view> args{location};
|
||||
EXPECT_NE(rl::RepoLensToolAround{}.execute(args), 0);
|
||||
}
|
||||
const std::vector<std::string_view> too_large{"file with spaces.txt:1", "--before", "1001"};
|
||||
EXPECT_NE(rl::RepoLensToolAround{}.execute(too_large), 0);
|
||||
const std::vector<std::string_view> negative{"file with spaces.txt:1", "--after", "-1"};
|
||||
EXPECT_NE(rl::RepoLensToolAround{}.execute(negative), 0);
|
||||
const std::vector<std::string_view> missing{"file with spaces.txt:1", "--after"};
|
||||
EXPECT_NE(rl::RepoLensToolAround{}.execute(missing), 0);
|
||||
}
|
||||
|
||||
TEST_F(Repository, MapWorksFromNestedDirectory)
|
||||
{
|
||||
const std::vector<std::string_view> args{"ROUTE"};
|
||||
EXPECT_EQ(rl::RepoLensToolMap{}.execute(args), 0);
|
||||
EXPECT_EQ(output.str(), "## route\nPath: src/\n");
|
||||
}
|
||||
|
||||
TEST_F(Repository, FindLiteralTruncationAndNoMatches)
|
||||
{
|
||||
const auto version = rl::run_process({"rg", "--version"}, root, [](std::string_view) {});
|
||||
if (version.exit_code != 0)
|
||||
{
|
||||
GTEST_SKIP() << "ripgrep unavailable: " << version.error;
|
||||
}
|
||||
{
|
||||
std::ofstream file(root / "matches.txt");
|
||||
for (int i = 0; i < 25; ++i)
|
||||
{
|
||||
file << "literal.* literal.*\n";
|
||||
}
|
||||
std::ofstream(root / ".gitignore") << "ignored.txt\n";
|
||||
std::ofstream(root / "ignored.txt") << "literal.*\n";
|
||||
}
|
||||
const std::vector<std::string_view> args{"literal.*"};
|
||||
EXPECT_EQ(rl::RepoLensToolFind{}.execute(args), 0);
|
||||
EXPECT_NE(output.str().find("MATCHES 50\nSHOWN 20\nTRUNCATED"), std::string::npos);
|
||||
EXPECT_NE(output.str().find("matches.txt:1\n"), std::string::npos);
|
||||
EXPECT_EQ(output.str().find("ignored.txt:"), std::string::npos);
|
||||
output.str("");
|
||||
const std::vector<std::string_view> absent{"nothing matches this"};
|
||||
EXPECT_EQ(rl::RepoLensToolFind{}.execute(absent), 0);
|
||||
EXPECT_NE(output.str().find("MATCHES 0\nSHOWN 0"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(Repository, ProcessPreservesArgumentsStreamsAndExitCode)
|
||||
{
|
||||
std::string captured;
|
||||
const auto executable = std::filesystem::path(REPO_TEST_EXECUTABLE).string();
|
||||
const std::string argument = R"(spaces "quotes" & $() \)";
|
||||
const auto result = rl::run_process({executable, "--process-fixture", argument}, root,
|
||||
[&](std::string_view chunk)
|
||||
{
|
||||
captured += chunk;
|
||||
});
|
||||
EXPECT_EQ(result.exit_code, 7);
|
||||
EXPECT_EQ(captured, argument);
|
||||
EXPECT_EQ(result.error, "fixture stderr");
|
||||
}
|
||||
|
||||
TEST_F(Repository, RecoverableFailuresReturnErrors)
|
||||
{
|
||||
std::vector<std::string> lines;
|
||||
std::string error;
|
||||
EXPECT_FALSE(rl::read_lines(root / "missing", lines, error));
|
||||
EXPECT_NE(error.find("Cannot open file"), std::string::npos);
|
||||
const auto missing = rl::run_process({"repo-lens-missing-executable-12345"}, root, [](std::string_view) {});
|
||||
EXPECT_NE(missing.exit_code, 0);
|
||||
EXPECT_FALSE(missing.error.empty());
|
||||
const auto empty = rl::run_process({}, root, [](std::string_view) {});
|
||||
EXPECT_NE(empty.exit_code, 0);
|
||||
std::error_code code;
|
||||
std::filesystem::remove(root / ".git", code);
|
||||
ASSERT_FALSE(code) << code.message();
|
||||
std::filesystem::path found;
|
||||
EXPECT_FALSE(rl::repository_root(found, error));
|
||||
EXPECT_NE(error.find("No repository root"), std::string::npos);
|
||||
}
|
||||
+12
-3
@@ -1,6 +1,15 @@
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
TEST(testmain, example)
|
||||
auto main(int argc, char* argv[]) -> int
|
||||
{
|
||||
EXPECT_EQ(42, 7*6);
|
||||
}
|
||||
if (argc == 3 && std::string_view(argv[1]) == "--process-fixture")
|
||||
{
|
||||
std::cout << argv[2];
|
||||
std::cerr << "fixture stderr";
|
||||
return 7;
|
||||
}
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
Reference in new issue
Block a user