Next: General Index, Previous: Special Topics, Up: Top [Contents][Index]
This section contains the entire source code of the standard hook file, that is built in to the monotone executable, and read before any user hooks files (unless --no-builtin-rcfiles is passed). It contains the default values for all hooks. See rcfiles.
-- Copyright (C) 2003 Graydon Hoare <graydon@pobox.com> -- -- This program is made available under the GNU GPL version 2.0 or -- greater. See the accompanying file COPYING for details. -- -- This program is distributed WITHOUT ANY WARRANTY; without even the -- implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -- PURPOSE. -- this is the standard set of lua hooks for monotone; -- user-provided files can override it or add to it. -- Since Lua 5.2, unpack and loadstrings are deprecated and are either moved -- to table.unpack() or replaced by load(). If lua was compiled without -- LUA_COMPAT_UNPACK and/or LUA_COMPAT_LOADSTRING, these two are not -- available and we add a similar compatibility layer, ourselves. unpack = unpack or table.unpack loadstring = loadstring or load function temp_file(namehint, filemodehint) local tdir tdir = os.getenv("TMPDIR") if tdir == nil then tdir = os.getenv("TMP") end if tdir == nil then tdir = os.getenv("TEMP") end if tdir == nil then tdir = "/tmp" end local filename if namehint == nil then filename = string.format("%s/mtn.XXXXXX", tdir) else filename = string.format("%s/mtn.%s.XXXXXX", tdir, namehint) end local filemode if filemodehint == nil then filemode = "r+" else filemode = filemodehint end local name = mkstemp(filename) local file = io.open(name, filemode) return file, name end function execute(path, ...) local pid local ret = -1 pid = spawn(path, ...) if (pid ~= -1) then ret, pid = wait(pid) end return ret end function execute_redirected(stdin, stdout, stderr, path, ...) local pid local ret = -1 io.flush(); pid = spawn_redirected(stdin, stdout, stderr, path, ...) if (pid ~= -1) then ret, pid = wait(pid) end return ret end -- Wrapper around execute to let user confirm in the case where a subprocess -- returns immediately -- This is needed to work around some brokenness with some merge tools -- (e.g. on OS X) function execute_confirm(path, ...) ret = execute(path, ...) if (ret ~= 0) then print(gettext("Press enter")) else print(gettext("Press enter when the subprocess has completed")) end io.read() return ret end -- attributes are persistent metadata about files (such as execute -- bit, ACLs, various special flags) which we want to have set and -- re-set any time the files are modified. the attributes themselves -- are stored in the roster associated with the revision. each (f,k,v) -- attribute triple turns into a call to attr_functions[k](f,v) in lua. if (attr_init_functions == nil) then attr_init_functions = {} end attr_init_functions["mtn:execute"] = function(filename) if (is_executable(filename)) then return "true" else return nil end end attr_init_functions["mtn:manual_merge"] = function(filename) if (binary_file(filename)) then return "true" -- binary files must be merged manually else return nil end end if (attr_functions == nil) then attr_functions = {} end attr_functions["mtn:execute"] = function(filename, value) if (value == "true") then set_executable(filename) else clear_executable(filename) end end function dir_matches(name, dir) -- helper for ignore_file, matching files within dir, or dir itself. -- eg for dir of 'CVS', matches CVS/, CVS/*, */CVS/ and */CVS/* if (string.find(name, "^" .. dir .. "/")) then return true end if (string.find(name, "^" .. dir .. "$")) then return true end if (string.find(name, "/" .. dir .. "/")) then return true end if (string.find(name, "/" .. dir .. "$")) then return true end return false end function portable_readline(f) line = f:read() if line ~= nil then line = string.gsub(line, "\r$","") -- strip possible \r left from windows editing end return line end function ignore_file(name) -- project specific if (ignored_files == nil) then ignored_files = {} local ignfile = io.open(".mtn-ignore", "r") if (ignfile ~= nil) then local line = portable_readline(ignfile) while (line ~= nil) do if line ~= "" then table.insert(ignored_files, line) end line = portable_readline(ignfile) end io.close(ignfile) end end local warn_reported_file = false for i, line in pairs(ignored_files) do if (line ~= nil) then local pcallstatus, result = pcall(function() return regex.search(line, name) end) if pcallstatus == true then -- no error from the regex.search call if result == true then return true end else -- regex.search had a problem, warn the user their -- .mtn-ignore file syntax is wrong if not warn_reported_file then io.stderr:write("mtn: warning: while matching file '" .. name .. "':\n") warn_reported_file = true end local prefix = ".mtn-ignore:" .. i .. ": warning: " io.stderr:write(prefix .. string.gsub(result, "\n", "\n" .. prefix) .. "\n\t- skipping this regex for " .. "all remaining files.\n") ignored_files[i] = nil end end end local file_pats = { -- c/c++ "%.a$", "%.so$", "%.o$", "%.la$", "%.lo$", "^core$", "/core$", "/core%.%d+$", -- java "%.class$", -- python "%.pyc$", "%.pyo$", -- gettext "%.g?mo$", -- intltool "%.intltool%-merge%-cache$", -- TeX "%.aux$", -- backup files "%.bak$", "%.orig$", "%.rej$", "%~$", -- vim creates .foo.swp files "%.[^/]*%.swp$", -- emacs creates #foo# files "%#[^/]*%#$", -- other VCSes (where metadata is stored in named files): "%.scc$", -- desktop/directory configuration metadata "^%.DS_Store$", "/%.DS_Store$", "^desktop%.ini$", "/desktop%.ini$" } local dir_pats = { -- autotools detritus: "autom4te%.cache", "%.deps", "%.libs", -- Cons/SCons detritus: "%.consign", "%.sconsign", -- other VCSes (where metadata is stored in named dirs): "CVS", "%.svn", "SCCS", "_darcs", "%.cdv", "%.git", "%.bzr", "%.hg" } for _, pat in ipairs(file_pats) do if string.find(name, pat) then return true end end for _, pat in ipairs(dir_pats) do if dir_matches(name, pat) then return true end end return false; end -- return true means "binary", false means "text", -- nil means "unknown, try to guess" function binary_file(name) -- some known binaries, return true local bin_pats = { "%.gif$", "%.jpe?g$", "%.png$", "%.bz2$", "%.gz$", "%.zip$", "%.class$", "%.jar$", "%.war$", "%.ear$" } -- some known text, return false local txt_pats = { "%.cc?$", "%.cxx$", "%.hh?$", "%.hxx$", "%.cpp$", "%.hpp$", "%.lua$", "%.texi$", "%.sql$", "%.java$" } local lowname=string.lower(name) for _, pat in ipairs(bin_pats) do if string.find(lowname, pat) then return true end end for _, pat in ipairs(txt_pats) do if string.find(lowname, pat) then return false end end -- unknown - read file and use the guess-binary -- monotone built-in function return guess_binary_file_contents(name) end -- given a file name, return a regular expression which will match -- lines that name top-level constructs in that file, or "", to disable -- matching. function get_encloser_pattern(name) -- texinfo has special sectioning commands if (string.find(name, "%.texi$")) then -- sectioning commands in texinfo: @node, @chapter, @top, -- @((sub)?sub)?section, @unnumbered(((sub)?sub)?sec)?, -- @appendix(((sub)?sub)?sec)?, @(|major|chap|sub(sub)?)heading return ("^@(" .. "node|chapter|top" .. "|((sub)?sub)?section" .. "|(unnumbered|appendix)(((sub)?sub)?sec)?" .. "|(major|chap|sub(sub)?)?heading" .. ")") end -- LaTeX has special sectioning commands. This rule is applied to ordinary -- .tex files too, since there's no reliable way to distinguish those from -- latex files anyway, and there's no good pattern we could use for -- arbitrary plain TeX anyway. if (string.find(name, "%.tex$") or string.find(name, "%.ltx$") or string.find(name, "%.latex$")) then return ("\\\\(" .. "part|chapter|paragraph|subparagraph" .. "|((sub)?sub)?section" .. ")") end -- There's no good way to find section headings in raw text, and trying -- just gives distracting output, so don't even try. if (string.find(name, "%.txt$") or string.upper(name) == "README") then return "" end -- This default is correct surprisingly often -- in pretty much any text -- written with code-like indentation. return "^[[:alnum:]$_]" end function edit_comment(user_log_message) local exe = nil -- top priority is VISUAL, then EDITOR, then a series of hardcoded -- defaults, if available. local visual = os.getenv("VISUAL") local editor = os.getenv("EDITOR") if (visual ~= nil) then exe = visual elseif (editor ~= nil) then exe = editor elseif (program_exists_in_path("editor")) then exe = "editor" elseif (program_exists_in_path("vi")) then exe = "vi" elseif (string.sub(get_ostype(), 1, 6) ~= "CYGWIN" and program_exists_in_path("notepad.exe")) then exe = "notepad" else io.write(gettext("Could not find editor to enter commit message\n" .. "Try setting the environment variable EDITOR\n")) return nil end local tmp, tname = temp_file() if (tmp == nil) then return nil end tmp:write(user_log_message) if user_log_message == "" or string.sub(user_log_message, -1) ~= "\n" then tmp:write("\n") end io.close(tmp) -- By historical convention, VISUAL and EDITOR can contain arguments -- (and, in fact, arbitrarily complicated shell constructs). Since Lua -- has no word-splitting functionality, we invoke the shell to deal with -- anything more complicated than a single word with no metacharacters. -- This, unfortunately, means we have to quote the file argument. if (not string.find(exe, "[^%w_.+-]")) then -- safe to call spawn directly if (execute(exe, tname) ~= 0) then io.write(string.format(gettext("Error running editor '%s' ".. "to enter log message\n"), exe)) os.remove(tname) return nil end else -- must use shell local shell = os.getenv("SHELL") if (shell == nil) then shell = "sh" end if (not program_exists_in_path(shell)) then io.write(string.format(gettext("Editor command '%s' needs a shell, ".. "but '%s' is not to be found"), exe, shell)) os.remove(tname) return nil end -- Single-quoted strings in both Bourne shell and csh can contain -- anything but a single quote. local safe_tname = " '" .. string.gsub(tname, "'", "'\\''") .. "'" if (execute(shell, "-c", editor .. safe_tname) ~= 0) then io.write(string.format(gettext("Error running editor '%s' ".. "to enter log message\n"), exe)) os.remove(tname) return nil end end tmp = io.open(tname, "r") if (tmp == nil) then os.remove(tname); return nil end local res = tmp:read("*a") io.close(tmp) os.remove(tname) return res end function get_local_key_name(key_identity) return key_identity.given_name end function persist_phrase_ok() return true end function use_inodeprints() return false end function get_date_format_spec(wanted) -- Return the strftime(3) specification to be used to print dates -- in human-readable format after conversion to the local timezone. -- The default uses the preferred date and time representation for -- the current locale, e.g. the output looks like this: "09/08/2009 -- 06:49:26 PM" for en_US and "date_time_long", or "08.09.2009" -- for de_DE and "date_short" -- -- A sampling of other possible formats you might want: -- default for your locale: "%c" (may include a confusing timezone label) -- 12 hour format: "%d %b %Y, %I:%M:%S %p" -- like ctime(3): "%a %b %d %H:%M:%S %Y" -- email style: "%a, %d %b %Y %H:%M:%S" -- ISO 8601: "%Y-%m-%d %H:%M:%S" or "%Y-%m-%dT%H:%M:%S" -- -- ISO 8601, no timezone conversion: "" --. if (wanted == "date_long" or wanted == "date_short") then return "%x" end if (wanted == "time_long" or wanted == "time_short") then return "%X" end return "%x %X" end -- trust evaluation hooks function intersection(a,b) local s={} local t={} for k,v in pairs(a) do s[v.name] = 1 end for k,v in pairs(b) do if s[v] ~= nil then table.insert(t,v) end end return t end function get_revision_cert_trust(signers, id, name, val) return true end -- This is only used by migration from old manifest-style ancestry function get_manifest_cert_trust(signers, id, name, val) return true end -- http://snippets.luacode.org/?p=snippets/String_to_Hex_String_68 function hex_dump(str,spacer) return (string.gsub(str,"(.)", function (c) return string.format("%02x%s",string.byte(c), spacer or "") end) ) end function accept_testresult_change_hex(old_results, new_results) local reqfile = io.open("_MTN/wanted-testresults", "r") if (reqfile == nil) then return true end local line = reqfile:read() local required = {} while (line ~= nil) do required[line] = true line = reqfile:read() end io.close(reqfile) for test, res in pairs(required) do if old_results[test] == true and new_results[test] ~= true then return false end end return true end function accept_testresult_change(old_results, new_results) -- Hex encode each of the key hashes to match those in 'wanted-testresults' local old_results_hex = {} for k, v in pairs(old_results) do old_results_hex[hex_dump(k)] = v end local new_results_hex = {} for k, v in pairs(new_results) do new_results_hex[hex_dump(k)] = v end return accept_testresult_change_hex(old_results_hex, new_results_hex) end -- merger support -- Fields in the mergers structure: -- cmd : a function that performs the merge operation using the chosen -- program, best try. -- available : a function that checks that the needed program is installed and -- in $PATH -- wanted : a function that checks if the user doesn't want to use this -- method, and returns false if so. This should normally return -- true, but in some cases, especially when the merger is really -- an editor, the user might have a preference in EDITOR and we -- need to respect that. -- NOTE: wanted is only used when the user has NOT defined the -- `merger' variable or the MTN_MERGE environment variable. mergers = {} -- This merger is designed to fail if there are any conflicts without trying to resolve them mergers.fail = { cmd = function (tbl) return false end, available = function () return true end, wanted = function () return true end } mergers.meld = { cmd = function (tbl) io.write(string.format( "\nWARNING: 'meld' was chosen to perform an external 3-way merge.\n".. "You must merge all changes to the *CENTER* file.\n\n" )) local path = "meld" local ret = execute(path, tbl.lfile, tbl.afile, tbl.rfile) if (ret ~= 0) then io.write(string.format(gettext("Error running merger '%s'\n"), path)) return false end return tbl.afile end , available = function () return program_exists_in_path("meld") end, wanted = function () return true end } mergers.diffuse = { cmd = function (tbl) io.write(string.format( "\nWARNING: 'diffuse' was chosen to perform an external 3-way merge.\n".. "You must merge all changes to the *CENTER* file.\n\n" )) local path = "diffuse" local ret = execute(path, tbl.lfile, tbl.afile, tbl.rfile) if (ret ~= 0) then io.write(string.format(gettext("Error running merger '%s'\n"), path)) return false end return tbl.afile end , available = function () return program_exists_in_path("diffuse") end, wanted = function () return true end } mergers.tortoise = { cmd = function (tbl) local path = "tortoisemerge" local ret = execute(path, string.format("/base:%s", tbl.afile), string.format("/theirs:%s", tbl.lfile), string.format("/mine:%s", tbl.rfile), string.format("/merged:%s", tbl.outfile)) if (ret ~= 0) then io.write(string.format(gettext("Error running merger '%s'\n"), path)) return false end return tbl.outfile end , available = function() return program_exists_in_path ("tortoisemerge") end, wanted = function () return true end } mergers.vim = { cmd = function (tbl) function execute_diff3(mine, yours, out) local diff3_args = { "diff3", "--merge", "--easy-only", } table.insert(diff3_args, string.gsub(mine, "\\", "/") .. "") table.insert(diff3_args, string.gsub(tbl.afile, "\\", "/") .. "") table.insert(diff3_args, string.gsub(yours, "\\", "/") .. "") return execute_redirected("", string.gsub(out, "\\", "/"), "", unpack(diff3_args)) end io.write (string.format("\nWARNING: 'vim' was chosen to perform ".. "an external 3-way merge.\n".. "You must merge all changes to the ".. "*LEFT* file.\n")) local vim if os.getenv ("DISPLAY") ~= nil and program_exists_in_path ("gvim") then vim = "gvim" else vim = "vim" end local lfile_merged = tbl.lfile .. ".merged" local rfile_merged = tbl.rfile .. ".merged" -- first merge lfile using diff3 local ret = execute_diff3(tbl.lfile, tbl.rfile, lfile_merged) if ret == 2 then io.write(string.format(gettext("Error running diff3 for merger '%s'\n"), vim)) os.remove(lfile_merged) return false end -- now merge rfile using diff3 ret = execute_diff3(tbl.rfile, tbl.lfile, rfile_merged) if ret == 2 then io.write(string.format(gettext("Error running diff3 for merger '%s'\n"), vim)) os.remove(lfile_merged) os.remove(rfile_merged) return false end os.rename(lfile_merged, tbl.lfile) os.rename(rfile_merged, tbl.rfile) local ret = execute(vim, "-f", "-d", "-c", string.format("silent file %s", tbl.outfile), tbl.lfile, tbl.rfile) if (ret ~= 0) then io.write(string.format(gettext("Error running merger '%s'\n"), vim)) return false end return tbl.outfile end , available = function () return program_exists_in_path("diff3") and (program_exists_in_path("vim") or program_exists_in_path("gvim")) end , wanted = function () local editor = os.getenv("EDITOR") if editor and not (string.find(editor, "vim") or string.find(editor, "gvim")) then return false end return true end } mergers.rcsmerge = { cmd = function (tbl) -- XXX: This is tough - should we check if conflict markers stay or not? -- If so, we should certainly give the user some way to still force -- the merge to proceed since they can appear in the files (and I saw -- that). --pasky local merge = os.getenv("MTN_RCSMERGE") if execute(merge, tbl.lfile, tbl.afile, tbl.rfile) == 0 then copy_text_file(tbl.lfile, tbl.outfile); return tbl.outfile end local ret = execute("vim", "-f", "-c", string.format("file %s", tbl.outfile ), tbl.lfile) if (ret ~= 0) then io.write(string.format(gettext("Error running merger '%s'\n"), "vim")) return false end return tbl.outfile end, available = function () local merge = os.getenv("MTN_RCSMERGE") return merge and program_exists_in_path(merge) and program_exists_in_path("vim") end , wanted = function () return os.getenv("MTN_RCSMERGE") ~= nil end } -- GNU diffutils based merging mergers.diffutils = { -- merge procedure execution cmd = function (tbl) -- parse options local option = {} option.partial = false option.diff3opts = "" option.sdiffopts = "" local options = os.getenv("MTN_MERGE_DIFFUTILS") if options ~= nil then for spec in string.gmatch(options, "%s*(%w[^,]*)%s*,?") do local name, value = string.match(spec, "^(%w+)=([^,]*)") if name == nil then name = spec value = true end if type(option[name]) == "nil" then io.write("mtn: " .. string.format(gettext("invalid \"diffutils\" merger option \"%s\""), name) .. "\n") return false end option[name] = value end end -- determine the diff3(1) command local diff3 = { "diff3", "--merge", "--label", string.format("%s [left]", tbl.left_path ), "--label", string.format("%s [ancestor]", tbl.anc_path ), "--label", string.format("%s [right]", tbl.right_path), } if option.diff3opts ~= "" then for opt in string.gmatch(option.diff3opts, "%s*([^%s]+)%s*") do table.insert(diff3, opt) end end table.insert(diff3, string.gsub(tbl.lfile, "\\", "/") .. "") table.insert(diff3, string.gsub(tbl.afile, "\\", "/") .. "") table.insert(diff3, string.gsub(tbl.rfile, "\\", "/") .. "") -- dispatch according to major operation mode if option.partial then -- partial batch/non-modal 3-way merge "resolution": -- simply merge content with help of conflict markers io.write("mtn: " .. gettext("3-way merge via GNU diffutils, resolving conflicts via conflict markers") .. "\n") local ret = execute_redirected("", string.gsub(tbl.outfile, "\\", "/"), "", unpack(diff3)) if ret == 2 then io.write("mtn: " .. gettext("error running GNU diffutils 3-way difference/merge tool \"diff3\"") .. "\n") return false end return tbl.outfile else -- real interactive/modal 3/2-way merge resolution: -- display 3-way merge conflict and perform 2-way merge resolution io.write("mtn: " .. gettext("3-way merge via GNU diffutils, resolving conflicts via interactive prompt") .. "\n") -- display 3-way merge conflict (batch) io.write("\n") io.write("mtn: " .. gettext("---- CONFLICT SUMMARY ------------------------------------------------") .. "\n") local ret = execute(unpack(diff3)) if ret == 2 then io.write("mtn: " .. gettext("error running GNU diffutils 3-way difference/merge tool \"diff3\"") .. "\n") return false end -- perform 2-way merge resolution (interactive) io.write("\n") io.write("mtn: " .. gettext("---- CONFLICT RESOLUTION ---------------------------------------------") .. "\n") local sdiff = { "sdiff", "--diff-program=diff", "--suppress-common-lines", "--minimal", "--output=" .. string.gsub(tbl.outfile, "\\", "/") } if option.sdiffopts ~= "" then for opt in string.gmatch(option.sdiffopts, "%s*([^%s]+)%s*") do table.insert(sdiff, opt) end end table.insert(sdiff, string.gsub(tbl.lfile, "\\", "/") .. "") table.insert(sdiff, string.gsub(tbl.rfile, "\\", "/") .. "") local ret = execute(unpack(sdiff)) if ret == 2 then io.write("mtn: " .. gettext("error running GNU diffutils 2-way merging tool \"sdiff\"") .. "\n") return false end return tbl.outfile end end, -- merge procedure availability check available = function () -- make sure the GNU diffutils tools are available return program_exists_in_path("diff3") and program_exists_in_path("sdiff") and program_exists_in_path("diff"); end, -- merge procedure request check wanted = function () -- assume it is requested (if it is available at all) return true end } mergers.emacs = { cmd = function (tbl) local emacs if program_exists_in_path("xemacs") then emacs = "xemacs" else emacs = "emacs" end local elisp = "(ediff-merge-files-with-ancestor \"%s\" \"%s\" \"%s\" nil \"%s\")" -- Converting backslashes is necessary on Win32 MinGW; emacs -- lisp string syntax says '\' is an escape. local ret = execute(emacs, "--eval", string.format(elisp, string.gsub (tbl.lfile, "\\", "/"), string.gsub (tbl.rfile, "\\", "/"), string.gsub (tbl.afile, "\\", "/"), string.gsub (tbl.outfile, "\\", "/"))) if (ret ~= 0) then io.write(string.format(gettext("Error running merger '%s'\n"), emacs)) return false end return tbl.outfile end, available = function () return program_exists_in_path("xemacs") or program_exists_in_path("emacs") end , wanted = function () local editor = os.getenv("EDITOR") if editor and not (string.find(editor, "emacs") or string.find(editor, "gnu")) then return false end return true end } mergers.xxdiff = { cmd = function (tbl) local path = "xxdiff" local ret = execute(path, "--title1", tbl.left_path, "--title2", tbl.right_path, "--title3", tbl.merged_path, tbl.lfile, tbl.afile, tbl.rfile, "--merge", "--merged-filename", tbl.outfile, "--exit-with-merge-status") if (ret ~= 0) then io.write(string.format(gettext("Error running merger '%s'\n"), path)) return false end return tbl.outfile end, available = function () return program_exists_in_path("xxdiff") end, wanted = function () return true end } mergers.kdiff3 = { cmd = function (tbl) local path = "kdiff3" local ret = execute(path, "--L1", tbl.anc_path, "--L2", tbl.left_path, "--L3", tbl.right_path, tbl.afile, tbl.lfile, tbl.rfile, "--merge", "--o", tbl.outfile) if (ret ~= 0) then io.write(string.format(gettext("Error running merger '%s'\n"), path)) return false end return tbl.outfile end, available = function () return program_exists_in_path("kdiff3") end, wanted = function () return true end } mergers.opendiff = { cmd = function (tbl) local path = "opendiff" -- As opendiff immediately returns, let user confirm manually local ret = execute_confirm(path, tbl.lfile,tbl.rfile, "-ancestor",tbl.afile, "-merge",tbl.outfile) if (ret ~= 0) then io.write(string.format(gettext("Error running merger '%s'\n"), path)) return false end return tbl.outfile end, available = function () return program_exists_in_path("opendiff") end, wanted = function () return true end } function write_to_temporary_file(data, namehint, filemodehint) tmp, filename = temp_file(namehint, filemodehint) if (tmp == nil) then return nil end; tmp:write(data) io.close(tmp) return filename end function copy_text_file(srcname, destname) src = io.open(srcname, "r") if (src == nil) then return nil end dest = io.open(destname, "w") if (dest == nil) then return nil end while true do local line = src:read() if line == nil then break end dest:write(line, "\n") end io.close(dest) io.close(src) end function read_contents_of_file(filename, mode) tmp = io.open(filename, mode) if (tmp == nil) then return nil end local data = tmp:read("*a") io.close(tmp) return data end function program_exists_in_path(program) return existsonpath(program) == 0 end function get_preferred_merge3_command (tbl) local default_order = {"diffuse", "kdiff3", "xxdiff", "opendiff", "tortoise", "emacs", "vim", "meld", "diffutils"} local function existmerger(name) local m = mergers[name] if type(m) == "table" and m.available(tbl) then return m.cmd end return nil end local function trymerger(name) local m = mergers[name] if type(m) == "table" and m.available(tbl) and m.wanted(tbl) then return m.cmd end return nil end -- Check if there's a merger given by the user. local mkey = os.getenv("MTN_MERGE") if not mkey then mkey = merger end if not mkey and os.getenv("MTN_RCSMERGE") then mkey = "rcsmerge" end -- If there was a user-given merger, see if it exists. If it does, return -- the cmd function. If not, return nil. local c if mkey then c = existmerger(mkey) end if c then return c,mkey end if mkey then return nil,mkey end -- If there wasn't any user-given merger, take the first that's available -- and wanted. for _,mkey in ipairs(default_order) do c = trymerger(mkey) ; if c then return c,mkey end end end function merge3 (anc_path, left_path, right_path, merged_path, ancestor, left, right) local ret = nil local tbl = {} tbl.anc_path = anc_path tbl.left_path = left_path tbl.right_path = right_path tbl.merged_path = merged_path tbl.afile = nil tbl.lfile = nil tbl.rfile = nil tbl.outfile = nil tbl.meld_exists = false tbl.lfile = write_to_temporary_file (left, "left", "r+b") tbl.afile = write_to_temporary_file (ancestor, "ancestor", "r+b") tbl.rfile = write_to_temporary_file (right, "right", "r+b") tbl.outfile = write_to_temporary_file ("", "merged", "r+b") if tbl.lfile ~= nil and tbl.rfile ~= nil and tbl.afile ~= nil and tbl.outfile ~= nil then local cmd,mkey = get_preferred_merge3_command (tbl) if cmd ~=nil then io.write ("mtn: " .. string.format(gettext("executing external 3-way merge via \"%s\" merger\n"), mkey)) ret = cmd (tbl) if not ret then ret = nil else ret = read_contents_of_file (ret, "rb") if string.len (ret) == 0 then ret = nil end end else if mkey then io.write (string.format("The possible commands for the "..mkey.." merger aren't available.\n".. "You may want to check that $MTN_MERGE or the lua variable `merger' is set\n".. "to something available. If you want to use vim or emacs, you can also\n".. "set $EDITOR to something appropriate.\n")) else io.write (string.format("No external 3-way merge command found.\n".. "You may want to check that $EDITOR is set to an editor that supports 3-way\n".. "merge, set this explicitly in your get_preferred_merge3_command hook,\n".. "or add a 3-way merge program to your path.\n")) end end end os.remove (tbl.lfile) os.remove (tbl.rfile) os.remove (tbl.afile) os.remove (tbl.outfile) return ret end -- expansion of values used in selector completion function expand_selector(str) -- something which looks like a generic cert pattern if string.find(str, "^[^=]*=.*$") then return ("c:" .. str) end -- something which looks like an email address if string.find(str, "[%w%-_]+@[%w%-_]+") then return ("a:" .. str) end -- something which looks like a branch name if string.find(str, "[%w%-]+%.[%w%-]+") then return ("b:" .. str) end -- a sequence of nothing but hex digits if string.find(str, "^%x+$") then return ("i:" .. str) end -- tries to expand as a date local dtstr = expand_date(str) if dtstr ~= nil then return ("d:" .. dtstr) end return nil end -- expansion of a date expression function expand_date(str) -- simple date patterns if string.find(str, "^19%d%d%-%d%d") or string.find(str, "^20%d%d%-%d%d") then return (str) end -- "now" if str == "now" then local t = os.time(os.date('!*t')) return os.date("!%Y-%m-%dT%H:%M:%S", t) end -- today don't uses the time # for xgettext's sake, an extra quote if str == "today" then local t = os.time(os.date('!*t')) return os.date("!%Y-%m-%d", t) end -- "yesterday", the source of all hangovers if str == "yesterday" then local t = os.time(os.date('!*t')) return os.date("!%Y-%m-%d", t - 86400) end -- "CVS style" relative dates such as "3 weeks ago" local trans = { minute = 60; hour = 3600; day = 86400; week = 604800; month = 2678400; year = 31536000 } local pos, len, n, type = string.find(str, "(%d+) ([minutehordaywk]+)s? ago") if trans[type] ~= nil then local t = os.time(os.date('!*t')) if trans[type] <= 3600 then return os.date("!%Y-%m-%dT%H:%M:%S", t - (n * trans[type])) else return os.date("!%Y-%m-%d", t - (n * trans[type])) end end return nil end external_diff_default_args = "-u" -- default external diff, works for gnu diff function external_diff(file_path, data_old, data_new, is_binary, diff_args, rev_old, rev_new) local old_file = write_to_temporary_file(data_old, nil, "r+b"); local new_file = write_to_temporary_file(data_new, nil, "r+b"); if diff_args == nil then diff_args = external_diff_default_args end execute("diff", diff_args, "--label", file_path .. "\told", old_file, "--label", file_path .. "\tnew", new_file); os.remove (old_file); os.remove (new_file); end -- netsync permissions hooks (and helper) function globish_match(glob, str) local pcallstatus, result = pcall(function() if (globish.match(glob, str)) then return true else return false end end) if pcallstatus == true then -- no error return result else -- globish.match had a problem return nil end end function _get_netsync_read_permitted(branch, ident, permfilename, state) if not exists(permfilename) or isdir(permfilename) then return false end local permfile = io.open(permfilename, "r") if (permfile == nil) then return false end local dat = permfile:read("*a") io.close(permfile) local res = parse_basic_io(dat) if res == nil then io.stderr:write("file "..permfilename.." cannot be parsed\n") return false,"continue" end state["matches"] = state["matches"] or false state["cont"] = state["cont"] or false for i, item in pairs(res) do -- legal names: pattern, allow, deny, continue if item.name == "pattern" then if state["matches"] and not state["cont"] then return false end state["matches"] = false state["cont"] = false for j, val in pairs(item.values) do if globish_match(val, branch) then state["matches"] = true end end elseif item.name == "allow" then if state["matches"] then for j, val in pairs(item.values) do if val == "*" then return true end if val == "" and ident == nil then return true end if ident ~= nil and val == ident.id then return true end if ident ~= nil and globish_match(val, ident.name) then return true end end end elseif item.name == "deny" then if state["matches"] then for j, val in pairs(item.values) do if val == "*" then return false end if val == "" and ident == nil then return false end if ident ~= nil and val == ident.id then return false end if ident ~= nil and globish_match(val, ident.name) then return false end end end elseif item.name == "continue" then if state["matches"] then state["cont"] = true for j, val in pairs(item.values) do if val == "false" or val == "no" then state["cont"] = false end end end elseif item.name ~= "comment" then io.stderr:write("unknown symbol in read-permissions: " .. item.name .. "\n") return false end end return false end function get_netsync_read_permitted(branch, ident) local permfilename = get_confdir() .. "/read-permissions" local permdirname = permfilename .. ".d" local state = {} if _get_netsync_read_permitted(branch, ident, permfilename, state) then return true end if isdir(permdirname) then local files = read_directory(permdirname) table.sort(files) for _,f in ipairs(files) do pf = permdirname.."/"..f if _get_netsync_read_permitted(branch, ident, pf, state) then return true end end end return false end function _get_netsync_write_permitted(ident, permfilename) if not exists(permfilename) or isdir(permfilename) then return false end local permfile = io.open(permfilename, "r") if (permfile == nil) then return false end local matches = false local line = permfile:read() while (not matches and line ~= nil) do local _, _, ln = string.find(line, "%s*([^%s]*)%s*") if ln == "*" then matches = true end if ln == ident.id then matches = true end if globish_match(ln, ident.name) then matches = true end line = permfile:read() end io.close(permfile) return matches end function get_netsync_write_permitted(ident) local permfilename = get_confdir() .. "/write-permissions" local permdirname = permfilename .. ".d" if _get_netsync_write_permitted(ident, permfilename) then return true end if isdir(permdirname) then local files = read_directory(permdirname) table.sort(files) for _,f in ipairs(files) do pf = permdirname.."/"..f if _get_netsync_write_permitted(ident, pf) then return true end end end return false end -- This is a simple function which assumes you're going to be spawning -- a copy of mtn, so reuses a common bit at the end for converting -- local args into remote args. You might need to massage the logic a -- bit if this doesn't fit your assumptions. function get_netsync_connect_command(uri, args) local argv = nil if uri["scheme"] == "ssh" and uri["host"] and uri["path"] then argv = { "ssh" } if uri["user"] then table.insert(argv, "-l") table.insert(argv, uri["user"]) end if uri["port"] then table.insert(argv, "-p") table.insert(argv, uri["port"]) end -- ssh://host/~/dir/file.mtn or -- ssh://host/~user/dir/file.mtn should be home-relative if string.find(uri["path"], "^/~") then uri["path"] = string.sub(uri["path"], 2) end table.insert(argv, uri["host"]) end if uri["scheme"] == "file" and uri["path"] then argv = { } end if uri["scheme"] == "ssh+ux" and uri["host"] and uri["path"] then argv = { "ssh" } if uri["user"] then table.insert(argv, "-l") table.insert(argv, uri["user"]) end if uri["port"] then table.insert(argv, "-p") table.insert(argv, uri["port"]) end -- ssh://host/~/dir/file.mtn or -- ssh://host/~user/dir/file.mtn should be home-relative if string.find(uri["path"], "^/~") then uri["path"] = string.sub(uri["path"], 2) end table.insert(argv, uri["host"]) table.insert(argv, get_remote_unix_socket_command(uri["host"])) table.insert(argv, "-") table.insert(argv, "UNIX-CONNECT:" .. uri["path"]) else if argv then -- start remote monotone process table.insert(argv, get_mtn_command(uri["host"])) if args["debug"] then table.insert(argv, "--verbose") else table.insert(argv, "--quiet") end table.insert(argv, "--db") table.insert(argv, uri["path"]) table.insert(argv, "serve") table.insert(argv, "--stdio") table.insert(argv, "--no-transport-auth") -- else scheme does not require starting a new remote -- process (ie mtn:) end end return argv end function use_transport_auth(uri) if uri["scheme"] == "ssh" or uri["scheme"] == "ssh+ux" or uri["scheme"] == "file" then return false else return true end end function get_mtn_command(host) return "mtn" end function get_remote_unix_socket_command(host) return "socat" end function get_default_command_options(command) local default_args = {} return default_args end function get_default_database_alias() return ":default.mtn" end function get_default_database_locations() local paths = {} table.insert(paths, get_confdir() .. "/databases") return paths end function get_default_database_glob() return "*.{mtn,db}" end hook_wrapper_dump = {} hook_wrapper_dump.depth = 0 hook_wrapper_dump._string = function(s) return string.format("%q", s) end hook_wrapper_dump._number = function(n) return tostring(n) end hook_wrapper_dump._boolean = function(b) if (b) then return "true" end return "false" end hook_wrapper_dump._userdata = function(u) return "nil --[[userdata]]" end -- if we really need to return / serialize functions we could do it -- like cbreak@irc.freenode.net did here: http://lua-users.org/wiki/TablePersistence hook_wrapper_dump._function = function(f) return "nil --[[function]]" end hook_wrapper_dump._nil = function(n) return "nil" end hook_wrapper_dump._thread = function(t) return "nil --[[thread]]" end hook_wrapper_dump._lightuserdata = function(l) return "nil --[[lightuserdata]]" end hook_wrapper_dump._table = function(t) local buf = '' if (hook_wrapper_dump.depth > 0) then buf = buf .. '{\n' end hook_wrapper_dump.depth = hook_wrapper_dump.depth + 1; for k,v in pairs(t) do buf = buf..string.format('%s[%s] = %s;\n', string.rep("\t", hook_wrapper_dump.depth - 1), hook_wrapper_dump["_" .. type(k)](k), hook_wrapper_dump["_" .. type(v)](v)) end hook_wrapper_dump.depth = hook_wrapper_dump.depth - 1; if (hook_wrapper_dump.depth > 0) then buf = buf .. string.rep("\t", hook_wrapper_dump.depth - 1) .. '}' end return buf end function hook_wrapper(func_name, ...) -- we have to ensure that nil arguments are restored properly for the -- function call, see http://lua-users.org/wiki/StoringNilsInTables local args = { n=select('#', ...), ... } for i=1,args.n do local val = assert(loadstring("return " .. args[i]), "argument "..args[i].." could not be evaluated")() assert(val ~= nil or args[i] == "nil", "argument "..args[i].." was evaluated to nil") args[i] = val end local res = { _G[func_name](unpack(args, 1, args.n)) } return hook_wrapper_dump._table(res) end do -- Hook functions are tables containing any of the following 6 items -- with associated functions: -- -- startup Corresponds to note_mtn_startup() -- start Corresponds to note_netsync_start() -- revision_received Corresponds to note_netsync_revision_received() -- revision_sent Corresponds to note_netsync_revision_sent() -- cert_received Corresponds to note_netsync_cert_received() -- cert_sent Corresponds to note_netsync_cert_sent() -- pubkey_received Corresponds to note_netsync_pubkey_received() -- pubkey_sent Corresponds to note_netsync_pubkey_sent() -- end Corresponds to note_netsync_end() -- -- Those functions take exactly the same arguments as the corresponding -- global functions, but return a different kind of value, a tuple -- composed of a return code and a value to be returned back to monotone. -- The codes are strings: -- "continue" and "stop" -- When the code "continue" is returned and there's another notifier, the -- second value is ignored and the next notifier is called. Otherwise, -- the second value is returned immediately. local hook_functions = {} local supported_items = { "startup", "start", "revision_received", "revision_sent", "cert_received", "cert_sent", "pubkey_received", "pubkey_sent", "end" } function _hook_functions_helper(f,...) local s = "continue" local v = nil for _,n in pairs(hook_functions) do if n[f] then s,v = n[f](...) end if s ~= "continue" then break end end return v end function note_mtn_startup(...) return _hook_functions_helper("startup",...) end function note_netsync_start(...) return _hook_functions_helper("start",...) end function note_netsync_revision_received(...) return _hook_functions_helper("revision_received",...) end function note_netsync_revision_sent(...) return _hook_functions_helper("revision_sent",...) end function note_netsync_cert_received(...) return _hook_functions_helper("cert_received",...) end function note_netsync_cert_sent(...) return _hook_functions_helper("cert_sent",...) end function note_netsync_pubkey_received(...) return _hook_functions_helper("pubkey_received",...) end function note_netsync_pubkey_sent(...) return _hook_functions_helper("pubkey_sent",...) end function note_netsync_end(...) return _hook_functions_helper("end",...) end function add_hook_functions(functions, precedence) if type(functions) ~= "table" or type(precedence) ~= "number" then return false, "Invalid type" end if hook_functions[precedence] then return false, "Precedence already taken" end local unknown_items = "" local warning = nil local is_member = function (s,t) for k,v in pairs(t) do if s == v then return true end end return false end for n,f in pairs(functions) do if type(n) == "string" then if not is_member(n, supported_items) then if unknown_items ~= "" then unknown_items = unknown_items .. "," end unknown_items = unknown_items .. n end if type(f) ~= "function" then return false, "Value for functions item "..n.." isn't a function" end else warning = "Non-string item keys found in functions table" end end if warning == nil and unknown_items ~= "" then warning = "Unknown item(s) " .. unknown_items .. " in functions table" end hook_functions[precedence] = functions return true, warning end function push_hook_functions(functions) local n = #hook_functions + 1 return add_hook_functions(functions, n) end -- Kept for backward compatibility function add_netsync_notifier(notifier, precedence) return add_hook_functions(notifier, precedence) end function push_netsync_notifier(notifier) return push_hook_functions(notifier) end end -- to ensure only mapped authors are allowed through -- return "" from unmapped_git_author -- and validate_git_author will fail function unmapped_git_author(author) -- replace "foo@bar" with "foo <foo@bar>" name = author:match("^([^<>]+)@[^<>]+$") if name then return name .. " <" .. author .. ">" end -- replace "<foo@bar>" with "foo <foo@bar>" name = author:match("^<([^<>]+)@[^<>]+>$") if name then return name .. " " .. author end -- replace "foo" with "foo <foo>" name = author:match("^[^<>@]+$") if name then return name .. " <" .. name .. ">" end return author -- unchanged end function validate_git_author(author) -- ensure author matches the "Name <email>" format git expects if author:match("^[^<]+ <[^>]*>$") then return true end return false end function get_man_page_formatter_command() local term_width = guess_terminal_width() - 2 -- The string returned is run in a process created with 'popen' -- (see cmd.cc manpage). -- -- On Unix (and POSIX compliant systems), 'popen' runs 'sh' with -- the inherited path. -- -- On MinGW, 'popen' runs 'cmd.exe' with the inherited path. MinGW -- does not (currently) provide nroff or equivalent. So we assume -- sh, nroff, locale and less are also installed, from Cygwin or -- some other toolset. -- -- GROFF_ENCODING is an environment variable that, when set, tells -- groff (called by nroff where applicable) to use preconv to convert -- the input from the given encoding to something groff understands. -- For example, groff doesn NOT understand raw UTF-8 as input, but -- it does understand unicode, which preconv will happily provide. -- This doesn't help people that don't use groff, unfortunately. -- Patches are welcome! if string.sub(get_ostype(), 1, 7) == "Windows" then return string.format("sh -c 'GROFF_ENCODING=`locale charmap` nroff -man -rLL=%dn' | less -R", term_width) else return string.format("GROFF_ENCODING=`locale charmap` nroff -man -rLL=%dn | less -R", term_width) end end
Next: General Index, Previous: Special Topics, Up: Top [Contents][Index]