Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,7 @@ cdidx report --output report.tgz --json
| `--exclude-path <glob>` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect` | Exclude glob-style path patterns. `*` and `?` are wildcards (repeatable) |
| `--exclude-tests` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect` | Exclude likely test files and prefer production code |
| `--include-generated` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `deps`, `impact`, `unused`, `hotspots` | Include files detected as generated code; generated files are excluded from query results by default |
| `--workspace-db <path>` | `deps` | Add another CodeIndex database to the file-dependency query. Repeat it to aggregate multiple workspace member DBs; JSON edges include `source_db` and `target_db` so same relative paths can be disambiguated. |
| `--snippet-lines <n>` | `search` | Search snippet length for human-readable output and JSON/MCP snippets (default: 8, max: 20) |
| `--snippet-focus <leftmost\|quality\|proximity>` | `search` | Choose how long search-result lines pick the visible focus when clamped. `quality` (default) prefers full-query matches and strong tokens; `proximity` favors dense multi-token clusters; `leftmost` keeps legacy earliest-match behavior. |
| `--max-line-width <n>` | `search`, `references`, `find`, `excerpt`, `inspect` | Clamp very long single-line snippet/reference/excerpt payloads around the relevant match (`0` disables clamping; default: 512, max: 4096) |
Expand Down Expand Up @@ -2807,6 +2808,7 @@ cdidx report --output report.tgz --json
| `--since <datetime>` | `search`, `definition`, `symbols`, `files` | 指定タイムスタンプ以降に変更されたファイルのみ(ISO 8601)。オフセットなしの値(例: `2024-01-01T00:00:00`)は UTC として解釈されるため、どのタイムゾーンから呼び出しても同じ UTC 時点になります。明示したい場合は末尾に `Z` または `+09:00` 等のオフセットを付与してください。 |
| `--no-dedup` | `search` | オーバーラップチャンク重複排除を無効化 |
| `--reverse` | `deps` | 逆引き: 指定パスに依存しているファイルを表示 |
| `--workspace-db <path>` | `deps` | file dependency query に別の CodeIndex DB を追加する。複数 member DB を集約する場合は繰り返し指定でき、JSON edge には同じ相対パスを区別できるよう `source_db` / `target_db` が含まれる。 |
| `--top <n>` | クエリ系 | `--limit` のエイリアス |
| `--color <when>` | 全コマンド | ANSI カラー出力の制御。`auto`(既定)、`always`、`never` を受け付ける。優先順位: `--color` フラグ > `CLICOLOR_FORCE` > `NO_COLOR` > `CLICOLOR=0` > 端末能力の自動判定。auto では redirected stdout と StringWriter 風のテスト capture を非 ANSI とみなし、Windows では ConPTY / Windows Terminal の virtual-terminal 対応と `WT_SESSION`、`WT_PROFILE_ID`、`TERM_PROGRAM`、非 `dumb` の `TERM` などの端末ヒントも見る。`cdidx symbols Foo \| less -R` のような pager pipe でも色を維持したい場合は `--color=always`、TTY 上でも ANSI を抑止したい場合は `--color=never`(または `NO_COLOR=1`)を指定する。 |
| `--palette <name>` | 全コマンド | カラー出力が有効なときに用いる ANSI パレットを選択する。`basic`(標準8色 SGR 30–37、最小 SSH/CI 端末向けの既定フォールバック)、`256`(256色 `\x1b[38;5;Nm`)、`truecolor`(24ビット RGB `\x1b[38;2;R;G;Bm`)を受け付ける。優先順位: `--palette` フラグ > `CDIDX_COLOR_PALETTE` 環境変数 > `COLORTERM` / `TERM` 自動判定。`basic` パレットは最小端末で読みにくい `\x1b[90m`(暗灰 / dim)を避ける。 |
Expand Down
18 changes: 18 additions & 0 deletions changelog.d/unreleased/1704.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
category: added
issues:
- 1704
affected:
- src/CodeIndex/Cli/QueryCommandRunner.cs
- src/CodeIndex/Cli/CliFlagSchema.cs
- src/CodeIndex/Models/QueryResults.cs
- USER_GUIDE.md
---

## English

