rlmgrep 0.1.13__tar.gz → 0.1.18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rlmgrep
3
- Version: 0.1.13
3
+ Version: 0.1.18
4
4
  Summary: Grep-shaped CLI search powered by DSPy RLM
5
5
  Author: rlmgrep
6
6
  License: MIT
@@ -128,7 +128,7 @@ rg -l "token" . | rlmgrep --files-from-stdin --answer "What does this token cont
128
128
  ## Input selection
129
129
 
130
130
  - Directories are searched recursively by default. Use `--no-recursive` to stop recursion.
131
- - Hidden files and `.gitignore` rules are respected by default. Use `--hidden` or `--no-ignore` to include them.
131
+ - Hidden files and ignore files (`.gitignore`, `.ignore`, `.rgignore`) are respected by default. Use `--hidden` or `--no-ignore` to include them.
132
132
  - `--type` uses built-in type mappings (e.g., `py`, `js`, `md`); unknown values are treated as file extensions.
133
133
  - `-g/--glob` matches path globs against normalized paths (forward slashes).
134
134
  - Paths are printed relative to the current working directory when possible.
@@ -115,7 +115,7 @@ rg -l "token" . | rlmgrep --files-from-stdin --answer "What does this token cont
115
115
  ## Input selection
116
116
 
117
117
  - Directories are searched recursively by default. Use `--no-recursive` to stop recursion.
118
- - Hidden files and `.gitignore` rules are respected by default. Use `--hidden` or `--no-ignore` to include them.
118
+ - Hidden files and ignore files (`.gitignore`, `.ignore`, `.rgignore`) are respected by default. Use `--hidden` or `--no-ignore` to include them.
119
119
  - `--type` uses built-in type mappings (e.g., `py`, `js`, `md`); unknown values are treated as file extensions.
120
120
  - `-g/--glob` matches path globs against normalized paths (forward slashes).
121
121
  - Paths are printed relative to the current working directory when possible.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rlmgrep"
3
- version = "0.1.13"
3
+ version = "0.1.18"
4
4
  description = "Grep-shaped CLI search powered by DSPy RLM"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.1.13"
2
+ __version__ = "0.1.18"
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  import argparse
4
4
  import os
5
5
  import sys
6
+ import shutil
7
+ import subprocess
6
8
  from pathlib import Path
7
9
 
8
10
  import dspy
@@ -11,7 +13,7 @@ from .config import ensure_default_config, load_config
11
13
  from .file_map import build_file_map
