(repoRoot, rel = "", seen = new Set())
| 577 | // an ancestor from looping forever and stops a linked subtree being scanned |
| 578 | // twice. |
| 579 | function walkTextFiles(repoRoot, rel = "", seen = new Set()) { |
| 580 | const abs = path.join(repoRoot, rel); |
| 581 | try { |
| 582 | const real = fs.realpathSync(abs); |
| 583 | if (seen.has(real)) return []; |
| 584 | seen.add(real); |
| 585 | } catch (e) { |
| 586 | return []; |
| 587 | } |
| 588 | const out = []; |
| 589 | let entries; |
| 590 | try { |
| 591 | entries = fs.readdirSync(abs, { withFileTypes: true }); |
| 592 | } catch (e) { |
| 593 | return out; |
| 594 | } |
| 595 | // Sorted so annotations come out in a stable order run to run. |
| 596 | entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); |
| 597 | for (const entry of entries) { |
| 598 | const childRel = rel ? `${rel}/${entry.name}` : entry.name; |
| 599 | let isDir = entry.isDirectory(); |
| 600 | let isFile = entry.isFile(); |
| 601 | if (!isDir && !isFile) { |
| 602 | if (!entry.isSymbolicLink()) continue; // socket, FIFO, device |
| 603 | try { |
| 604 | const stat = fs.statSync(path.join(abs, entry.name)); // follows the link |
| 605 | isDir = stat.isDirectory(); |
| 606 | isFile = stat.isFile(); |
| 607 | } catch (e) { |
| 608 | continue; // dangling link |
| 609 | } |
| 610 | } |
| 611 | if (isDir) { |
| 612 | if (SCAN_SKIP_DIRS.has(entry.name)) continue; |
| 613 | out.push(...walkTextFiles(repoRoot, childRel, seen)); |
| 614 | } else if (isFile) { |
| 615 | out.push(childRel); |
| 616 | } |
| 617 | } |
| 618 | return out; |
| 619 | } |
| 620 | |
| 621 | function probeKind(repoRoot, repoPath) { |
| 622 | try { |
no outgoing calls