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