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
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,21 @@ fuori [OPTIONS]
| `-V`, `--version` | Show version |
| `-v`, `--verbose` | Show progress during export |
| `-o`, `--output <path>` | Output path (`-` for stdout) |
| `--no-clobber` | Fail if output already exists |
| `--allow-sensitive` | Export files even if they match sensitive-file protection rules |
| `--no-git` | Force filesystem selection |
| `--from-stdin` | Read paths from stdin |
| `--staged` | Export staged files |
| `--unstaged` | Export unstaged tracked files |
| `--diff <range>` | Export files changed in a diff range |
| `--from-stdin` | Read paths from stdin |
| `-0`, `--null` | Use NUL as the stdin delimiter (requires `--from-stdin`) |
| `--line-numbers` | Prefix exported code lines with line numbers |
| `--tree` / `--no-tree` | Include/omit project tree (default: on) |
| `--tree-depth <n>` | Limit tree render depth |
| `--line-numbers` | Prefix exported code lines with line numbers |
| `-s <size_kb>` | Max file size in KB (default: 100) |
| `--warn-tokens <n>` | Warn above token threshold (default: 200k) |
| `--max-tokens <n>` | Hard-fail above token threshold |
| `--no-clobber` | Fail if output already exists |
| `--no-git` | Force filesystem selection |
| `--no-default-ignore` | Disable built-in default ignore patterns in filesystem mode |
| `--allow-sensitive` | Export files even if they match sensitive-file protection rules |

Git selection flags (`--staged`, `--unstaged`, `--diff`) and `--from-stdin` are mutually exclusive; `--no-git` cannot be combined with them.

