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

Archive Download this file

Branches

Tags

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