isomorphic-git 1.41.0 → 1.41.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,8 +5,8 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
6
6
 
7
7
  var ignore = _interopDefault(require('ignore'));
8
- var AsyncLock = _interopDefault(require('async-lock'));
9
8
  var Hash = _interopDefault(require('sha.js/sha1.js'));
9
+ var AsyncLock = _interopDefault(require('async-lock'));
10
10
  var crc32 = _interopDefault(require('crc-32'));
11
11
  var pako = _interopDefault(require('pako'));
12
12
  var crypto$1 = require('crypto');
@@ -1524,10 +1524,43 @@ function compareStats(entry, stats, filemode = true, trustino = true) {
1524
1524
  return staleness
1525
1525
  }
1526
1526
 
1527
- // import Lock from '../utils.js'
1527
+ // A single process-wide lock shared by everything that serializes file
1528
+ // operations (the index, refs, the shallow file, blob writes). It is keyed by
1529
+ // path, so unrelated resources never contend. Centralizing it here also means
1530
+ // there is exactly one place to reset it (see `_resetLock`).
1531
+ //
1532
+ // Created lazily (not at module load) so this module has no top-level side
1533
+ // effect and stays tree-shakeable.
1534
+ let lock;
1528
1535
 
1529
- // const lm = new LockManager()
1530
- let lock = null;
1536
+ function getLock() {
1537
+ if (lock === undefined) lock = new AsyncLock({ maxPending: Infinity });
1538
+ return lock
1539
+ }
1540
+
1541
+ /**
1542
+ * Run `callback` while holding the shared lock for `key`, then release it.
1543
+ *
1544
+ * @template T
1545
+ * @param {string | string[]} key - Resource key(s) to lock (usually a filepath).
1546
+ * @param {() => (Promise<T> | T)} callback
1547
+ * @returns {Promise<T>}
1548
+ */
1549
+ function acquireLock(key, callback) {
1550
+ return getLock().acquire(key, callback)
1551
+ }
1552
+
1553
+ /**
1554
+ * Discard the shared lock so the next `acquireLock` starts a fresh one. Intended
1555
+ * for test harnesses that run many independent scenarios inside one long-lived
1556
+ * module instance: if a scenario leaves an operation stalled (e.g. a filesystem
1557
+ * that hangs), its still-held lock would otherwise block every later scenario
1558
+ * that touches the same path. Resetting abandons that stalled queue so the next
1559
+ * scenario starts clean. It is a no-op for normal library use.
1560
+ */
1561
+ function _resetLock() {
1562
+ lock = undefined;
1563
+ }
1531
1564
 
1532
1565
  const IndexCache = Symbol('IndexCache');
1533
1566
 
@@ -1598,10 +1631,9 @@ class GitIndexManager {
1598
1631
  }
1599
1632
 
1600
1633
  const filepath = `${gitdir}/index`;
1601
- if (lock === null) lock = new AsyncLock({ maxPending: Infinity });
1602
1634
  let result;
1603
1635
  let unmergedPaths = [];
1604
- await lock.acquire(filepath, async () => {
1636
+ await acquireLock(filepath, async () => {
1605
1637
  // Acquire a file lock while we're reading the index
1606
1638
  // to make sure other processes aren't writing to it
1607
1639
  // simultaneously, which could result in a corrupted index.
@@ -1874,13 +1906,6 @@ function assertWritableRef(ref) {
1874
1906
  }
1875
1907
  }
1876
1908
 
1877
- let lock$1;
1878
-
1879
- async function acquireLock(ref, callback) {
1880
- if (lock$1 === undefined) lock$1 = new AsyncLock();
1881
- return lock$1.acquire(ref, callback)
1882
- }
1883
-
1884
1909
  /**
1885
1910
  * A class for managing Git references, including reading, writing, deleting, and resolving refs.
1886
1911
  */
@@ -2485,21 +2510,76 @@ async function collect(iterable) {
2485
2510
  return result
2486
2511
  }
2487
2512
 
2513
+ // The `@` that ends the credentials is the last one in the authority, and the
2514
+ // scheme is matched case-insensitively: `HTTPS://user:pass@host/x` is a URL
2515
+ // like any other, and leaving the credentials in it is what `fetch` rejects.
2516
+ // The authority ends at the first `/`, `?` or `#`, so an `@` that lives in a
2517
+ // query or a fragment is left where it is.
2518
+ const CREDENTIALS = /^(https?:\/\/)([^/?#]+)@/i;
2519
+
2488
2520
  function extractAuthFromUrl(url) {
2489
2521
  // For whatever reason, the `fetch` API does not convert credentials embedded in the URL
2490
2522
  // into Basic Authentication headers automatically. Instead it throws an error!
2491
2523
  // So we must manually parse the URL, rip out the user:password portion if it is present
2492
2524
  // and compute the Authorization header.
2493
- // Note: I tried using new URL(url) but that throws a security exception in Edge. :rolleyes:
2494
- let userpass = url.match(/^https?:\/\/([^/]+)@/);
2525
+ const credentials = url.match(CREDENTIALS);
2495
2526
  // No credentials, return the url unmodified and an empty auth object
2496
- if (userpass == null) return { url, auth: {} }
2497
- userpass = userpass[1];
2498
- const [username, password] = userpass.split(':');
2499
- // Remove credentials from URL
2500
- url = url.replace(`${userpass}@`, '');
2501
- // Has credentials, return the fetch-safe URL and the parsed credentials
2502
- return { url, auth: { username, password } }
2527
+ if (credentials == null) return { url, auth: {} }
2528
+ // Has credentials, return the fetch-safe URL and the parsed credentials.
2529
+ // The credentials are cut out of the original string rather than read back
2530
+ // off the parsed URL: serializing that one would also drop an explicit
2531
+ // `:443` and append a slash to a URL that has no path, and this return value
2532
+ // is what the request goes out against.
2533
+ return {
2534
+ url: url.replace(CREDENTIALS, '$1'),
2535
+ auth: parseUserinfo(url, credentials[2]),
2536
+ }
2537
+ }
2538
+
2539
+ function parseUserinfo(url, userpass) {
2540
+ // `URL` splits the two halves the way the platform does, so the first colon
2541
+ // separates them and any later one stays in the password. Only the split is
2542
+ // taken from it, for the reason above.
2543
+ const parsed = parseUrl(url);
2544
+ // A password is absent, not empty, when no colon was written at all. `URL`
2545
+ // reports `''` for both, and the difference is worth keeping.
2546
+ const hasPassword = userpass.indexOf(':') !== -1;
2547
+
2548
+ if (parsed !== null) {
2549
+ return {
2550
+ username: decodeUserinfo(parsed.username),
2551
+ password: hasPassword ? decodeUserinfo(parsed.password) : undefined,
2552
+ }
2553
+ }
2554
+
2555
+ // No `URL` to parse with, or it rejected a string the expression above
2556
+ // accepted. Split on the first colon by hand, without the percent-decoding.
2557
+ const separatorIndex = userpass.indexOf(':');
2558
+ return {
2559
+ username: hasPassword ? userpass.slice(0, separatorIndex) : userpass,
2560
+ password: hasPassword ? userpass.slice(separatorIndex + 1) : undefined,
2561
+ }
2562
+ }
2563
+
2564
+ function parseUrl(url) {
2565
+ if (typeof URL !== 'function') return null
2566
+ try {
2567
+ return new URL(url)
2568
+ } catch (_) {
2569
+ return null
2570
+ }
2571
+ }
2572
+
2573
+ // `URL` hands back the userinfo still percent-encoded, so `p:ss` arrives as
2574
+ // `p%3Ass` and has to be decoded before it reaches the Authorization header. A
2575
+ // lone `%` is not something the parser encodes, and it makes the decode throw,
2576
+ // so the raw value stands in whenever the decode fails.
2577
+ function decodeUserinfo(value) {
2578
+ try {
2579
+ return decodeURIComponent(value)
2580
+ } catch (_) {
2581
+ return value
2582
+ }
2503
2583
  }
2504
2584
 
2505
2585
  class EmptyServerResponseError extends BaseError {
@@ -3191,8 +3271,6 @@ function parseRemoteUrl({ url }) {
3191
3271
  }
3192
3272
  }
3193
3273
 
3194
- let lock$2 = null;
3195
-
3196
3274
  class GitShallowManager {
3197
3275
  /**
3198
3276
  * Reads the `shallow` file in the Git repository and returns a set of object IDs (OIDs).
@@ -3203,10 +3281,9 @@ class GitShallowManager {
3203
3281
  * @returns {Promise<Set<string>>} - A set of shallow object IDs.
3204
3282
  */
3205
3283
  static async read({ fs, gitdir }) {
3206
- if (lock$2 === null) lock$2 = new AsyncLock();
3207
3284
  const filepath = join(gitdir, 'shallow');
3208
3285
  const oids = new Set();
3209
- await lock$2.acquire(filepath, async function () {
3286
+ await acquireLock(filepath, async function () {
3210
3287
  const text = await fs.read(filepath, { encoding: 'utf8' });
3211
3288
  if (text === null) return oids // no file
3212
3289
  if (text.trim() === '') return oids // empty file
@@ -3229,18 +3306,17 @@ class GitShallowManager {
3229
3306
  * @returns {Promise<void>}
3230
3307
  */
3231
3308
  static async write({ fs, gitdir, oids }) {
3232
- if (lock$2 === null) lock$2 = new AsyncLock();
3233
3309
  const filepath = join(gitdir, 'shallow');
3234
3310
  if (oids.size > 0) {
3235
3311
  const text = [...oids].join('\n') + '\n';
3236
- await lock$2.acquire(filepath, async function () {
3312
+ await acquireLock(filepath, async function () {
3237
3313
  await fs.write(filepath, text, {
3238
3314
  encoding: 'utf8',
3239
3315
  });
3240
3316
  });
3241
3317
  } else {
3242
3318
  // No shallows
3243
- await lock$2.acquire(filepath, async function () {
3319
+ await acquireLock(filepath, async function () {
3244
3320
  await fs.rm(filepath);
3245
3321
  });
3246
3322
  }
@@ -5958,12 +6034,6 @@ const _TreeMap = {
5958
6034
  workdir: WORKDIR,
5959
6035
  };
5960
6036
 
5961
- let lock$3;
5962
- async function acquireLock$1(ref, callback) {
5963
- if (lock$3 === undefined) lock$3 = new AsyncLock();
5964
- return lock$3.acquire(ref, callback)
5965
- }
5966
-
5967
6037
  // make sure filepath, blob type and blob object (from loose objects) plus oid are in sync and valid
5968
6038
  async function checkAndWriteBlob(fs, gitdir, dir, filepath, oid = null) {
5969
6039
  const currentFilepath = join(dir, filepath);
@@ -5980,7 +6050,7 @@ async function checkAndWriteBlob(fs, gitdir, dir, filepath, oid = null) {
5980
6050
  : undefined;
5981
6051
  let retOid = objContent ? oid : undefined;
5982
6052
  if (!objContent) {
5983
- await acquireLock$1(currentFilepath, async () => {
6053
+ await acquireLock(currentFilepath, async () => {
5984
6054
  const object = stats.isSymbolicLink()
5985
6055
  ? await fs.readlink(currentFilepath).then(posixifyPathBuffer)
5986
6056
  : await fs.read(currentFilepath);
@@ -6195,7 +6265,7 @@ async function applyTreeChanges({
6195
6265
  });
6196
6266
 
6197
6267
  // apply the changes to work dir
6198
- await acquireLock$1(gitdir, async () => {
6268
+ await acquireLock(gitdir, async () => {
6199
6269
  for (const op of ops) {
6200
6270
  const currentFilepath = join(dir, op.filepath);
6201
6271
  switch (op.method) {
@@ -6426,7 +6496,7 @@ class GitStashManager {
6426
6496
  );
6427
6497
  const filepath = this.refLogsStashPath;
6428
6498
 
6429
- await acquireLock$1(filepath, async () => {
6499
+ await acquireLock(filepath, async () => {
6430
6500
  const appendTo = (await this.fs.exists(filepath))
6431
6501
  ? await this.fs.read(filepath, 'utf8')
6432
6502
  : '';
package/managers/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import ignore from 'ignore';
2
- import AsyncLock from 'async-lock';
3
2
  import Hash from 'sha.js/sha1.js';
3
+ import AsyncLock from 'async-lock';
4
4
  import crc32 from 'crc-32';
5
5
  import pako from 'pako';
6
6
 
@@ -1517,10 +1517,43 @@ function compareStats(entry, stats, filemode = true, trustino = true) {
1517
1517
  return staleness
1518
1518
  }
1519
1519
 
1520
- // import Lock from '../utils.js'
1520
+ // A single process-wide lock shared by everything that serializes file
1521
+ // operations (the index, refs, the shallow file, blob writes). It is keyed by
1522
+ // path, so unrelated resources never contend. Centralizing it here also means
1523
+ // there is exactly one place to reset it (see `_resetLock`).
1524
+ //
1525
+ // Created lazily (not at module load) so this module has no top-level side
1526
+ // effect and stays tree-shakeable.
1527
+ let lock;
1521
1528
 
1522
- // const lm = new LockManager()
1523
- let lock = null;
1529
+ function getLock() {
1530
+ if (lock === undefined) lock = new AsyncLock({ maxPending: Infinity });
1531
+ return lock
1532
+ }
1533
+
1534
+ /**
1535
+ * Run `callback` while holding the shared lock for `key`, then release it.
1536
+ *
1537
+ * @template T
1538
+ * @param {string | string[]} key - Resource key(s) to lock (usually a filepath).
1539
+ * @param {() => (Promise<T> | T)} callback
1540
+ * @returns {Promise<T>}
1541
+ */
1542
+ function acquireLock(key, callback) {
1543
+ return getLock().acquire(key, callback)
1544
+ }
1545
+
1546
+ /**
1547
+ * Discard the shared lock so the next `acquireLock` starts a fresh one. Intended
1548
+ * for test harnesses that run many independent scenarios inside one long-lived
1549
+ * module instance: if a scenario leaves an operation stalled (e.g. a filesystem
1550
+ * that hangs), its still-held lock would otherwise block every later scenario
1551
+ * that touches the same path. Resetting abandons that stalled queue so the next
1552
+ * scenario starts clean. It is a no-op for normal library use.
1553
+ */
1554
+ function _resetLock() {
1555
+ lock = undefined;
1556
+ }
1524
1557
 
1525
1558
  const IndexCache = Symbol('IndexCache');
1526
1559
 
@@ -1591,10 +1624,9 @@ class GitIndexManager {
1591
1624
  }
1592
1625
 
1593
1626
  const filepath = `${gitdir}/index`;
1594
- if (lock === null) lock = new AsyncLock({ maxPending: Infinity });
1595
1627
  let result;
1596
1628
  let unmergedPaths = [];
1597
- await lock.acquire(filepath, async () => {
1629
+ await acquireLock(filepath, async () => {
1598
1630
  // Acquire a file lock while we're reading the index
1599
1631
  // to make sure other processes aren't writing to it
1600
1632
  // simultaneously, which could result in a corrupted index.
@@ -1867,13 +1899,6 @@ function assertWritableRef(ref) {
1867
1899
  }
1868
1900
  }
1869
1901
 
1870
- let lock$1;
1871
-
1872
- async function acquireLock(ref, callback) {
1873
- if (lock$1 === undefined) lock$1 = new AsyncLock();
1874
- return lock$1.acquire(ref, callback)
1875
- }
1876
-
1877
1902
  /**
1878
1903
  * A class for managing Git references, including reading, writing, deleting, and resolving refs.
1879
1904
  */
@@ -2478,21 +2503,76 @@ async function collect(iterable) {
2478
2503
  return result
2479
2504
  }
2480
2505
 
2506
+ // The `@` that ends the credentials is the last one in the authority, and the
2507
+ // scheme is matched case-insensitively: `HTTPS://user:pass@host/x` is a URL
2508
+ // like any other, and leaving the credentials in it is what `fetch` rejects.
2509
+ // The authority ends at the first `/`, `?` or `#`, so an `@` that lives in a
2510
+ // query or a fragment is left where it is.
2511
+ const CREDENTIALS = /^(https?:\/\/)([^/?#]+)@/i;
2512
+
2481
2513
  function extractAuthFromUrl(url) {
2482
2514
  // For whatever reason, the `fetch` API does not convert credentials embedded in the URL
2483
2515
  // into Basic Authentication headers automatically. Instead it throws an error!
2484
2516
  // So we must manually parse the URL, rip out the user:password portion if it is present
2485
2517
  // and compute the Authorization header.
2486
- // Note: I tried using new URL(url) but that throws a security exception in Edge. :rolleyes:
2487
- let userpass = url.match(/^https?:\/\/([^/]+)@/);
2518
+ const credentials = url.match(CREDENTIALS);
2488
2519
  // No credentials, return the url unmodified and an empty auth object
2489
- if (userpass == null) return { url, auth: {} }
2490
- userpass = userpass[1];
2491
- const [username, password] = userpass.split(':');
2492
- // Remove credentials from URL
2493
- url = url.replace(`${userpass}@`, '');
2494
- // Has credentials, return the fetch-safe URL and the parsed credentials
2495
- return { url, auth: { username, password } }
2520
+ if (credentials == null) return { url, auth: {} }
2521
+ // Has credentials, return the fetch-safe URL and the parsed credentials.
2522
+ // The credentials are cut out of the original string rather than read back
2523
+ // off the parsed URL: serializing that one would also drop an explicit
2524
+ // `:443` and append a slash to a URL that has no path, and this return value
2525
+ // is what the request goes out against.
2526
+ return {
2527
+ url: url.replace(CREDENTIALS, '$1'),
2528
+ auth: parseUserinfo(url, credentials[2]),
2529
+ }
2530
+ }
2531
+
2532
+ function parseUserinfo(url, userpass) {
2533
+ // `URL` splits the two halves the way the platform does, so the first colon
2534
+ // separates them and any later one stays in the password. Only the split is
2535
+ // taken from it, for the reason above.
2536
+ const parsed = parseUrl(url);
2537
+ // A password is absent, not empty, when no colon was written at all. `URL`
2538
+ // reports `''` for both, and the difference is worth keeping.
2539
+ const hasPassword = userpass.indexOf(':') !== -1;
2540
+
2541
+ if (parsed !== null) {
2542
+ return {
2543
+ username: decodeUserinfo(parsed.username),
2544
+ password: hasPassword ? decodeUserinfo(parsed.password) : undefined,
2545
+ }
2546
+ }
2547
+
2548
+ // No `URL` to parse with, or it rejected a string the expression above
2549
+ // accepted. Split on the first colon by hand, without the percent-decoding.
2550
+ const separatorIndex = userpass.indexOf(':');
2551
+ return {
2552
+ username: hasPassword ? userpass.slice(0, separatorIndex) : userpass,
2553
+ password: hasPassword ? userpass.slice(separatorIndex + 1) : undefined,
2554
+ }
2555
+ }
2556
+
2557
+ function parseUrl(url) {
2558
+ if (typeof URL !== 'function') return null
2559
+ try {
2560
+ return new URL(url)
2561
+ } catch (_) {
2562
+ return null
2563
+ }
2564
+ }
2565
+
2566
+ // `URL` hands back the userinfo still percent-encoded, so `p:ss` arrives as
2567
+ // `p%3Ass` and has to be decoded before it reaches the Authorization header. A
2568
+ // lone `%` is not something the parser encodes, and it makes the decode throw,
2569
+ // so the raw value stands in whenever the decode fails.
2570
+ function decodeUserinfo(value) {
2571
+ try {
2572
+ return decodeURIComponent(value)
2573
+ } catch (_) {
2574
+ return value
2575
+ }
2496
2576
  }
2497
2577
 
2498
2578
  class EmptyServerResponseError extends BaseError {
@@ -3184,8 +3264,6 @@ function parseRemoteUrl({ url }) {
3184
3264
  }
3185
3265
  }
3186
3266
 
3187
- let lock$2 = null;
3188
-
3189
3267
  class GitShallowManager {
3190
3268
  /**
3191
3269
  * Reads the `shallow` file in the Git repository and returns a set of object IDs (OIDs).
@@ -3196,10 +3274,9 @@ class GitShallowManager {
3196
3274
  * @returns {Promise<Set<string>>} - A set of shallow object IDs.
3197
3275
  */
3198
3276
  static async read({ fs, gitdir }) {
3199
- if (lock$2 === null) lock$2 = new AsyncLock();
3200
3277
  const filepath = join(gitdir, 'shallow');
3201
3278
  const oids = new Set();
3202
- await lock$2.acquire(filepath, async function () {
3279
+ await acquireLock(filepath, async function () {
3203
3280
  const text = await fs.read(filepath, { encoding: 'utf8' });
3204
3281
  if (text === null) return oids // no file
3205
3282
  if (text.trim() === '') return oids // empty file
@@ -3222,18 +3299,17 @@ class GitShallowManager {
3222
3299
  * @returns {Promise<void>}
3223
3300
  */
3224
3301
  static async write({ fs, gitdir, oids }) {
3225
- if (lock$2 === null) lock$2 = new AsyncLock();
3226
3302
  const filepath = join(gitdir, 'shallow');
3227
3303
  if (oids.size > 0) {
3228
3304
  const text = [...oids].join('\n') + '\n';
3229
- await lock$2.acquire(filepath, async function () {
3305
+ await acquireLock(filepath, async function () {
3230
3306
  await fs.write(filepath, text, {
3231
3307
  encoding: 'utf8',
3232
3308
  });
3233
3309
  });
3234
3310
  } else {
3235
3311
  // No shallows
3236
- await lock$2.acquire(filepath, async function () {
3312
+ await acquireLock(filepath, async function () {
3237
3313
  await fs.rm(filepath);
3238
3314
  });
3239
3315
  }
@@ -5945,12 +6021,6 @@ const _TreeMap = {
5945
6021
  workdir: WORKDIR,
5946
6022
  };
5947
6023
 
5948
- let lock$3;
5949
- async function acquireLock$1(ref, callback) {
5950
- if (lock$3 === undefined) lock$3 = new AsyncLock();
5951
- return lock$3.acquire(ref, callback)
5952
- }
5953
-
5954
6024
  // make sure filepath, blob type and blob object (from loose objects) plus oid are in sync and valid
5955
6025
  async function checkAndWriteBlob(fs, gitdir, dir, filepath, oid = null) {
5956
6026
  const currentFilepath = join(dir, filepath);
@@ -5967,7 +6037,7 @@ async function checkAndWriteBlob(fs, gitdir, dir, filepath, oid = null) {
5967
6037
  : undefined;
5968
6038
  let retOid = objContent ? oid : undefined;
5969
6039
  if (!objContent) {
5970
- await acquireLock$1(currentFilepath, async () => {
6040
+ await acquireLock(currentFilepath, async () => {
5971
6041
  const object = stats.isSymbolicLink()
5972
6042
  ? await fs.readlink(currentFilepath).then(posixifyPathBuffer)
5973
6043
  : await fs.read(currentFilepath);
@@ -6182,7 +6252,7 @@ async function applyTreeChanges({
6182
6252
  });
6183
6253
 
6184
6254
  // apply the changes to work dir
6185
- await acquireLock$1(gitdir, async () => {
6255
+ await acquireLock(gitdir, async () => {
6186
6256
  for (const op of ops) {
6187
6257
  const currentFilepath = join(dir, op.filepath);
6188
6258
  switch (op.method) {
@@ -6413,7 +6483,7 @@ class GitStashManager {
6413
6483
  );
6414
6484
  const filepath = this.refLogsStashPath;
6415
6485
 
6416
- await acquireLock$1(filepath, async () => {
6486
+ await acquireLock(filepath, async () => {
6417
6487
  const appendTo = (await this.fs.exists(filepath))
6418
6488
  ? await this.fs.read(filepath, 'utf8')
6419
6489
  : '';