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]