Repository: isomorphic-git/isomorphic-git
Pull request #1514 (2022-03-14 20:18:59)

feat: add supports array of files
This PR requires https://github.com/isomorphic-git/isomorphic-git/pull/1507

## I'm modifying a parameter to an existing command X:

- [x] add parameter to the function in `src/api/X.js` (and `src/commands/X.js` if necessary)
- [x] document the parameter in the JSDoc comment above the function
- [x] add a test case in `__tests__/test-X.js` if possible
- [x] if this is your first time contributing, run `npm run add-contributor` and follow the prompts to add yourself to the README
- [ ] squash merge the PR with commit message "feat(X): Added 'bar' parameter"


Patch:
diff --git a/src/api/add.js b/src/api/add.js
index 80a90eb8..27173103 100644
--- a/src/api/add.js
+++ b/src/api/add.js
@@ -1,6 +1,7 @@
 // @ts-check
 import '../typedefs.js'
 
+import { MultipleGitError } from '../errors/MultipleGitError'
 import { NotFoundError } from '../errors/NotFoundError.js'
 import { GitIgnoreManager } from '../managers/GitIgnoreManager.js'
 import { GitIndexManager } from '../managers/GitIndexManager.js'
@@ -8,7 +9,6 @@ import { FileSystem } from '../models/FileSystem.js'
 import { _writeObject } from '../storage/writeObject.js'
 import { assertParameter } from '../utils/assertParameter.js'
 import { join } from '../utils/join.js'
