1-- Copyright (C) 2003 Graydon Hoare <graydon@pobox.com> 2-- 3-- This program is made available under the GNU GPL version 2.0 or 4-- greater. See the accompanying file COPYING for details. 5-- 6-- This program is distributed WITHOUT ANY WARRANTY; without even the 7-- implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 8-- PURPOSE. 9 10-- this is the standard set of lua hooks for monotone; 11-- user-provided files can override it or add to it. 12 13-- Since Lua 5.2, unpack and loadstrings are deprecated and are either moved 14-- to table.unpack() or replaced by load(). If lua was compiled without 15-- LUA_COMPAT_UNPACK and/or LUA_COMPAT_LOADSTRING, these two are not 16-- available and we add a similar compatibility layer, ourselves. 17unpack = unpack or table.unpack 18loadstring = loadstring or load 19 20function temp_file(namehint, filemodehint) 21 local tdir 22 tdir = os.getenv("TMPDIR") 23 if tdir == nil then tdir = os.getenv("TMP") end 24 if tdir == nil then tdir = os.getenv("TEMP") end 25 if tdir == nil then tdir = "/tmp" end 26 local filename 27 if namehint == nil then 28 filename = string.format("%s/mtn.XXXXXX", tdir) 29 else 30 filename = string.format("%s/mtn.%s.XXXXXX", tdir, namehint) 31 end 32 local filemode 33 if filemodehint == nil then 34 filemode = "r+" 35 else 36 filemode = filemodehint 37 end 38 local name = mkstemp(filename) 39 local file = io.open(name, filemode) 40 return file, name 41end 42 43function execute(path, ...) 44 local pid 45 local ret = -1 46 pid = spawn(path, ...) 47 if (pid ~= -1) then ret, pid = wait(pid) end 48 return ret 49end 50 51function execute_redirected(stdin, stdout, stderr, path, ...) 52 local pid 53 local ret = -1 54 io.flush(); 55 pid = spawn_redirected(stdin, stdout, stderr, path, ...) 56 if (pid ~= -1) then ret, pid = wait(pid) end 57 return ret 58end 59 60-- Wrapper around execute to let user confirm in the case where a subprocess 61-- returns immediately 62-- This is needed to work around some brokenness with some merge tools 63-- (e.g. on OS X) 64function execute_confirm(path, ...) 65 ret = execute(path, ...) 66 67 if (ret ~= 0) 68 then 69 print(gettext("Press enter")) 70 else 71 print(gettext("Press enter when the subprocess has completed")) 72 end 73 io.read() 74 return ret 75end 76 77-- attributes are persistent metadata about files (such as execute 78-- bit, ACLs, various special flags) which we want to have set and 79-- re-set any time the files are modified. the attributes themselves 80-- are stored in the roster associated with the revision. each (f,k,v) 81-- attribute triple turns into a call to attr_functions[k](f,v) in lua. 82 83if (attr_init_functions == nil) then 84 attr_init_functions = {} 85end 86 87attr_init_functions["mtn:execute"] = 88 function(filename) 89 if (is_executable(filename)) then 90 return "true" 91 else 92 return nil 93 end 94 end 95 96attr_init_functions["mtn:manual_merge"] = 97 function(filename) 98 if (binary_file(filename)) then 99 return "true" -- binary files must be merged manually 100 else 101 return nil 102 end 103 end 104 105if (attr_functions == nil) then 106 attr_functions = {} 107end 108 109attr_functions["mtn:execute"] = 110 function(filename, value) 111 if (value == "true") then 112 set_executable(filename) 113 else 114 clear_executable(filename) 115 end 116 end 117 118function dir_matches(name, dir) 119 -- helper for ignore_file, matching files within dir, or dir itself. 120 -- eg for dir of 'CVS', matches CVS/, CVS/*, */CVS/ and */CVS/* 121 if (string.find(name, "^" .. dir .. "/")) then return true end 122 if (string.find(name, "^" .. dir .. "$")) then return true end 123 if (string.find(name, "/" .. dir .. "/")) then return true end 124 if (string.find(name, "/" .. dir .. "$")) then return true end 125 return false 126end 127 128function portable_readline(f) 129 line = f:read() 130 if line ~= nil then 131 line = string.gsub(line, "\r$","") -- strip possible \r left from windows editing 132 end 133 return line 134end 135 136function ignore_file(name) 137 -- project specific 138 if (ignored_files == nil) then 139 ignored_files = {} 140 local ignfile = io.open(".mtn-ignore", "r") 141 if (ignfile ~= nil) then 142 local line = portable_readline(ignfile) 143 while (line ~= nil) do 144 if line ~= "" then 145 table.insert(ignored_files, line) 146 end 147 line = portable_readline(ignfile) 148 end 149 io.close(ignfile) 150 end 151 end 152 153 local warn_reported_file = false 154 for i, line in pairs(ignored_files) 155 do 156 if (line ~= nil) then 157 local pcallstatus, result = pcall(function() 158 return regex.search(line, name) 159 end) 160 if pcallstatus == true then 161 -- no error from the regex.search call 162 if result == true then return true end 163 else 164 -- regex.search had a problem, warn the user their 165 -- .mtn-ignore file syntax is wrong 166 if not warn_reported_file then 167 io.stderr:write("mtn: warning: while matching file '" 168 .. name .. "':\n") 169 warn_reported_file = true 170 end 171 local prefix = ".mtn-ignore:" .. i .. ": warning: " 172 io.stderr:write(prefix 173 .. string.gsub(result, "\n", "\n" .. prefix) 174 .. "\n\t- skipping this regex for " 175 .. "all remaining files.\n") 176 ignored_files[i] = nil 177 end 178 end 179 end 180 181 local file_pats = { 182 -- c/c++ 183 "%.a$", "%.so$", "%.o$", "%.la$", "%.lo$", "^core$", 184 "/core$", "/core%.%d+$", 185 -- java 186 "%.class$", 187 -- python 188 "%.pyc$", "%.pyo$", 189 -- gettext 190 "%.g?mo$", 191 -- intltool 192 "%.intltool%-merge%-cache$", 193 -- TeX 194 "%.aux$", 195 -- backup files 196 "%.bak$", "%.orig$", "%.rej$", "%~$", 197 -- vim creates .foo.swp files 198 "%.[^/]*%.swp$", 199 -- emacs creates #foo# files 200 "%#[^/]*%#$", 201 -- other VCSes (where metadata is stored in named files): 202 "%.scc$", 203 -- desktop/directory configuration metadata 204 "^%.DS_Store$", "/%.DS_Store$", "^desktop%.ini$", "/desktop%.ini$" 205 } 206 207 local dir_pats = { 208 -- autotools detritus: 209 "autom4te%.cache", "%.deps", "%.libs", 210 -- Cons/SCons detritus: 211 "%.consign", "%.sconsign", 212 -- other VCSes (where metadata is stored in named dirs): 213 "CVS", "%.svn", "SCCS", "_darcs", "%.cdv", "%.git", "%.bzr", "%.hg" 214 } 215 216 for _, pat in ipairs(file_pats) do 217 if string.find(name, pat) then return true end 218 end 219 for _, pat in ipairs(dir_pats) do 220 if dir_matches(name, pat) then return true end 221 end 222 223 return false; 224end 225 226-- return true means "binary", false means "text", 227-- nil means "unknown, try to guess" 228function binary_file(name) 229 -- some known binaries, return true 230 local bin_pats = { 231 "%.gif$", "%.jpe?g$", "%.png$", "%.bz2$", "%.gz$", "%.zip$", 232 "%.class$", "%.jar$", "%.war$", "%.ear$" 233 } 234 235 -- some known text, return false 236 local txt_pats = { 237 "%.cc?$", "%.cxx$", "%.hh?$", "%.hxx$", "%.cpp$", "%.hpp$", 238 "%.lua$", "%.texi$", "%.sql$", "%.java$" 239 } 240 241 local lowname=string.lower(name) 242 for _, pat in ipairs(bin_pats) do 243 if string.find(lowname, pat) then return true end 244 end 245 for _, pat in ipairs(txt_pats) do 246 if string.find(lowname, pat) then return false end 247 end 248 249 -- unknown - read file and use the guess-binary 250 -- monotone built-in function 251 return guess_binary_file_contents(name) 252end 253 254-- given a file name, return a regular expression which will match 255-- lines that name top-level constructs in that file, or "", to disable 256-- matching. 257function get_encloser_pattern(name) 258 -- texinfo has special sectioning commands 259 if (string.find(name, "%.texi$")) then 260 -- sectioning commands in texinfo: @node, @chapter, @top, 261 -- @((sub)?sub)?section, @unnumbered(((sub)?sub)?sec)?, 262 -- @appendix(((sub)?sub)?sec)?, @(|major|chap|sub(sub)?)heading 263 return ("^@(" 264 .. "node|chapter|top" 265 .. "|((sub)?sub)?section" 266 .. "|(unnumbered|appendix)(((sub)?sub)?sec)?" 267 .. "|(major|chap|sub(sub)?)?heading" 268 .. ")") 269 end 270 -- LaTeX has special sectioning commands. This rule is applied to ordinary 271 -- .tex files too, since there's no reliable way to distinguish those from 272 -- latex files anyway, and there's no good pattern we could use for 273 -- arbitrary plain TeX anyway. 274 if (string.find(name, "%.tex$") 275 or string.find(name, "%.ltx$") 276 or string.find(name, "%.latex$")) then 277 return ("\\\\(" 278 .. "part|chapter|paragraph|subparagraph" 279 .. "|((sub)?sub)?section" 280 .. ")") 281 end 282 -- There's no good way to find section headings in raw text, and trying 283 -- just gives distracting output, so don't even try. 284 if (string.find(name, "%.txt$") 285 or string.upper(name) == "README") then 286 return "" 287 end 288 -- This default is correct surprisingly often -- in pretty much any text 289 -- written with code-like indentation. 290 return "^[[:alnum:]$_]" 291end 292 293function edit_comment(user_log_message) 294 local exe = nil 295 296 -- top priority is VISUAL, then EDITOR, then a series of hardcoded 297 -- defaults, if available. 298 299 local visual = os.getenv("VISUAL") 300 local editor = os.getenv("EDITOR") 301 if (visual ~= nil) then exe = visual 302 elseif (editor ~= nil) then exe = editor 303 elseif (program_exists_in_path("editor")) then exe = "editor" 304 elseif (program_exists_in_path("vi")) then exe = "vi" 305 elseif (string.sub(get_ostype(), 1, 6) ~= "CYGWIN" and 306 program_exists_in_path("notepad.exe")) then exe = "notepad" 307 else 308 io.write(gettext("Could not find editor to enter commit message\n" 309 .. "Try setting the environment variable EDITOR\n")) 310 return nil 311 end 312 313 local tmp, tname = temp_file() 314 if (tmp == nil) then return nil end 315 tmp:write(user_log_message) 316 if user_log_message == "" or string.sub(user_log_message, -1) ~= "\n" then 317 tmp:write("\n") 318 end 319 io.close(tmp) 320 321 -- By historical convention, VISUAL and EDITOR can contain arguments 322 -- (and, in fact, arbitrarily complicated shell constructs). Since Lua 323 -- has no word-splitting functionality, we invoke the shell to deal with 324 -- anything more complicated than a single word with no metacharacters. 325 -- This, unfortunately, means we have to quote the file argument. 326 327 if (not string.find(exe, "[^%w_.+-]")) then 328 -- safe to call spawn directly 329 if (execute(exe, tname) ~= 0) then 330 io.write(string.format(gettext("Error running editor '%s' ".. 331 "to enter log message\n"), 332 exe)) 333 os.remove(tname) 334 return nil 335 end 336 else 337 -- must use shell 338 local shell = os.getenv("SHELL") 339 if (shell == nil) then shell = "sh" end 340 if (not program_exists_in_path(shell)) then 341 io.write(string.format(gettext("Editor command '%s' needs a shell, ".. 342 "but '%s' is not to be found"), 343 exe, shell)) 344 os.remove(tname) 345 return nil 346 end 347 348 -- Single-quoted strings in both Bourne shell and csh can contain 349 -- anything but a single quote. 350 local safe_tname = " '" .. string.gsub(tname, "'", "'\\''") .. "'" 351 352 if (execute(shell, "-c", editor .. safe_tname) ~= 0) then 353 io.write(string.format(gettext("Error running editor '%s' ".. 354 "to enter log message\n"), 355 exe)) 356 os.remove(tname) 357 return nil 358 end 359 end 360 361 tmp = io.open(tname, "r") 362 if (tmp == nil) then os.remove(tname); return nil end 363 local res = tmp:read("*a") 364 io.close(tmp) 365 os.remove(tname) 366 return res 367end 368 369 370function get_local_key_name(key_identity) 371 return key_identity.given_name 372end 373 374 375function persist_phrase_ok() 376 return true 377end 378 379 380function use_inodeprints() 381 return false 382end 383 384function get_date_format_spec(wanted) 385 -- Return the strftime(3) specification to be used to print dates 386 -- in human-readable format after conversion to the local timezone. 387 -- The default uses the preferred date and time representation for 388 -- the current locale, e.g. the output looks like this: "09/08/2009 389 -- 06:49:26 PM" for en_US and "date_time_long", or "08.09.2009" 390 -- for de_DE and "date_short" 391 -- 392 -- A sampling of other possible formats you might want: 393 -- default for your locale: "%c" (may include a confusing timezone label) 394 -- 12 hour format: "%d %b %Y, %I:%M:%S %p" 395 -- like ctime(3): "%a %b %d %H:%M:%S %Y" 396 -- email style: "%a, %d %b %Y %H:%M:%S" 397 -- ISO 8601: "%Y-%m-%d %H:%M:%S" or "%Y-%m-%dT%H:%M:%S" 398 -- 399 -- ISO 8601, no timezone conversion: "" 400 --. 401 if (wanted == "date_long" or wanted == "date_short") then 402 return "%x" 403 end 404 if (wanted == "time_long" or wanted == "time_short") then 405 return "%X" 406 end 407 return "%x %X" 408end 409 410-- trust evaluation hooks 411 412function intersection(a,b) 413 local s={} 414 local t={} 415 for k,v in pairs(a) do s[v.name] = 1 end 416 for k,v in pairs(b) do if s[v] ~= nil then table.insert(t,v) end end 417 return t 418end 419 420function get_revision_cert_trust(signers, id, name, val) 421 return true 422end 423 424-- This is only used by migration from old manifest-style ancestry 425function get_manifest_cert_trust(signers, id, name, val) 426 return true 427end 428 429-- http://snippets.luacode.org/?p=snippets/String_to_Hex_String_68 430function hex_dump(str,spacer) 431 return (string.gsub(str,"(.)", 432 function (c) 433 return string.format("%02x%s",string.byte(c), spacer or "") 434 end) 435 ) 436end 437 438function accept_testresult_change_hex(old_results, new_results) 439 local reqfile = io.open("_MTN/wanted-testresults", "r") 440 if (reqfile == nil) then return true end 441 local line = reqfile:read() 442 local required = {} 443 while (line ~= nil) 444 do 445 required[line] = true 446 line = reqfile:read() 447 end 448 io.close(reqfile) 449 for test, res in pairs(required) 450 do 451 if old_results[test] == true and new_results[test] ~= true 452 then 453 return false 454 end 455 end 456 return true 457end 458 459function accept_testresult_change(old_results, new_results) 460 -- Hex encode each of the key hashes to match those in 'wanted-testresults' 461 local old_results_hex = {} 462 for k, v in pairs(old_results) do 463 old_results_hex[hex_dump(k)] = v 464 end 465 466 local new_results_hex = {} 467 for k, v in pairs(new_results) do 468 new_results_hex[hex_dump(k)] = v 469 end 470 471 return accept_testresult_change_hex(old_results_hex, new_results_hex) 472end 473 474-- merger support 475 476-- Fields in the mergers structure: 477-- cmd : a function that performs the merge operation using the chosen 478-- program, best try. 479-- available : a function that checks that the needed program is installed and 480-- in $PATH 481-- wanted : a function that checks if the user doesn't want to use this 482-- method, and returns false if so. This should normally return 483-- true, but in some cases, especially when the merger is really 484-- an editor, the user might have a preference in EDITOR and we 485-- need to respect that. 486-- NOTE: wanted is only used when the user has NOT defined the 487-- `merger' variable or the MTN_MERGE environment variable. 488mergers = {} 489 490-- This merger is designed to fail if there are any conflicts without trying to resolve them 491mergers.fail = { 492 cmd = function (tbl) return false end, 493 available = function () return true end, 494 wanted = function () return true end 495} 496 497mergers.meld = { 498 cmd = function (tbl) 499 io.write(string.format( 500 "\nWARNING: 'meld' was chosen to perform an external 3-way merge.\n".. 501 "You must merge all changes to the *CENTER* file.\n\n" 502 )) 503 local path = "meld" 504 local ret = execute(path, tbl.lfile, tbl.afile, tbl.rfile) 505 if (ret ~= 0) then 506 io.write(string.format(gettext("Error running merger '%s'\n"), path)) 507 return false 508 end 509 return tbl.afile 510 end , 511 available = function () return program_exists_in_path("meld") end, 512 wanted = function () return true end 513} 514 515mergers.diffuse = { 516 cmd = function (tbl) 517 io.write(string.format( 518 "\nWARNING: 'diffuse' was chosen to perform an external 3-way merge.\n".. 519 "You must merge all changes to the *CENTER* file.\n\n" 520 )) 521 local path = "diffuse" 522 local ret = execute(path, tbl.lfile, tbl.afile, tbl.rfile) 523 if (ret ~= 0) then 524 io.write(string.format(gettext("Error running merger '%s'\n"), path)) 525 return false 526 end 527 return tbl.afile 528 end , 529 available = function () return program_exists_in_path("diffuse") end, 530 wanted = function () return true end 531} 532 533mergers.tortoise = { 534 cmd = function (tbl) 535 local path = "tortoisemerge" 536 local ret = execute(path, 537 string.format("/base:%s", tbl.afile), 538 string.format("/theirs:%s", tbl.lfile), 539 string.format("/mine:%s", tbl.rfile), 540 string.format("/merged:%s", tbl.outfile)) 541 if (ret ~= 0) then 542 io.write(string.format(gettext("Error running merger '%s'\n"), path)) 543 return false 544 end 545 return tbl.outfile 546 end , 547 available = function() return program_exists_in_path ("tortoisemerge") end, 548 wanted = function () return true end 549} 550 551mergers.vim = { 552 cmd = function (tbl) 553 function execute_diff3(mine, yours, out) 554 local diff3_args = { 555 "diff3", 556 "--merge", 557 "--easy-only", 558 } 559 table.insert(diff3_args, string.gsub(mine, "\\", "/") .. "") 560 table.insert(diff3_args, string.gsub(tbl.afile, "\\", "/") .. "") 561 table.insert(diff3_args, string.gsub(yours, "\\", "/") .. "") 562 563 return execute_redirected("", string.gsub(out, "\\", "/"), "", unpack(diff3_args)) 564 end 565 566 io.write (string.format("\nWARNING: 'vim' was chosen to perform ".. 567 "an external 3-way merge.\n".. 568 "You must merge all changes to the ".. 569 "*LEFT* file.\n")) 570 571 local vim 572 if os.getenv ("DISPLAY") ~= nil and program_exists_in_path ("gvim") then 573 vim = "gvim" 574 else 575 vim = "vim" 576 end 577 578 local lfile_merged = tbl.lfile .. ".merged" 579 local rfile_merged = tbl.rfile .. ".merged" 580 581 -- first merge lfile using diff3 582 local ret = execute_diff3(tbl.lfile, tbl.rfile, lfile_merged) 583 if ret == 2 then 584 io.write(string.format(gettext("Error running diff3 for merger '%s'\n"), vim)) 585 os.remove(lfile_merged) 586 return false 587 end 588 589 -- now merge rfile using diff3 590 ret = execute_diff3(tbl.rfile, tbl.lfile, rfile_merged) 591 if ret == 2 then 592 io.write(string.format(gettext("Error running diff3 for merger '%s'\n"), vim)) 593 os.remove(lfile_merged) 594 os.remove(rfile_merged) 595 return false 596 end 597 598 os.rename(lfile_merged, tbl.lfile) 599 os.rename(rfile_merged, tbl.rfile) 600 601 local ret = execute(vim, "-f", "-d", "-c", string.format("silent file %s", tbl.outfile), 602 tbl.lfile, tbl.rfile) 603 if (ret ~= 0) then 604 io.write(string.format(gettext("Error running merger '%s'\n"), vim)) 605 return false 606 end 607 return tbl.outfile 608 end , 609 available = 610 function () 611 return program_exists_in_path("diff3") and 612 (program_exists_in_path("vim") or 613 program_exists_in_path("gvim")) 614 end , 615 wanted = 616 function () 617 local editor = os.getenv("EDITOR") 618 if editor and 619 not (string.find(editor, "vim") or 620 string.find(editor, "gvim")) then 621 return false 622 end 623 return true 624 end 625} 626 627mergers.rcsmerge = { 628 cmd = function (tbl) 629 -- XXX: This is tough - should we check if conflict markers stay or not? 630 -- If so, we should certainly give the user some way to still force 631 -- the merge to proceed since they can appear in the files (and I saw 632 -- that). --pasky 633 local merge = os.getenv("MTN_RCSMERGE") 634 if execute(merge, tbl.lfile, tbl.afile, tbl.rfile) == 0 then 635 copy_text_file(tbl.lfile, tbl.outfile); 636 return tbl.outfile 637 end 638 local ret = execute("vim", "-f", "-c", string.format("file %s", tbl.outfile 639), 640 tbl.lfile) 641 if (ret ~= 0) then 642 io.write(string.format(gettext("Error running merger '%s'\n"), "vim")) 643 return false 644 end 645 return tbl.outfile 646 end, 647 available = 648 function () 649 local merge = os.getenv("MTN_RCSMERGE") 650 return merge and 651 program_exists_in_path(merge) and program_exists_in_path("vim") 652 end , 653 wanted = function () return os.getenv("MTN_RCSMERGE") ~= nil end 654} 655 656-- GNU diffutils based merging 657mergers.diffutils = { 658 -- merge procedure execution 659 cmd = function (tbl) 660 -- parse options 661 local option = {} 662 option.partial = false 663 option.diff3opts = "" 664 option.sdiffopts = "" 665 local options = os.getenv("MTN_MERGE_DIFFUTILS") 666 if options ~= nil then 667 for spec in string.gmatch(options, "%s*(%w[^,]*)%s*,?") do 668 local name, value = string.match(spec, "^(%w+)=([^,]*)") 669 if name == nil then 670 name = spec 671 value = true 672 end 673 if type(option[name]) == "nil" then 674 io.write("mtn: " .. string.format(gettext("invalid \"diffutils\" merger option \"%s\""), name) .. "\n") 675 return false 676 end 677 option[name] = value 678 end 679 end 680 681 -- determine the diff3(1) command 682 local diff3 = { 683 "diff3", 684 "--merge", 685 "--label", string.format("%s [left]", tbl.left_path ), 686 "--label", string.format("%s [ancestor]", tbl.anc_path ), 687 "--label", string.format("%s [right]", tbl.right_path), 688 } 689 if option.diff3opts ~= "" then 690 for opt in string.gmatch(option.diff3opts, "%s*([^%s]+)%s*") do 691 table.insert(diff3, opt) 692 end 693 end 694 table.insert(diff3, string.gsub(tbl.lfile, "\\", "/") .. "") 695 table.insert(diff3, string.gsub(tbl.afile, "\\", "/") .. "") 696 table.insert(diff3, string.gsub(tbl.rfile, "\\", "/") .. "") 697 698 -- dispatch according to major operation mode 699 if option.partial then 700 -- partial batch/non-modal 3-way merge "resolution": 701 -- simply merge content with help of conflict markers 702 io.write("mtn: " .. gettext("3-way merge via GNU diffutils, resolving conflicts via conflict markers") .. "\n") 703 local ret = execute_redirected("", string.gsub(tbl.outfile, "\\", "/"), "", unpack(diff3)) 704 if ret == 2 then 705 io.write("mtn: " .. gettext("error running GNU diffutils 3-way difference/merge tool \"diff3\"") .. "\n") 706 return false 707 end 708 return tbl.outfile 709 else 710 -- real interactive/modal 3/2-way merge resolution: 711 -- display 3-way merge conflict and perform 2-way merge resolution 712 io.write("mtn: " .. gettext("3-way merge via GNU diffutils, resolving conflicts via interactive prompt") .. "\n") 713 714 -- display 3-way merge conflict (batch) 715 io.write("\n") 716 io.write("mtn: " .. gettext("---- CONFLICT SUMMARY ------------------------------------------------") .. "\n") 717 local ret = execute(unpack(diff3)) 718 if ret == 2 then 719 io.write("mtn: " .. gettext("error running GNU diffutils 3-way difference/merge tool \"diff3\"") .. "\n") 720 return false 721 end 722 723 -- perform 2-way merge resolution (interactive) 724 io.write("\n") 725 io.write("mtn: " .. gettext("---- CONFLICT RESOLUTION ---------------------------------------------") .. "\n") 726 local sdiff = { 727 "sdiff", 728 "--diff-program=diff", 729 "--suppress-common-lines", 730 "--minimal", 731 "--output=" .. string.gsub(tbl.outfile, "\\", "/") 732 } 733 if option.sdiffopts ~= "" then 734 for opt in string.gmatch(option.sdiffopts, "%s*([^%s]+)%s*") do 735 table.insert(sdiff, opt) 736 end 737 end 738 table.insert(sdiff, string.gsub(tbl.lfile, "\\", "/") .. "") 739 table.insert(sdiff, string.gsub(tbl.rfile, "\\", "/") .. "") 740 local ret = execute(unpack(sdiff)) 741 if ret == 2 then 742 io.write("mtn: " .. gettext("error running GNU diffutils 2-way merging tool \"sdiff\"") .. "\n") 743 return false 744 end 745 return tbl.outfile 746 end 747 end, 748 749 -- merge procedure availability check 750 available = function () 751 -- make sure the GNU diffutils tools are available 752 return program_exists_in_path("diff3") and 753 program_exists_in_path("sdiff") and 754 program_exists_in_path("diff"); 755 end, 756 757 -- merge procedure request check 758 wanted = function () 759 -- assume it is requested (if it is available at all) 760 return true 761 end 762} 763 764mergers.emacs = { 765 cmd = function (tbl) 766 local emacs 767 if program_exists_in_path("xemacs") then 768 emacs = "xemacs" 769 else 770 emacs = "emacs" 771 end 772 local elisp = "(ediff-merge-files-with-ancestor \"%s\" \"%s\" \"%s\" nil \"%s\")" 773 -- Converting backslashes is necessary on Win32 MinGW; emacs 774 -- lisp string syntax says '\' is an escape. 775 local ret = execute(emacs, "--eval", 776 string.format(elisp, 777 string.gsub (tbl.lfile, "\\", "/"), 778 string.gsub (tbl.rfile, "\\", "/"), 779 string.gsub (tbl.afile, "\\", "/"), 780 string.gsub (tbl.outfile, "\\", "/"))) 781 if (ret ~= 0) then 782 io.write(string.format(gettext("Error running merger '%s'\n"), emacs)) 783 return false 784 end 785 return tbl.outfile 786 end, 787 available = 788 function () 789 return program_exists_in_path("xemacs") or 790 program_exists_in_path("emacs") 791 end , 792 wanted = 793 function () 794 local editor = os.getenv("EDITOR") 795 if editor and 796 not (string.find(editor, "emacs") or 797 string.find(editor, "gnu")) then 798 return false 799 end 800 return true 801 end 802} 803 804mergers.xxdiff = { 805 cmd = function (tbl) 806 local path = "xxdiff" 807 local ret = execute(path, 808 "--title1", tbl.left_path, 809 "--title2", tbl.right_path, 810 "--title3", tbl.merged_path, 811 tbl.lfile, tbl.afile, tbl.rfile, 812 "--merge", 813 "--merged-filename", tbl.outfile, 814 "--exit-with-merge-status") 815 if (ret ~= 0) then 816 io.write(string.format(gettext("Error running merger '%s'\n"), path)) 817 return false 818 end 819 return tbl.outfile 820 end, 821 available = function () return program_exists_in_path("xxdiff") end, 822 wanted = function () return true end 823} 824 825mergers.kdiff3 = { 826 cmd = function (tbl) 827 local path = "kdiff3" 828 local ret = execute(path, 829 "--L1", tbl.anc_path, 830 "--L2", tbl.left_path, 831 "--L3", tbl.right_path, 832 tbl.afile, tbl.lfile, tbl.rfile, 833 "--merge", 834 "--o", tbl.outfile) 835 if (ret ~= 0) then 836 io.write(string.format(gettext("Error running merger '%s'\n"), path)) 837 return false 838 end 839 return tbl.outfile 840 end, 841 available = function () return program_exists_in_path("kdiff3") end, 842 wanted = function () return true end 843} 844 845mergers.opendiff = { 846 cmd = function (tbl) 847 local path = "opendiff" 848 -- As opendiff immediately returns, let user confirm manually 849 local ret = execute_confirm(path, 850 tbl.lfile,tbl.rfile, 851 "-ancestor",tbl.afile, 852 "-merge",tbl.outfile) 853 if (ret ~= 0) then 854 io.write(string.format(gettext("Error running merger '%s'\n"), path)) 855 return false 856 end 857 return tbl.outfile 858 end, 859 available = function () return program_exists_in_path("opendiff") end, 860 wanted = function () return true end 861} 862 863function write_to_temporary_file(data, namehint, filemodehint) 864 tmp, filename = temp_file(namehint, filemodehint) 865 if (tmp == nil) then 866 return nil 867 end; 868 tmp:write(data) 869 io.close(tmp) 870 return filename 871end 872 873function copy_text_file(srcname, destname) 874 src = io.open(srcname, "r") 875 if (src == nil) then return nil end 876 dest = io.open(destname, "w") 877 if (dest == nil) then return nil end 878 879 while true do 880 local line = src:read() 881 if line == nil then break end 882 dest:write(line, "\n") 883 end 884 885 io.close(dest) 886 io.close(src) 887end 888 889function read_contents_of_file(filename, mode) 890 tmp = io.open(filename, mode) 891 if (tmp == nil) then 892 return nil 893 end 894 local data = tmp:read("*a") 895 io.close(tmp) 896 return data 897end 898 899function program_exists_in_path(program) 900 return existsonpath(program) == 0 901end 902 903function get_preferred_merge3_command (tbl) 904 local default_order = {"diffuse", "kdiff3", "xxdiff", "opendiff", 905 "tortoise", "emacs", "vim", "meld", "diffutils"} 906 local function existmerger(name) 907 local m = mergers[name] 908 if type(m) == "table" and m.available(tbl) then 909 return m.cmd 910 end 911 return nil 912 end 913 local function trymerger(name) 914 local m = mergers[name] 915 if type(m) == "table" and m.available(tbl) and m.wanted(tbl) then 916 return m.cmd 917 end 918 return nil 919 end 920 -- Check if there's a merger given by the user. 921 local mkey = os.getenv("MTN_MERGE") 922 if not mkey then mkey = merger end 923 if not mkey and os.getenv("MTN_RCSMERGE") then mkey = "rcsmerge" end 924 -- If there was a user-given merger, see if it exists. If it does, return 925 -- the cmd function. If not, return nil. 926 local c 927 if mkey then c = existmerger(mkey) end 928 if c then return c,mkey end 929 if mkey then return nil,mkey end 930 -- If there wasn't any user-given merger, take the first that's available 931 -- and wanted. 932 for _,mkey in ipairs(default_order) do 933 c = trymerger(mkey) ; if c then return c,mkey end 934 end 935end 936 937function merge3 (anc_path, left_path, right_path, merged_path, ancestor, left, right) 938 local ret = nil 939 local tbl = {} 940 941 tbl.anc_path = anc_path 942 tbl.left_path = left_path 943 tbl.right_path = right_path 944 945 tbl.merged_path = merged_path 946 tbl.afile = nil 947 tbl.lfile = nil 948 tbl.rfile = nil 949 tbl.outfile = nil 950 tbl.meld_exists = false 951 tbl.lfile = write_to_temporary_file (left, "left", "r+b") 952 tbl.afile = write_to_temporary_file (ancestor, "ancestor", "r+b") 953 tbl.rfile = write_to_temporary_file (right, "right", "r+b") 954 tbl.outfile = write_to_temporary_file ("", "merged", "r+b") 955 956 if tbl.lfile ~= nil and tbl.rfile ~= nil and tbl.afile ~= nil and tbl.outfile ~= nil 957 then 958 local cmd,mkey = get_preferred_merge3_command (tbl) 959 if cmd ~=nil 960 then 961 io.write ("mtn: " .. string.format(gettext("executing external 3-way merge via \"%s\" merger\n"), mkey)) 962 ret = cmd (tbl) 963 if not ret then 964 ret = nil 965 else 966 ret = read_contents_of_file (ret, "rb") 967 if string.len (ret) == 0 968 then 969 ret = nil 970 end 971 end 972 else 973 if mkey then 974 io.write (string.format("The possible commands for the "..mkey.." merger aren't available.\n".. 975 "You may want to check that $MTN_MERGE or the lua variable `merger' is set\n".. 976 "to something available. If you want to use vim or emacs, you can also\n".. 977 "set $EDITOR to something appropriate.\n")) 978 else 979 io.write (string.format("No external 3-way merge command found.\n".. 980 "You may want to check that $EDITOR is set to an editor that supports 3-way\n".. 981 "merge, set this explicitly in your get_preferred_merge3_command hook,\n".. 982 "or add a 3-way merge program to your path.\n")) 983 end 984 end 985 end 986 987 os.remove (tbl.lfile) 988 os.remove (tbl.rfile) 989 os.remove (tbl.afile) 990 os.remove (tbl.outfile) 991 992 return ret 993end 994 995-- expansion of values used in selector completion 996 997function expand_selector(str) 998 999 -- something which looks like a generic cert pattern 1000 if string.find(str, "^[^=]*=.*$") 1001 then 1002 return ("c:" .. str) 1003 end 1004 1005 -- something which looks like an email address 1006 if string.find(str, "[%w%-_]+@[%w%-_]+") 1007 then 1008 return ("a:" .. str) 1009 end 1010 1011 -- something which looks like a branch name 1012 if string.find(str, "[%w%-]+%.[%w%-]+") 1013 then 1014 return ("b:" .. str) 1015 end 1016 1017 -- a sequence of nothing but hex digits 1018 if string.find(str, "^%x+$") 1019 then 1020 return ("i:" .. str) 1021 end 1022 1023 -- tries to expand as a date 1024 local dtstr = expand_date(str) 1025 if dtstr ~= nil 1026 then 1027 return ("d:" .. dtstr) 1028 end 1029 1030 return nil 1031end 1032 1033-- expansion of a date expression 1034function expand_date(str) 1035 -- simple date patterns 1036 if string.find(str, "^19%d%d%-%d%d") 1037 or string.find(str, "^20%d%d%-%d%d") 1038 then 1039 return (str) 1040 end 1041 1042 -- "now" 1043 if str == "now" 1044 then 1045 local t = os.time(os.date('!*t')) 1046 return os.date("!%Y-%m-%dT%H:%M:%S", t) 1047 end 1048 1049 -- today don't uses the time # for xgettext's sake, an extra quote 1050 if str == "today" 1051 then 1052 local t = os.time(os.date('!*t')) 1053 return os.date("!%Y-%m-%d", t) 1054 end 1055 1056 -- "yesterday", the source of all hangovers 1057 if str == "yesterday" 1058 then 1059 local t = os.time(os.date('!*t')) 1060 return os.date("!%Y-%m-%d", t - 86400) 1061 end 1062 1063 -- "CVS style" relative dates such as "3 weeks ago" 1064 local trans = { 1065 minute = 60; 1066 hour = 3600; 1067 day = 86400; 1068 week = 604800; 1069 month = 2678400; 1070 year = 31536000 1071 } 1072 local pos, len, n, type = string.find(str, "(%d+) ([minutehordaywk]+)s? ago") 1073 if trans[type] ~= nil 1074 then 1075 local t = os.time(os.date('!*t')) 1076 if trans[type] <= 3600 1077 then 1078 return os.date("!%Y-%m-%dT%H:%M:%S", t - (n * trans[type])) 1079 else 1080 return os.date("!%Y-%m-%d", t - (n * trans[type])) 1081 end 1082 end 1083 1084 return nil 1085end 1086 1087 1088external_diff_default_args = "-u" 1089 1090-- default external diff, works for gnu diff 1091function external_diff(file_path, data_old, data_new, is_binary, diff_args, rev_old, rev_new) 1092 local old_file = write_to_temporary_file(data_old, nil, "r+b"); 1093 local new_file = write_to_temporary_file(data_new, nil, "r+b"); 1094 1095 if diff_args == nil then diff_args = external_diff_default_args end 1096 execute("diff", diff_args, "--label", file_path .. "\told", old_file, "--label", file_path .. "\tnew", new_file); 1097 1098 os.remove (old_file); 1099 os.remove (new_file); 1100end 1101 1102-- netsync permissions hooks (and helper) 1103 1104function globish_match(glob, str) 1105 local pcallstatus, result = pcall(function() if (globish.match(glob, str)) then return true else return false end end) 1106 if pcallstatus == true then 1107 -- no error 1108 return result 1109 else 1110 -- globish.match had a problem 1111 return nil 1112 end 1113end 1114 1115function _get_netsync_read_permitted(branch, ident, permfilename, state) 1116 if not exists(permfilename) or isdir(permfilename) then 1117 return false 1118 end 1119 local permfile = io.open(permfilename, "r") 1120 if (permfile == nil) then return false end 1121 local dat = permfile:read("*a") 1122 io.close(permfile) 1123 local res = parse_basic_io(dat) 1124 if res == nil then 1125 io.stderr:write("file "..permfilename.." cannot be parsed\n") 1126 return false,"continue" 1127 end 1128 state["matches"] = state["matches"] or false 1129 state["cont"] = state["cont"] or false 1130 for i, item in pairs(res) 1131 do 1132 -- legal names: pattern, allow, deny, continue 1133 if item.name == "pattern" then 1134 if state["matches"] and not state["cont"] then return false end 1135 state["matches"] = false 1136 state["cont"] = false 1137 for j, val in pairs(item.values) do 1138 if globish_match(val, branch) then state["matches"] = true end 1139 end 1140 elseif item.name == "allow" then if state["matches"] then 1141 for j, val in pairs(item.values) do 1142 if val == "*" then return true end 1143 if val == "" and ident == nil then return true end 1144 if ident ~= nil and val == ident.id then return true end 1145 if ident ~= nil and globish_match(val, ident.name) then return true end 1146 end 1147 end elseif item.name == "deny" then if state["matches"] then 1148 for j, val in pairs(item.values) do 1149 if val == "*" then return false end 1150 if val == "" and ident == nil then return false end 1151 if ident ~= nil and val == ident.id then return false end 1152 if ident ~= nil and globish_match(val, ident.name) then return false end 1153 end 1154 end elseif item.name == "continue" then if state["matches"] then 1155 state["cont"] = true 1156 for j, val in pairs(item.values) do 1157 if val == "false" or val == "no" then 1158 state["cont"] = false 1159 end 1160 end 1161 end elseif item.name ~= "comment" then 1162 io.stderr:write("unknown symbol in read-permissions: " .. item.name .. "\n") 1163 return false 1164 end 1165 end 1166 return false 1167end 1168 1169function get_netsync_read_permitted(branch, ident) 1170 local permfilename = get_confdir() .. "/read-permissions" 1171 local permdirname = permfilename .. ".d" 1172 local state = {} 1173 if _get_netsync_read_permitted(branch, ident, permfilename, state) then 1174 return true 1175 end 1176 if isdir(permdirname) then 1177 local files = read_directory(permdirname) 1178 table.sort(files) 1179 for _,f in ipairs(files) do 1180 pf = permdirname.."/"..f 1181 if _get_netsync_read_permitted(branch, ident, pf, state) then 1182 return true 1183 end 1184 end 1185 end 1186 return false 1187end 1188 1189function _get_netsync_write_permitted(ident, permfilename) 1190 if not exists(permfilename) or isdir(permfilename) then return false end 1191 local permfile = io.open(permfilename, "r") 1192 if (permfile == nil) then 1193 return false 1194 end 1195 local matches = false 1196 local line = permfile:read() 1197 while (not matches and line ~= nil) do 1198 local _, _, ln = string.find(line, "%s*([^%s]*)%s*") 1199 if ln == "*" then matches = true end 1200 if ln == ident.id then matches = true end 1201 if globish_match(ln, ident.name) then matches = true end 1202 line = permfile:read() 1203 end 1204 io.close(permfile) 1205 return matches 1206end 1207 1208function get_netsync_write_permitted(ident) 1209 local permfilename = get_confdir() .. "/write-permissions" 1210 local permdirname = permfilename .. ".d" 1211 if _get_netsync_write_permitted(ident, permfilename) then return true end 1212 if isdir(permdirname) then 1213 local files = read_directory(permdirname) 1214 table.sort(files) 1215 for _,f in ipairs(files) do 1216 pf = permdirname.."/"..f 1217 if _get_netsync_write_permitted(ident, pf) then return true end 1218 end 1219 end 1220 return false 1221end 1222 1223-- This is a simple function which assumes you're going to be spawning 1224-- a copy of mtn, so reuses a common bit at the end for converting 1225-- local args into remote args. You might need to massage the logic a 1226-- bit if this doesn't fit your assumptions. 1227 1228function get_netsync_connect_command(uri, args) 1229 1230 local argv = nil 1231 1232 if uri["scheme"] == "ssh" 1233 and uri["host"] 1234 and uri["path"] then 1235 1236 argv = { "ssh" } 1237 if uri["user"] then 1238 table.insert(argv, "-l") 1239 table.insert(argv, uri["user"]) 1240 end 1241 if uri["port"] then 1242 table.insert(argv, "-p") 1243 table.insert(argv, uri["port"]) 1244 end 1245 1246 -- ssh://host/~/dir/file.mtn or 1247 -- ssh://host/~user/dir/file.mtn should be home-relative 1248 if string.find(uri["path"], "^/~") then 1249 uri["path"] = string.sub(uri["path"], 2) 1250 end 1251 1252 table.insert(argv, uri["host"]) 1253 end 1254 1255 if uri["scheme"] == "file" and uri["path"] then 1256 argv = { } 1257 end 1258 1259 if uri["scheme"] == "ssh+ux" 1260 and uri["host"] 1261 and uri["path"] then 1262 1263 argv = { "ssh" } 1264 if uri["user"] then 1265 table.insert(argv, "-l") 1266 table.insert(argv, uri["user"]) 1267 end 1268 if uri["port"] then 1269 table.insert(argv, "-p") 1270 table.insert(argv, uri["port"]) 1271 end 1272 1273 -- ssh://host/~/dir/file.mtn or 1274 -- ssh://host/~user/dir/file.mtn should be home-relative 1275 if string.find(uri["path"], "^/~") then 1276 uri["path"] = string.sub(uri["path"], 2) 1277 end 1278 1279 table.insert(argv, uri["host"]) 1280 table.insert(argv, get_remote_unix_socket_command(uri["host"])) 1281 table.insert(argv, "-") 1282 table.insert(argv, "UNIX-CONNECT:" .. uri["path"]) 1283 else 1284 if argv then 1285 -- start remote monotone process 1286 1287 table.insert(argv, get_mtn_command(uri["host"])) 1288 1289 if args["debug"] then 1290 table.insert(argv, "--verbose") 1291 else 1292 table.insert(argv, "--quiet") 1293 end 1294 1295 table.insert(argv, "--db") 1296 table.insert(argv, uri["path"]) 1297 table.insert(argv, "serve") 1298 table.insert(argv, "--stdio") 1299 table.insert(argv, "--no-transport-auth") 1300 1301 -- else scheme does not require starting a new remote 1302 -- process (ie mtn:) 1303 end 1304 end 1305 return argv 1306end 1307 1308function use_transport_auth(uri) 1309 if uri["scheme"] == "ssh" 1310 or uri["scheme"] == "ssh+ux" 1311 or uri["scheme"] == "file" then 1312 return false 1313 else 1314 return true 1315 end 1316end 1317 1318function get_mtn_command(host) 1319 return "mtn" 1320end 1321 1322function get_remote_unix_socket_command(host) 1323 return "socat" 1324end 1325 1326function get_default_command_options(command) 1327 local default_args = {} 1328 return default_args 1329end 1330 1331function get_default_database_alias() 1332 return ":default.mtn" 1333end 1334 1335function get_default_database_locations() 1336 local paths = {} 1337 table.insert(paths, get_confdir() .. "/databases") 1338 return paths 1339end 1340 1341function get_default_database_glob() 1342 return "*.{mtn,db}" 1343end 1344 1345hook_wrapper_dump = {} 1346hook_wrapper_dump.depth = 0 1347hook_wrapper_dump._string = function(s) return string.format("%q", s) end 1348hook_wrapper_dump._number = function(n) return tostring(n) end 1349hook_wrapper_dump._boolean = function(b) if (b) then return "true" end return "false" end 1350hook_wrapper_dump._userdata = function(u) return "nil --[[userdata]]" end 1351-- if we really need to return / serialize functions we could do it 1352-- like cbreak@irc.freenode.net did here: http://lua-users.org/wiki/TablePersistence 1353hook_wrapper_dump._function = function(f) return "nil --[[function]]" end 1354hook_wrapper_dump._nil = function(n) return "nil" end 1355hook_wrapper_dump._thread = function(t) return "nil --[[thread]]" end 1356hook_wrapper_dump._lightuserdata = function(l) return "nil --[[lightuserdata]]" end 1357 1358hook_wrapper_dump._table = function(t) 1359 local buf = '' 1360 if (hook_wrapper_dump.depth > 0) then 1361 buf = buf .. '{\n' 1362 end 1363 hook_wrapper_dump.depth = hook_wrapper_dump.depth + 1; 1364 for k,v in pairs(t) do 1365 buf = buf..string.format('%s[%s] = %s;\n', 1366 string.rep("\t", hook_wrapper_dump.depth - 1), 1367 hook_wrapper_dump["_" .. type(k)](k), 1368 hook_wrapper_dump["_" .. type(v)](v)) 1369 end 1370 hook_wrapper_dump.depth = hook_wrapper_dump.depth - 1; 1371 if (hook_wrapper_dump.depth > 0) then 1372 buf = buf .. string.rep("\t", hook_wrapper_dump.depth - 1) .. '}' 1373 end 1374 return buf 1375end 1376 1377function hook_wrapper(func_name, ...) 1378 -- we have to ensure that nil arguments are restored properly for the 1379 -- function call, see http://lua-users.org/wiki/StoringNilsInTables 1380 local args = { n=select('#', ...), ... } 1381 for i=1,args.n do 1382 local val = assert(loadstring("return " .. args[i]), 1383 "argument "..args[i].." could not be evaluated")() 1384 assert(val ~= nil or args[i] == "nil", 1385 "argument "..args[i].." was evaluated to nil") 1386 args[i] = val 1387 end 1388 local res = { _G[func_name](unpack(args, 1, args.n)) } 1389 return hook_wrapper_dump._table(res) 1390end 1391 1392do 1393 -- Hook functions are tables containing any of the following 6 items 1394 -- with associated functions: 1395 -- 1396 -- startup Corresponds to note_mtn_startup() 1397 -- start Corresponds to note_netsync_start() 1398 -- revision_received Corresponds to note_netsync_revision_received() 1399 -- revision_sent Corresponds to note_netsync_revision_sent() 1400 -- cert_received Corresponds to note_netsync_cert_received() 1401 -- cert_sent Corresponds to note_netsync_cert_sent() 1402 -- pubkey_received Corresponds to note_netsync_pubkey_received() 1403 -- pubkey_sent Corresponds to note_netsync_pubkey_sent() 1404 -- end Corresponds to note_netsync_end() 1405 -- 1406 -- Those functions take exactly the same arguments as the corresponding 1407 -- global functions, but return a different kind of value, a tuple 1408 -- composed of a return code and a value to be returned back to monotone. 1409 -- The codes are strings: 1410 -- "continue" and "stop" 1411 -- When the code "continue" is returned and there's another notifier, the 1412 -- second value is ignored and the next notifier is called. Otherwise, 1413 -- the second value is returned immediately. 1414 local hook_functions = {} 1415 local supported_items = { 1416 "startup", 1417 "start", "revision_received", "revision_sent", "cert_received", "cert_sent", 1418 "pubkey_received", "pubkey_sent", "end" 1419 } 1420 1421 function _hook_functions_helper(f,...) 1422 local s = "continue" 1423 local v = nil 1424 for _,n in pairs(hook_functions) do 1425 if n[f] then 1426 s,v = n[f](...) 1427 end 1428 if s ~= "continue" then 1429 break 1430 end 1431 end 1432 return v 1433 end 1434 function note_mtn_startup(...) 1435 return _hook_functions_helper("startup",...) 1436 end 1437 function note_netsync_start(...) 1438 return _hook_functions_helper("start",...) 1439 end 1440 function note_netsync_revision_received(...) 1441 return _hook_functions_helper("revision_received",...) 1442 end 1443 function note_netsync_revision_sent(...) 1444 return _hook_functions_helper("revision_sent",...) 1445 end 1446 function note_netsync_cert_received(...) 1447 return _hook_functions_helper("cert_received",...) 1448 end 1449 function note_netsync_cert_sent(...) 1450 return _hook_functions_helper("cert_sent",...) 1451 end 1452 function note_netsync_pubkey_received(...) 1453 return _hook_functions_helper("pubkey_received",...) 1454 end 1455 function note_netsync_pubkey_sent(...) 1456 return _hook_functions_helper("pubkey_sent",...) 1457 end 1458 function note_netsync_end(...) 1459 return _hook_functions_helper("end",...) 1460 end 1461 1462 function add_hook_functions(functions, precedence) 1463 if type(functions) ~= "table" or type(precedence) ~= "number" then 1464 return false, "Invalid type" 1465 end 1466 if hook_functions[precedence] then 1467 return false, "Precedence already taken" 1468 end 1469 1470 local unknown_items = "" 1471 local warning = nil 1472 local is_member = 1473 function (s,t) 1474 for k,v in pairs(t) do if s == v then return true end end 1475 return false 1476 end 1477 1478 for n,f in pairs(functions) do 1479 if type(n) == "string" then 1480 if not is_member(n, supported_items) then 1481 if unknown_items ~= "" then 1482 unknown_items = unknown_items .. "," 1483 end 1484 unknown_items = unknown_items .. n 1485 end 1486 if type(f) ~= "function" then 1487 return false, "Value for functions item "..n.." isn't a function" 1488 end 1489 else 1490 warning = "Non-string item keys found in functions table" 1491 end 1492 end 1493 1494 if warning == nil and unknown_items ~= "" then 1495 warning = "Unknown item(s) " .. unknown_items .. " in functions table" 1496 end 1497 1498 hook_functions[precedence] = functions 1499 return true, warning 1500 end 1501 function push_hook_functions(functions) 1502 local n = #hook_functions + 1 1503 return add_hook_functions(functions, n) 1504 end 1505 1506 -- Kept for backward compatibility 1507 function add_netsync_notifier(notifier, precedence) 1508 return add_hook_functions(notifier, precedence) 1509 end 1510 function push_netsync_notifier(notifier) 1511 return push_hook_functions(notifier) 1512 end 1513end 1514 1515-- to ensure only mapped authors are allowed through 1516-- return "" from unmapped_git_author 1517-- and validate_git_author will fail 1518 1519function unmapped_git_author(author) 1520 -- replace "foo@bar" with "foo <foo@bar>" 1521 name = author:match("^([^<>]+)@[^<>]+$") 1522 if name then 1523 return name .. " <" .. author .. ">" 1524 end 1525 1526 -- replace "<foo@bar>" with "foo <foo@bar>" 1527 name = author:match("^<([^<>]+)@[^<>]+>$") 1528 if name then 1529 return name .. " " .. author 1530 end 1531 1532 -- replace "foo" with "foo <foo>" 1533 name = author:match("^[^<>@]+$") 1534 if name then 1535 return name .. " <" .. name .. ">" 1536 end 1537 1538 return author -- unchanged 1539end 1540 1541function validate_git_author(author) 1542 -- ensure author matches the "Name <email>" format git expects 1543 if author:match("^[^<]+ <[^>]*>$") then 1544 return true 1545 end 1546 1547 return false 1548end 1549 1550function get_man_page_formatter_command() 1551 local term_width = guess_terminal_width() - 2 1552 -- The string returned is run in a process created with 'popen' 1553 -- (see cmd.cc manpage). 1554 -- 1555 -- On Unix (and POSIX compliant systems), 'popen' runs 'sh' with 1556 -- the inherited path. 1557 -- 1558 -- On MinGW, 'popen' runs 'cmd.exe' with the inherited path. MinGW 1559 -- does not (currently) provide nroff or equivalent. So we assume 1560 -- sh, nroff, locale and less are also installed, from Cygwin or 1561 -- some other toolset. 1562 -- 1563 -- GROFF_ENCODING is an environment variable that, when set, tells 1564 -- groff (called by nroff where applicable) to use preconv to convert 1565 -- the input from the given encoding to something groff understands. 1566 -- For example, groff doesn NOT understand raw UTF-8 as input, but 1567 -- it does understand unicode, which preconv will happily provide. 1568 -- This doesn't help people that don't use groff, unfortunately. 1569 -- Patches are welcome! 1570 if string.sub(get_ostype(), 1, 7) == "Windows" then 1571 return string.format("sh -c 'GROFF_ENCODING=`locale charmap` nroff -man -rLL=%dn' | less -R", term_width) 1572 else 1573 return string.format("GROFF_ENCODING=`locale charmap` nroff -man -rLL=%dn | less -R", term_width) 1574 end 1575end 1576 1577