Skip to content

Commit abf8ffd

Browse files
committed
Working state handling composer.json and .lock files
0 parents  commit abf8ffd

File tree

1 file changed

+163
-0
lines changed

1 file changed

+163
-0
lines changed

merge.php

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
define('CONFLICT_PLACEHOLDER', '=c=o=n=f=(\d+)=l=i=c=t=');
5+
define('JSON_ENCODE_OPTIONS', JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
6+
// file states
7+
$states = ['ancestor', 'ours', 'theirs'];
8+
// unique object that represents a void
9+
$void = (object) [];
10+
// conflicts we have identified
11+
$conflicts = [];
12+
// options for json encode
13+
$markerLen = $argv[4];
14+
15+
// grab the contents of each file state
16+
foreach ($states as $i => $state) {
17+
// read and parse the composer file
18+
$$state = json_decode(file_get_contents($argv[$i + 1]), true);
19+
20+
// check for malformed json
21+
if (json_last_error() !== JSON_ERROR_NONE || !is_array($$state)) {
22+
exit(1);
23+
}
24+
}
25+
26+
// determine whether an array is assoc
27+
function isAssoc($array) {
28+
return array_keys($array) !== range(0, count($array) - 1);
29+
}
30+
31+
// create an auto-incrementing conflict placeholder
32+
function conflictPlaceholder($ours, $theirs) {
33+
global $conflicts;
34+
$conflicts[] = [$ours, $theirs];
35+
return str_replace('(\d+)', count($conflicts) - 1, CONFLICT_PLACEHOLDER);
36+
}
37+
38+
// merge a set of assoc arrays
39+
function merge($ancestor, $ours, $theirs) {
40+
global $states, $void, $conflict;
41+
42+
// get the unique union of all keys from theirs and ours
43+
$keys = array_keys($ours + $theirs);
44+
45+
// reconcile each key
46+
foreach ($keys as $key) {
47+
// get the value of this key from each state
48+
foreach ($states as $state) {
49+
${rtrim($state, 's') . 'Value'} = is_array($$state) && array_key_exists($key, $$state) ? ${$state}[$key] : $void;
50+
}
51+
52+
// same value? or theirs was the same as the ancestor? all good!
53+
if ($ourValue === $theirValue || $theirValue === $ancestorValue) {
54+
continue;
55+
}
56+
57+
// is their value newer? (i.e. ours is the same as the ancestor)
58+
if ($ourValue === $ancestorValue) {
59+
// is this key absent in theirs?
60+
if ($theirValue === $void) {
61+
unset($ours[$key]);
62+
} else {
63+
$ours[$key] = $theirValue;
64+
}
65+
}
66+
// are both values arrays?
67+
else if (is_array($ourValue) && is_array($theirValue)) {
68+
// are the arrays both assoc or both non-assoc?
69+
if (($isAssoc = isAssoc($ourValue)) === isAssoc($theirValue)) {
70+
// are they both assoc?
71+
if ($isAssoc) {
72+
$ours[$key] = merge($ancestorValue, $ourValue, $theirValue);
73+
}
74+
// both non-assoc
75+
else {
76+
// @todo provide option to merge these arrays?
77+
$ours[$key] = conflictPlaceholder($ourValue, $theirValue);
78+
}
79+
}
80+
// one is assoc, the other is non-assoc - that's a conflict!
81+
else {
82+
$ours[$key] = conflictPlaceholder($ourValue, $theirValue);
83+
}
84+
}
85+
// differing values - we have a conflict!
86+
else {
87+
$ours[$key] = conflictPlaceholder($ourValue, $theirValue);
88+
}
89+
}
90+
91+
return $ours;
92+
}
93+
94+
// special handling for lock files
95+
if (strtolower($argv[5]) === 'composer.lock') {
96+
$packageKeys = ['packages', 'packages-dev', 'aliases'];
97+
98+
foreach ($states as $state) {
99+
// ensure we don't get a conflict on the content hash
100+
${$state}['content-hash'] = null;
101+
102+
// convert package array to assoc array, mapping package name to json-encoded package definition
103+
foreach ($packageKeys as $key) {
104+
$newArray = [];
105+
foreach (${$state}[$key] as $package) {
106+
// @todo this is different for alias
107+
$newArray[$package['name']] = json_encode($package);
108+
}
109+
${$state}[$key] = $newArray;
110+
}
111+
}
112+
113+
// perform the merge
114+
$merged = merge($ancestor, $ours, $theirs);
115+
116+
// convert the package arrays back
117+
foreach ($packageKeys as $key) {
118+
// sort the packages
119+
ksort($merged[$key]);
120+
121+
$newArray = [];
122+
foreach ($merged[$key] as $package) {
123+
// if we have a conflict, convert the conflicting values back to package definition arrays
124+
if (preg_match('/^' . CONFLICT_PLACEHOLDER . '$/', $package, $matches)) {
125+
$conflicts[$matches[1]] = array_map(function($json) {
126+
return json_decode($json, true);
127+
}, $conflicts[$matches[1]]);
128+
$newArray[] = $package;
129+
}
130+
// otherwise just convert the value back to a package definition array
131+
else {
132+
$newArray[] = json_decode($package, true);
133+
}
134+
}
135+
$merged[$key] = $newArray;
136+
}
137+
138+
// update the content-hash key
139+
$merged['content-hash'] = (count($conflicts) ? 'Merge conflict!' : 'Auto-merged!') . ' Run `composer update --lock` to regenerate';
140+
141+
$merged = json_encode($merged, JSON_ENCODE_OPTIONS);
142+
} else {
143+
$merged = json_encode(merge($ancestor, $ours, $theirs), JSON_ENCODE_OPTIONS);
144+
}
145+
146+
// if we have conflicts, replace the conflict markers with the actual conflicting values
147+
if (count($conflicts)) {
148+
$merged = preg_replace_callback('/^(\s+)(.+)"' . CONFLICT_PLACEHOLDER . '"(,?)$/m', function ($matches) use ($conflicts, $markerLen) {
149+
list(, $space, $property, $conflictNum, $comma) = $matches;
150+
// replace the conflict placeholder with theirs/ours values surrounded by conflict markers
151+
return str_repeat('<', $markerLen) . " HEAD\n"
152+
. $space . $property . preg_replace('/\n/', "\n$space", json_encode($conflicts[$conflictNum][1], JSON_ENCODE_OPTIONS)) . $comma . "\n"
153+
. str_repeat('=', $markerLen) . "\n"
154+
. $space . $property . preg_replace('/\n/', "\n$space", json_encode($conflicts[$conflictNum][0], JSON_ENCODE_OPTIONS)) . $comma . "\n"
155+
. str_repeat('>', $markerLen);
156+
}, $merged);
157+
}
158+
159+
// update the file
160+
file_put_contents($argv[2], $merged . "\n");
161+
162+
// determine exit status based on whether there were conflicts
163+
exit(count($conflicts) ? 1 : 0);

0 commit comments

Comments
 (0)