-import { posixifyPathBuffer } from '../utils/posixifyPathBuffer.js'
 
 /**
  * Add a file to the git index (aka staging area)
@@ -17,7 +17,7 @@ import { posixifyPathBuffer } from '../utils/posixifyPathBuffer.js'
  * @param {FsClient} args.fs - a file system implementation
  * @param {string} args.dir - The [working tree](dir-vs-gitdir.md) directory path
  * @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
- * @param {string} args.filepath - The path to the file to add to the index
+ * @param {string|string[]} args.filepath - The path to the file to add to the index
  * @param {object} [args.cache] - a [cache](cache.md) object
  *
  * @returns {Promise<void>} Resolves successfully once the git index has been updated
@@ -39,11 +39,11 @@ export async function add({
     assertParameter('fs', _fs)
     assertParameter('dir', dir)
     assertParameter('gitdir', gitdir)
-    assertParameter('filepath', filepath)
+    assertParameter('filepaths', filepath)
 
     const fs = new FileSystem(_fs)
-    await GitIndexManager.acquire({ fs, gitdir, cache }, async function(index) {
-      await addToIndex({ dir, gitdir, fs, filepath, index })
+    await GitIndexManager.acquire({ fs, gitdir, cache }, async index => {
+      return addToIndex({ dir, gitdir, fs, filepath, index })
     })
   } catch (err) {
     err.caller = 'git.add'
@@ -53,27 +53,54 @@ export async function add({
 
 async function addToIndex({ dir, gitdir, fs, filepath, index }) {
   // TODO: Should ignore UNLESS it's already in the index.
-  const ignored = await GitIgnoreManager.isIgnored({
-    fs,
-    dir,
-    gitdir,
-    filepath,
+  filepath = Array.isArray(filepath) ? filepath : [filepath]
+  const promises = filepath.map(async currentFilepath => {
+    const ignored = await GitIgnoreManager.isIgnored({
+      fs,
+      dir,
+      gitdir,
+      filepath: currentFilepath,
+    })
+    if (ignored) return
+    const stats = await fs.lstat(join(dir, currentFilepath))
+    if (!stats) throw new NotFoundError(currentFilepath)
+
+    if (stats.isDirectory()) {
+      const children = await fs.readdir(join(dir, currentFilepath))
+      const promises = children.map(child =>
+        addToIndex({
+          dir,
+          gitdir,
+          fs,
+          filepath: [join(currentFilepath, child)],
+          index,
+        })
+      )
+      await Promise.all(promises)
+    } else {
+      const object = stats.isSymbolicLink()
+        ? await fs.readlink(join(dir, currentFilepath))
+        : await fs.read(join(dir, currentFilepath))
+      if (object === null) throw new NotFoundError(currentFilepath)
+      const oid = await _writeObject({ fs, gitdir, type: 'blob', object })
+      index.insert({ filepath: currentFilepath, stats, oid })
+    }
   })
-  if (ignored) return
-  const stats = await fs.lstat(join(dir, filepath))
-  if (!stats) throw new NotFoundError(filepath)
-  if (stats.isDirectory()) {
-    const children = await fs.readdir(join(dir, filepath))
-    const promises = children.map(child =>
-      addToIndex({ dir, gitdir, fs, filepath: join(filepath, child), index })
-    )
-    await Promise.all(promises)
-  } else {
-    const object = stats.isSymbolicLink()
-      ? await fs.readlink(join(dir, filepath)).then(posixifyPathBuffer)
-      : await fs.read(join(dir, filepath))
-    if (object === null) throw new NotFoundError(filepath)
-    const oid = await _writeObject({ fs, gitdir, type: 'blob', object })
-    index.insert({ filepath, stats, oid })
+
+  const settledPromises = await Promise.allSettled(promises)
+  const rejectedPromises = settledPromises
+    .filter(settle => settle.status === 'rejected')
+    .map(settle => settle.reason)
+  if (rejectedPromises.length > 1) {
+    throw new MultipleGitError(rejectedPromises)
   }
+  if (rejectedPromises.length === 1) {
+    throw rejectedPromises[0]
+  }
+
+  const fulfilledPromises = settledPromises
+    .filter(settle => settle.status === 'fulfilled' && settle.value)
+    .map(settle => settle.value)
+
+  return fulfilledPromises
 }
diff --git a/src/errors/MultipleGitError.js b/src/errors/MultipleGitError.js
new file mode 100644
index 00000000..6727e31f
--- /dev/null
+++ b/src/errors/MultipleGitError.js
@@ -0,0 +1,18 @@
+import { BaseError } from './BaseError.js'
+
+export class MultipleGitError extends BaseError {
+  /**
+   * @param {Error[]} errors
+   * @param {string} message
+   */
+  constructor(errors) {
+    super(
+      `There are multiple errors that were thrown by the method. Please refer to the "errors" property to see more`
+    )
+    this.code = this.name = MultipleGitError.code
+    this.data = { errors }
+    this.errors = errors
+  }
+}
+/** @type {'MultipleGitError'} */
+MultipleGitError.code = 'MultipleGitError'
diff --git a/src/managers/GitIndexManager.js b/src/managers/GitIndexManager.js
index 3427394a..27329c36 100644
--- a/src/managers/GitIndexManager.js
+++ b/src/managers/GitIndexManager.js
@@ -53,7 +53,7 @@ export class GitIndexManager {
     const filepath = `${gitdir}/index`
     if (lock === null) lock = new AsyncLock({ maxPending: Infinity })
     let result
-    await lock.acquire(filepath, async function() {
+    await lock.acquire(filepath, async () => {
       // Acquire a file lock while we're reading the index
       // to make sure other processes aren't writing to it
       // simultaneously, which could result in a corrupted index.
@@ -73,6 +73,7 @@ export class GitIndexManager {
         index._dirty = false
       }
     })
+
     return result
   }
 }
diff --git a/website/versioned_docs/version-1.x/add.md b/website/versioned_docs/version-1.x/add.md
index bc496526..3eba882c 100644
--- a/website/versioned_docs/version-1.x/add.md
+++ b/website/versioned_docs/version-1.x/add.md
@@ -7,14 +7,14 @@ original_id: add
 
 Add a file to the git index (aka staging area)
 
-| param          | type [= default]           | description                                               |
-| -------------- | -------------------------- | --------------------------------------------------------- |
-| [**fs**](./fs) | FsClient                   | a file system implementation                              |
-| **dir**        | string                     | The [working tree](dir-vs-gitdir.md) directory path       |
-| **gitdir**     | string = join(dir, '.git') | The [git directory](dir-vs-gitdir.md) path                |
-| **filepath**   | string                     | The path to the file to add to the index                  |
-| cache          | object                     | a [cache](cache.md) object                                |
-| return         | Promise\<void\>            | Resolves successfully once the git index has been updated |
+| param          | type [= default]                | description                                               |
+| -------------- | ------------------------------- | --------------------------------------------------------- |
+| [**fs**](./fs) | FsClient                        | a file system implementation                              |
+| **dir**        | string                          | The [working tree](dir-vs-gitdir.md) directory path       |
+| **gitdir**     | string = join(dir, '.git')      | The [git directory](dir-vs-gitdir.md) path                |
+| **filepath**   | string  &#124;  Array\<string\> | The path to the file to add to the index                  |
+| cache          | object                          | a [cache](cache.md) object                                |
+| return         | Promise\<void\>                 | Resolves successfully once the git index has been updated |
 
 Example Code:
 


Test patch:
diff --git a/__tests__/test-add.js b/__tests__/test-add.js
index 1b070563..934896c6 100644
--- a/__tests__/test-add.js
+++ b/__tests__/test-add.js
@@ -36,6 +36,67 @@ describe('add', () => {
     await add({ fs, dir, filepath: 'b.txt' })
     expect((await listFiles({ fs, dir })).length).toEqual(3)
   })
+  it('multiple files', async () => {
+    // Setup
+    const { fs, dir } = await makeFixture('test-add')
+    // Test
+    await init({ fs, dir })
+    await add({ fs, dir, filepath: ['a.txt', 'a-copy.txt', 'b.txt'] })
+    expect((await listFiles({ fs, dir })).length).toEqual(3)
+  })
+  it('multiple files with one failure (normal error)', async () => {
+    // Setup
+    const { fs, dir } = await makeFixture('test-add')
+    // Test
+    await init({ fs, dir })
+    let err = null
+    try {
+      await add({ fs, dir, filepath: ['a.txt', 'a-copy.txt', 'non-existent'] })
+    } catch (e) {
+      err = e
+    }
+    expect(err.caller).toEqual('git.add')
+    expect(err.name).toEqual('NotFoundError')
+  })
+  it('multiple files with 2 failures (MultipleGitError) and an ignored file', async () => {
+    // Setup
+    const { fs, dir } = await makeFixture('test-add')
+    await writeGitIgnore(fs, dir)
+
+    // Test
+    await init({ fs, dir })
+    let err = null
+    try {
+      await add({
+        fs,
+        dir,
+        filepath: ['a.txt', 'i.txt', 'non-existent', 'also-non-existent'],
+      })
+    } catch (e) {
+      err = e
+    }
+    expect(err.caller).toEqual('git.add')
+    expect(err.name).toEqual('MultipleGitError')
+    expect(err.errors.length).toEqual(2)
+    err.errors.forEach(e => {
+      expect(e.name).toEqual('NotFoundError')
+    })
+  })
+  it('multiple files with 1 ignored', async () => {
+    // Setup
+    const { fs, dir } = await makeFixture('test-add')
+    await writeGitIgnore(fs, dir)
+
+    // Test
+    await init({ fs, dir })
+    await add({
+      fs,
+      dir,
+      filepath: ['a.txt', 'i.txt'],
+    })
+    expect((await listFiles({ fs, dir })).length).toEqual(1)
+    expect(await listFiles({ fs, dir })).toEqual(['a.txt'])
+  })
   it('symlink', async () => {
     // Setup
     const { fs, dir } = await makeFixture('test-add')