- **`deps` can aggregate explicit workspace member databases (#1704)** — `deps --workspace-db <path>` can be repeated to include dependency edges from additional CodeIndex databases, and JSON edges now include `source_db` / `target_db` tags when workspace DB aggregation is active.

## 日本語

- **`deps` が明示指定した workspace member DB を集約できるようになりました (#1704)** — `deps --workspace-db <path>` を繰り返し指定して追加の CodeIndex DB から dependency edge を含められるようになり、workspace DB 集約時の JSON edge には `source_db` / `target_db` タグが含まれます。
3 changes: 3 additions & 0 deletions src/CodeIndex/Cli/CliFlagSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ internal static class CliFlagSchema
"validate", "deps", "impact", "unused", "hotspots", "db", "vacuum", "report", "batch", "mcp",
];

private static readonly string[] WorkspaceDbCommands = ["deps"];

private static readonly string[] DataDirCommands =
[
"index", "search", "definition", "references", "callers", "callees",
Expand Down Expand Up @@ -179,6 +181,7 @@ private static IReadOnlyList<CliFlag> BuildAll()
return new List<CliFlag>
{
new() { Name = "--db", ValuePlaceholder = "<path>", Description = "Database path", Commands = Set(DbPathCommands) },
new() { Name = "--workspace-db", ValuePlaceholder = "<path>", Description = "Additional workspace member database path for dependency aggregation", Commands = Set(WorkspaceDbCommands) },
new() { Name = "--data-dir", ValuePlaceholder = "<dir>", Description = "Directory containing codeindex.db; overrides CDIDX_DATA_DIR/XDG/workspace defaults", Commands = Set(DataDirCommands) },
new() { Name = "--json", Description = "JSON output; search also accepts --json=array for a single JSON array", Commands = Set(JsonCommands) },
new() { Name = "--quiet", ShortName = "-q", Description = "Suppress informational stderr output; errors still print", Commands = Set(AllCommands.ToArray()) },
Expand Down
169 changes: 168 additions & 1 deletion src/CodeIndex/Cli/QueryCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2630,7 +2630,7 @@ public static int RunDeps(string[] cmdArgs, JsonSerializerOptions jsonOptions)
return WithDb(options, jsonOptions, reader =>
{
var reverse = cmdArgs.Any(a => a == "--reverse");
var results = reader.GetFileDependencies(options.Limit, options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, reverse);
var results = GetWorkspaceFileDependencies(reader, options, reverse);
var baseSqlGraphSignal = reader.GetSqlGraphContractSignal(options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests);
var sqlGraphSignal = results.Count == 0
? baseSqlGraphSignal
Expand Down Expand Up @@ -2678,6 +2678,163 @@ public static int RunDeps(string[] cmdArgs, JsonSerializerOptions jsonOptions)
});
}

private static List<FileDependencyResult> GetWorkspaceFileDependencies(DbReader primaryReader, QueryCommandOptions options, bool reverse)
{
var results = primaryReader.GetFileDependencies(options.Limit, options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, reverse);
if (options.WorkspaceDbPaths.Count == 0)
return results;

var primaryDb = Path.GetFullPath(DbPathResolver.NormalizeDbPath(options.DbPath));
var memberDbs = options.WorkspaceDbPaths
.Select(path => Path.GetFullPath(DbPathResolver.NormalizeDbPath(path)))
.Prepend(primaryDb)
.Distinct(StringComparer.Ordinal)
.ToList();
TagFileDependencyResults(results, primaryDb);
foreach (var normalizedDbPath in memberDbs.Skip(1))
{
using var db = new DbContext(normalizedDbPath);
db.TryMigrateForRead();
var reader = new DbReader(db) { IncludeGenerated = primaryReader.IncludeGenerated };
var memberResults = reader.GetFileDependencies(options.Limit, options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, reverse);
TagFileDependencyResults(memberResults, normalizedDbPath);
results.AddRange(memberResults);
}

foreach (var sourceDb in memberDbs)
foreach (var targetDb in memberDbs)
{
if (string.Equals(sourceDb, targetDb, StringComparison.Ordinal))
continue;
results.AddRange(GetCrossDatabaseFileDependencies(sourceDb, targetDb, options, reverse));
}

return results
.OrderByDescending(result => result.ReferenceCount)
.ThenBy(result => result.SourceDb, StringComparer.Ordinal)
.ThenBy(result => result.SourcePath, StringComparer.Ordinal)
.ThenBy(result => result.TargetDb, StringComparer.Ordinal)
.ThenBy(result => result.TargetPath, StringComparer.Ordinal)
.Take(options.Limit)
.ToList();
}

private static List<FileDependencyResult> GetCrossDatabaseFileDependencies(string sourceDbPath, string targetDbPath, QueryCommandOptions options, bool reverse)
{
var builder = new SqliteConnectionStringBuilder
{
DataSource = sourceDbPath,
Mode = SqliteOpenMode.ReadOnly,
};
using var connection = new SqliteConnection(builder.ConnectionString);
connection.Open();
using var attach = connection.CreateCommand();
attach.CommandText = "ATTACH DATABASE @targetDb AS targetdb";
attach.Parameters.AddWithValue("@targetDb", targetDbPath);
attach.ExecuteNonQuery();

using var cmd = connection.CreateCommand();
var sourcePathExpr = reverse ? "dst.path" : "src.path";
var targetPathExpr = reverse ? "src.path" : "dst.path";
cmd.CommandText = $@"
SELECT {sourcePathExpr} AS source_path,
{targetPathExpr} AS target_path,
COUNT(*) AS reference_count,
GROUP_CONCAT(DISTINCT r.symbol_name) AS symbols
FROM symbol_references r
JOIN files src ON src.id = r.file_id
JOIN targetdb.symbols s ON s.name = r.symbol_name
JOIN targetdb.files dst ON dst.id = s.file_id
WHERE 1 = 1";
if (options.Lang != null)
{
cmd.CommandText += " AND src.lang = @lang AND dst.lang = @lang";
cmd.Parameters.AddWithValue("@lang", options.Lang);
}
AddCrossDatabasePathFilters(cmd, "src", options.PathPatterns, include: !reverse);
AddCrossDatabasePathFilters(cmd, "dst", options.PathPatterns, include: reverse);
AddCrossDatabaseExcludeFilters(cmd, "src", options.ExcludePaths, include: !reverse);
AddCrossDatabaseExcludeFilters(cmd, "dst", options.ExcludePaths, include: reverse);
if (options.ExcludeTests)
cmd.CommandText += reverse
? " AND dst.path NOT LIKE '%test%' COLLATE NOCASE"
: " AND src.path NOT LIKE '%test%' COLLATE NOCASE";
cmd.CommandText += @"
GROUP BY source_path, target_path
ORDER BY reference_count DESC, source_path, target_path
LIMIT @limit";
cmd.Parameters.AddWithValue("@limit", options.Limit);

var results = new List<FileDependencyResult>();
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
results.Add(new FileDependencyResult
{
SourcePath = reader.GetString(0),
TargetPath = reader.GetString(1),
SourceDb = reverse ? targetDbPath : sourceDbPath,
TargetDb = reverse ? sourceDbPath : targetDbPath,
ReferenceCount = reader.GetInt32(2),
Symbols = reader.IsDBNull(3) ? string.Empty : reader.GetString(3),
});
}
return results;
}

private static void AddCrossDatabasePathFilters(SqliteCommand cmd, string alias, IReadOnlyList<string> patterns, bool include)
{
if (!include || patterns.Count == 0)
return;
var parts = new List<string>(patterns.Count);
for (var i = 0; i < patterns.Count; i++)
{
var name = $"@crossPath{alias}{i}";
parts.Add($"{alias}.path LIKE {name} ESCAPE '\\'");
cmd.Parameters.AddWithValue(name, CrossDatabaseGlobToLikePattern(patterns[i]));
}
cmd.CommandText += " AND (" + string.Join(" OR ", parts) + ")";
}

private static void AddCrossDatabaseExcludeFilters(SqliteCommand cmd, string alias, IReadOnlyList<string> patterns, bool include)
{
if (!include || patterns.Count == 0)
return;
for (var i = 0; i < patterns.Count; i++)
{
var name = $"@crossExclude{alias}{i}";
cmd.CommandText += $" AND {alias}.path NOT LIKE {name} ESCAPE '\\'";
cmd.Parameters.AddWithValue(name, CrossDatabaseGlobToLikePattern(patterns[i]));
}
}

private static string CrossDatabaseGlobToLikePattern(string pattern)
{
var builder = new System.Text.StringBuilder(pattern.Length);
foreach (var ch in pattern)
{
builder.Append(ch switch
{
'*' => '%',
'?' => '_',
'%' => "\\%",
'_' => "\\_",
'\\' => "\\\\",
_ => ch,
});
}
return builder.ToString();
}

private static void TagFileDependencyResults(IEnumerable<FileDependencyResult> results, string dbPath)
{
foreach (var result in results)
{
result.SourceDb = dbPath;
result.TargetDb = dbPath;
}
}

public static int RunHotspots(string[] cmdArgs, JsonSerializerOptions jsonOptions)
{
bool groupByName = cmdArgs.Any(a => a == "--group-by-name");
Expand Down Expand Up @@ -3516,6 +3673,7 @@ public static QueryCommandOptions ParseArgs(string[] args, bool jsonDefault, boo
bool contextAfterExplicit = false;
var pathPatterns = new List<string>();
var userPathPatterns = new List<string>();
var workspaceDbPaths = new List<string>();
var projectFilters = new List<string>();
string? solutionFilter = null;
var excludePaths = new List<string>();
Expand Down Expand Up @@ -3643,6 +3801,12 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
else
AddParseError(dbPathError!);
break;
case "--workspace-db":
if (TryReadStringOptionValue(args, ref i, "--workspace-db", inlineValue, allowSeparatedDashPrefixedLiteralValue: true, out var workspaceDbPath, out var workspaceDbError))
workspaceDbPaths.Add(workspaceDbPath!);
else
AddParseError(workspaceDbError!);
break;
case "--data-dir":
if (TryReadStringOptionValue(args, ref i, "--data-dir", inlineValue, allowSeparatedDashPrefixedLiteralValue: true, out var dataDirValue, out var dataDirError))
{
Expand Down Expand Up @@ -4128,6 +4292,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
SnippetFocus = snippetFocus,
MaxLineWidth = maxLineWidth,
PathPatterns = pathPatterns,
WorkspaceDbPaths = workspaceDbPaths,
ProjectFilters = projectFilters,
SolutionFilter = solutionFilter,
ExcludePaths = excludePaths,
Expand Down Expand Up @@ -6238,6 +6403,7 @@ private static void WriteSqlGraphContractWarningIfNeeded(bool json, SqlGraphCont
private static readonly Dictionary<string, string> MissingOptionValueHints = new(StringComparer.Ordinal)
{
["--db"] = "pass a path to a CodeIndex SQLite database, e.g. `--db .cdidx/codeindex.db` or `--db file:///absolute/path/to/codeindex.db?immutable=1`, or omit `--db` to use `.cdidx/codeindex.db`.",
["--workspace-db"] = "pass a path to another workspace member CodeIndex SQLite database. Repeat the flag to aggregate multiple member DBs.",
["--data-dir"] = "pass a directory where cdidx should store `codeindex.db`, e.g. `--data-dir /var/cache/cdidx`.",
["--limit"] = "pass a positive integer, e.g. `--limit 20` (default 20).",
["--top"] = "pass a positive integer, e.g. `--top 20` (alias for `--limit`, default 20).",
Expand Down Expand Up @@ -6573,6 +6739,7 @@ public sealed class QueryCommandOptions
public SearchSnippetFocusMode SnippetFocus { get; init; } = SearchSnippetFocusMode.Quality;
public int MaxLineWidth { get; init; } = LineWidthFormatter.DefaultMaxLineWidth;
public List<string> PathPatterns { get; init; } = [];
public List<string> WorkspaceDbPaths { get; init; } = [];
public List<string> ProjectFilters { get; init; } = [];
public string? SolutionFilter { get; init; }
public List<string> ExcludePaths { get; init; } = [];
Expand Down
4 changes: 4 additions & 0 deletions src/CodeIndex/Models/QueryResults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,10 @@ public class FileDependencyResult
{
public string SourcePath { get; set; } = string.Empty;
public string TargetPath { get; set; } = string.Empty;
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? SourceDb { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? TargetDb { get; set; }
public int ReferenceCount { get; set; }
public string Symbols { get; set; } = string.Empty;
}
Loading
Loading