isomorphic-git 1.17.3 → 1.18.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.js CHANGED
@@ -233,6 +233,19 @@ import diff3Merge from 'diff3';
233
233
  * @return {{signature: string} | Promise<{signature: string}>} - an 'ASCII armor' encoded "detached" signature
234
234
  */
235
235
 
236
+ /**
237
+ * @typedef {Object} MergeDriverParams
238
+ * @property {Array<string>} branches
239
+ * @property {Array<string>} contents
240
+ * @property {string} path
241
+ */
242
+
243
+ /**
244
+ * @callback MergeDriverCallback
245
+ * @param {MergeDriverParams} args
246
+ * @return {{cleanMerge: boolean, mergedText: string} | Promise<{cleanMerge: boolean, mergedText: string}>}
247
+ */
248
+
236
249
  /**
237
250
  * @callback WalkerMap
238
251
  * @param {string} filename
@@ -3196,6 +3209,21 @@ class MergeNotSupportedError extends BaseError {
3196
3209
  /** @type {'MergeNotSupportedError'} */
3197
3210
  MergeNotSupportedError.code = 'MergeNotSupportedError';
3198
3211
 
3212
+ class MergeConflictError extends BaseError {
3213
+ /**
3214
+ * @param {Array<string>} filepaths
3215
+ */
3216
+ constructor(filepaths) {
3217
+ super(
3218
+ `Automatic merge failed with one or more merge conflicts in the following files: ${filepaths.toString()}. Fix conflicts then commit the result.`
3219
+ );
3220
+ this.code = this.name = MergeConflictError.code;
3221
+ this.data = { filepaths };
3222
+ }
3223
+ }
3224
+ /** @type {'MergeConflictError'} */
3225
+ MergeConflictError.code = 'MergeConflictError';
3226
+
3199
3227
  class MissingNameError extends BaseError {
3200
3228
  /**
3201
3229
  * @param {'author'|'committer'|'tagger'} role
@@ -3366,6 +3394,7 @@ var Errors = /*#__PURE__*/Object.freeze({
3366
3394
  InvalidRefNameError: InvalidRefNameError,
3367
3395
  MaxDepthError: MaxDepthError,
3368
3396
  MergeNotSupportedError: MergeNotSupportedError,
3397
+ MergeConflictError: MergeConflictError,
3369
3398
  MissingNameError: MissingNameError,
3370
3399
  MissingParameterError: MissingParameterError,
3371
3400
  MultipleGitError: MultipleGitError,
@@ -4627,7 +4656,15 @@ async function _commit({
4627
4656
  // Probably an initial commit
4628
4657
  parent = [];
4629
4658
  }
4659
+ } else {
4660
+ // ensure that the parents are oids, not refs
4661
+ parent = await Promise.all(
4662
+ parent.map(p => {
4663
+ return GitRefManager.resolve({ fs, gitdir, ref: p })
4664
+ })
4665
+ );
4630
4666
  }
4667
+
4631
4668
  let comm = GitCommit.from({
4632
4669
  tree,
4633
4670
  parent,
@@ -6940,8 +6977,8 @@ function filterCapabilities(server, client) {
6940
6977
 
6941
6978
  const pkg = {
6942
6979
  name: 'isomorphic-git',
6943
- version: '1.17.3',
6944
- agent: 'git/isomorphic-git@1.17.3',
6980
+ version: '1.18.2',
6981
+ agent: 'git/isomorphic-git@1.18.2',
6945
6982
  };
6946
6983
 
6947
6984
  class FIFO {
@@ -8417,16 +8454,14 @@ async function _findMergeBase({ fs, cache, gitdir, oids }) {
8417
8454
 
8418
8455
  const LINEBREAKS = /^.*(\r?\n|$)/gm;
8419
8456
 
8420
- function mergeFile({
8421
- ourContent,
8422
- baseContent,
8423
- theirContent,
8424
- ourName = 'ours',
8425
- baseName = 'base',
8426
- theirName = 'theirs',
8427
- format = 'diff',
8428
- markerSize = 7,
8429
- }) {
8457
+ function mergeFile({ branches, contents }) {
8458
+ const ourName = branches[1];
8459
+ const theirName = branches[2];
8460
+
8461
+ const baseContent = contents[0];
8462
+ const ourContent = contents[1];
8463
+ const theirContent = contents[2];
8464
+
8430
8465
  const ours = ourContent.match(LINEBREAKS);
8431
8466
  const base = baseContent.match(LINEBREAKS);
8432
8467
  const theirs = theirContent.match(LINEBREAKS);
@@ -8434,9 +8469,12 @@ function mergeFile({
8434
8469
  // Here we let the diff3 library do the heavy lifting.
8435
8470
  const result = diff3Merge(ours, base, theirs);
8436
8471
 
8472
+ const markerSize = 7;
8473
+
8437
8474
  // Here we note whether there are conflicts and format the results
8438
8475
  let mergedText = '';
8439
8476
  let cleanMerge = true;
8477
+
8440
8478
  for (const item of result) {
8441
8479
  if (item.ok) {
8442
8480
  mergedText += item.ok.join('');
@@ -8445,10 +8483,7 @@ function mergeFile({
8445
8483
  cleanMerge = false;
8446
8484
  mergedText += `${'<'.repeat(markerSize)} ${ourName}\n`;
8447
8485
  mergedText += item.conflict.a.join('');
8448
- if (format === 'diff3') {
8449
- mergedText += `${'|'.repeat(markerSize)} ${baseName}\n`;
8450
- mergedText += item.conflict.o.join('');
8451
- }
8486
+
8452
8487
  mergedText += `${'='.repeat(markerSize)}\n`;
8453
8488
  mergedText += item.conflict.b.join('');
8454
8489
  mergedText += `${'>'.repeat(markerSize)} ${theirName}\n`;
@@ -8474,6 +8509,8 @@ function mergeFile({
8474
8509
  * @param {string} [args.baseName='base'] - The name to use in conflicted files (in diff3 format) for the base hunks
8475
8510
  * @param {string} [args.theirName='theirs'] - The name to use in conflicted files for their hunks
8476
8511
  * @param {boolean} [args.dryRun=false]
8512
+ * @param {boolean} [args.abortOnConflict=false]
8513
+ * @param {MergeDriverCallback} [args.mergeDriver]
8477
8514
  *
8478
8515
  * @returns {Promise<string>} - The SHA-1 object id of the merged tree
8479
8516
  *
@@ -8490,11 +8527,17 @@ async function mergeTree({
8490
8527
  baseName = 'base',
8491
8528
  theirName = 'theirs',
8492
8529
  dryRun = false,
8530
+ abortOnConflict = true,
8531
+ mergeDriver,
8493
8532
  }) {
8494
8533
  const ourTree = TREE({ ref: ourOid });
8495
8534
  const baseTree = TREE({ ref: baseOid });
8496
8535
  const theirTree = TREE({ ref: theirOid });
8497
8536
 
8537
+ const unmergedFiles = [];
8538
+
8539
+ let cleanMerge = true;
8540
+
8498
8541
  const results = await _walk({
8499
8542
  fs,
8500
8543
  cache,
@@ -8555,6 +8598,11 @@ async function mergeTree({
8555
8598
  ourName,
8556
8599
  baseName,
8557
8600
  theirName,
8601
+ mergeDriver,
8602
+ }).then(r => {
8603
+ cleanMerge = r.cleanMerge;
8604
+ unmergedFiles.push(filepath);
8605
+ return r.mergeResult
8558
8606
  })
8559
8607
  }
8560
8608
  // all other types of conflicts fail
@@ -8590,6 +8638,29 @@ async function mergeTree({
8590
8638
  return parent
8591
8639
  },
8592
8640
  });
8641
+
8642
+ if (!cleanMerge) {
8643
+ if (dir && !abortOnConflict) {
8644
+ await _walk({
8645
+ fs,
8646
+ cache,
8647
+ dir,
8648
+ gitdir,
8649
+ trees: [TREE({ ref: results.oid })],
8650
+ map: async function(filepath, [entry]) {
8651
+ const path = `${dir}/${filepath}`;
8652
+ if ((await entry.type()) === 'blob') {
8653
+ const mode = await entry.mode();
8654
+ const content = new TextDecoder().decode(await entry.content());
8655
+ await fs.write(path, content, { mode });
8656
+ }
8657
+ return true
8658
+ },
8659
+ });
8660
+ }
8661
+ throw new MergeConflictError(unmergedFiles)
8662
+ }
8663
+
8593
8664
  return results.oid
8594
8665
  }
8595
8666
 
@@ -8628,9 +8699,8 @@ async function modified(entry, base) {
8628
8699
  * @param {string} [args.ourName]
8629
8700
  * @param {string} [args.baseName]
8630
8701
  * @param {string} [args.theirName]
8631
- * @param {string} [args.format]
8632
- * @param {number} [args.markerSize]
8633
8702
  * @param {boolean} [args.dryRun = false]
8703
+ * @param {MergeDriverCallback} [args.mergeDriver]
8634
8704
  *
8635
8705
  */
8636
8706
  async function mergeBlobs({
@@ -8643,9 +8713,8 @@ async function mergeBlobs({
8643
8713
  ourName,
8644
8714
  theirName,
8645
8715
  baseName,
8646
- format,
8647
- markerSize,
8648
8716
  dryRun,
8717
+ mergeDriver = mergeFile,
8649
8718
  }) {
8650
8719
  const type = 'blob';
8651
8720
  // Compute the new mode.
@@ -8656,30 +8725,33 @@ async function mergeBlobs({
8656
8725
  : await ours.mode();
8657
8726
  // The trivial case: nothing to merge except maybe mode
8658
8727
  if ((await ours.oid()) === (await theirs.oid())) {
8659
- return { mode, path, oid: await ours.oid(), type }
8728
+ return {
8729
+ cleanMerge: true,
8730
+ mergeResult: { mode, path, oid: await ours.oid(), type },
8731
+ }
8660
8732
  }
8661
8733
  // if only one side made oid changes, return that side's oid
8662
8734
  if ((await ours.oid()) === (await base.oid())) {
8663
- return { mode, path, oid: await theirs.oid(), type }
8735
+ return {
8736
+ cleanMerge: true,
8737
+ mergeResult: { mode, path, oid: await theirs.oid(), type },
8738
+ }
8664
8739
  }
8665
8740
  if ((await theirs.oid()) === (await base.oid())) {
8666
- return { mode, path, oid: await ours.oid(), type }
8741
+ return {
8742
+ cleanMerge: true,
8743
+ mergeResult: { mode, path, oid: await ours.oid(), type },
8744
+ }
8667
8745
  }
8668
8746
  // if both sides made changes do a merge
8669
- const { mergedText, cleanMerge } = mergeFile({
8670
- ourContent: Buffer.from(await ours.content()).toString('utf8'),
8671
- baseContent: Buffer.from(await base.content()).toString('utf8'),
8672
- theirContent: Buffer.from(await theirs.content()).toString('utf8'),
8673
- ourName,
8674
- theirName,
8675
- baseName,
8676
- format,
8677
- markerSize,
8747
+ const ourContent = Buffer.from(await ours.content()).toString('utf8');
8748
+ const baseContent = Buffer.from(await base.content()).toString('utf8');
8749
+ const theirContent = Buffer.from(await theirs.content()).toString('utf8');
8750
+ const { mergedText, cleanMerge } = await mergeDriver({
8751
+ branches: [baseName, ourName, theirName],
8752
+ contents: [baseContent, ourContent, theirContent],
8753
+ path,
8678
8754
  });
8679
- if (!cleanMerge) {
8680
- // all other types of conflicts fail
8681
- throw new MergeNotSupportedError()
8682
- }
8683
8755
  const oid = await _writeObject({
8684
8756
  fs,
8685
8757
  gitdir,
@@ -8687,7 +8759,8 @@ async function mergeBlobs({
8687
8759
  object: Buffer.from(mergedText, 'utf8'),
8688
8760
  dryRun,
8689
8761
  });
8690
- return { mode, path, oid, type }
8762
+
8763
+ return { cleanMerge, mergeResult: { mode, path, oid, type } }
8691
8764
  }
8692
8765
 
8693
8766
  // @ts-check
@@ -8715,6 +8788,7 @@ async function mergeBlobs({
8715
8788
  * @param {boolean} args.fastForwardOnly
8716
8789
  * @param {boolean} args.dryRun
8717
8790
  * @param {boolean} args.noUpdateBranch
8791
+ * @param {boolean} args.abortOnConflict
8718
8792
  * @param {string} [args.message]
8719
8793
  * @param {Object} args.author
8720
8794
  * @param {string} args.author.name
@@ -8728,6 +8802,7 @@ async function mergeBlobs({
8728
8802
  * @param {number} args.committer.timezoneOffset
8729
8803
  * @param {string} [args.signingKey]
8730
8804
  * @param {SignCallback} [args.onSign] - a PGP signing implementation
8805
+ * @param {MergeDriverCallback} [args.mergeDriver]
8731
8806
  *
8732
8807
  * @returns {Promise<MergeResult>} Resolves to a description of the merge operation
8733
8808
  *
@@ -8735,6 +8810,7 @@ async function mergeBlobs({
8735
8810
  async function _merge({
8736
8811
  fs,
8737
8812
  cache,
8813
+ dir,
8738
8814
  gitdir,
8739
8815
  ours,
8740
8816
  theirs,
@@ -8742,11 +8818,13 @@ async function _merge({
8742
8818
  fastForwardOnly = false,
8743
8819
  dryRun = false,
8744
8820
  noUpdateBranch = false,
8821
+ abortOnConflict = true,
8745
8822
  message,
8746
8823
  author,
8747
8824
  committer,
8748
8825
  signingKey,
8749
8826
  onSign,
8827
+ mergeDriver,
8750
8828
  }) {
8751
8829
  if (ours === undefined) {
8752
8830
  ours = await _currentBranch({ fs, gitdir, fullname: true });
@@ -8806,14 +8884,17 @@ async function _merge({
8806
8884
  const tree = await mergeTree({
8807
8885
  fs,
8808
8886
  cache,
8887
+ dir,
8809
8888
  gitdir,
8810
8889
  ourOid,
8811
8890
  theirOid,
8812
8891
  baseOid,
8813
- ourName: ours,
8892
+ ourName: abbreviateRef(ours),
8814
8893
  baseName: 'base',
8815
- theirName: theirs,
8894
+ theirName: abbreviateRef(theirs),
8816
8895
  dryRun,
8896
+ abortOnConflict,
8897
+ mergeDriver,
8817
8898
  });
8818
8899
  if (!message) {
8819
8900
  message = `Merge branch '${abbreviateRef(theirs)}' into ${abbreviateRef(
@@ -10905,15 +10986,63 @@ async function log({
10905
10986
  /**
10906
10987
  * Merge two branches
10907
10988
  *
10908
- * ## Limitations
10909
- *
10910
- * Currently it does not support incomplete merges. That is, if there are merge conflicts it cannot solve
10911
- * with the built in diff3 algorithm it will not modify the working dir, and will throw a [`MergeNotSupportedError`](./errors.md#mergenotsupportedError) error.
10912
- *
10913
10989
  * Currently it will fail if multiple candidate merge bases are found. (It doesn't yet implement the recursive merge strategy.)
10914
10990
  *
10915
10991
  * Currently it does not support selecting alternative merge strategies.
10916
10992
  *
10993
+ * Currently it is not possible to abort an incomplete merge. To restore the worktree to a clean state, you will need to checkout an earlier commit.
10994
+ *
10995
+ * Currently it does not directly support the behavior of `git merge --continue`. To complete a merge after manual conflict resolution, you will need to add and commit the files manually, and specify the appropriate parent commits.
10996
+ *
10997
+ * ## Manually resolving merge conflicts
10998
+ * By default, if isomorphic-git encounters a merge conflict it cannot resolve using the builtin diff3 algorithm or provided merge driver, it will abort and throw a `MergeNotSupportedError`.
10999
+ * This leaves the index and working tree untouched.
11000
+ *
11001
+ * When `abortOnConflict` is set to `false`, and a merge conflict cannot be automatically resolved, a `MergeConflictError` is thrown and the results of the incomplete merge will be written to the working directory.
11002
+ * This includes conflict markers in files with unresolved merge conflicts.
11003
+ *
11004
+ * To complete the merge, edit the conflicting files as you see fit, and then add and commit the resolved merge.
11005
+ *
11006
+ * For a proper merge commit, be sure to specify the branches or commits you are merging in the `parent` argument to `git.commit`.
11007
+ * For example, say we are merging the branch `feature` into the branch `main` and there is a conflict we want to resolve manually.
11008
+ * The flow would look like this:
11009
+ *
11010
+ * ```
11011
+ * await git.merge({
11012
+ * fs,
11013
+ * dir,
11014
+ * ours: 'main',
11015
+ * theirs: 'feature',
11016
+ * abortOnConflict: false,
11017
+ * }).catch(e => {
11018
+ * if (e instanceof Errors.MergeConflictError) {
11019
+ * console.log(
11020
+ * 'Automatic merge failed for the following files: '
11021
+ * + `${e.data}. `
11022
+ * + 'Resolve these conflicts and then commit your changes.'
11023
+ * )
11024
+ * } else throw e
11025
+ * })
11026
+ *
11027
+ * // This is the where we manually edit the files that have been written to the working directory
11028
+ * // ...
11029
+ * // Files have been edited and we are ready to commit
11030
+ *
11031
+ * await git.add({
11032
+ * fs,
11033
+ * dir,
11034
+ * filepath: '.',
11035
+ * })
11036
+ *
11037
+ * await git.commit({
11038
+ * fs,
11039
+ * dir,
11040
+ * ref: 'main',
11041
+ * message: "Merge branch 'feature' into main",
11042
+ * parent: ['main', 'feature'], // Be sure to specify the parents when creating a merge commit
11043
+ * })
11044
+ * ```
11045
+ *
10917
11046
  * @param {object} args
10918
11047
  * @param {FsClient} args.fs - a file system client
10919
11048
  * @param {SignCallback} [args.onSign] - a PGP signing implementation
@@ -10925,6 +11054,7 @@ async function log({
10925
11054
  * @param {boolean} [args.fastForwardOnly = false] - If true, then non-fast-forward merges will throw an Error instead of performing a merge.
10926
11055
  * @param {boolean} [args.dryRun = false] - If true, simulates a merge so you can test whether it would succeed.
10927
11056
  * @param {boolean} [args.noUpdateBranch = false] - If true, does not update the branch pointer after creating the commit.
11057
+ * @param {boolean} [args.abortOnConflict = true] - If true, merges with conflicts will not update the worktree or index.
10928
11058
  * @param {string} [args.message] - Overrides the default auto-generated merge commit message
10929
11059
  * @param {Object} [args.author] - passed to [commit](commit.md) when creating a merge commit
10930
11060
  * @param {string} [args.author.name] - Default is `user.name` config.
@@ -10938,6 +11068,7 @@ async function log({
10938
11068
  * @param {number} [args.committer.timezoneOffset] - Set the committer timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
10939
11069
  * @param {string} [args.signingKey] - passed to [commit](commit.md) when creating a merge commit
10940
11070
  * @param {object} [args.cache] - a [cache](cache.md) object
11071
+ * @param {MergeDriverCallback} [args.mergeDriver] - a [merge driver](mergeDriver.md) implementation
10941
11072
  *
10942
11073
  * @returns {Promise<MergeResult>} Resolves to a description of the merge operation
10943
11074
  * @see MergeResult
@@ -10963,11 +11094,13 @@ async function merge({
10963
11094
  fastForwardOnly = false,
10964
11095
  dryRun = false,
10965
11096
  noUpdateBranch = false,
11097
+ abortOnConflict = true,
10966
11098
  message,
10967
11099
  author: _author,
10968
11100
  committer: _committer,
10969
11101
  signingKey,
10970
11102
  cache = {},
11103
+ mergeDriver,
10971
11104
  }) {
10972
11105
  try {
10973
11106
  assertParameter('fs', _fs);
@@ -10994,6 +11127,7 @@ async function merge({
10994
11127
  return await _merge({
10995
11128
  fs,
10996
11129
  cache,
11130
+ dir,
10997
11131
  gitdir,
10998
11132
  ours,
10999
11133
  theirs,
@@ -11001,11 +11135,13 @@ async function merge({
11001
11135
  fastForwardOnly,
11002
11136
  dryRun,
11003
11137
  noUpdateBranch,
11138
+ abortOnConflict,
11004
11139
  message,
11005
11140
  author,
11006
11141
  committer,
11007
11142
  signingKey,
11008
11143
  onSign,
11144
+ mergeDriver,
11009
11145
  })
11010
11146
  } catch (err) {
11011
11147
  err.caller = 'git.merge';