Expand All @@ -123,12 +125,13 @@ fuori -s 50 # 50 KB file size cap
fuori --warn-tokens 100000 # Earlier token warning
fuori --max-tokens 270000 # Hard token budget
fuori -o out.md --no-clobber # Refuse to overwrite
fuori --no-git --no-default-ignore # Disable built-in filesystem ignore defaults
fuori --allow-sensitive # Export files that secret protection would skip
```

## Ignore Rules

Place a `.gitignore` file in the working directory to exclude files and patterns from the export.
Filesystem mode always honors a local `.gitignore` file when present.
These rules apply in `--no-git` mode and during automatic fallback outside Git repositories.

Supported syntax:
Expand All @@ -139,7 +142,7 @@ Supported syntax:
- Root-anchored patterns (`/pattern`)
- Recursive globs (`**/node_modules/`, `**/*.pyc`)

In filesystem mode, `fuori` also applies a built-in default ignore list when no `.gitignore` is present:
In filesystem mode, `fuori` also applies a built-in default ignore list unless `--no-default-ignore` is set:

| Category | Patterns |
|---|---|
Expand All @@ -149,7 +152,9 @@ In filesystem mode, `fuori` also applies a built-in default ignore list when no
| Compiled artifacts | `*.o`, `*.a`, `*.so`, `*.exe`, `*.dll` |
| Environment / OS | `.env`, `.DS_Store`, `*.log` |

To export paths that match the default list, use `--from-stdin`.
Use `--no-default-ignore` to disable only the built-in defaults. Local `.gitignore` rules still apply.

To bypass ignore-based selection entirely, use `--from-stdin`.

## File Size Limit

Expand Down
11 changes: 8 additions & 3 deletions src/ignore.c
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,10 @@ int ignored_directory_may_have_included_descendants(const char* dirpath,
return 0;
}

int load_ignore_patterns(const char* ignore_file, IgnorePattern** patterns, size_t* count) {
int load_ignore_patterns(const char* ignore_file,
int include_default_ignores,
IgnorePattern** patterns,
size_t* count) {
const char* default_ignores[] = {
".git/",
"node_modules/",
Expand All @@ -393,8 +396,10 @@ int load_ignore_patterns(const char* ignore_file, IgnorePattern** patterns, size
};

size_t default_count = 0;
while (default_ignores[default_count] != NULL) {
default_count++;
if (include_default_ignores) {
while (default_ignores[default_count] != NULL) {
default_count++;
}
}

size_t capacity = default_count + 16;
Expand Down
5 changes: 4 additions & 1 deletion src/ignore.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ int resolve_ignore_state(const char* filepath,
int ignored_directory_may_have_included_descendants(const char* dirpath,
const IgnorePattern* patterns,
size_t count);
int load_ignore_patterns(const char* ignore_file, IgnorePattern** patterns, size_t* count);
int load_ignore_patterns(const char* ignore_file,
int include_default_ignores,
IgnorePattern** patterns,
size_t* count);
void free_ignore_patterns(IgnorePattern* patterns, size_t count);

#endif
5 changes: 4 additions & 1 deletion src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,10 @@ int main(int argc, char* argv[]) {
ctx.output_path = options.output_path;

if (options.resolved_mode == FILE_SELECTION_RECURSIVE) {
if (load_ignore_patterns(IGNORE_FILE, &ctx.ignore_patterns, &ctx.ignore_count) != 0) {
if (load_ignore_patterns(IGNORE_FILE,
!options.no_default_ignore,
&ctx.ignore_patterns,
&ctx.ignore_count) != 0) {
fprintf(stderr, "Error: Failed to initialize ignore patterns.\n");
goto cleanup;
}
Expand Down
13 changes: 8 additions & 5 deletions src/options.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,24 @@ void print_usage(const char* argv0) {
printf(" -V, --version Show version information\n");
printf(" -v, --verbose Show progress information\n");
printf(" -o, --output Set output path (use '-' for stdout)\n");
printf(" --no-clobber Fail if output file already exists\n");
printf(" --allow-sensitive Export files even if they match sensitive-file protection rules\n");
printf(" --no-git Force recursive filesystem selection instead of auto Git detection\n");
printf(" --from-stdin Read paths from stdin instead of using Git or filesystem selection\n");
printf(" --staged Export staged files from the current Git subtree\n");
printf(" --unstaged Export unstaged tracked files from the current Git subtree\n");
printf(" --diff <r> Export files changed by a git diff range (for example main...HEAD)\n");
printf(" --from-stdin Read paths from stdin instead of using Git or filesystem selection\n");
printf(" --from-stdin, --staged, --unstaged, and --diff are mutually exclusive\n");
printf(" -0, --null Use NUL as the input record delimiter instead of newline (requires --from-stdin)\n");
printf(" --line-numbers Prefix exported code lines with line numbers\n");
printf(" --tree Include a directory tree section (default)\n");
printf(" --no-tree Omit the directory tree section\n");
printf(" --tree-depth Limit tree rendering depth to N levels\n");
printf(" --line-numbers Prefix exported code lines with line numbers\n");
printf(" -s <size_kb> Set maximum file size limit in KB (default: 100)\n");
printf(" --warn-tokens Warn if estimated tokens exceed N (default: %d)\n",
DEFAULT_WARN_TOKENS);
printf(" --max-tokens Fail if estimated tokens exceed N\n");
printf(" --no-clobber Fail if output file already exists\n");
printf(" --no-git Force recursive filesystem selection instead of auto Git detection\n");
printf(" --no-default-ignore Disable built-in default ignore patterns in filesystem mode\n");
printf(" --allow-sensitive Export files even if they match sensitive-file protection rules\n");
}

int parse_cli_options(int argc, char* argv[], CliOptions* options) {
Expand Down Expand Up @@ -130,6 +131,8 @@ int parse_cli_options(int argc, char* argv[], CliOptions* options) {
options->output_is_stdout = (strcmp(options->output_path, "-") == 0);
} else if (strcmp(argv[i], "--no-git") == 0) {
force_no_git = 1;
} else if (strcmp(argv[i], "--no-default-ignore") == 0) {
options->no_default_ignore = 1;
} else if (strcmp(argv[i], "--from-stdin") == 0) {
if (options->requested_mode != FILE_SELECTION_AUTO) {
print_selection_mode_conflict();
Expand Down
1 change: 1 addition & 0 deletions src/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ typedef struct {
int stdin_null_delim;
int show_tree;
int show_line_numbers;
int no_default_ignore;
int allow_sensitive;
size_t max_file_size;
size_t tree_depth;
Expand Down
26 changes: 26 additions & 0 deletions tests/test_cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ trap 'rm -rf "$TMPDIR"' EXIT INT TERM
(cd "$BIN_DIR" && "$BIN" --help >"$TMPDIR/help_stdout.txt" 2>"$TMPDIR/help_stderr.txt")
assert_contains "$TMPDIR/help_stdout.txt" "--allow-sensitive"
assert_contains "$TMPDIR/help_stdout.txt" "--line-numbers"
assert_contains "$TMPDIR/help_stdout.txt" "--no-default-ignore"
assert_file_equals "$TMPDIR/help_stderr.txt" ""

OUTSIDE="$TMPDIR/outside"
Expand Down Expand Up @@ -292,6 +293,31 @@ EOF_IGNORE_KEEP
(cd "$IGNORE_NEGATION_DIR" && "$BIN" --no-git -o - >ignore_negation_stdout.txt 2>ignore_negation_stderr.txt)
assert_not_contains "$IGNORE_NEGATION_DIR/ignore_negation_stdout.txt" "build/keep\\.txt"

NO_DEFAULT_IGNORE_DIR="$TMPDIR/no_default_ignore"
mkdir -p "$NO_DEFAULT_IGNORE_DIR/build"
cat >"$NO_DEFAULT_IGNORE_DIR/.gitignore" <<'EOF_NO_DEFAULT_IGNORE'
local.txt
EOF_NO_DEFAULT_IGNORE
cat >"$NO_DEFAULT_IGNORE_DIR/main.c" <<'EOF_NO_DEFAULT_MAIN'
int main(void) { return 0; }
EOF_NO_DEFAULT_MAIN
cat >"$NO_DEFAULT_IGNORE_DIR/build/keep.txt" <<'EOF_NO_DEFAULT_BUILD'
built artifact but intentionally exported
EOF_NO_DEFAULT_BUILD
cat >"$NO_DEFAULT_IGNORE_DIR/local.txt" <<'EOF_NO_DEFAULT_LOCAL'
still ignored by local rule
EOF_NO_DEFAULT_LOCAL

(cd "$NO_DEFAULT_IGNORE_DIR" && "$BIN" --no-git --no-tree -o - >default_ignore_stdout.txt 2>default_ignore_stderr.txt)
assert_contains "$NO_DEFAULT_IGNORE_DIR/default_ignore_stdout.txt" "## main.c"
assert_not_contains "$NO_DEFAULT_IGNORE_DIR/default_ignore_stdout.txt" "build/keep.txt"
assert_not_contains "$NO_DEFAULT_IGNORE_DIR/default_ignore_stdout.txt" "## local.txt"

(cd "$NO_DEFAULT_IGNORE_DIR" && "$BIN" --no-git --no-default-ignore --no-tree -o - >no_default_ignore_stdout.txt 2>no_default_ignore_stderr.txt)
assert_contains "$NO_DEFAULT_IGNORE_DIR/no_default_ignore_stdout.txt" "## main.c"
assert_contains "$NO_DEFAULT_IGNORE_DIR/no_default_ignore_stdout.txt" "## build/keep.txt"
assert_not_contains "$NO_DEFAULT_IGNORE_DIR/no_default_ignore_stdout.txt" "## local.txt"

STDIN_DIR="$TMPDIR/stdin_selection"
mkdir -p "$STDIN_DIR/ignored"
cat >"$STDIN_DIR/.gitignore" <<'EOF_STDIN_IGNORE'
Expand Down
2 changes: 1 addition & 1 deletion tests/test_ignore.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ static int run_case(const IgnoreCase* test_case) {
return 1;
}

if (load_ignore_patterns(template, &patterns, &count) != 0) {
if (load_ignore_patterns(template, 0, &patterns, &count) != 0) {
perror("load_ignore_patterns");
unlink(template);
return 1;
Expand Down