Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abe3d227e2 | ||
|
|
a9372a9742 | ||
|
|
01fdf16213 |
@@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Cross-platform (node) port of git_ignore_find2merge.sh / .bat
|
||||||
|
//
|
||||||
|
// Search [ROOTPATH], merge [GLOBALPATH/.gitignore_global] and [ROOTPATH/<dir>/.gitignore.local.txt]
|
||||||
|
// into [ROOTPATH/<dir>/.gitignore], for every <dir> that has a .gitignore.
|
||||||
|
// (Judged from .gitignore, not from .git — some git repos keep privacy and hide .git.)
|
||||||
|
//
|
||||||
|
// usage:
|
||||||
|
// node git_ignore_find2merge.js [rootPath] [globalPath] [y|other]
|
||||||
|
// rootPath start of tree search, must be a directory (default: prompt, blank = current dir)
|
||||||
|
// globalPath path to .gitignore_global, or its parent dir, or an http(s) URL
|
||||||
|
// (default: https://git.tic.cc/opx/sysconfig/raw/branch/main/nixhome/.gitignore_global)
|
||||||
|
// yesNo 'y' to write, anything else = dry-run (list candidates without writing)
|
||||||
|
//
|
||||||
|
// node git_ignore_find2merge.js # interactive, dry-run
|
||||||
|
// node git_ignore_find2merge.js ~/Shared y
|
||||||
|
// node git_ignore_find2merge.js /srv/projects /srv/proj/nixhome y
|
||||||
|
//
|
||||||
|
// Same fixes as seafile_ignore_find2merge.js:
|
||||||
|
// - blank globalPath falls back to the default URL, as in the .sh
|
||||||
|
// - ask() treats stdin EOF as blank, so non-interactive pipes don't hang
|
||||||
|
// - the default global is a URL: fetched with fetch() at 30s timeout + one retry
|
||||||
|
// (the .sh's `cat $GLOBALPATH` branch never ran for URLs — it went to the curl branch)
|
||||||
|
// - the root dir itself is processed too (the .sh's find -name '[^.]*' never matched the root)
|
||||||
|
// - non-'y' is a dry-run that still lists the candidate dirs (the .sh just exited silently)
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const os = require('os')
|
||||||
|
const path = require('path')
|
||||||
|
const readline = require('readline')
|
||||||
|
|
||||||
|
const DEFAULT_GLOBAL_URL = 'https://git.tic.cc/opx/sysconfig/raw/branch/main/nixhome/.gitignore_global'
|
||||||
|
const GLOBAL_FILE_NAME = '.gitignore_global'
|
||||||
|
// same skip list as: grep -E -v 'node_modules|uni_modules|\.deploy_git|\.git|.svn|\.vscode|\.wrangler|unpackage|_webroot|_logstore|_datasotre|_archive|_filestore|_ssl'
|
||||||
|
const SKIP_DIR_PATTERN = /node_modules|uni_modules|\.deploy_git|\.git|\.svn|\.vscode|\.wrangler|unpackage|_webroot|_logstore|_datasotre|_archive|_filestore|_ssl/
|
||||||
|
const MAX_DEPTH = 3 // relative to ROOTPATH, matching find -mindepth 0 -maxdepth 3
|
||||||
|
|
||||||
|
function resolveHome (p) {
|
||||||
|
if (p === '~') return os.homedir()
|
||||||
|
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2))
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ask (question) {
|
||||||
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
||||||
|
const answer = await new Promise(resolve => {
|
||||||
|
let settled = false
|
||||||
|
const finish = value => { if (!settled) { settled = true; resolve(value) } }
|
||||||
|
rl.question(question, a => finish(a))
|
||||||
|
rl.on('close', () => finish('')) // stdin ended without input (EOF): treat as blank, like the .sh's `read`
|
||||||
|
})
|
||||||
|
rl.close()
|
||||||
|
return answer.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// find . -mindepth 0 -maxdepth 3 -type d -name '[^.]*' (relative paths, root itself excluded)
|
||||||
|
function collectCandidateDirs (rootPath) {
|
||||||
|
const candidates = []
|
||||||
|
function walk (dir, depth) {
|
||||||
|
if (depth > MAX_DEPTH) return
|
||||||
|
let entries
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue
|
||||||
|
if (entry.name.startsWith('.')) continue // -name '[^.]*' excludes hidden dirs
|
||||||
|
const absDir = path.join(dir, entry.name)
|
||||||
|
const relDir = path.relative(rootPath, absDir)
|
||||||
|
if (relDir.split(path.sep).length > MAX_DEPTH) continue
|
||||||
|
candidates.push(relDir)
|
||||||
|
walk(absDir, depth + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(rootPath, 1)
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGlobalContent (globalPathInput) {
|
||||||
|
const globalPath = resolveHome(globalPathInput)
|
||||||
|
if (/^https?:\/\//i.test(globalPath)) {
|
||||||
|
console.log('√√√ GLOBALPATH (url) = [[' + globalPath + ']]')
|
||||||
|
// git.tic.cc's TCP connect can exceed node's default 10s connect timeout; allow 30s and retry once.
|
||||||
|
let lastError
|
||||||
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(globalPath, { signal: AbortSignal.timeout(30000) })
|
||||||
|
if (!response.ok) throw new Error('HTTP ' + response.status)
|
||||||
|
return await response.text()
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error
|
||||||
|
console.log('... fetch attempt ' + attempt + ' failed: ' + (error.cause?.code || error.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('××× Fetch failed: ' + (lastError.cause?.code || lastError.message) + ' for [[' + globalPath + ']]. Exit now...')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
let globalFile = globalPath
|
||||||
|
if (fs.existsSync(globalPath) && fs.statSync(globalPath).isDirectory()) {
|
||||||
|
globalFile = path.join(globalPath, GLOBAL_FILE_NAME)
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(globalFile)) {
|
||||||
|
console.log('××× Not found [[' + globalFile + ']]. Exit now...')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
console.log('√√√ GLOBALPATH = [[' + globalFile + ']]')
|
||||||
|
return fs.readFileSync(globalFile, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main () {
|
||||||
|
console.log('')
|
||||||
|
console.log('Search in [ROOTPATH], merge [GLOBALPATH/.gitignore_global] and [ROOTPATH/*/.gitignore.local.txt] files to .gitignore')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// --- ROOTPATH ---
|
||||||
|
let rootPath = process.argv[2]
|
||||||
|
const rootIsDir = p => {
|
||||||
|
try { return fs.statSync(p).isDirectory() } catch { return false }
|
||||||
|
}
|
||||||
|
if (!(rootPath && rootIsDir(resolveHome(rootPath)))) {
|
||||||
|
console.log('::*** Enter [root path] or [leave blank] for default [[' + process.cwd() + ']] to start tree search for git repositories')
|
||||||
|
rootPath = await ask('***:: ')
|
||||||
|
if (!rootPath) rootPath = process.cwd()
|
||||||
|
rootPath = path.resolve(resolveHome(rootPath))
|
||||||
|
} else {
|
||||||
|
rootPath = path.resolve(resolveHome(rootPath))
|
||||||
|
}
|
||||||
|
if (!rootIsDir(rootPath)) {
|
||||||
|
console.log('××× [[' + rootPath + ']] not exist! Exit now. ***')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
console.log('√√√ ROOTPATH = [[' + rootPath + ']]')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// --- GLOBALPATH ---
|
||||||
|
let globalPathInput = process.argv[3]
|
||||||
|
if (!globalPathInput) {
|
||||||
|
console.log('::*** Enter [path to .gitignore_global] or [leave blank] for default [[' + DEFAULT_GLOBAL_URL + ']]')
|
||||||
|
globalPathInput = await ask('***:: ')
|
||||||
|
if (!globalPathInput) globalPathInput = DEFAULT_GLOBAL_URL // blank → default URL, as in the .sh
|
||||||
|
}
|
||||||
|
const globalContent = await loadGlobalContent(globalPathInput)
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// --- y/N ---
|
||||||
|
let yesNo = process.argv[4]
|
||||||
|
if (!yesNo) {
|
||||||
|
console.log('::*** Enter [y] to start updating, or [anything else] for dry-run')
|
||||||
|
yesNo = await ask('***:: ')
|
||||||
|
}
|
||||||
|
const willWrite = yesNo === 'y'
|
||||||
|
|
||||||
|
console.log('*** Starting from [[' + rootPath + ']] ***')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// --- search + merge ---
|
||||||
|
// A dir is a candidate iff it has a .gitignore (judged from .gitignore, not from .git).
|
||||||
|
const candidateDirs = collectCandidateDirs(rootPath).sort()
|
||||||
|
// The root dir itself is also a candidate: the old .sh's find -name '[^.]*' never matched the root.
|
||||||
|
if (fs.existsSync(path.join(rootPath, '.gitignore'))) candidateDirs.unshift('.')
|
||||||
|
|
||||||
|
let updatedCount = 0
|
||||||
|
for (const relDir of candidateDirs) {
|
||||||
|
const absDir = relDir === '.' ? rootPath : path.join(rootPath, relDir)
|
||||||
|
if (relDir !== '.' && SKIP_DIR_PATTERN.test(relDir)) continue
|
||||||
|
const targetFile = path.join(absDir, '.gitignore')
|
||||||
|
const localFile = path.join(absDir, '.gitignore.local.txt')
|
||||||
|
if (!fs.existsSync(targetFile)) continue
|
||||||
|
console.log('---- updating [[' + relDir + '/.gitignore]] ----')
|
||||||
|
if (willWrite) {
|
||||||
|
let merged = globalContent
|
||||||
|
if (fs.existsSync(localFile)) merged += fs.readFileSync(localFile, 'utf8')
|
||||||
|
fs.writeFileSync(targetFile, merged)
|
||||||
|
updatedCount++
|
||||||
|
}
|
||||||
|
console.log('')
|
||||||
|
}
|
||||||
|
if (willWrite) {
|
||||||
|
console.log('*** done: ' + updatedCount + ' file(s) updated ***')
|
||||||
|
} else {
|
||||||
|
console.log('*** dry-run: no file written (enter y to update) ***')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -94,6 +94,8 @@ OneDrive/Icon
|
|||||||
*/.dart_tool/
|
*/.dart_tool/
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
*/.flutter-plugins-dependencies
|
*/.flutter-plugins-dependencies
|
||||||
|
build/
|
||||||
|
*/build/
|
||||||
|
|
||||||
# ---- iOS / macOS:CocoaPods 检出目录(Podfile.lock 仍提交共享,只忽略 Pods/ 实体)----
|
# ---- iOS / macOS:CocoaPods 检出目录(Podfile.lock 仍提交共享,只忽略 Pods/ 实体)----
|
||||||
Pods/
|
Pods/
|
||||||
|
|||||||
+3
-1
@@ -87,13 +87,15 @@ OneDrive/Icon
|
|||||||
# Flutter/Dart 机器本地产物(各端由 `flutter pub get` / `pod install` / 构建在本机重生成,不跨机同步)。
|
# Flutter/Dart 机器本地产物(各端由 `flutter pub get` / `pod install` / 构建在本机重生成,不跨机同步)。
|
||||||
# 双机开发时 .dart_tool/package_config.json 内是绝对路径(file:///Users/<用户>/.pub-cache/...),同步过去会让另一台机器编译报 No such file or directory。
|
# 双机开发时 .dart_tool/package_config.json 内是绝对路径(file:///Users/<用户>/.pub-cache/...),同步过去会让另一台机器编译报 No such file or directory。
|
||||||
# 注:pub 缓存在 ~/.pub-cache,位于资料库根之外,seafile 同步不到,无需在此列出。
|
# 注:pub 缓存在 ~/.pub-cache,位于资料库根之外,seafile 同步不到,无需在此列出。
|
||||||
# 注:build/ 未全局忽略——tic-www-vue / cox-user-vue / cox-team-vue 等把 build/ 纳入 git 跟踪,全局忽略会波及;按需放各项目 seafile-ignore.local.txt。
|
# 注:build/ 未全局忽略,以防波及用了该常见词的目录;按需放各项目 seafile-ignore.local.txt。
|
||||||
|
|
||||||
# ---- Dart / Flutter:包解析 + 构建产物(核心,换机必跑 `flutter pub get` 重生成)----
|
# ---- Dart / Flutter:包解析 + 构建产物(核心,换机必跑 `flutter pub get` 重生成)----
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
*/.dart_tool/
|
*/.dart_tool/
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
*/.flutter-plugins-dependencies
|
*/.flutter-plugins-dependencies
|
||||||
|
build/
|
||||||
|
*/build/
|
||||||
|
|
||||||
# ---- iOS / macOS:CocoaPods 检出目录(Podfile.lock 仍提交共享,只忽略 Pods/ 实体)----
|
# ---- iOS / macOS:CocoaPods 检出目录(Podfile.lock 仍提交共享,只忽略 Pods/ 实体)----
|
||||||
Pods/
|
Pods/
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
// - the root dir itself is processed too (the .sh's find -name '[^.]*' never matched the root dir)
|
// - the root dir itself is processed too (the .sh's find -name '[^.]*' never matched the root dir)
|
||||||
// - a dir is updated iff it has seafile-ignore.txt OR seafile-ignore.local.txt (the .sh's `.git` check was dropped)
|
// - a dir is updated iff it has seafile-ignore.txt OR seafile-ignore.local.txt (the .sh's `.git` check was dropped)
|
||||||
// - tree search is max depth 3, skipping hidden dirs and the known build/dep dirs
|
// - tree search is max depth 3, skipping hidden dirs and the known build/dep dirs
|
||||||
|
// - blank globalPath falls back to the default URL, as in the .sh (first draft missed this → "Not found [[]]")
|
||||||
// - the .sh does `cat $GLOBALPATH` which fails when the default is a URL; here http(s) is fetched with fetch(),
|
// - the .sh does `cat $GLOBALPATH` which fails when the default is a URL; here http(s) is fetched with fetch(),
|
||||||
// a local file / parent-dir keeps the original behavior.
|
// a local file / parent-dir keeps the original behavior.
|
||||||
|
|
||||||
@@ -41,7 +42,12 @@ function resolveHome (p) {
|
|||||||
|
|
||||||
async function ask (question) {
|
async function ask (question) {
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
||||||
const answer = await new Promise(resolve => rl.question(question, resolve))
|
const answer = await new Promise(resolve => {
|
||||||
|
let settled = false
|
||||||
|
const finish = value => { if (!settled) { settled = true; resolve(value) } }
|
||||||
|
rl.question(question, a => finish(a))
|
||||||
|
rl.on('close', () => finish('')) // stdin ended without input (EOF): treat as blank, like the .sh's `read`
|
||||||
|
})
|
||||||
rl.close()
|
rl.close()
|
||||||
return answer.trim()
|
return answer.trim()
|
||||||
}
|
}
|
||||||
@@ -75,12 +81,20 @@ async function loadGlobalContent (globalPathInput) {
|
|||||||
const globalPath = resolveHome(globalPathInput)
|
const globalPath = resolveHome(globalPathInput)
|
||||||
if (/^https?:\/\//i.test(globalPath)) {
|
if (/^https?:\/\//i.test(globalPath)) {
|
||||||
console.log('√√√ GLOBALPATH (url) = [[' + globalPath + ']]')
|
console.log('√√√ GLOBALPATH (url) = [[' + globalPath + ']]')
|
||||||
const response = await fetch(globalPath)
|
// git.tic.cc's TCP connect can exceed node's default 10s connect timeout; allow 30s and retry once.
|
||||||
if (!response.ok) {
|
let lastError
|
||||||
console.log('××× Fetch failed: HTTP ' + response.status + ' for [[' + globalPath + ']]. Exit now...')
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||||
process.exit(1)
|
try {
|
||||||
|
const response = await fetch(globalPath, { signal: AbortSignal.timeout(30000) })
|
||||||
|
if (!response.ok) throw new Error('HTTP ' + response.status)
|
||||||
|
return await response.text()
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error
|
||||||
|
console.log('... fetch attempt ' + attempt + ' failed: ' + (error.cause?.code || error.message))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return response.text()
|
console.log('××× Fetch failed: ' + (lastError.cause?.code || lastError.message) + ' for [[' + globalPath + ']]. Exit now...')
|
||||||
|
process.exit(1)
|
||||||
}
|
}
|
||||||
let globalFile = globalPath
|
let globalFile = globalPath
|
||||||
if (fs.existsSync(globalPath) && fs.statSync(globalPath).isDirectory()) {
|
if (fs.existsSync(globalPath) && fs.statSync(globalPath).isDirectory()) {
|
||||||
@@ -119,6 +133,7 @@ async function main () {
|
|||||||
if (!globalPathInput) {
|
if (!globalPathInput) {
|
||||||
console.log('::*** Enter [path to seafile-ignore.global.txt] or [leave blank] for default [[' + DEFAULT_GLOBAL_URL + ']]')
|
console.log('::*** Enter [path to seafile-ignore.global.txt] or [leave blank] for default [[' + DEFAULT_GLOBAL_URL + ']]')
|
||||||
globalPathInput = await ask('***:: ')
|
globalPathInput = await ask('***:: ')
|
||||||
|
if (!globalPathInput) globalPathInput = DEFAULT_GLOBAL_URL // blank → default URL, as in the .sh
|
||||||
}
|
}
|
||||||
const globalContent = await loadGlobalContent(globalPathInput)
|
const globalContent = await loadGlobalContent(globalPathInput)
|
||||||
console.log('')
|
console.log('')
|
||||||
|
|||||||
Reference in New Issue
Block a user