@@ -2311,6 +2311,88 @@ gulp.task("lint-licenses", function (done) {
23112311 } ) ;
23122312} ) ;
23132313
2314+ gulp . task ( "lint-chmod" , function ( done ) {
2315+ console . log ( "\n### Checking executable bit on tracked files" ) ;
2316+
2317+ // Tracked files allowed to keep the executable bit (shebang scripts).
2318+ const EXECUTABLE_FILES = new Set ( [ "test/chromium/test-telemetry.js" ] ) ;
2319+
2320+ let lsFiles ;
2321+ try {
2322+ lsFiles = execSync ( "git ls-files -s" , { encoding : "utf8" } ) ;
2323+ } catch ( e ) {
2324+ done ( e ) ;
2325+ return ;
2326+ }
2327+
2328+ const offenders = [ ] ;
2329+ for ( const line of lsFiles . split ( "\n" ) ) {
2330+ if ( ! line ) {
2331+ continue ;
2332+ }
2333+ // "<mode> <sha> <stage>\t<path>"
2334+ const tab = line . indexOf ( "\t" ) ;
2335+ const mode = line . slice ( 0 , 6 ) ;
2336+ const file = line . slice ( tab + 1 ) ;
2337+ if ( mode === "100755" && ! EXECUTABLE_FILES . has ( file ) ) {
2338+ offenders . push ( file ) ;
2339+ }
2340+ }
2341+
2342+ if ( offenders . length === 0 ) {
2343+ console . log ( "files checked, no errors found" ) ;
2344+ done ( ) ;
2345+ return ;
2346+ }
2347+
2348+ // --fix here also stages the chmod (via `git update-index`), which is
2349+ // unlike every other --fix in `gulp lint` (those only touch the working
2350+ // tree). Restrict the auto-fix to a direct `gulp lint-chmod --fix`
2351+ // invocation so the umbrella `gulp lint --fix` never silently stages.
2352+ const fix =
2353+ process . argv . includes ( "lint-chmod" ) && process . argv . includes ( "--fix" ) ;
2354+ if ( ! fix ) {
2355+ for ( const file of offenders . sort ( ) ) {
2356+ console . log ( ` Unexpected executable bit: ${ file } ` ) ;
2357+ }
2358+ done (
2359+ new Error (
2360+ "Executable-bit check failed (run `gulp lint-chmod --fix` to clear, then commit)."
2361+ )
2362+ ) ;
2363+ return ;
2364+ }
2365+
2366+ // Drop the bit on disk too: `git update-index --chmod=-x` only edits the
2367+ // index, so a later `git add` would re-read the still-executable working
2368+ // tree and undo the staged chmod.
2369+ for ( const file of offenders ) {
2370+ try {
2371+ const { mode } = fs . statSync ( file ) ;
2372+ fs . chmodSync ( file , mode & ~ 0o111 ) ;
2373+ } catch ( e ) {
2374+ done ( e ) ;
2375+ return ;
2376+ }
2377+ }
2378+
2379+ // Chunk the path list so we stay well under ARG_MAX.
2380+ const CHUNK = 256 ;
2381+ for ( let i = 0 ; i < offenders . length ; i += CHUNK ) {
2382+ const result = spawnSync (
2383+ "git" ,
2384+ [ "update-index" , "--chmod=-x" , "--" , ...offenders . slice ( i , i + CHUNK ) ] ,
2385+ { stdio : "inherit" }
2386+ ) ;
2387+ if ( result . status !== 0 ) {
2388+ done ( new Error ( "git update-index failed." ) ) ;
2389+ return ;
2390+ }
2391+ }
2392+ console . log ( `cleared executable bit on ${ offenders . length } file(s)` ) ;
2393+ done ( ) ;
2394+ } ) ;
2395+
23142396gulp . task ( "lint" , function ( done ) {
23152397 console . log ( "\n### Linting JS/CSS/JSON/SVG/HTML files" ) ;
23162398
@@ -2375,7 +2457,7 @@ gulp.task("lint", function (done) {
23752457 return ;
23762458 }
23772459
2378- gulp . task ( "lint-licenses" ) ( done ) ;
2460+ gulp . series ( "lint-licenses" , "lint-chmod ") ( done ) ;
23792461 } ) ;
23802462} ) ;
23812463
0 commit comments