12
14
  from .ingest import (
13
15
  FileRecord,
14
- build_gitignore_spec,
16
+ build_ignore_spec,
15
17
  collect_candidates,
16
18
  load_files,
17
19
  resolve_type_exts,
@@ -88,7 +90,12 @@ def _parse_args(argv: list[str]) -> argparse.Namespace:
88
90
  parser.add_argument("-m", dest="max_count", type=int, default=None, help="Max matching lines per file")
89
91
  parser.add_argument("-a", "--text", dest="binary_as_text", action="store_true", help="Search binary files as text")
90
92
  parser.add_argument("--hidden", action="store_true", help="Include hidden files and directories")
91
- parser.add_argument("--no-ignore", dest="no_ignore", action="store_true", help="Do not respect .gitignore")
93
+ parser.add_argument(
94
+ "--no-ignore",
95
+ dest="no_ignore",
96
+ action="store_true",
97
+ help="Do not respect ignore files (.gitignore/.ignore/.rgignore)",
98
+ )
92
99
  parser.add_argument("--answer", action="store_true", help="Print a narrative answer before grep output")
93
100
  parser.add_argument("-y", "--yes", action="store_true", help="Skip file count confirmation")
94
101
  parser.add_argument(
@@ -147,11 +154,65 @@ def _pick(cli_value, config: dict, key: str, default=None):
147
154
  return default
148
155
 
149
156
 
150
- def _find_git_root(start: Path) -> Path | None:
157
+ def _find_git_root(start: Path) -> tuple[Path | None, Path | None]:
151
158
  for p in [start, *start.parents]:
152
- if (p / ".git").is_dir():
153
- return p
154
- return None
159
+ git_path = p / ".git"
160
+ if git_path.is_dir():
161
+ return p, git_path
162
+ if git_path.is_file():
163
+ try:
164
+ raw = git_path.read_text(encoding="utf-8", errors="ignore").strip()
165
+ except Exception:
166
+ raw = ""
167
+ if raw.startswith("gitdir:"):
168
+ git_dir = raw.split(":", 1)[1].strip()
169
+ git_dir_path = Path(git_dir)
170
+ if not git_dir_path.is_absolute():
171
+ git_dir_path = (p / git_dir_path).resolve()
172
+ return p, git_dir_path
173
+ return p, None
174
+ return None, None
175
+
176
+
177
+ def _global_ignore_paths(cwd: Path | None = None) -> list[Path]:
178
+ paths: list[Path] = []
179
+ cwd = cwd or Path.cwd()
180
+ if shutil.which("git"):
181
+ try:
182
+ result = subprocess.run(
183
+ ["git", "config", "--get", "--path", "core.excludesfile"],
184
+ cwd=cwd,
185
+ capture_output=True,
186
+ text=True,
187
+ check=False,
188
+ )
189
+ value = (result.stdout or "").strip()
190
+ except Exception:
191
+ value = ""
192
+ if value:
193
+ candidate = Path(value).expanduser()
194
+ if candidate.exists():
195
+ paths.append(candidate)
196
+
197
+ xdg_config = os.getenv("XDG_CONFIG_HOME")
198
+ if xdg_config:
199
+ default_path = Path(xdg_config) / "git" / "ignore"
200
+ else:
201
+ default_path = Path.home() / ".config" / "git" / "ignore"
202
+ if default_path.exists():
203
+ paths.append(default_path)
204
+
205
+ legacy = Path.home() / ".gitignore_global"
206
+ if legacy.exists():
207
+ paths.append(legacy)
208
+
209
+ seen: set[Path] = set()
210
+ unique: list[Path] = []
211
+ for p in paths:
212
+ if p not in seen:
213
+ seen.add(p)
214
+ unique.append(p)
215
+ return unique
155
216
 
156
217
 
157
218
  def _env_value(name: str) -> str | None:
@@ -442,8 +503,13 @@ def main(argv: list[str] | None = None) -> int:
442
503
  ignore_spec = None
443
504
  ignore_root = None
444
505
  if not args.no_ignore:
445
- ignore_root = _find_git_root(cwd) or cwd
446
- ignore_spec = build_gitignore_spec(ignore_root)
506
+ git_root, git_dir = _find_git_root(cwd)
507
+ ignore_root = git_root or cwd
508
+ extra_ignores: list[Path] = []
509
+ if git_dir is not None:
510
+ extra_ignores.append(git_dir / "info" / "exclude")
511
+ extra_ignores.extend(_global_ignore_paths(ignore_root))
512
+ ignore_spec = build_ignore_spec(ignore_root, extra_paths=extra_ignores)
447
513
 
448
514
  candidates = collect_candidates(
449
515
  input_paths,
@@ -164,18 +164,28 @@ def collect_files(paths: Iterable[str], recursive: bool = True) -> list[Path]:
164
164
  return files
165
165
 
166
166
 
167
- def build_gitignore_spec(root: Path) -> "pathspec.PathSpec | None":
168
- if pathspec is None:
169
- return None
167
+ IGNORE_FILENAMES = {".gitignore", ".ignore", ".rgignore"}
168
+
169
+
170
+ def build_ignore_spec(
171
+ root: Path, extra_paths: Iterable[Path] | None = None
172
+ ) -> "pathspec.PathSpec | None":
170
173
  root = root.resolve()
171
- gitignore_paths: list[Path] = []
174
+ ignore_paths: list[Path] = []
175
+ extra_paths = list(extra_paths or [])
176
+
172
177
  for dirpath, dirnames, filenames in os.walk(root):
173
178
  if ".git" in dirnames:
174
179
  dirnames.remove(".git")
175
- if ".gitignore" in filenames:
176
- gitignore_paths.append(Path(dirpath) / ".gitignore")
180
+ for name in filenames:
181
+ if name in IGNORE_FILENAMES:
182
+ ignore_paths.append(Path(dirpath) / name)
183
+
184
+ for extra in extra_paths:
185
+ if extra.exists():
186
+ ignore_paths.append(extra)
177
187
 
178
- if not gitignore_paths:
188
+ if not ignore_paths:
179
189
  return None
180
190
 
181
191
  def _sort_key(p: Path) -> tuple[int, str]:
@@ -186,14 +196,16 @@ def build_gitignore_spec(root: Path) -> "pathspec.PathSpec | None":
186
196
  except ValueError:
187
197
  return 0, p.as_posix()
188
198
 
189
- gitignore_paths.sort(key=_sort_key)
199
+ ignore_paths.sort(key=_sort_key)
190
200
 
191
201
  patterns: list[str] = []
192
- for gi in gitignore_paths:
202
+ for gi in ignore_paths:
193
203
  try:
194
204
  rel_dir = gi.parent.relative_to(root).as_posix()
195
205
  except ValueError:
196
206
  rel_dir = ""
207
+ if rel_dir in {".", ""}:
208
+ rel_dir = ""
197
209
  try:
198
210
  raw_lines = gi.read_text(encoding="utf-8", errors="ignore").splitlines()
199
211
  except Exception:
@@ -202,17 +214,38 @@ def build_gitignore_spec(root: Path) -> "pathspec.PathSpec | None":
202
214
  line = raw.rstrip("\n")
203
215
  if not line:
204
216
  continue
217
+ escaped = False
205
218
  if line.startswith("\\#") or line.startswith("\\!"):
206
219
  line = line[1:]
207
- elif line.startswith("#"):
220
+ escaped = True
221
+ if not escaped and line.startswith("#"):
208
222
  continue
209
- negated = line.startswith("!")
210
- if negated:
223
+
224
+ negated = False
225
+ if not escaped and line.startswith("!"):
226
+ negated = True
211
227
  line = line[1:]
228
+ if not line:
229
+ continue
230
+
231
+ anchored = False
212
232
  if line.startswith("/"):
233
+ anchored = True
213
234
  line = line[1:]
235
+ if not line:
236
+ continue
237
+
214
238
  if rel_dir:
215
- line = f"{rel_dir}/{line}"
239
+ if anchored:
240
+ line = f"{rel_dir}/{line}"
241
+ elif "/" in line:
242
+ line = f"{rel_dir}/{line}"
243
+ else:
244
+ line = f"{rel_dir}/**/{line}"
245
+ else:
246
+ if anchored:
247
+ line = f"/{line}"
248
+
216
249
  if negated:
217
250
  line = "!" + line
218
251
  patterns.append(line)
@@ -314,6 +347,12 @@ def collect_candidates(
314
347
  ) -> list[Path]:
315
348
  files = collect_files(paths, recursive=recursive)
316
349
  explicit_files: set[Path] = set()
350
+ ignore_root_resolved: Path | None = None
351
+ if ignore_root is not None:
352
+ try:
353
+ ignore_root_resolved = ignore_root.resolve()
354
+ except Exception:
355
+ ignore_root_resolved = ignore_root
317
356
  for raw in paths:
318
357
  p = Path(raw)
319
358
  if p.exists() and p.is_file():
@@ -330,9 +369,9 @@ def collect_candidates(
330
369
  except ValueError:
331
370
  key = fp.as_posix()
332
371
 
333
- if ignore_spec is not None and ignore_root is not None and not is_explicit:
372
+ if ignore_spec is not None and ignore_root_resolved is not None and not is_explicit:
334
373
  try:
335
- rel = fp.relative_to(ignore_root).as_posix()
374
+ rel = fp_resolved.relative_to(ignore_root_resolved).as_posix()
336
375
  except ValueError:
337
376
  rel = None
338
377
  if rel and ignore_spec.match_file(rel):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rlmgrep
3
- Version: 0.1.13
3
+ Version: 0.1.18
4
4
  Summary: Grep-shaped CLI search powered by DSPy RLM
5
5
  Author: rlmgrep
6
6
  License: MIT
@@ -128,7 +128,7 @@ rg -l "token" . | rlmgrep --files-from-stdin --answer "What does this token cont
128
128
  ## Input selection
129
129
 
130
130
  - Directories are searched recursively by default. Use `--no-recursive` to stop recursion.
131
- - Hidden files and `.gitignore` rules are respected by default. Use `--hidden` or `--no-ignore` to include them.
131
+ - Hidden files and ignore files (`.gitignore`, `.ignore`, `.rgignore`) are respected by default. Use `--hidden` or `--no-ignore` to include them.
132
132
  - `--type` uses built-in type mappings (e.g., `py`, `js`, `md`); unknown values are treated as file extensions.
133
133
  - `-g/--glob` matches path globs against normalized paths (forward slashes).
134
134
  - Paths are printed relative to the current working directory when possible.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes