isomorphic-git 1.40.0 → 1.40.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.
package/index.cjs CHANGED
@@ -1332,6 +1332,23 @@ class InvalidOidError extends BaseError {
1332
1332
  /** @type {'InvalidOidError'} */
1333
1333
  InvalidOidError.code = 'InvalidOidError';
1334
1334
 
1335
+ class InvalidRefNameError extends BaseError {
1336
+ /**
1337
+ * @param {string} ref
1338
+ * @param {string} suggestion
1339
+ * @param {boolean} canForce
1340
+ */
1341
+ constructor(ref, suggestion) {
1342
+ super(
1343
+ `"${ref}" would be an invalid git reference. (Hint: a valid alternative would be "${suggestion}".)`
1344
+ );
1345
+ this.code = this.name = InvalidRefNameError.code;
1346
+ this.data = { ref, suggestion };
1347
+ }
1348
+ }
1349
+ /** @type {'InvalidRefNameError'} */
1350
+ InvalidRefNameError.code = 'InvalidRefNameError';
1351
+
1335
1352
  class NoRefspecError extends BaseError {
1336
1353
  /**
1337
1354
  * @param {string} remote
@@ -2003,6 +2020,16 @@ const refpaths = ref => [
2003
2020
  // @see https://git-scm.com/docs/gitrepository-layout
2004
2021
  const GIT_FILES = ['config', 'description', 'index', 'shallow', 'commondir'];
2005
2022
 
2023
+ // These names are legal one-level ref names, so nothing upstream of here
2024
+ // rejects them, and writing to one lands on the repository file itself.
2025
+ // Canonical git refuses too: `git update-ref index <oid>` fails with
2026
+ // "cannot lock ref 'index': reference broken" and leaves the file alone.
2027
+ function assertWritableRef(ref) {
2028
+ if (GIT_FILES.includes(ref)) {
2029
+ throw new InvalidRefNameError(ref, `refs/heads/${ref}`)
2030
+ }
2031
+ }
2032
+
2006
2033
  let lock$1;
2007
2034
 
2008
2035
  async function acquireLock(ref, callback) {
@@ -2057,6 +2084,31 @@ class GitRefManager {
2057
2084
  }
2058
2085
  const refspec = GitRefSpecSet.from(refspecs);
2059
2086
  const actualRefsToWrite = new Map();
2087
+ // Work out every destination this call will write, before touching the
2088
+ // repository. Translating is pure, so it can happen up here; the writes
2089
+ // themselves stay at the end of the function.
2090
+ const refTranslations = refspec.translate([...refs.keys()]);
2091
+ const symrefTranslations = [];
2092
+ for (const [serverRef, translatedRef] of refspec.translate([
2093
+ ...symrefs.keys(),
2094
+ ])) {
2095
+ const symtarget = refspec.translateOne(symrefs.get(serverRef));
2096
+ if (symtarget) {
2097
+ symrefTranslations.push([translatedRef, `ref: ${symtarget}`]);
2098
+ }
2099
+ }
2100
+ // The local side of a refspec is whatever `remote.<name>.fetch` says, so a
2101
+ // config like `+refs/heads/main:index` lands a write on `.git/index`.
2102
+ // Refuse it here rather than at the write loop: `pruneTags` and `prune`
2103
+ // delete refs in between, so a later throw leaves the repository pruned
2104
+ // and not updated. The tags added below are always `refs/tags/...`, which
2105
+ // is never a system file, so nothing is missed by checking this early.
2106
+ for (const [, translatedRef] of refTranslations) {
2107
+ assertWritableRef(translatedRef);
2108
+ }
2109
+ for (const [translatedRef] of symrefTranslations) {
2110
+ assertWritableRef(translatedRef);
2111
+ }
2060
2112
  // Delete all current tags if the pruneTags argument is true.
2061
2113
  if (pruneTags) {
2062
2114
  const tags = await GitRefManager.listRefs({
@@ -2084,18 +2136,12 @@ class GitRefManager {
2084
2136
  }
2085
2137
  }
2086
2138
  // Combine refs and symrefs giving symrefs priority
2087
- const refTranslations = refspec.translate([...refs.keys()]);
2088
2139
  for (const [serverRef, translatedRef] of refTranslations) {
2089
2140
  const value = refs.get(serverRef);
2090
2141
  actualRefsToWrite.set(translatedRef, value);
2091
2142
  }
2092
- const symrefTranslations = refspec.translate([...symrefs.keys()]);
2093
- for (const [serverRef, translatedRef] of symrefTranslations) {
2094
- const value = symrefs.get(serverRef);
2095
- const symtarget = refspec.translateOne(value);
2096
- if (symtarget) {
2097
- actualRefsToWrite.set(translatedRef, `ref: ${symtarget}`);
2098
- }
2143
+ for (const [translatedRef, value] of symrefTranslations) {
2144
+ actualRefsToWrite.set(translatedRef, value);
2099
2145
  }
2100
2146
  // If `prune` argument is true, clear out the existing local refspec roots
2101
2147
  const pruned = [];
@@ -2154,6 +2200,7 @@ class GitRefManager {
2154
2200
  // TODO: make this less crude?
2155
2201
  static async writeRef({ fs, gitdir, ref, value }) {
2156
2202
  // Validate input
2203
+ assertWritableRef(ref);
2157
2204
  if (!value.match(/[0-9a-f]{40}/)) {
2158
2205
  throw new InvalidOidError(value)
2159
2206
  }
@@ -2173,6 +2220,7 @@ class GitRefManager {
2173
2220
  * @returns {Promise<void>}
2174
2221
  */
2175
2222
  static async writeSymbolicRef({ fs, gitdir, ref, value }) {
2223
+ assertWritableRef(ref);
2176
2224
  await acquireLock(ref, async () =>
2177
2225
  fs.write(join(gitdir, ref), 'ref: ' + `${value.trim()}\n`, 'utf8')
2178
2226
  );
@@ -2201,6 +2249,8 @@ class GitRefManager {
2201
2249
  * @returns {Promise<void>}
2202
2250
  */
2203
2251
  static async deleteRefs({ fs, gitdir, refs }) {
2252
+ // Validate input
2253
+ refs.forEach(assertWritableRef);
2204
2254
  // Delete regular ref
2205
2255
  await Promise.all(refs.map(ref => fs.rm(join(gitdir, ref))));
2206
2256
  // Delete any packed ref
@@ -2324,7 +2374,7 @@ class GitRefManager {
2324
2374
  // We need to alternate between the file system and the packed-refs
2325
2375
  const packedMap = await GitRefManager.packedRefs({ fs, gitdir });
2326
2376
  // Look in all the proper paths, in this order
2327
- const allpaths = refpaths(ref);
2377
+ const allpaths = refpaths(ref).filter(p => !GIT_FILES.includes(p)); // exclude git system files (#709)
2328
2378
  for (const ref of allpaths) {
2329
2379
  const refExists = await acquireLock(ref, async () =>
2330
2380
  fs.exists(`${gitdir}/${ref}`)
@@ -3824,23 +3874,6 @@ class InvalidFilepathError extends BaseError {
3824
3874
  /** @type {'InvalidFilepathError'} */
3825
3875
  InvalidFilepathError.code = 'InvalidFilepathError';
3826
3876
 
3827
- class InvalidRefNameError extends BaseError {
3828
- /**
3829
- * @param {string} ref
3830
- * @param {string} suggestion
3831
- * @param {boolean} canForce
3832
- */
3833
- constructor(ref, suggestion) {
3834
- super(
3835
- `"${ref}" would be an invalid git reference. (Hint: a valid alternative would be "${suggestion}".)`
3836
- );
3837
- this.code = this.name = InvalidRefNameError.code;
3838
- this.data = { ref, suggestion };
3839
- }
3840
- }
3841
- /** @type {'InvalidRefNameError'} */
3842
- InvalidRefNameError.code = 'InvalidRefNameError';
3843
-
3844
3877
  class MaxDepthError extends BaseError {
3845
3878
  /**
3846
3879
  * @param {number} depth
@@ -5732,6 +5765,18 @@ async function add({
5732
5765
  }
5733
5766
  }
5734
5767
 
5768
+ // True for a path the index already holds, and for a folder holding one. The
5769
+ // exact lookup is a Map hit; the prefix scan only runs for folders, since a
5770
+ // tracked file always answers on the first check.
5771
+ function isTracked(index, filepath) {
5772
+ if (index.has({ filepath })) return true
5773
+ const prefix = `${filepath}/`;
5774
+ for (const entry of index.entriesMap.keys()) {
5775
+ if (entry.startsWith(prefix)) return true
5776
+ }
5777
+ return false
5778
+ }
5779
+
5735
5780
  async function addToIndex({
5736
5781
  dir,
5737
5782
  gitdir,
@@ -5742,10 +5787,13 @@ async function addToIndex({
5742
5787
  parallel,
5743
5788
  autocrlf,
5744
5789
  }) {
5745
- // TODO: Should ignore UNLESS it's already in the index.
5746
5790
  filepath = Array.isArray(filepath) ? filepath : [filepath];
5747
5791
  const promises = filepath.map(async currentFilepath => {
5748
- if (!force) {
5792
+ // .gitignore governs untracked paths. Once a path is in the index, adding a
5793
+ // matching rule does not stop canonical git from staging further changes,
5794
+ // and an ignored folder is still walked for the sake of what it tracks:
5795
+ // the recursion below re-checks every child, so only tracked ones get in.
5796
+ if (!force && !isTracked(index, currentFilepath)) {
5749
5797
  const ignored = await GitIgnoreManager.isIgnored({
5750
5798
  fs,
5751
5799
  dir,
@@ -9524,8 +9572,8 @@ function filterCapabilities(server, client) {
9524
9572
 
9525
9573
  const pkg = {
9526
9574
  name: 'isomorphic-git',
9527
- version: '1.40.0',
9528
- agent: 'git/isomorphic-git@1.40.0',
9575
+ version: '1.40.2',
9576
+ agent: 'git/isomorphic-git@1.40.2',
9529
9577
  };
9530
9578
 
9531
9579
  class FIFO {
@@ -16308,15 +16356,6 @@ async function status({
16308
16356
 
16309
16357
  const fs = new FileSystem(_fs);
16310
16358
  const updatedGitdir = await discoverGitdir({ fsp: fs, dotgit: gitdir });
16311
- const ignored = await GitIgnoreManager.isIgnored({
16312
- fs,
16313
- gitdir: updatedGitdir,
16314
- dir,
16315
- filepath,
16316
- });
16317
- if (ignored) {
16318
- return 'ignored'
16319
- }
16320
16359
  const headTree = await getHeadTree({ fs, cache, gitdir: updatedGitdir });
16321
16360
  const treeOid = await getOidAtPath({
16322
16361
  fs,
@@ -16334,6 +16373,20 @@ async function status({
16334
16373
  return null
16335
16374
  }
16336
16375
  );
16376
+ // .gitignore governs untracked files. A file in HEAD or in the index is
16377
+ // tracked, so canonical git keeps reporting its real state, and
16378
+ // `git check-ignore` does not match it. statusMatrix already does this.
16379
+ if (treeOid === null && indexEntry === null) {
16380
+ const ignored = await GitIgnoreManager.isIgnored({
16381
+ fs,
16382
+ gitdir: updatedGitdir,
16383
+ dir,
16384
+ filepath,
16385
+ });
16386
+ if (ignored) {
16387
+ return 'ignored'
16388
+ }
16389
+ }
16337
16390
  const stats = await fs.lstat(join(dir, filepath));
16338
16391
 
16339
16392
  const H = treeOid !== null; // head
package/index.js CHANGED
@@ -1325,6 +1325,23 @@ class InvalidOidError extends BaseError {
1325
1325
  /** @type {'InvalidOidError'} */
1326
1326
  InvalidOidError.code = 'InvalidOidError';
1327
1327
 
1328
+ class InvalidRefNameError extends BaseError {
1329
+ /**
1330
+ * @param {string} ref
1331
+ * @param {string} suggestion
1332
+ * @param {boolean} canForce
1333
+ */
1334
+ constructor(ref, suggestion) {
1335
+ super(
1336
+ `"${ref}" would be an invalid git reference. (Hint: a valid alternative would be "${suggestion}".)`
1337
+ );
1338
+ this.code = this.name = InvalidRefNameError.code;
1339
+ this.data = { ref, suggestion };
1340
+ }
1341
+ }
1342
+ /** @type {'InvalidRefNameError'} */
1343
+ InvalidRefNameError.code = 'InvalidRefNameError';
1344
+
1328
1345
  class NoRefspecError extends BaseError {
1329
1346
  /**
1330
1347
  * @param {string} remote
@@ -1996,6 +2013,16 @@ const refpaths = ref => [
1996
2013
  // @see https://git-scm.com/docs/gitrepository-layout
1997
2014
  const GIT_FILES = ['config', 'description', 'index', 'shallow', 'commondir'];
1998
2015
 
2016
+ // These names are legal one-level ref names, so nothing upstream of here
2017
+ // rejects them, and writing to one lands on the repository file itself.
2018
+ // Canonical git refuses too: `git update-ref index <oid>` fails with
2019
+ // "cannot lock ref 'index': reference broken" and leaves the file alone.
2020
+ function assertWritableRef(ref) {
2021
+ if (GIT_FILES.includes(ref)) {
2022
+ throw new InvalidRefNameError(ref, `refs/heads/${ref}`)
2023
+ }
2024
+ }
2025
+
1999
2026
  let lock$1;
2000
2027
 
2001
2028
  async function acquireLock(ref, callback) {
@@ -2050,6 +2077,31 @@ class GitRefManager {
2050
2077
  }
2051
2078
  const refspec = GitRefSpecSet.from(refspecs);
2052
2079
  const actualRefsToWrite = new Map();
2080
+ // Work out every destination this call will write, before touching the
2081
+ // repository. Translating is pure, so it can happen up here; the writes
2082
+ // themselves stay at the end of the function.
2083
+ const refTranslations = refspec.translate([...refs.keys()]);
2084
+ const symrefTranslations = [];
2085
+ for (const [serverRef, translatedRef] of refspec.translate([
2086
+ ...symrefs.keys(),
2087
+ ])) {
2088
+ const symtarget = refspec.translateOne(symrefs.get(serverRef));
2089
+ if (symtarget) {
2090
+ symrefTranslations.push([translatedRef, `ref: ${symtarget}`]);
2091
+ }
2092
+ }
2093
+ // The local side of a refspec is whatever `remote.<name>.fetch` says, so a
2094
+ // config like `+refs/heads/main:index` lands a write on `.git/index`.
2095
+ // Refuse it here rather than at the write loop: `pruneTags` and `prune`
2096
+ // delete refs in between, so a later throw leaves the repository pruned
2097
+ // and not updated. The tags added below are always `refs/tags/...`, which
2098
+ // is never a system file, so nothing is missed by checking this early.
2099
+ for (const [, translatedRef] of refTranslations) {
2100
+ assertWritableRef(translatedRef);
2101
+ }
2102
+ for (const [translatedRef] of symrefTranslations) {
2103
+ assertWritableRef(translatedRef);
2104
+ }
2053
2105
  // Delete all current tags if the pruneTags argument is true.
2054
2106
  if (pruneTags) {
2055
2107
  const tags = await GitRefManager.listRefs({
@@ -2077,18 +2129,12 @@ class GitRefManager {
2077
2129
  }
2078
2130
  }
2079
2131
  // Combine refs and symrefs giving symrefs priority
2080
- const refTranslations = refspec.translate([...refs.keys()]);
2081
2132
  for (const [serverRef, translatedRef] of refTranslations) {
2082
2133
  const value = refs.get(serverRef);
2083
2134
  actualRefsToWrite.set(translatedRef, value);
2084
2135
  }
2085
- const symrefTranslations = refspec.translate([...symrefs.keys()]);
2086
- for (const [serverRef, translatedRef] of symrefTranslations) {
2087
- const value = symrefs.get(serverRef);
2088
- const symtarget = refspec.translateOne(value);
2089
- if (symtarget) {
2090
- actualRefsToWrite.set(translatedRef, `ref: ${symtarget}`);
2091
- }
2136
+ for (const [translatedRef, value] of symrefTranslations) {
2137
+ actualRefsToWrite.set(translatedRef, value);
2092
2138
  }
2093
2139
  // If `prune` argument is true, clear out the existing local refspec roots
2094
2140
  const pruned = [];
@@ -2147,6 +2193,7 @@ class GitRefManager {
2147
2193
  // TODO: make this less crude?
2148
2194
  static async writeRef({ fs, gitdir, ref, value }) {
2149
2195
  // Validate input
2196
+ assertWritableRef(ref);
2150
2197
  if (!value.match(/[0-9a-f]{40}/)) {
2151
2198
  throw new InvalidOidError(value)
2152
2199
  }
@@ -2166,6 +2213,7 @@ class GitRefManager {
2166
2213
  * @returns {Promise<void>}
2167
2214
  */
2168
2215
  static async writeSymbolicRef({ fs, gitdir, ref, value }) {
2216
+ assertWritableRef(ref);
2169
2217
  await acquireLock(ref, async () =>
2170
2218
  fs.write(join(gitdir, ref), 'ref: ' + `${value.trim()}\n`, 'utf8')
2171
2219
  );
@@ -2194,6 +2242,8 @@ class GitRefManager {
2194
2242
  * @returns {Promise<void>}
2195
2243
  */
2196
2244
  static async deleteRefs({ fs, gitdir, refs }) {
2245
+ // Validate input
2246
+ refs.forEach(assertWritableRef);
2197
2247
  // Delete regular ref
2198
2248
  await Promise.all(refs.map(ref => fs.rm(join(gitdir, ref))));
2199
2249
  // Delete any packed ref
@@ -2317,7 +2367,7 @@ class GitRefManager {
2317
2367
  // We need to alternate between the file system and the packed-refs
2318
2368
  const packedMap = await GitRefManager.packedRefs({ fs, gitdir });
2319
2369
  // Look in all the proper paths, in this order
2320
- const allpaths = refpaths(ref);
2370
+ const allpaths = refpaths(ref).filter(p => !GIT_FILES.includes(p)); // exclude git system files (#709)
2321
2371
  for (const ref of allpaths) {
2322
2372
  const refExists = await acquireLock(ref, async () =>
2323
2373
  fs.exists(`${gitdir}/${ref}`)
@@ -3811,23 +3861,6 @@ class InvalidFilepathError extends BaseError {
3811
3861
  /** @type {'InvalidFilepathError'} */
3812
3862
  InvalidFilepathError.code = 'InvalidFilepathError';
3813
3863
 
3814
- class InvalidRefNameError extends BaseError {
3815
- /**
3816
- * @param {string} ref
3817
- * @param {string} suggestion
3818
- * @param {boolean} canForce
3819
- */
3820
- constructor(ref, suggestion) {
3821
- super(
3822
- `"${ref}" would be an invalid git reference. (Hint: a valid alternative would be "${suggestion}".)`
3823
- );
3824
- this.code = this.name = InvalidRefNameError.code;
3825
- this.data = { ref, suggestion };
3826
- }
3827
- }
3828
- /** @type {'InvalidRefNameError'} */
3829
- InvalidRefNameError.code = 'InvalidRefNameError';
3830
-
3831
3864
  class MaxDepthError extends BaseError {
3832
3865
  /**
3833
3866
  * @param {number} depth
@@ -5719,6 +5752,18 @@ async function add({
5719
5752
  }
5720
5753
  }
5721
5754
 
5755
+ // True for a path the index already holds, and for a folder holding one. The
5756
+ // exact lookup is a Map hit; the prefix scan only runs for folders, since a
5757
+ // tracked file always answers on the first check.
5758
+ function isTracked(index, filepath) {
5759
+ if (index.has({ filepath })) return true
5760
+ const prefix = `${filepath}/`;
5761
+ for (const entry of index.entriesMap.keys()) {
5762
+ if (entry.startsWith(prefix)) return true
5763
+ }
5764
+ return false
5765
+ }
5766
+
5722
5767
  async function addToIndex({
5723
5768
  dir,
5724
5769
  gitdir,
@@ -5729,10 +5774,13 @@ async function addToIndex({
5729
5774
  parallel,
5730
5775
  autocrlf,
5731
5776
  }) {
5732
- // TODO: Should ignore UNLESS it's already in the index.
5733
5777
  filepath = Array.isArray(filepath) ? filepath : [filepath];
5734
5778
  const promises = filepath.map(async currentFilepath => {
5735
- if (!force) {
5779
+ // .gitignore governs untracked paths. Once a path is in the index, adding a
5780
+ // matching rule does not stop canonical git from staging further changes,
5781
+ // and an ignored folder is still walked for the sake of what it tracks:
5782
+ // the recursion below re-checks every child, so only tracked ones get in.
5783
+ if (!force && !isTracked(index, currentFilepath)) {
5736
5784
  const ignored = await GitIgnoreManager.isIgnored({
5737
5785
  fs,
5738
5786
  dir,
@@ -9511,8 +9559,8 @@ function filterCapabilities(server, client) {
9511
9559
 
9512
9560
  const pkg = {
9513
9561
  name: 'isomorphic-git',
9514
- version: '1.40.0',
9515
- agent: 'git/isomorphic-git@1.40.0',
9562
+ version: '1.40.2',
9563
+ agent: 'git/isomorphic-git@1.40.2',
9516
9564
  };
9517
9565
 
9518
9566
  class FIFO {
@@ -16295,15 +16343,6 @@ async function status({
16295
16343
 
16296
16344
  const fs = new FileSystem(_fs);
16297
16345
  const updatedGitdir = await discoverGitdir({ fsp: fs, dotgit: gitdir });
16298
- const ignored = await GitIgnoreManager.isIgnored({
16299
- fs,
16300
- gitdir: updatedGitdir,
16301
- dir,
16302
- filepath,
16303
- });
16304
- if (ignored) {
16305
- return 'ignored'
16306
- }
16307
16346
  const headTree = await getHeadTree({ fs, cache, gitdir: updatedGitdir });
16308
16347
  const treeOid = await getOidAtPath({
16309
16348
  fs,
@@ -16321,6 +16360,20 @@ async function status({
16321
16360
  return null
16322
16361
  }
16323
16362
  );
16363
+ // .gitignore governs untracked files. A file in HEAD or in the index is
16364
+ // tracked, so canonical git keeps reporting its real state, and
16365
+ // `git check-ignore` does not match it. statusMatrix already does this.
16366
+ if (treeOid === null && indexEntry === null) {
16367
+ const ignored = await GitIgnoreManager.isIgnored({
16368
+ fs,
16369
+ gitdir: updatedGitdir,
16370
+ dir,
16371
+ filepath,
16372
+ });
16373
+ if (ignored) {
16374
+ return 'ignored'
16375
+ }
16376
+ }
16324
16377
  const stats = await fs.lstat(join(dir, filepath));
16325
16378
 
16326
16379
  const H = treeOid !== null; // head