monotone

monotone Mtn Source Tree

Root/std_hooks.lua

1
2-- this is the standard set of lua hooks for monotone;
3-- user-provided files can override it or add to it.
4
5function temp_file(namehint)
6 local tdir
7 tdir = os.getenv("TMPDIR")
8 if tdir == nil then tdir = os.getenv("TMP") end
9 if tdir == nil then tdir = os.getenv("TEMP") end
10 if tdir == nil then tdir = "/tmp" end
11 local filename
12 if namehint == nil then
13 filename = string.format("%s/mtn.XXXXXX", tdir)
14 else
15 filename = string.format("%s/mtn.%s.XXXXXX", tdir, namehint)
16 end
17 local name = mkstemp(filename)
18 local file = io.open(name, "r+")
19 return file, name
20end
21
22function execute(path, ...)
23 local pid
24 local ret = -1
25 pid = spawn(path, unpack(arg))
26 if (pid ~= -1) then ret, pid = wait(pid) end
27 return ret
28end
29
30function execute_redirected(stdin, stdout, stderr, path, ...)
31 local pid
32 local ret = -1
33 io.flush();
34 pid = spawn_redirected(stdin, stdout, stderr, path, unpack(arg))
35 if (pid ~= -1) then ret, pid = wait(pid) end
36 return ret
37end
38
39-- Wrapper around execute to let user confirm in the case where a subprocess
40-- returns immediately
41-- This is needed to work around some brokenness with some merge tools
42-- (e.g. on OS X)
43function execute_confirm(path, ...)
44 ret = execute(path, unpack(arg))
45
46 if (ret ~= 0)
47 then
48 print(gettext("Press enter"))
49 else
50 print(gettext("Press enter when the subprocess has completed"))
51 end
52 io.read()
53 return ret
54end
55
56-- attributes are persistent metadata about files (such as execute
57-- bit, ACLs, various special flags) which we want to have set and
58-- re-set any time the files are modified. the attributes themselves
59-- are stored in the roster associated with the revision. each (f,k,v)
60-- attribute triple turns into a call to attr_functions[k](f,v) in lua.
61
62if (attr_init_functions == nil) then
63 attr_init_functions = {}
64end
65
66attr_init_functions["mtn:execute"] =
67 function(filename)
68 if (is_executable(filename)) then
69 return "true"
70 else
71 return nil
72 end
73 end
74
75attr_init_functions["mtn:manual_merge"] =
76 function(filename)
77 if (binary_file(filename)) then
78 return "true" -- binary files must be merged manually
79 else
80 return nil
81 end
82 end
83
84if (attr_functions == nil) then
85 attr_functions = {}
86end
87
88attr_functions["mtn:execute"] =
89 function(filename, value)
90 if (value == "true") then
91 make_executable(filename)
92 end
93 end
94
95function dir_matches(name, dir)
96 -- helper for ignore_file, matching files within dir, or dir itself.
97 -- eg for dir of 'CVS', matches CVS/, CVS/*, */CVS/ and */CVS/*
98 if (string.find(name, "^" .. dir .. "/")) then return true end
99 if (string.find(name, "^" .. dir .. "$")) then return true end
100 if (string.find(name, "/" .. dir .. "/")) then return true end
101 if (string.find(name, "/" .. dir .. "$")) then return true end
102 return false
103end
104
105function ignore_file(name)
106 -- project specific
107 if (ignored_files == nil) then
108 ignored_files = {}
109 local ignfile = io.open(".mtn-ignore", "r")
110 if (ignfile ~= nil) then
111 local line = ignfile:read()
112 while (line ~= nil) do
113 if line ~= "" then
114 table.insert(ignored_files, line)
115 end
116 line = ignfile:read()
117 end
118 io.close(ignfile)
119 end
120 end
121
122 local warn_reported_file = false
123 for i, line in pairs(ignored_files)
124 do
125 if (line ~= nil) then
126 local pcallstatus, result = pcall(function()
127 return regex.search(line, name)
128 end)
129 if pcallstatus == true then
130 -- no error from the regex.search call
131 if result == true then return true end
132 else
133 -- regex.search had a problem, warn the user their
134 -- .mtn-ignore file syntax is wrong
135 if not warn_reported_file then
136 io.stderr:write("mtn: warning: while matching file '"
137 .. name .. "':\n")
138 warn_reported_file = true
139 end
140 io.stderr:write(".mtn-ignore:" .. i .. ": warning: " .. result
141 .. "\n\t- skipping this regex for "
142 .. "all remaining files.\n")
143 ignored_files[i] = nil
144 end
145 end
146 end
147
148 local file_pats = {
149 -- c/c++
150 "%.a$", "%.so$", "%.o$", "%.la$", "%.lo$", "^core$",
151 "/core$", "/core%.%d+$",
152 -- java
153 "%.class$",
154 -- python
155 "%.pyc$", "%.pyo$",
156 -- gettext
157 "%.g?mo$",
158 -- intltool
159 "%.intltool%-merge%-cache$",
160 -- TeX
161 "%.aux$",
162 -- backup files
163 "%.bak$", "%.orig$", "%.rej$", "%~$",
164 -- vim creates .foo.swp files
165 "%.[^/]*%.swp$",
166 -- emacs creates #foo# files
167 "%#[^/]*%#$",
168 -- other VCSes (where metadata is stored in named files):
169 "%.scc$",
170 -- desktop/directory configuration metadata
171 "^%.DS_Store$", "/%.DS_Store$", "^desktop%.ini$", "/desktop%.ini$"
172 }
173
174 local dir_pats = {
175 -- autotools detritus:
176 "autom4te%.cache", "%.deps", "%.libs",
177 -- Cons/SCons detritus:
178 "%.consign", "%.sconsign",
179 -- other VCSes (where metadata is stored in named dirs):
180 "CVS", "%.svn", "SCCS", "_darcs", "%.cdv", "%.git", "%.bzr", "%.hg"
181 }
182
183 for _, pat in ipairs(file_pats) do
184 if string.find(name, pat) then return true end
185 end
186 for _, pat in ipairs(dir_pats) do
187 if dir_matches(name, pat) then return true end
188 end
189
190 return false;
191end
192
193-- return true means "binary", false means "text",
194-- nil means "unknown, try to guess"
195function binary_file(name)
196 -- some known binaries, return true
197 local bin_pats = {
198 "%.gif$", "%.jpe?g$", "%.png$", "%.bz2$", "%.gz$", "%.zip$",
199 "%.class$", "%.jar$", "%.war$", "%.ear$"
200 }
201
202 -- some known text, return false
203 local txt_pats = {
204 "%.cc?$", "%.cxx$", "%.hh?$", "%.hxx$", "%.cpp$", "%.hpp$",
205 "%.lua$", "%.texi$", "%.sql$", "%.java$"
206 }
207
208 local lowname=string.lower(name)
209 for _, pat in ipairs(bin_pats) do
210 if string.find(lowname, pat) then return true end
211 end
212 for _, pat in ipairs(txt_pats) do
213 if string.find(lowname, pat) then return false end
214 end
215
216 -- unknown - read file and use the guess-binary
217 -- monotone built-in function
218 return guess_binary_file_contents(name)
219end
220
221-- given a file name, return a regular expression which will match
222-- lines that name top-level constructs in that file, or "", to disable
223-- matching.
224function get_encloser_pattern(name)
225 -- texinfo has special sectioning commands
226 if (string.find(name, "%.texi$")) then
227 -- sectioning commands in texinfo: @node, @chapter, @top,
228 -- @((sub)?sub)?section, @unnumbered(((sub)?sub)?sec)?,
229 -- @appendix(((sub)?sub)?sec)?, @(|major|chap|sub(sub)?)heading
230 return ("^@("
231 .. "node|chapter|top"
232 .. "|((sub)?sub)?section"
233 .. "|(unnumbered|appendix)(((sub)?sub)?sec)?"
234 .. "|(major|chap|sub(sub)?)?heading"
235 .. ")")
236 end
237 -- LaTeX has special sectioning commands. This rule is applied to ordinary
238 -- .tex files too, since there's no reliable way to distinguish those from
239 -- latex files anyway, and there's no good pattern we could use for
240 -- arbitrary plain TeX anyway.
241 if (string.find(name, "%.tex$")
242 or string.find(name, "%.ltx$")
243 or string.find(name, "%.latex$")) then
244 return ("\\\\("
245 .. "part|chapter|paragraph|subparagraph"
246 .. "|((sub)?sub)?section"
247 .. ")")
248 end
249 -- There's no good way to find section headings in raw text, and trying
250 -- just gives distracting output, so don't even try.
251 if (string.find(name, "%.txt$")
252 or string.upper(name) == "README") then
253 return ""
254 end
255 -- This default is correct surprisingly often -- in pretty much any text
256 -- written with code-like indentation.
257 return "^[[:alnum:]$_]"
258end
259
260function edit_comment(basetext, user_log_message)
261 local exe = nil
262 if (program_exists_in_path("vi")) then exe = "vi" end
263 if (string.sub(get_ostype(), 1, 6) ~= "CYGWIN" and program_exists_in_path("notepad.exe")) then exe = "notepad.exe" end
264 local debian_editor = io.open("/usr/bin/editor")
265 if (debian_editor ~= nil) then
266 debian_editor:close()
267 exe = "/usr/bin/editor"
268 end
269 local visual = os.getenv("VISUAL")
270 if (visual ~= nil) then exe = visual end
271 local editor = os.getenv("EDITOR")
272 if (editor ~= nil) then exe = editor end
273
274 if (exe == nil) then
275 io.write("Could not find editor to enter commit message\n"
276 .. "Try setting the environment variable EDITOR\n")
277 return nil
278 end
279
280 local tmp, tname = temp_file()
281 if (tmp == nil) then return nil end
282 basetext = "MTN: " .. string.gsub(basetext, "\n", "\nMTN: ") .. "\n"
283 tmp:write(user_log_message)
284 if user_log_message == "" or string.sub(user_log_message, -1) ~= "\n" then
285 tmp:write("\n")
286 end
287 tmp:write(basetext)
288 io.close(tmp)
289
290 if (execute(exe, tname) ~= 0) then
291 io.write(string.format(gettext("Error running editor '%s' to enter log message\n"),
292 exe))
293 os.remove(tname)
294 return nil
295 end
296
297 tmp = io.open(tname, "r")
298 if (tmp == nil) then os.remove(tname); return nil end
299 local res = ""
300 local line = tmp:read()
301 while(line ~= nil) do
302 if (not string.find(line, "^MTN:")) then
303 res = res .. line .. "\n"
304 end
305 line = tmp:read()
306 end
307 io.close(tmp)
308 os.remove(tname)
309 return res
310end
311
312
313function persist_phrase_ok()
314 return true
315end
316
317
318function use_inodeprints()
319 return false
320end
321
322
323-- trust evaluation hooks
324
325function intersection(a,b)
326 local s={}
327 local t={}
328 for k,v in pairs(a) do s[v] = 1 end
329 for k,v in pairs(b) do if s[v] ~= nil then table.insert(t,v) end end
330 return t
331end
332
333function get_revision_cert_trust(signers, id, name, val)
334 return true
335end
336
337function get_manifest_cert_trust(signers, id, name, val)
338 return true
339end
340
341function get_file_cert_trust(signers, id, name, val)
342 return true
343end
344
345function accept_testresult_change(old_results, new_results)
346 local reqfile = io.open("_MTN/wanted-testresults", "r")
347 if (reqfile == nil) then return true end
348 local line = reqfile:read()
349 local required = {}
350 while (line ~= nil)
351 do
352 required[line] = true
353 line = reqfile:read()
354 end
355 io.close(reqfile)
356 for test, res in pairs(required)
357 do
358 if old_results[test] == true and new_results[test] ~= true
359 then
360 return false
361 end
362 end
363 return true
364end
365
366-- merger support
367
368-- Fields in the mergers structure:
369-- cmd : a function that performs the merge operation using the chosen
370-- program, best try.
371-- available : a function that checks that the needed program is installed and
372-- in $PATH
373-- wanted : a function that checks if the user doesn't want to use this
374-- method, and returns false if so. This should normally return
375-- true, but in some cases, especially when the merger is really
376-- an editor, the user might have a preference in EDITOR and we
377-- need to respect that.
378-- NOTE: wanted is only used when the user has NOT defined the
379-- `merger' variable or the MTN_MERGE environment variable.
380mergers = {}
381
382-- This merger is designed to fail if there are any conflicts without trying to resolve them
383mergers.fail = {
384 cmd = function (tbl) return false end,
385 available = function () return true end,
386 wanted = function () return true end
387}
388
389mergers.meld = {
390 cmd = function (tbl)
391 io.write (string.format("\nWARNING: 'meld' was choosen to perform external 3-way merge.\n"..
392 "You should merge all changes to *CENTER* file due to limitation of program\n"..
393 "arguments.\n\n"))
394 local path = "meld"
395 local ret = execute(path, tbl.lfile, tbl.afile, tbl.rfile)
396 if (ret ~= 0) then
397 io.write(string.format(gettext("Error running merger '%s'\n"), path))
398 return false
399 end
400 return tbl.afile
401 end ,
402 available = function () return program_exists_in_path("meld") end,
403 wanted = function () return true end
404}
405
406mergers.tortoise = {
407 cmd = function (tbl)
408 local path = "tortoisemerge"
409 local ret = execute(path,
410 string.format("/base:%s", tbl.afile),
411 string.format("/theirs:%s", tbl.lfile),
412 string.format("/mine:%s", tbl.rfile),
413 string.format("/merged:%s", tbl.outfile))
414 if (ret ~= 0) then
415 io.write(string.format(gettext("Error running merger '%s'\n"), path))
416 return false
417 end
418 return tbl.outfile
419 end ,
420 available = function() return program_exists_in_path ("tortoisemerge") end,
421 wanted = function () return true end
422}
423
424mergers.vim = {
425 cmd = function (tbl)
426 io.write (string.format("\nWARNING: 'vim' was choosen to perform external 3-way merge.\n"..
427 "You should merge all changes to *LEFT* file due to limitation of program\n"..
428 "arguments. The order of the files is ancestor, left, right.\n\n"))
429 local vim
430 local exec
431 if os.getenv ("DISPLAY") ~= nil and program_exists_in_path ("gvim") then
432 vim = "gvim"
433 exec = execute_confirm
434 else
435 vim = "vim"
436 exec = execute
437 end
438 local ret = exec(vim, "-f", "-d", "-c", string.format("file %s", tbl.outfile),
439 tbl.afile, tbl.lfile, tbl.rfile)
440 if (ret ~= 0) then
441 io.write(string.format(gettext("Error running merger '%s'\n"), vim))
442 return false
443 end
444 return tbl.outfile
445 end ,
446 available =
447 function ()
448 return program_exists_in_path("vim") or
449 program_exists_in_path("gvim")
450 end ,
451 wanted =
452 function ()
453 local editor = os.getenv("EDITOR")
454 if editor and
455 not (string.find(editor, "vim") or
456 string.find(editor, "gvim")) then
457 return false
458 end
459 return true
460 end
461}
462
463mergers.rcsmerge = {
464 cmd = function (tbl)
465 -- XXX: This is tough - should we check if conflict markers stay or not?
466 -- If so, we should certainly give the user some way to still force
467 -- the merge to proceed since they can appear in the files (and I saw
468 -- that). --pasky
469 local merge = os.getenv("MTN_RCSMERGE")
470 if execute(merge, tbl.lfile, tbl.afile, tbl.rfile) == 0 then
471 copy_text_file(tbl.lfile, tbl.outfile);
472 return tbl.outfile
473 end
474 local ret = execute("vim", "-f", "-c", string.format("file %s", tbl.outfile
475),
476 tbl.lfile)
477 if (ret ~= 0) then
478 io.write(string.format(gettext("Error running merger '%s'\n"), "vim"))
479 return false
480 end
481 return tbl.outfile
482 end,
483 available =
484 function ()
485 local merge = os.getenv("MTN_RCSMERGE")
486 return merge and
487 program_exists_in_path(merge) and program_exists_in_path("vim")
488 end ,
489 wanted = function () return os.getenv("MTN_RCSMERGE") ~= nil end
490}
491
492-- GNU diffutils based merging
493mergers.diffutils = {
494 -- merge procedure execution
495 cmd = function (tbl)
496 -- parse options
497 local option = {}
498 option.partial = false
499 option.diff3opts = ""
500 option.sdiffopts = ""
501 local options = os.getenv("MTN_MERGE_DIFFUTILS")
502 if options ~= nil then
503 for spec in string.gmatch(options, "%s*(%w[^,]*)%s*,?") do
504 local name, value = string.match(spec, "^(%w+)=([^,]*)")
505 if name == nil then
506 name = spec
507 value = true
508 end
509 if type(option[name]) == "nil" then
510 io.write("mtn: " .. string.format(gettext("invalid \"diffutils\" merger option \"%s\""), name) .. "\n")
511 return false
512 end
513 option[name] = value
514 end
515 end
516
517 -- determine the diff3(1) command
518 local diff3 = {
519 "diff3",
520 "--merge",
521 "--label", string.format("%s [left]", tbl.left_path ),
522 "--label", string.format("%s [ancestor]", tbl.anc_path ),
523 "--label", string.format("%s [right]", tbl.right_path),
524 }
525 if option.diff3opts ~= "" then
526 for opt in string.gmatch(option.diff3opts, "%s*([^%s]+)%s*") do
527 table.insert(diff3, opt)
528 end
529 end
530 table.insert(diff3, string.gsub(tbl.lfile, "\\", "/") .. "")
531 table.insert(diff3, string.gsub(tbl.afile, "\\", "/") .. "")
532 table.insert(diff3, string.gsub(tbl.rfile, "\\", "/") .. "")
533
534 -- dispatch according to major operation mode
535 if option.partial then
536 -- partial batch/non-modal 3-way merge "resolution":
537 -- simply merge content with help of conflict markers
538 io.write("mtn: " .. gettext("3-way merge via GNU diffutils, resolving conflicts via conflict markers") .. "\n")
539 local ret = execute_redirected("", string.gsub(tbl.outfile, "\\", "/"), "", unpack(diff3))
540 if ret == 2 then
541 io.write("mtn: " .. gettext("error running GNU diffutils 3-way difference/merge tool \"diff3\"") .. "\n")
542 return false
543 end
544 return tbl.outfile
545 else
546 -- real interactive/modal 3/2-way merge resolution:
547 -- display 3-way merge conflict and perform 2-way merge resolution
548 io.write("mtn: " .. gettext("3-way merge via GNU diffutils, resolving conflicts via interactive prompt") .. "\n")
549
550 -- display 3-way merge conflict (batch)
551 io.write("\n")
552 io.write("mtn: " .. gettext("---- CONFLICT SUMMARY ------------------------------------------------") .. "\n")
553 local ret = execute(unpack(diff3))
554 if ret == 2 then
555 io.write("mtn: " .. gettext("error running GNU diffutils 3-way difference/merge tool \"diff3\"") .. "\n")
556 return false
557 end
558
559 -- perform 2-way merge resolution (interactive)
560 io.write("\n")
561 io.write("mtn: " .. gettext("---- CONFLICT RESOLUTION ---------------------------------------------") .. "\n")
562 local sdiff = {
563 "sdiff",
564 "--diff-program=diff",
565 "--suppress-common-lines",
566 "--minimal",
567 "--output=" .. string.gsub(tbl.outfile, "\\", "/")
568 }
569 if option.sdiffopts ~= "" then
570 for opt in string.gmatch(option.sdiffopts, "%s*([^%s]+)%s*") do
571 table.insert(sdiff, opt)
572 end
573 end
574 table.insert(sdiff, string.gsub(tbl.lfile, "\\", "/") .. "")
575 table.insert(sdiff, string.gsub(tbl.rfile, "\\", "/") .. "")
576 local ret = execute(unpack(sdiff))
577 if ret == 2 then
578 io.write("mtn: " .. gettext("error running GNU diffutils 2-way merging tool \"sdiff\"") .. "\n")
579 return false
580 end
581 return tbl.outfile
582 end
583 end,
584
585 -- merge procedure availability check
586 available = function ()
587 -- make sure the GNU diffutils tools are available
588 return program_exists_in_path("diff3") and
589 program_exists_in_path("sdiff") and
590 program_exists_in_path("diff");
591 end,
592
593 -- merge procedure request check
594 wanted = function ()
595 -- assume it is requested (if it is available at all)
596 return true
597 end
598}
599
600mergers.emacs = {
601 cmd = function (tbl)
602 local emacs
603 if program_exists_in_path("xemacs") then
604 emacs = "xemacs"
605 else
606 emacs = "emacs"
607 end
608 local elisp = "(ediff-merge-files-with-ancestor \"%s\" \"%s\" \"%s\" nil \"%s\")"
609 -- Converting backslashes is necessary on Win32 MinGW; emacs
610 -- lisp string syntax says '\' is an escape.
611 local ret = execute(emacs, "--eval",
612 string.format(elisp,
613 string.gsub (tbl.lfile, "\\", "/"),
614 string.gsub (tbl.rfile, "\\", "/"),
615 string.gsub (tbl.afile, "\\", "/"),
616 string.gsub (tbl.outfile, "\\", "/")))
617 if (ret ~= 0) then
618 io.write(string.format(gettext("Error running merger '%s'\n"), emacs))
619 return false
620 end
621 return tbl.outfile
622 end,
623 available =
624 function ()
625 return program_exists_in_path("xemacs") or
626 program_exists_in_path("emacs")
627 end ,
628 wanted =
629 function ()
630 local editor = os.getenv("EDITOR")
631 if editor and
632 not (string.find(editor, "emacs") or
633 string.find(editor, "gnu")) then
634 return false
635 end
636 return true
637 end
638}
639
640mergers.xxdiff = {
641 cmd = function (tbl)
642 local path = "xxdiff"
643 local ret = execute(path,
644 "--title1", tbl.left_path,
645 "--title2", tbl.right_path,
646 "--title3", tbl.merged_path,
647 tbl.lfile, tbl.afile, tbl.rfile,
648 "--merge",
649 "--merged-filename", tbl.outfile,
650 "--exit-with-merge-status")
651 if (ret ~= 0) then
652 io.write(string.format(gettext("Error running merger '%s'\n"), path))
653 return false
654 end
655 return tbl.outfile
656 end,
657 available = function () return program_exists_in_path("xxdiff") end,
658 wanted = function () return true end
659}
660
661mergers.kdiff3 = {
662 cmd = function (tbl)
663 local path = "kdiff3"
664 local ret = execute(path,
665 "--L1", tbl.anc_path,
666 "--L2", tbl.left_path,
667 "--L3", tbl.right_path,
668 tbl.afile, tbl.lfile, tbl.rfile,
669 "--merge",
670 "--o", tbl.outfile)
671 if (ret ~= 0) then
672 io.write(string.format(gettext("Error running merger '%s'\n"), path))
673 return false
674 end
675 return tbl.outfile
676 end,
677 available = function () return program_exists_in_path("kdiff3") end,
678 wanted = function () return true end
679}
680
681mergers.opendiff = {
682 cmd = function (tbl)
683 local path = "opendiff"
684 -- As opendiff immediately returns, let user confirm manually
685 local ret = execute_confirm(path,
686 tbl.lfile,tbl.rfile,
687 "-ancestor",tbl.afile,
688 "-merge",tbl.outfile)
689 if (ret ~= 0) then
690 io.write(string.format(gettext("Error running merger '%s'\n"), path))
691 return false
692 end
693 return tbl.outfile
694 end,
695 available = function () return program_exists_in_path("opendiff") end,
696 wanted = function () return true end
697}
698
699function write_to_temporary_file(data, namehint)
700 tmp, filename = temp_file(namehint)
701 if (tmp == nil) then
702 return nil
703 end;
704 tmp:write(data)
705 io.close(tmp)
706 return filename
707end
708
709function copy_text_file(srcname, destname)
710 src = io.open(srcname, "r")
711 if (src == nil) then return nil end
712 dest = io.open(destname, "w")
713 if (dest == nil) then return nil end
714
715 while true do
716 local line = src:read()
717 if line == nil then break end
718 dest:write(line, "\n")
719 end
720
721 io.close(dest)
722 io.close(src)
723end
724
725function read_contents_of_file(filename, mode)
726 tmp = io.open(filename, mode)
727 if (tmp == nil) then
728 return nil
729 end
730 local data = tmp:read("*a")
731 io.close(tmp)
732 return data
733end
734
735function program_exists_in_path(program)
736 return existsonpath(program) == 0
737end
738
739function get_preferred_merge3_command (tbl)
740 local default_order = {"kdiff3", "xxdiff", "opendiff", "tortoise", "emacs", "vim", "meld", "diffutils"}
741 local function existmerger(name)
742 local m = mergers[name]
743 if type(m) == "table" and m.available(tbl) then
744 return m.cmd
745 end
746 return nil
747 end
748 local function trymerger(name)
749 local m = mergers[name]
750 if type(m) == "table" and m.available(tbl) and m.wanted(tbl) then
751 return m.cmd
752 end
753 return nil
754 end
755 -- Check if there's a merger given by the user.
756 local mkey = os.getenv("MTN_MERGE")
757 if not mkey then mkey = merger end
758 if not mkey and os.getenv("MTN_RCSMERGE") then mkey = "rcsmerge" end
759 -- If there was a user-given merger, see if it exists. If it does, return
760 -- the cmd function. If not, return nil.
761 local c
762 if mkey then c = existmerger(mkey) end
763 if c then return c,mkey end
764 if mkey then return nil,mkey end
765 -- If there wasn't any user-given merger, take the first that's available
766 -- and wanted.
767 for _,mkey in ipairs(default_order) do
768 c = trymerger(mkey) ; if c then return c,nil end
769 end
770end
771
772function merge3 (anc_path, left_path, right_path, merged_path, ancestor, left, right)
773 local ret = nil
774 local tbl = {}
775
776 tbl.anc_path = anc_path
777 tbl.left_path = left_path
778 tbl.right_path = right_path
779
780 tbl.merged_path = merged_path
781 tbl.afile = nil
782 tbl.lfile = nil
783 tbl.rfile = nil
784 tbl.outfile = nil
785 tbl.meld_exists = false
786 tbl.lfile = write_to_temporary_file (left, "left")
787 tbl.afile = write_to_temporary_file (ancestor, "ancestor")
788 tbl.rfile = write_to_temporary_file (right, "right")
789 tbl.outfile = write_to_temporary_file ("", "merged")
790
791 if tbl.lfile ~= nil and tbl.rfile ~= nil and tbl.afile ~= nil and tbl.outfile ~= nil
792 then
793 local cmd,mkey = get_preferred_merge3_command (tbl)
794 if cmd ~=nil
795 then
796 io.write ("mtn: " .. string.format(gettext("executing external 3-way merge via \"%s\" merger\n"), mkey))
797 ret = cmd (tbl)
798 if not ret then
799 ret = nil
800 else
801 ret = read_contents_of_file (ret, "r")
802 if string.len (ret) == 0
803 then
804 ret = nil
805 end
806 end
807 else
808 if mkey then
809 io.write (string.format("The possible commands for the "..mkey.." merger aren't available.\n"..
810 "You may want to check that $MTN_MERGE or the lua variable `merger' is set\n"..
811 "to something available. If you want to use vim or emacs, you can also\n"..
812"set $EDITOR to something appropriate.\n"))
813 else
814 io.write (string.format("No external 3-way merge command found.\n"..
815 "You may want to check that $EDITOR is set to an editor that supports 3-way\n"..
816 "merge, set this explicitly in your get_preferred_merge3_command hook,\n"..
817 "or add a 3-way merge program to your path.\n"))
818 end
819 end
820 end
821
822 os.remove (tbl.lfile)
823 os.remove (tbl.rfile)
824 os.remove (tbl.afile)
825 os.remove (tbl.outfile)
826
827 return ret
828end
829
830-- expansion of values used in selector completion
831
832function expand_selector(str)
833
834 -- something which looks like a generic cert pattern
835 if string.find(str, "^[^=]*=.*$")
836 then
837 return ("c:" .. str)
838 end
839
840 -- something which looks like an email address
841 if string.find(str, "[%w%-_]+@[%w%-_]+")
842 then
843 return ("a:" .. str)
844 end
845
846 -- something which looks like a branch name
847 if string.find(str, "[%w%-]+%.[%w%-]+")
848 then
849 return ("b:" .. str)
850 end
851
852 -- a sequence of nothing but hex digits
853 if string.find(str, "^%x+$")
854 then
855 return ("i:" .. str)
856 end
857
858 -- tries to expand as a date
859 local dtstr = expand_date(str)
860 if dtstr ~= nil
861 then
862 return ("d:" .. dtstr)
863 end
864
865 return nil
866end
867
868-- expansion of a date expression
869function expand_date(str)
870 -- simple date patterns
871 if string.find(str, "^19%d%d%-%d%d")
872 or string.find(str, "^20%d%d%-%d%d")
873 then
874 return (str)
875 end
876
877 -- "now"
878 if str == "now"
879 then
880 local t = os.time(os.date('!*t'))
881 return os.date("%FT%T", t)
882 end
883
884 -- today don't uses the time # for xgettext's sake, an extra quote
885 if str == "today"
886 then
887 local t = os.time(os.date('!*t'))
888 return os.date("%F", t)
889 end
890
891 -- "yesterday", the source of all hangovers
892 if str == "yesterday"
893 then
894 local t = os.time(os.date('!*t'))
895 return os.date("%F", t - 86400)
896 end
897
898 -- "CVS style" relative dates such as "3 weeks ago"
899 local trans = {
900 minute = 60;
901 hour = 3600;
902 day = 86400;
903 week = 604800;
904 month = 2678400;
905 year = 31536000
906 }
907 local pos, len, n, type = string.find(str, "(%d+) ([minutehordaywk]+)s? ago")
908 if trans[type] ~= nil
909 then
910 local t = os.time(os.date('!*t'))
911 if trans[type] <= 3600
912 then
913 return os.date("%FT%T", t - (n * trans[type]))
914 else
915 return os.date("%F", t - (n * trans[type]))
916 end
917 end
918
919 return nil
920end
921
922
923external_diff_default_args = "-u"
924
925-- default external diff, works for gnu diff
926function external_diff(file_path, data_old, data_new, is_binary, diff_args, rev_old, rev_new)
927 local old_file = write_to_temporary_file(data_old);
928 local new_file = write_to_temporary_file(data_new);
929
930 if diff_args == nil then diff_args = external_diff_default_args end
931 execute("diff", diff_args, "--label", file_path .. "\told", old_file, "--label", file_path .. "\tnew", new_file);
932
933 os.remove (old_file);
934 os.remove (new_file);
935end
936
937-- netsync permissions hooks (and helper)
938
939function globish_match(glob, str)
940 local pcallstatus, result = pcall(function() if (globish.match(glob, str)) then return true else return false end end)
941 if pcallstatus == true then
942 -- no error
943 return result
944 else
945 -- globish.match had a problem
946 return nil
947 end
948end
949
950function get_netsync_read_permitted(branch, ident)
951 local permfile = io.open(get_confdir() .. "/read-permissions", "r")
952 if (permfile == nil) then return false end
953 local dat = permfile:read("*a")
954 io.close(permfile)
955 local res = parse_basic_io(dat)
956 if res == nil then
957 io.stderr:write("file read-permissions cannot be parsed\n")
958 return false
959 end
960 local matches = false
961 local cont = false
962 for i, item in pairs(res)
963 do
964 -- legal names: pattern, allow, deny, continue
965 if item.name == "pattern" then
966 if matches and not cont then return false end
967 matches = false
968 cont = false
969 for j, val in pairs(item.values) do
970 if globish_match(val, branch) then matches = true end
971 end
972 elseif item.name == "allow" then if matches then
973 for j, val in pairs(item.values) do
974 if val == "*" then return true end
975 if val == "" and ident == nil then return true end
976 if globish_match(val, ident) then return true end
977 end
978 end elseif item.name == "deny" then if matches then
979 for j, val in pairs(item.values) do
980 if val == "*" then return false end
981 if val == "" and ident == nil then return false end
982 if globish_match(val, ident) then return false end
983 end
984 end elseif item.name == "continue" then if matches then
985 cont = true
986 for j, val in pairs(item.values) do
987 if val == "false" or val == "no" then cont = false end
988 end
989 end elseif item.name ~= "comment" then
990 io.stderr:write("unknown symbol in read-permissions: " .. item.name .. "\n")
991 return false
992 end
993 end
994 return false
995end
996
997function get_netsync_write_permitted(ident)
998 local permfile = io.open(get_confdir() .. "/write-permissions", "r")
999 if (permfile == nil) then
1000 return false
1001 end
1002 local matches = false
1003 local line = permfile:read()
1004 while (not matches and line ~= nil) do
1005 local _, _, ln = string.find(line, "%s*([^%s]*)%s*")
1006 if ln == "*" then matches = true end
1007 if globish_match(ln, ident) then matches = true end
1008 line = permfile:read()
1009 end
1010 io.close(permfile)
1011 return matches
1012end
1013
1014-- This is a simple function which assumes you're going to be spawning
1015-- a copy of mtn, so reuses a common bit at the end for converting
1016-- local args into remote args. You might need to massage the logic a
1017-- bit if this doesn't fit your assumptions.
1018
1019function get_netsync_connect_command(uri, args)
1020
1021 local argv = nil
1022
1023 if uri["scheme"] == "ssh"
1024 and uri["host"]
1025 and uri["path"] then
1026
1027 argv = { "ssh" }
1028 if uri["user"] then
1029 table.insert(argv, "-l")
1030 table.insert(argv, uri["user"])
1031 end
1032 if uri["port"] then
1033 table.insert(argv, "-p")
1034 table.insert(argv, uri["port"])
1035 end
1036
1037 -- ssh://host/~/dir/file.mtn or
1038 -- ssh://host/~user/dir/file.mtn should be home-relative
1039 if string.find(uri["path"], "^/~") then
1040 uri["path"] = string.sub(uri["path"], 2)
1041 end
1042
1043 table.insert(argv, uri["host"])
1044 end
1045
1046 if uri["scheme"] == "file" and uri["path"] then
1047 argv = { }
1048 end
1049
1050 if uri["scheme"] == "ssh+ux"
1051 and uri["host"]
1052 and uri["path"] then
1053
1054 argv = { "ssh" }
1055 if uri["user"] then
1056 table.insert(argv, "-l")
1057 table.insert(argv, uri["user"])
1058 end
1059 if uri["port"] then
1060 table.insert(argv, "-p")
1061 table.insert(argv, uri["port"])
1062 end
1063
1064 -- ssh://host/~/dir/file.mtn or
1065 -- ssh://host/~user/dir/file.mtn should be home-relative
1066 if string.find(uri["path"], "^/~") then
1067 uri["path"] = string.sub(uri["path"], 2)
1068 end
1069
1070 table.insert(argv, uri["host"])
1071 table.insert(argv, get_remote_unix_socket_command(uri["host"]))
1072 table.insert(argv, "-")
1073 table.insert(argv, "UNIX-CONNECT:" .. uri["path"])
1074 else
1075 -- start remote monotone process
1076 if argv then
1077
1078 table.insert(argv, get_mtn_command(uri["host"]))
1079
1080 if args["debug"] then
1081 table.insert(argv, "--debug")
1082 else
1083 table.insert(argv, "--quiet")
1084 end
1085
1086 table.insert(argv, "--db")
1087 table.insert(argv, uri["path"])
1088 table.insert(argv, "serve")
1089 table.insert(argv, "--stdio")
1090 table.insert(argv, "--no-transport-auth")
1091
1092 end
1093 end
1094 return argv
1095end
1096
1097function use_transport_auth(uri)
1098 if uri["scheme"] == "ssh"
1099 or uri["scheme"] == "ssh+ux"
1100 or uri["scheme"] == "file" then
1101 return false
1102 else
1103 return true
1104 end
1105end
1106
1107function get_mtn_command(host)
1108 return "mtn"
1109end
1110
1111function get_remote_unix_socket_command(host)
1112 return "socat"
1113end
1114
1115-- Netsync notifiers are tables containing 5 functions:
1116-- start, revision_received, cert_received, pubkey_received and end
1117-- Those functions take exactly the same arguments as the corresponding
1118-- note_netsync functions, but return a different kind of value, a tuple
1119-- composed of a return code and a value to be returned back to monotone.
1120-- The codes are strings:
1121-- "continue" and "stop"
1122-- When the code "continue" is returned and there's another notifier, the
1123-- second value is ignored and the next notifier is called. Otherwise,
1124-- the second value is returned immediately.
1125netsync_notifiers = {}
1126
1127function _note_netsync_helper(f,...)
1128 local s = "continue"
1129 local v = nil
1130 for _,n in pairs(netsync_notifiers) do
1131 if n[f] then
1132 s,v = n[f](...)
1133 end
1134 if s ~= "continue" then
1135 break
1136 end
1137 end
1138 return v
1139end
1140function note_netsync_start(...)
1141 return _note_netsync_helper("start",...)
1142end
1143function note_netsync_revision_received(...)
1144 return _note_netsync_helper("revision_received",...)
1145end
1146function note_netsync_cert_received(...)
1147 return _note_netsync_helper("cert_received",...)
1148end
1149function note_netsync_pubkey_received(...)
1150 return _note_netsync_helper("pubkey_received",...)
1151end
1152function note_netsync_end(...)
1153 return _note_netsync_helper("end",...)
1154end
1155
1156function add_netsync_notifier(notifier, precedence)
1157 if type(notifier) ~= "table" or type(precedence) ~= "number" then
1158 return false, "Invalid tyoe"
1159 end
1160 if netsync_notifiers[precedence] then
1161 return false, "Precedence already taken"
1162 end
1163 local warning = nil
1164 for n,f in pairs(notifier) do
1165 if type(n) ~= "string" or n ~= "start"
1166 and n ~= "revision_received"
1167 and n ~= "cert_received"
1168 and n ~= "pubkey_received"
1169 and n ~= "end" then
1170 warning = "Unknown item found in notifier table"
1171 elseif type(f) ~= "function" then
1172 return false, "Value for notifier item "..n.." isn't a function"
1173 end
1174 end
1175 netsync_notifiers[precedence] = notifier
1176 return true, warning
1177end

Archive Download this file

Branches

Tags

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