11import { execUtils , semverUtils } from '@yarnpkg/core' ;
2- import { npath } from '@yarnpkg/fslib' ;
2+ import { npath , ppath , xfs } from '@yarnpkg/fslib' ;
33import { tests } from 'pkg-tests-core' ;
44
55const TESTED_URLS = {
@@ -21,6 +21,110 @@ const TESTED_URLS = {
2121 [ `https://github.com/yarnpkg/util-deprecate.git#b3562c2798507869edb767da869cd7b85487726d` ] : { version : `1.0.0` , runOnCI : true } ,
2222} ;
2323
24+ const makeCloneMetricsWrapper = ( { realGitPath, metricsDir} : { realGitPath : string , metricsDir : string } ) => `
25+ #!/usr/bin/env node
26+ const fs = require('fs');
27+ const path = require('path');
28+ const {spawn} = require('child_process');
29+
30+ const realGitPath = ${ JSON . stringify ( realGitPath ) } ;
31+ const metricsDir = ${ JSON . stringify ( metricsDir ) } ;
32+ const stateFile = path.join(metricsDir, 'state');
33+ const lockDir = path.join(metricsDir, 'lock');
34+
35+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
36+
37+ async function withLock(fn) {
38+ while (true) {
39+ try {
40+ await fs.promises.mkdir(lockDir);
41+ break;
42+ } catch (error) {
43+ if (error && error.code === 'EEXIST') {
44+ await sleep(10);
45+ continue;
46+ }
47+
48+ throw error;
49+ }
50+ }
51+
52+ try {
53+ return await fn();
54+ } finally {
55+ await fs.promises.rmdir(lockDir);
56+ }
57+ }
58+
59+ async function readState() {
60+ const content = await fs.promises.readFile(stateFile, 'utf8');
61+ const [currentLine = '0', maxLine = '0'] = content.trim().split(/\\r\\n|\\r|\\n/);
62+
63+ return {
64+ current: Number(currentLine),
65+ max: Number(maxLine),
66+ };
67+ }
68+
69+ async function writeState(current, max) {
70+ await fs.promises.writeFile(stateFile, \`\${current}\\n\${max}\\n\`);
71+ }
72+
73+ async function incrementCounter() {
74+ await withLock(async () => {
75+ const {current, max} = await readState();
76+ const nextCurrent = current + 1;
77+ const nextMax = Math.max(max, nextCurrent);
78+
79+ await writeState(nextCurrent, nextMax);
80+ });
81+ }
82+
83+ async function decrementCounter() {
84+ await withLock(async () => {
85+ const {current, max} = await readState();
86+ await writeState(current - 1, max);
87+ });
88+ }
89+
90+ function runGit(args) {
91+ return new Promise((resolve, reject) => {
92+ const child = spawn(realGitPath, args, {stdio: 'inherit'});
93+
94+ child.on('error', reject);
95+ child.on('exit', code => {
96+ resolve(typeof code === 'number' ? code : 1);
97+ });
98+ });
99+ }
100+
101+ async function main() {
102+ const args = process.argv.slice(2);
103+
104+ if (args[0] === 'clone') {
105+ await incrementCounter();
106+ await sleep(200);
107+
108+ const exitCode = await (async () => {
109+ try {
110+ return await runGit(args);
111+ } finally {
112+ await decrementCounter();
113+ }
114+ })();
115+
116+ process.exit(exitCode);
117+ }
118+
119+ process.exit(await runGit(args));
120+ }
121+
122+ main().catch(error => {
123+ console.error(error);
124+ process.exit(1);
125+ });
126+ ` . trimStart ( ) ;
127+
24128describe ( `Protocols` , ( ) => {
25129 describe ( `git:` , ( ) => {
26130 for ( const [ url , { version, runOnCI} ] of Object . entries ( TESTED_URLS ) ) {
@@ -156,6 +260,51 @@ describe(`Protocols`, () => {
156260 ) ,
157261 ) ;
158262
263+ tests . testIf (
264+ ( ) => process . platform !== `win32` ,
265+ `it should respect cloneConcurrency when cloning git repositories` ,
266+ makeTemporaryEnv (
267+ {
268+ dependencies : {
269+ [ `pkg-a` ] : tests . startPackageServer ( ) . then ( url => `${ url } /repositories/deep-projects.git#cwd=projects/pkg-a` ) ,
270+ [ `pkg-b` ] : tests . startPackageServer ( ) . then ( url => `${ url } /repositories/deep-projects.git#cwd=projects/pkg-b` ) ,
271+ [ `lib-a` ] : tests . startPackageServer ( ) . then ( url => `${ url } /repositories/deep-projects.git#cwd=projects/pkg-a&workspace=lib` ) ,
272+ [ `lib-b` ] : tests . startPackageServer ( ) . then ( url => `${ url } /repositories/deep-projects.git#cwd=projects/pkg-b&workspace=lib` ) ,
273+ } ,
274+ } ,
275+ {
276+ cloneConcurrency : 1 ,
277+ } ,
278+ async ( { path, run, source} ) => {
279+ const { stdout : gitPathStdout } = await execUtils . execvp ( `which` , [ `git` ] , { cwd : path } ) ;
280+ const realGitPath = gitPathStdout . trim ( ) ;
281+
282+ const binDir = ppath . join ( path , `bin` ) ;
283+ const metricsDir = ppath . join ( path , `.clone-metrics` ) ;
284+ const stateFile = ppath . join ( metricsDir , `state` ) ;
285+ const wrapperPath = ppath . join ( binDir , `git` ) ;
286+
287+ await xfs . mkdirPromise ( binDir , { recursive : true } ) ;
288+ await xfs . mkdirPromise ( metricsDir , { recursive : true } ) ;
289+ await xfs . writeFilePromise ( stateFile , `0\n0\n` ) ;
290+ await xfs . writeFilePromise ( wrapperPath , makeCloneMetricsWrapper ( {
291+ realGitPath,
292+ metricsDir : npath . fromPortablePath ( metricsDir ) ,
293+ } ) ) ;
294+
295+ await xfs . chmodPromise ( wrapperPath , 0o755 ) ;
296+
297+ await run ( `install` ) ;
298+
299+ const [ currentLine , maxLine ] = ( await xfs . readFilePromise ( stateFile , `utf8` ) ) . trim ( ) . split ( / \r \n | \r | \n / ) ;
300+
301+ expect ( Number ( currentLine ) ) . toBe ( 0 ) ;
302+ expect ( Number ( maxLine ) ) . toBeGreaterThan ( 0 ) ;
303+ expect ( Number ( maxLine ) ) . toBeLessThanOrEqual ( 1 ) ;
304+ } ,
305+ ) ,
306+ ) ;
307+
159308 test (
160309 `it should use Yarn Classic to setup classic repositories` ,
161310 makeTemporaryEnv (
0 commit comments