monotone

monotone Mtn Source Tree

Root/src/std_hooks.lua

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

Archive Download this file

Branches

Tags

Quick Links:     www.monotone.ca    -     Downloads    -     Documentation    -     Wiki    -     Code Forge    -     Build Status