| 10214 | } |
| 10215 | |
| 10216 | static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) { |
| 10217 | char *project = get_project_arg(args); |
| 10218 | char *base_branch = cbm_mcp_get_string_arg(args, "base_branch"); |
| 10219 | char *since = cbm_mcp_get_string_arg(args, "since"); |
| 10220 | char *scope = cbm_mcp_get_string_arg(args, "scope"); |
| 10221 | int depth = cbm_mcp_get_int_arg(args, "depth", MCP_DEFAULT_BFS_DEPTH); |
| 10222 | depth = clamp_mcp_depth(depth, "detect_changes"); |
| 10223 | |
| 10224 | /* scope: "files" = just changed files, "symbols" = files + symbols (default) */ |
| 10225 | bool want_symbols = !scope || strcmp(scope, "symbols") == 0 || strcmp(scope, "impact") == 0; |
| 10226 | |
| 10227 | /* `since` (e.g. "HEAD~10", "v0.5.0") is the documented diff base but was |
| 10228 | * previously parsed and never used: it takes precedence over base_branch. |
| 10229 | * Route it through base_branch so the shared shell-arg validation and the |
| 10230 | * existing `<base>...HEAD` (three-dot) diff apply unchanged — `since` thus |
| 10231 | * adopts the same merge-base semantics base_branch already uses. */ |
| 10232 | if (since && since[0]) { |
| 10233 | free(base_branch); |
| 10234 | base_branch = since; /* transfer ownership */ |
| 10235 | since = NULL; |
| 10236 | } |
| 10237 | free(since); /* no-op after the swap (since is NULL); frees it otherwise */ |
| 10238 | |
| 10239 | if (!base_branch) { |
| 10240 | base_branch = heap_strdup("main"); |
| 10241 | } |
| 10242 | |
| 10243 | /* Reject shell metacharacters, and a leading '-', in the user-supplied |
| 10244 | * branch name. base_branch is spliced into `git diff --name-only |
| 10245 | * "<base>"...HEAD`; a value starting with '-' would be read by git as an |
| 10246 | * option rather than a ref (e.g. `--output=<path>` writes the diff to an |
| 10247 | * arbitrary file). A real git ref never begins with '-'. */ |
| 10248 | if (!cbm_validate_shell_arg(base_branch) || base_branch[0] == '-' || |
| 10249 | !validate_windows_cmd_interpolation_arg(base_branch)) { |
| 10250 | free(project); |
| 10251 | free(base_branch); |
| 10252 | free(scope); |
| 10253 | return cbm_mcp_text_result("base_branch contains invalid characters", true); |
| 10254 | } |
| 10255 | |
| 10256 | char *root_path = get_project_root(srv, project); |
| 10257 | if (!root_path) { |
| 10258 | char *err = build_no_store_error(project); |
| 10259 | char *res = cbm_mcp_text_result(err, true); |
| 10260 | free(err); |
| 10261 | free(project); |
| 10262 | free(base_branch); |
| 10263 | free(scope); |
| 10264 | return res; |
| 10265 | } |
| 10266 | |
| 10267 | if (!validate_search_path_arg(root_path) || |
| 10268 | !validate_windows_cmd_interpolation_arg(root_path)) { |
| 10269 | free(root_path); |
| 10270 | free(project); |
| 10271 | free(base_branch); |
| 10272 | free(scope); |
| 10273 | return cbm_mcp_text_result("project path contains invalid characters", true); |
no test coverage detected