-
Notifications
You must be signed in to change notification settings - Fork 2
/
auto_merge.lua
468 lines (396 loc) · 13.9 KB
/
auto_merge.lua
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
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
package.path = "./xml2lua/?.lua;" .. package.path
local xml2lua = require("xml2lua")
local xmlhandler = require("xmlhandler.tree")
local string_gsub = string.gsub
local string_format = string.format
local table_insert = table.insert
local table_sort = table.sort
local function execute_command_without_format(cmd)
local handle = io.popen(cmd, "r")
local t = {}
for line in handle:lines() do
t[#t + 1] = line
end
local tbl_rc = {handle:close()}
if tbl_rc[1] ~= true then
print("Error|execute_command|[" .. cmd .. "]")
for _, v in ipairs(t) do
print(string_format("%s", v))
end
for k, v in pairs(tbl_rc) do
print(string_format("k(%s) v(%s)", k, v))
end
assert(false)
end
return t, tbl_rc
end
local function execute_command(fmt, ...)
local handle = io.popen(string_format(fmt, ...), "r")
local t = {}
for line in handle:lines() do
t[#t + 1] = line
end
local tbl_rc = {handle:close()}
if tbl_rc[1] ~= true then
print(string_format("Error|execute_command|[%s]", string_format(fmt, ...)))
for _, v in ipairs(t) do
print(string_format("%s", v))
end
for k, v in pairs(tbl_rc) do
print(string_format("k(%s) v(%s)", k, v))
end
assert(false)
end
return t, tbl_rc
end
local function svn_command(fmt, ...)
return execute_command(string_format(_ENV.SVN_CMD .. " " .. fmt, ...))
end
local function string_split(str, sep, func)
local tbl_fields = {}
local pattern = string_format("([^%s]+)", sep)
if func then
string_gsub(str, pattern, function(c)
tbl_fields[#tbl_fields + 1] = func(c)
end)
else
string_gsub(str, pattern, function(c)
tbl_fields[#tbl_fields + 1] = c
end)
end
return tbl_fields
end
local function hash_table_sort(t, sort_func)
local array = {}
for k, v in pairs(t) do
table_insert(array, {key = k, value = v})
end
table_sort(array, sort_func)
return array
end
local function get_log(svn_path, begin_revision, end_revision)
local xml_parser = xml2lua.parser(xmlhandler)
end_revision = end_revision or "HEAD"
xml_parser:parse( table.concat(svn_command("log %s -r%s:%s --xml", svn_path, begin_revision, end_revision), "\n") )
local tbl_log
-- 当 begin_revision - end_revision 两个版本之间没有任务变更时, xmlhandler.root.log.logentry 为 nil
if xmlhandler.root.log.logentry == nil then
tbl_log = {}
else
tbl_log = xmlhandler.root.log.logentry
-- 当 begin_revision - end_revision 两个版本之间只有一条变更记录时, xmlhandler.root.log.logentry 不会以序列的形式返回
-- 详见 https://github.com/manoelcampos/xml2lua/blob/master/example1.lua#L25-L31
if #tbl_log <= 0 then
tbl_log = {tbl_log}
else
tbl_log = xmlhandler.root.log.logentry
end
end
local t = {}
for k, v in ipairs(tbl_log) do
t[#t + 1] = {revision = tonumber(v._attr.revision), author = v.author, msg = v.msg, date = v.date}
end
return true, t
end
--
local function revert(relative_to_root_path)
svn_command("revert --depth infinity %s", relative_to_root_path)
svn_command("resolve --accept working %s", relative_to_root_path)
end
--
local function update_dir(workdir)
svn_command("update %s", workdir)
end
local tbl_merged_item_op = {}
tbl_merged_item_op['A'] = true
tbl_merged_item_op['D'] = true
tbl_merged_item_op['C'] = true
tbl_merged_item_op['U'] = true
-- 格式化参数
local function format_vars(fmt, tbl_vars)
if tbl_vars then
return (string_gsub(fmt, "%${([%w_]+)}", tbl_vars))
else
return fmt
end
end
--
local function merge(commit_log_fmt, svn_relative_to_root_path, svn_log, workdir, tbl_execlude_path)
-- 每次 merge 前都执行 update, 防止出现 svn: E195020: Cannot merge into mixed-revision working copy [xxx:xxx]; try updating first
update_dir(workdir)
local tbl_merge_response = svn_command("merge ^%s -c%s %s --accept 'postpone'", svn_relative_to_root_path, svn_log.revision, workdir)
if #tbl_merge_response <= 0 then
-- svn merge 没有任何返回, 通常是已经合并过代码
return true, {}
end
-- 检查第一行的返回信息 --- Merging rxxx into '/xxx/':
local merge_revision = tonumber(tbl_merge_response[1]:match("^%-%-%- Merging r(.*) into .*$"))
if not merge_revision then
return false, string_format("error|merge|invalid first line of response|%s", tbl_merge_response[1])
end
-- 返回信息的版本不一致
if merge_revision ~= svn_log.revision then
return false, string_format("error|merge|merge_revision not match|%s|%s", merge_revision, svn_log.revision)
end
-- 冲突文件列表
local tbl_conflicts = {} -- = {{file}, ...}
-- 非冲突文件列表
local tbl_normal = {} -- = {{file}, ...}
-- 从第二行开始获取变更的文件列表
local tbl_revert_path = {} -- 已经 revert 过的 svn 相对路径
for i = 2, #tbl_merge_response do
-- Summary of conflicts: 之后的信息不处理
if tbl_merge_response[i] == "Summary of conflicts:" then
break
end
local op, file = tbl_merge_response[i]:match("^ *(%a) *(%/.*)$")
if not tbl_merged_item_op[op] then
return false, string_format("error|merge|invalid merged item|%s|%s", tbl_merge_response[i], op)
end
-- 检查排除的路径
for _, execlude_path in ipairs(tbl_execlude_path) do
-- 获取相对于 svn 分支目录的相对路径
local relative_to_root_path = file:match(string_format("^%s(.*)", workdir:gsub("%p","%%%0")))
if relative_to_root_path:match(execlude_path) then
-- revert 过父目录, 子目录不再进行 revert
for _, revert_path in ipairs(tbl_revert_path) do
if file:match(string_format("^%s(.*)", revert_path:gsub("%p", "%%%0"))) then
goto continue
end
end
print(string_format("\tSkip|file(%s)", relative_to_root_path))
revert(file) -- 当文件路径中包含 @ 字符时, 需要在未尾也加上 @
table_insert(tbl_revert_path, file)
goto continue
end
end
-- 打印合并操作
print(string_format("\t%s\t%s", op, file))
-- 记录冲突文件并还原
if op == 'C' then
tbl_conflicts[#tbl_conflicts + 1] = file
revert(file)
print(string_format("\trevert %s", file)) -- 打印回滚的路径
else
tbl_normal[#tbl_normal + 1] = file
end
::continue::
end
if #tbl_normal > 0 then
local commit_log = format_vars(commit_log_fmt, {from_svn_relative_to_root_path = svn_relative_to_root_path, from_revision = svn_log.revision, from_commit_log = svn_log.msg})
execute_command_without_format(_ENV.SVN_CMD .. " " .. string_format([[commit -m"%s" %s]], commit_log:gsub("\"","\\\""), workdir))
end
return true, tbl_conflicts
end
local function write_file(file_name, mode, fmt, ...)
local f = assert(io.open(file_name, mode), string_format("write_file|file_name(%s)", file_name))
local success, msg = f:write(string_format(fmt, ...))
if not success then
error(msg)
end
f:close()
end
local function read_file(file_name, check_file_exist)
if check_file_exist == nil then
check_file_exist = true
end
local f = io.open(file_name, "r")
if not f then
if check_file_exist then
error(string_format("read_file|file_name(%s)", file_name))
else
return ""
end
end
local s = f:read("a")
if not s then
error(string_format("read_file|file_name(%s)", file_name))
end
f:close()
return s
end
local function check_exclude(svn_log_info, tbl_execlude_rule)
for _, execlude_rule in ipairs(tbl_execlude_rule) do
for k, v in pairs(execlude_rule) do
-- 配置了非法字段, 跳过不处理
if not svn_log_info[k] then
goto continue_1
end
-- 排除规则不匹配
if svn_log_info[k] ~= v then
goto continue_2
end
::continue_1::
end
-- 排除规则完全匹配
-- print(string_format("\tMatch exclude rule|revision(%s)|author(%s)|msg(%s)", execlude_rule.revision, execlude_rule.author, execlude_rule.msg))
print(string_format("\tSkip|revision(%s)|author(%s)|msg(%s)", svn_log_info.revision, svn_log_info.author, svn_log_info.msg))
if true then
return true
end
::continue_2::
end
return false
end
local function get_local_svn_relative_to_root_path(local_path, svn_url)
local tbl_response = svn_command("info %s", local_path)
for k, v in pairs(tbl_response) do
-- URL: http://xxx.xxx
local svn_relative_to_root_path = v:match(string_format("^URL: %s(.*)$", svn_url))
if svn_relative_to_root_path then
return svn_relative_to_root_path
end
end
return nil
end
local function auto_merge(config_file, begin_revision, end_revision)
local config = require(config_file)
assert(type(config) == "table", "Invalid config")
local svn_url = config.svn_url
local svn_relative_to_root_path = config.svn_relative_to_root_path
local workdir = config.workdir
local report_file = config.report_file
local last_merged_revision_store = config.last_merged_revision_store
local tbl_execlude_rule = config.execlude_rule
local tbl_execlude_path = config.execlude_path
local svn_path = string_format("%s%s", svn_url, svn_relative_to_root_path)
local last_merged_revision = tonumber(read_file(last_merged_revision_store, false))
local commit_log_fmt = config.commit_log_fmt or [[merge from ${from_svn_relative_to_root_path} ${from_revision}]]
local abort = config.abort
assert(begin_revision or last_merged_revision, string_format("begin_revision(%s)|last_merged_revision(%s)|you must specify the revision", begin_revision, last_merged_revision))
_ENV.SVN_CMD = config.svn_cmd
local tbl_final_report = {} --[[
= {
[author] = {
[revition] = {
relative_to_root_path,
...
},
...
},
...
}
]]
revert(workdir)
update_dir(workdir)
local success, msg = get_log(svn_path, begin_revision or last_merged_revision, end_revision)
if success then
local tbl_svn_log = msg
for _, svn_log in ipairs(tbl_svn_log) do
if begin_revision then
if svn_log.revision < begin_revision then
goto continue
end
else
if svn_log.revision <= last_merged_revision then
goto continue
end
end
--
print(string_format("merge revision = [%s], author = [%s]", svn_log.revision, svn_log.author))
if check_exclude(svn_log, tbl_execlude_rule) then
goto continue
end
--
success, msg = merge(commit_log_fmt, svn_relative_to_root_path, svn_log, workdir, tbl_execlude_path)
if not success then
error(msg)
else
local tbl_conflicts = msg
if #tbl_conflicts > 0 then
local f = io.open(report_file, "a") or io.output()
for _, conflicts_file in ipairs(tbl_conflicts) do
tbl_final_report[svn_log.author] = tbl_final_report[svn_log.author] or {}
tbl_final_report[svn_log.author][svn_log.revision] = tbl_final_report[svn_log.author][svn_log.revision] or {}
-- 获取相对于 svn 分支目录的相对路径
local relative_to_root_path = conflicts_file:match(string_format("^%s(.*)", workdir:gsub("%p","%%%0")))
tbl_final_report[svn_log.author][svn_log.revision][#tbl_final_report[svn_log.author][svn_log.revision] + 1] = relative_to_root_path
f:write(string_format("%s|%s|%s\n", svn_log.author, svn_log.revision, relative_to_root_path))
end
f:close()
-- 当合并出现冲突时, 停止合并
if abort == true then
break
end
end
end
::continue::
write_file(last_merged_revision_store, "w", "%s", svn_log.revision)
end
-- 输出最终报告
local target = get_local_svn_relative_to_root_path(workdir, svn_url)
local f = io.output()
if next(tbl_final_report) then
f:write("------------------------------------------------------------------------\n")
f:write("Summary of conflicts:\n")
end
for author, v1 in pairs(tbl_final_report) do
f:write(string_format("%s\n", author))
for revision, tbl_relative_to_root_path in pairs(v1) do
f:write(string_format("\t merge %s %s to %s\n", svn_relative_to_root_path, revision, target))
for _, relative_to_root_path in ipairs(tbl_relative_to_root_path) do
f:write(string_format("\t\t%s\n", relative_to_root_path))
end
end
end
f:close()
else
error(msg)
end
end
local function print_conflicts(config_file)
local config = require(config_file)
_ENV.SVN_CMD = config.svn_cmd
assert(type(config) == "table", "Invalid config")
local report_file = config.report_file
local svn_url = config.svn_url
local svn_relative_to_root_path = config.svn_relative_to_root_path
local workdir = config.workdir
local target = get_local_svn_relative_to_root_path(workdir, svn_url)
local tbl_final_report = {}
local f = io.open(report_file, "r")
if not f then
print(string_format("can not found report_file(%s)", report_file))
return
end
for line in f:lines() do
if line == "" then
goto continue
end
local author, revision, file = line:match("(.*)%|(.*)%|(.*)")
if not author then
error(string_format("line(%s)", line))
end
tbl_final_report[author] = tbl_final_report[author] or {}
tbl_final_report[author][revision] = tbl_final_report[author][revision] or {}
tbl_final_report[author][revision][#tbl_final_report[author][revision] + 1] = file
::continue::
end
f = io.output()
if next(tbl_final_report) then
f:write("------------------------------------------------------------------------\n")
f:write("Summary of conflicts:\n")
end
for author, v1 in pairs(tbl_final_report) do
f:write(string_format("%s\n", author))
for _, v2 in pairs(hash_table_sort(v1, function(a, b) return a.key < b.key end)) do -- .key 即为 revision
local revision = v2.key
local tbl_relative_to_root_path = v2.value
f:write(string_format("\t merge %s %s to %s\n", svn_relative_to_root_path, revision, target))
for _, relative_to_root_path in ipairs(tbl_relative_to_root_path) do
f:write(string_format("\t\t%s\n", relative_to_root_path))
end
end
end
f:close()
end
--
local oper_type = select(1, ...)
local config_file = select(2, ...)
local begin_revision = tonumber(select(3, ...) or "")
local end_revision = tonumber(select(4, ...) or "")
local tbl_oper_func = {}
tbl_oper_func["print_conflicts"] = print_conflicts
tbl_oper_func["auto_merge"] = auto_merge
local func = assert(tbl_oper_func[oper_type], string_format("Invalid oper_type(%s)", oper_type))
func(config_file, begin_revision, end_revision)