• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1const parseJSON = require('json-parse-even-better-errors')
2const { diff } = require('just-diff')
3const { diffApply } = require('just-diff-apply')
4
5const globalObjectProperties = Object.getOwnPropertyNames(Object.prototype)
6
7const stripBOM = content => {
8  content = content.toString()
9  // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
10  // because the buffer-to-string conversion in `fs.readFileSync()`
11  // translates it to FEFF, the UTF-16 BOM.
12  if (content.charCodeAt(0) === 0xFEFF) {
13    content = content.slice(1)
14  }
15  return content
16}
17
18const PARENT_RE = /\|{7,}/g
19const OURS_RE = /<{7,}/g
20const THEIRS_RE = /={7,}/g
21const END_RE = />{7,}/g
22
23const isDiff = str =>
24  str.match(OURS_RE) && str.match(THEIRS_RE) && str.match(END_RE)
25
26const parseConflictJSON = (str, reviver, prefer) => {
27  prefer = prefer || 'ours'
28  if (prefer !== 'theirs' && prefer !== 'ours') {
29    throw new TypeError('prefer param must be "ours" or "theirs" if set')
30  }
31
32  str = stripBOM(str)
33
34  if (!isDiff(str)) {
35    return parseJSON(str)
36  }
37
38  const pieces = str.split(/[\n\r]+/g).reduce((acc, line) => {
39    if (line.match(PARENT_RE)) {
40      acc.state = 'parent'
41    } else if (line.match(OURS_RE)) {
42      acc.state = 'ours'
43    } else if (line.match(THEIRS_RE)) {
44      acc.state = 'theirs'
45    } else if (line.match(END_RE)) {
46      acc.state = 'top'
47    } else {
48      if (acc.state === 'top' || acc.state === 'ours') {
49        acc.ours += line
50      }
51      if (acc.state === 'top' || acc.state === 'theirs') {
52        acc.theirs += line
53      }
54      if (acc.state === 'top' || acc.state === 'parent') {
55        acc.parent += line
56      }
57    }
58    return acc
59  }, {
60    state: 'top',
61    ours: '',
62    theirs: '',
63    parent: '',
64  })
65
66  // this will throw if either piece is not valid JSON, that's intended
67  const parent = parseJSON(pieces.parent, reviver)
68  const ours = parseJSON(pieces.ours, reviver)
69  const theirs = parseJSON(pieces.theirs, reviver)
70
71  return prefer === 'ours'
72    ? resolve(parent, ours, theirs)
73    : resolve(parent, theirs, ours)
74}
75
76const isObj = obj => obj && typeof obj === 'object'
77
78const copyPath = (to, from, path, i) => {
79  const p = path[i]
80  if (isObj(to[p]) && isObj(from[p]) &&
81      Array.isArray(to[p]) === Array.isArray(from[p])) {
82    return copyPath(to[p], from[p], path, i + 1)
83  }
84  to[p] = from[p]
85}
86
87// get the diff from parent->ours and applying our changes on top of theirs.
88// If they turned an object into a non-object, then put it back.
89const resolve = (parent, ours, theirs) => {
90  const dours = diff(parent, ours)
91  for (let i = 0; i < dours.length; i++) {
92    if (globalObjectProperties.find(prop => dours[i].path.includes(prop))) {
93      continue
94    }
95    try {
96      diffApply(theirs, [dours[i]])
97    } catch (e) {
98      copyPath(theirs, ours, dours[i].path, 0)
99    }
100  }
101  return theirs
102}
103
104module.exports = Object.assign(parseConflictJSON, { isDiff })
105