-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathvmfmerge.py
341 lines (261 loc) · 9.76 KB
/
vmfmerge.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
"""
vmfmerge.py
By DKY
Version 0.1.2 BETA
VMF Merge Tool
"""
__version__ = '0.1.2 BETA'
import os
import sys
import copy
import shutil
from datetime import datetime
from argparse import ArgumentParser
from collections import OrderedDict
from vmf import VMF, InvalidVMF, load_vmfs, get_parent, compare_vmfs
from vmfdelta import (
DeltaMergeConflict,
merge_delta_lists, create_conflict_resolution_deltas,
)
def parse_args(argv):
parser = ArgumentParser(
description="VMF Merge Tool",
)
parser.add_argument(
'--version',
action='version',
version=__version__,
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help="Noisily display progress messages throughout the procedure.",
)
parser.add_argument(
'vmfs',
nargs='+',
metavar='vmf',
help="The name of a *.vmf file, or the path to a *.vmf file.",
)
parser.add_argument(
'-n', '--no-auto-parent',
action='store_true',
help=
"Do not try to automatically figure out which VMF is the parent "
"VMF. Instead, simply assume that the first VMF in the argument "
"list is the parent. (Can be dangerous-- Use with care!)"
,
)
parser.add_argument(
'-i', '--dump-individual',
action='store_true',
help=
"Instead of merging, output a list of individual per-file deltas "
"to stdout."
,
)
parser.add_argument(
'-p', '--dump-proposed',
action='store_true',
help=
"Instead of merging, output a list of all proposed merge deltas "
"to stdout."
,
)
parser.add_argument(
'-A', '--aggressive',
action='store_true',
help="Enable aggressive conflict resolution.",
)
return parser.parse_args(argv)
def do_merge(
parent, children,
dumpIndividual=False, dumpProposed=False,
aggressive=False, verbose=False,
noParentSideEffects=False, noChildSideEffects=False,
update_callback=lambda *args, **kwargs: None
):
""" Performs a merge of the given children's deltas into the parent.
If `dumpIndividual` is True, this prints and returns the individual deltas
for each child (in `{child : deltaList}` form), and does nothing else.
If `dumpProposed` is True, this prints and returns a list of the proposed
merged deltas, and does nothing else.
The `aggressive` flag is not currently implemented.
If `noParentSideEffects` is True, this leaves the given parent VMF
untouched, modifying a deep copy instead of the original.
If `noChildSideEffects` is True, this leaves the given child VMFs untouched,
modifying deep copies instead of the originals.
The `update_callback` will be called at each stage of the process with the
following arguments:
update_callback(message, progress, maxProgress)
... where `message` is a human-readable message describing the current
stage, `progress` is a 1-indexed integer representing the current stage
number, and `maxProgress` is the number of required stages in this merge.
If both `dumpIndividual` and `dumpProposed` are False, this function
returns a list of deltas that were found to conflict during the merge.
If there were no conflicted deltas found during the process, an empty list
is returned.
"""
class ProgressTracker:
# Not including the conflict resolution step.
NUM_MERGE_STEPS = 3
def __init__(self, children):
self.progress = 0
self.maxProgress = (
int(noParentSideEffects)
+ int(noChildSideEffects) * len(children)
+ len(children)
+ self.NUM_MERGE_STEPS
)
def update(
self,
message,
preIncrement=False, postIncrement=True,
finished=False,
):
if preIncrement:
self.progress += 1
print(message)
update_callback(
message,
progress=min(self.progress, self.maxProgress),
maxProgress=self.maxProgress,
finished=finished,
)
if postIncrement:
self.progress += 1
# We're gonna be modifying this soon.
children = children[:]
progressTracker = ProgressTracker(children)
# If we don't want side-effects on the parent VMF, we should deep-copy it.
if noParentSideEffects:
progressTracker.update("Preparing parent VMF for merge...")
parent = copy.deepcopy(parent)
# If we don't want side-effects on the child VMFs, we should deep-copy them.
if noChildSideEffects:
for i, child in enumerate(children):
progressTracker.update(
"Preparing {} for merge...".format(child.get_filename())
)
children[i] = copy.deepcopy(child)
# Generate lists of deltas for each child.
deltaListForChild = OrderedDict()
for i, child in enumerate(children):
progressTracker.update(
"Generating delta list for {}...".format(
os.path.basename(child.path)
)
)
deltas = compare_vmfs(parent, child)
deltaListForChild[child] = deltas
if dumpIndividual:
for child, deltas in deltaListForChild.items():
print("Deltas for {}:".format(child.path))
print('\n'.join(repr(delta) for delta in deltas))
print("")
return deltaListForChild
# Fix up all deltas so that they have references to their origin VMF.
for child, deltas in deltaListForChild.items():
for delta in deltas:
delta.originVMF = child
# Merge the delta lists into a single list of deltas, to be applied on top
# of the parent.
progressTracker.update("Merging deltas...")
deltaLists = list(deltaListForChild.values())
try:
mergedDeltas = merge_delta_lists(
deltaLists,
aggressive=aggressive,
verbose=verbose,
)
except DeltaMergeConflict as e:
print(str(e))
mergedDeltas = e.partialDeltas
print("")
print("Conflicted deltas:")
conflictedDeltas = e.conflictedDeltas
for delta in conflictedDeltas:
print("From {}:".format(delta.get_origin_filename()), repr(delta))
print("")
progressTracker.maxProgress += 1
progressTracker.update("Creating Manual Merge VisGroups...")
conflictResolutionDeltas = create_conflict_resolution_deltas(
parent, conflictedDeltas,
verbose=verbose,
)
if verbose:
print()
print("Conflict resolution deltas:")
print('\n'.join(repr(delta) for delta in conflictResolutionDeltas))
print()
mergedDeltas += conflictResolutionDeltas
else:
conflictedDeltas = []
if dumpProposed:
print("Merged deltas:")
print('\n'.join(repr(delta) for delta in mergedDeltas))
return mergedDeltas
# Apply the merged deltas to the parent.
progressTracker.update("Applying deltas...")
parent.apply_deltas(mergedDeltas, verbose=verbose)
# Write the mutated parent to the target VMF path.
progressTracker.update("Writing merged VMF...")
def get_merged_vmf_path(parentPath):
parentDir = os.path.dirname(parentPath)
parentFileName = os.path.basename(parentPath)
parentName, ext = os.path.splitext(parentFileName)
mergedFileName = parentName + '_merged' + ext
mergedFilePath = os.path.join(parentDir, mergedFileName)
# Make sure the output filename is unique...
i = 0
while os.path.exists(mergedFilePath):
mergedFileName = parentName + '_merged_' + str(i) + ext
mergedFilePath = os.path.join(parentDir, mergedFileName)
i += 1
return mergedFilePath
parent.write_path(get_merged_vmf_path(parent.path))
# parent.write_path('out.vmf')
# Done!
progressTracker.update("Done!", finished=True)
return conflictedDeltas
def main(argv):
args = parse_args(argv[1:])
vmfPaths = args.vmfs
verbose = args.verbose
aggressive = args.aggressive
autoParent = not args.no_auto_parent
dumpIndividual = args.dump_individual
dumpProposed = args.dump_proposed
if dumpProposed and dumpIndividual:
sys.stderr.write(
"ERROR: --dump-individual and --dump-proposed are mutually "
"exclusive!\n"
)
return 1
startTime = datetime.now()
# Load all VMFs.
print("Loading VMFs...")
try:
vmfs = load_vmfs(vmfPaths)
except InvalidVMF as e:
sys.stderr.write(
"ERROR: {} is invalid: {}\n".format(e.path, e.message)
)
return 1
# Determine the parent VMF.
if autoParent:
parent = get_parent(vmfs)
else:
parent = vmfs[0]
# Determine the child VMFs.
children = [vmf for vmf in vmfs if vmf is not parent]
# Go!
do_merge(
parent, children,
dumpIndividual, dumpProposed,
aggressive, verbose,
)
print("Total time: {}".format(datetime.now() - startTime))
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))