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 function execute_diff3(mine, yours, out)
427 local diff3_args = {
428 "diff3",
429 "--merge",
430 "--easy-only",
431 }
432 table.insert(diff3_args, string.gsub(mine, "\\", "/") .. "")
433 table.insert(diff3_args, string.gsub(tbl.afile, "\\", "/") .. "")
434 table.insert(diff3_args, string.gsub(yours, "\\", "/") .. "")
435
436 return execute_redirected("", string.gsub(out, "\\", "/"), "", unpack(diff3_args))
437 end
438
439 io.write (string.format("\nWARNING: 'vim' was choosen to perform external 3-way merge.\n"..
440 "You should merge all changes to *LEFT* file due to limitation of program\n"..
441 "arguments.\n\n"))
442
443 local vim
444 if os.getenv ("DISPLAY") ~= nil and program_exists_in_path ("gvim") then
445 vim = "gvim"
446 else
447 vim = "vim"
448 end
449
450 local lfile_merged = tbl.lfile .. ".merged"
451 local rfile_merged = tbl.rfile .. ".merged"
452
453 -- first merge lfile using diff3
454 local ret = execute_diff3(tbl.lfile, tbl.rfile, lfile_merged)
455 if ret == 2 then
456 io.write(string.format(gettext("Error running diff3 for merger '%s'\n"), vim))
457 os.remove(lfile_merged)
458 return false
459 end
460
461 -- now merge rfile using diff3
462 ret = execute_diff3(tbl.rfile, tbl.lfile, rfile_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 os.remove(rfile_merged)
467 return false
468 end
469
470 os.rename(lfile_merged, tbl.lfile)
471 os.rename(rfile_merged, tbl.rfile)
472
473 local ret = execute(vim, "-f", "-d", "-c", string.format("file %s", tbl.outfile),
474 tbl.lfile, tbl.rfile)
475 if (ret ~= 0) then
476 io.write(string.format(gettext("Error running merger '%s'\n"), vim))
477 return false
478 end
479 return tbl.outfile
480 end ,
481 available =
482 function ()
483 return program_exists_in_path("diff3") and
484 (program_exists_in_path("vim") or
485 program_exists_in_path("gvim"))
486 end ,
487 wanted =
488 function ()
489 local editor = os.getenv("EDITOR")
490 if editor and
491 not (string.find(editor, "vim") or
492 string.find(editor, "gvim")) then
493 return false
494 end
495 return true
496 end
497}
498
499mergers.rcsmerge = {
500 cmd = function (tbl)
501 -- XXX: This is tough - should we check if conflict markers stay or not?
502 -- If so, we should certainly give the user some way to still force
503 -- the merge to proceed since they can appear in the files (and I saw
504 -- that). --pasky
505 local merge = os.getenv("MTN_RCSMERGE")
506 if execute(merge, tbl.lfile, tbl.afile, tbl.rfile) == 0 then
507 copy_text_file(tbl.lfile, tbl.outfile);
508 return tbl.outfile
509 end
510 local ret = execute("vim", "-f", "-c", string.format("file %s", tbl.outfile
511),
512 tbl.lfile)
513 if (ret ~= 0) then
514 io.write(string.format(gettext("Error running merger '%s'\n"), "vim"))
515 return false
516 end
517 return tbl.outfile
518 end,
519 available =
520 function ()
521 local merge = os.getenv("MTN_RCSMERGE")
522 return merge and
523 program_exists_in_path(merge) and program_exists_in_path("vim")
524 end ,
525 wanted = function () return os.getenv("MTN_RCSMERGE") ~= nil end
526}
527
528-- GNU diffutils based merging
529mergers.diffutils = {
530 -- merge procedure execution
531 cmd = function (tbl)
532 -- parse options
533 local option = {}
534 option.partial = false
535 option.diff3opts = ""
536 option.sdiffopts = ""
537 local options = os.getenv("MTN_MERGE_DIFFUTILS")
538 if options ~= nil then
539 for spec in string.gmatch(options, "%s*(%w[^,]*)%s*,?") do
540 local name, value = string.match(spec, "^(%w+)=([^,]*)")
541 if name == nil then
542 name = spec
543 value = true
544 end
545 if type(option[name]) == "nil" then
546 io.write("mtn: " .. string.format(gettext("invalid \"diffutils\" merger option \"%s\""), name) .. "\n")
547 return false
548 end
549 option[name] = value
550 end
551 end
552
553 -- determine the diff3(1) command
554 local diff3 = {
555 "diff3",
556 "--merge",
557 "--label", string.format("%s [left]", tbl.left_path ),
558 "--label", string.format("%s [ancestor]", tbl.anc_path ),
559 "--label", string.format("%s [right]", tbl.right_path),
560 }
561 if option.diff3opts ~= "" then
562 for opt in string.gmatch(option.diff3opts, "%s*([^%s]+)%s*") do
563 table.insert(diff3, opt)
564 end
565 end
566 table.insert(diff3, string.gsub(tbl.lfile, "\\", "/") .. "")
567 table.insert(diff3, string.gsub(tbl.afile, "\\", "/") .. "")
568 table.insert(diff3, string.gsub(tbl.rfile, "\\", "/") .. "")
569
570 -- dispatch according to major operation mode
571 if option.partial then
572 -- partial batch/non-modal 3-way merge "resolution":
573 -- simply merge content with help of conflict markers
574 io.write("mtn: " .. gettext("3-way merge via GNU diffutils, resolving conflicts via conflict markers") .. "\n")
575 local ret = execute_redirected("", string.gsub(tbl.outfile, "\\", "/"), "", unpack(diff3))
576 if ret == 2 then
577 io.write("mtn: " .. gettext("error running GNU diffutils 3-way difference/merge tool \"diff3\"") .. "\n")
578 return false
579 end
580 return tbl.outfile
581 else
582 -- real interactive/modal 3/2-way merge resolution:
583 -- display 3-way merge conflict and perform 2-way merge resolution
584 io.write("mtn: " .. gettext("3-way merge via GNU diffutils, resolving conflicts via interactive prompt") .. "\n")
585
586 -- display 3-way merge conflict (batch)
587 io.write("\n")
588 io.write("mtn: " .. gettext("---- CONFLICT SUMMARY ------------------------------------------------") .. "\n")
589 local ret = execute(unpack(diff3))
590 if ret == 2 then
591 io.write("mtn: " .. gettext("error running GNU diffutils 3-way difference/merge tool \"diff3\"") .. "\n")
592 return false
593 end
594
595 -- perform 2-way merge resolution (interactive)
596 io.write("\n")
597 io.write("mtn: " .. gettext("---- CONFLICT RESOLUTION ---------------------------------------------") .. "\n")
598 local sdiff = {
599 "sdiff",
600 "--diff-program=diff",
601 "--suppress-common-lines",
602 "--minimal",
603 "--output=" .. string.gsub(tbl.outfile, "\\", "/")
604 }
605 if option.sdiffopts ~= "" then
606 for opt in string.gmatch(option.sdiffopts, "%s*([^%s]+)%s*") do
607 table.insert(sdiff, opt)
608 end
609 end
610 table.insert(sdiff, string.gsub(tbl.lfile, "\\", "/") .. "")
611 table.insert(sdiff, string.gsub(tbl.rfile, "\\", "/") .. "")
612 local ret = execute(unpack(sdiff))
613 if ret == 2 then
614 io.write("mtn: " .. gettext("error running GNU diffutils 2-way merging tool \"sdiff\"") .. "\n")
615 return false
616 end
617 return tbl.outfile
618 end
619 end,
620
621 -- merge procedure availability check
622 available = function ()
623 -- make sure the GNU diffutils tools are available
624 return program_exists_in_path("diff3") and
625 program_exists_in_path("sdiff") and
626 program_exists_in_path("diff");
627 end,
628
629 -- merge procedure request check
630 wanted = function ()
631 -- assume it is requested (if it is available at all)
632 return true
633 end
634}
635
636mergers.emacs = {
637 cmd = function (tbl)
638 local emacs
639 if program_exists_in_path("xemacs") then
640 emacs = "xemacs"
641 else
642 emacs = "emacs"
643 end
644 local elisp = "(ediff-merge-files-with-ancestor \"%s\" \"%s\" \"%s\" nil \"%s\")"
645 -- Converting backslashes is necessary on Win32 MinGW; emacs
646 -- lisp string syntax says '\' is an escape.
647 local ret = execute(emacs, "--eval",
648 string.format(elisp,
649 string.gsub (tbl.lfile, "\\", "/"),
650 string.gsub (tbl.rfile, "\\", "/"),
651 string.gsub (tbl.afile, "\\", "/"),
652 string.gsub (tbl.outfile, "\\", "/")))
653 if (ret ~= 0) then
654 io.write(string.format(gettext("Error running merger '%s'\n"), emacs))
655 return false
656 end
657 return tbl.outfile
658 end,
659 available =
660 function ()
661 return program_exists_in_path("xemacs") or
662 program_exists_in_path("emacs")
663 end ,
664 wanted =
665 function ()
666 local editor = os.getenv("EDITOR")
667 if editor and
668 not (string.find(editor, "emacs") or
669 string.find(editor, "gnu")) then
670 return false
671 end
672 return true
673 end
674}
675
676mergers.xxdiff = {
677 cmd = function (tbl)
678 local path = "xxdiff"
679 local ret = execute(path,
680 "--title1", tbl.left_path,
681 "--title2", tbl.right_path,
682 "--title3", tbl.merged_path,
683 tbl.lfile, tbl.afile, tbl.rfile,
684 "--merge",
685 "--merged-filename", tbl.outfile,
686 "--exit-with-merge-status")
687 if (ret ~= 0) then
688 io.write(string.format(gettext("Error running merger '%s'\n"), path))
689 return false
690 end
691 return tbl.outfile
692 end,
693 available = function () return program_exists_in_path("xxdiff") end,
694 wanted = function () return true end
695}
696
697mergers.kdiff3 = {
698 cmd = function (tbl)
699 local path = "kdiff3"
700 local ret = execute(path,
701 "--L1", tbl.anc_path,
702 "--L2", tbl.left_path,
703 "--L3", tbl.right_path,
704 tbl.afile, tbl.lfile, tbl.rfile,
705 "--merge",
706 "--o", tbl.outfile)
707 if (ret ~= 0) then
708 io.write(string.format(gettext("Error running merger '%s'\n"), path))
709 return false
710 end
711 return tbl.outfile
712 end,
713 available = function () return program_exists_in_path("kdiff3") end,
714 wanted = function () return true end
715}
716
717mergers.opendiff = {
718 cmd = function (tbl)
719 local path = "opendiff"
720 -- As opendiff immediately returns, let user confirm manually
721 local ret = execute_confirm(path,
722 tbl.lfile,tbl.rfile,
723 "-ancestor",tbl.afile,
724 "-merge",tbl.outfile)
725 if (ret ~= 0) then
726 io.write(string.format(gettext("Error running merger '%s'\n"), path))
727 return false
728 end
729 return tbl.outfile
730 end,
731 available = function () return program_exists_in_path("opendiff") end,
732 wanted = function () return true end
733}
734
735function write_to_temporary_file(data, namehint)
736 tmp, filename = temp_file(namehint)
737 if (tmp == nil) then
738 return nil
739 end;
740 tmp:write(data)
741 io.close(tmp)
742 return filename
743end
744
745function copy_text_file(srcname, destname)
746 src = io.open(srcname, "r")
747 if (src == nil) then return nil end
748 dest = io.open(destname, "w")
749 if (dest == nil) then return nil end
750
751 while true do
752 local line = src:read()
753 if line == nil then break end
754 dest:write(line, "\n")
755 end
756
757 io.close(dest)
758 io.close(src)
759end
760
761function read_contents_of_file(filename, mode)
762 tmp = io.open(filename, mode)
763 if (tmp == nil) then
764 return nil
765 end
766 local data = tmp:read("*a")
767 io.close(tmp)
768 return data
769end
770
771function program_exists_in_path(program)
772 return existsonpath(program) == 0
773end
774
775function get_preferred_merge3_command (tbl)
776 local default_order = {"kdiff3", "xxdiff", "opendiff", "tortoise", "emacs", "vim", "meld", "diffutils"}
777 local function existmerger(name)
778 local m = mergers[name]
779 if type(m) == "table" and m.available(tbl) then
780 return m.cmd
781 end
782 return nil
783 end
784 local function trymerger(name)
785 local m = mergers[name]
786 if type(m) == "table" and m.available(tbl) and m.wanted(tbl) then
787 return m.cmd
788 end
789 return nil
790 end
791 -- Check if there's a merger given by the user.
792 local mkey = os.getenv("MTN_MERGE")
793 if not mkey then mkey = merger end
794 if not mkey and os.getenv("MTN_RCSMERGE") then mkey = "rcsmerge" end
795 -- If there was a user-given merger, see if it exists. If it does, return
796 -- the cmd function. If not, return nil.
797 local c
798 if mkey then c = existmerger(mkey) end
799 if c then return c,mkey end
800 if mkey then return nil,mkey end
801 -- If there wasn't any user-given merger, take the first that's available
802 -- and wanted.
803 for _,mkey in ipairs(default_order) do
804 c = trymerger(mkey) ; if c then return c,mkey end
805 end
806end
807
808function merge3 (anc_path, left_path, right_path, merged_path, ancestor, left, right)
809 local ret = nil
810 local tbl = {}
811
812 tbl.anc_path = anc_path
813 tbl.left_path = left_path
814 tbl.right_path = right_path
815
816 tbl.merged_path = merged_path
817 tbl.afile = nil
818 tbl.lfile = nil
819 tbl.rfile = nil
820 tbl.outfile = nil
821 tbl.meld_exists = false
822 tbl.lfile = write_to_temporary_file (left, "left")
823 tbl.afile = write_to_temporary_file (ancestor, "ancestor")
824 tbl.rfile = write_to_temporary_file (right, "right")
825 tbl.outfile = write_to_temporary_file ("", "merged")
826
827 if tbl.lfile ~= nil and tbl.rfile ~= nil and tbl.afile ~= nil and tbl.outfile ~= nil
828 then
829 local cmd,mkey = get_preferred_merge3_command (tbl)
830 if cmd ~=nil
831 then
832 io.write ("mtn: " .. string.format(gettext("executing external 3-way merge via \"%s\" merger\n"), mkey))
833 ret = cmd (tbl)
834 if not ret then
835 ret = nil
836 else
837 ret = read_contents_of_file (ret, "r")
838 if string.len (ret) == 0
839 then
840 ret = nil
841 end
842 end
843 else
844 if mkey then
845 io.write (string.format("The possible commands for the "..mkey.." merger aren't available.\n"..
846 "You may want to check that $MTN_MERGE or the lua variable `merger' is set\n"..
847 "to something available. If you want to use vim or emacs, you can also\n"..
848"set $EDITOR to something appropriate.\n"))
849 else
850 io.write (string.format("No external 3-way merge command found.\n"..
851 "You may want to check that $EDITOR is set to an editor that supports 3-way\n"..
852 "merge, set this explicitly in your get_preferred_merge3_command hook,\n"..
853 "or add a 3-way merge program to your path.\n"))
854 end
855 end
856 end
857
858 os.remove (tbl.lfile)
859 os.remove (tbl.rfile)
860 os.remove (tbl.afile)
861 os.remove (tbl.outfile)
862
863 return ret
864end
865
866-- expansion of values used in selector completion
867
868function expand_selector(str)
869
870 -- something which looks like a generic cert pattern
871 if string.find(str, "^[^=]*=.*$")
872 then
873 return ("c:" .. str)
874 end
875
876 -- something which looks like an email address
877 if string.find(str, "[%w%-_]+@[%w%-_]+")
878 then
879 return ("a:" .. str)
880 end
881
882 -- something which looks like a branch name
883 if string.find(str, "[%w%-]+%.[%w%-]+")
884 then
885 return ("b:" .. str)
886 end
887
888 -- a sequence of nothing but hex digits
889 if string.find(str, "^%x+$")
890 then
891 return ("i:" .. str)
892 end
893
894 -- tries to expand as a date
895 local dtstr = expand_date(str)
896 if dtstr ~= nil
897 then
898 return ("d:" .. dtstr)
899 end
900
901 return nil
902end
903
904-- expansion of a date expression
905function expand_date(str)
906 -- simple date patterns
907 if string.find(str, "^19%d%d%-%d%d")
908 or string.find(str, "^20%d%d%-%d%d")
909 then
910 return (str)
911 end
912
913 -- "now"
914 if str == "now"
915 then
916 local t = os.time(os.date('!*t'))
917 return os.date("%FT%T", t)
918 end
919
920 -- today don't uses the time # for xgettext's sake, an extra quote
921 if str == "today"
922 then
923 local t = os.time(os.date('!*t'))
924 return os.date("%F", t)
925 end
926
927 -- "yesterday", the source of all hangovers
928 if str == "yesterday"
929 then
930 local t = os.time(os.date('!*t'))
931 return os.date("%F", t - 86400)
932 end
933
934 -- "CVS style" relative dates such as "3 weeks ago"
935 local trans = {
936 minute = 60;
937 hour = 3600;
938 day = 86400;
939 week = 604800;
940 month = 2678400;
941 year = 31536000
942 }
943 local pos, len, n, type = string.find(str, "(%d+) ([minutehordaywk]+)s? ago")
944 if trans[type] ~= nil
945 then
946 local t = os.time(os.date('!*t'))
947 if trans[type] <= 3600
948 then
949 return os.date("%FT%T", t - (n * trans[type]))
950 else
951 return os.date("%F", t - (n * trans[type]))
952 end
953 end
954
955 return nil
956end
957
958
959external_diff_default_args = "-u"
960
961-- default external diff, works for gnu diff
962function external_diff(file_path, data_old, data_new, is_binary, diff_args, rev_old, rev_new)
963 local old_file = write_to_temporary_file(data_old);
964 local new_file = write_to_temporary_file(data_new);
965
966 if diff_args == nil then diff_args = external_diff_default_args end
967 execute("diff", diff_args, "--label", file_path .. "\told", old_file, "--label", file_path .. "\tnew", new_file);
968
969 os.remove (old_file);
970 os.remove (new_file);
971end
972
973-- netsync permissions hooks (and helper)
974
975function globish_match(glob, str)
976 local pcallstatus, result = pcall(function() if (globish.match(glob, str)) then return true else return false end end)
977 if pcallstatus == true then
978 -- no error
979 return result
980 else
981 -- globish.match had a problem
982 return nil
983 end
984end
985
986function get_netsync_read_permitted(branch, ident)
987 local permfile = io.open(get_confdir() .. "/read-permissions", "r")
988 if (permfile == nil) then return false end
989 local dat = permfile:read("*a")
990 io.close(permfile)
991 local res = parse_basic_io(dat)
992 if res == nil then
993 io.stderr:write("file read-permissions cannot be parsed\n")
994 return false
995 end
996 local matches = false
997 local cont = false
998 for i, item in pairs(res)
999 do
1000 -- legal names: pattern, allow, deny, continue
1001 if item.name == "pattern" then
1002 if matches and not cont then return false end
1003 matches = false
1004 cont = false
1005 for j, val in pairs(item.values) do
1006 if globish_match(val, branch) then matches = true end
1007 end
1008 elseif item.name == "allow" then if matches then
1009 for j, val in pairs(item.values) do
1010 if val == "*" then return true end
1011 if val == "" and ident == nil then return true end
1012 if globish_match(val, ident) then return true end
1013 end
1014 end elseif item.name == "deny" then if matches then
1015 for j, val in pairs(item.values) do
1016 if val == "*" then return false end
1017 if val == "" and ident == nil then return false end
1018 if globish_match(val, ident) then return false end
1019 end
1020 end elseif item.name == "continue" then if matches then
1021 cont = true
1022 for j, val in pairs(item.values) do
1023 if val == "false" or val == "no" then cont = false end
1024 end
1025 end elseif item.name ~= "comment" then
1026 io.stderr:write("unknown symbol in read-permissions: " .. item.name .. "\n")
1027 return false
1028 end
1029 end
1030 return false
1031end
1032
1033function get_netsync_write_permitted(ident)
1034 local permfile = io.open(get_confdir() .. "/write-permissions", "r")
1035 if (permfile == nil) then
1036 return false
1037 end
1038 local matches = false
1039 local line = permfile:read()
1040 while (not matches and line ~= nil) do
1041 local _, _, ln = string.find(line, "%s*([^%s]*)%s*")
1042 if ln == "*" then matches = true end
1043 if globish_match(ln, ident) then matches = true end
1044 line = permfile:read()
1045 end
1046 io.close(permfile)
1047 return matches
1048end
1049
1050-- This is a simple function which assumes you're going to be spawning
1051-- a copy of mtn, so reuses a common bit at the end for converting
1052-- local args into remote args. You might need to massage the logic a
1053-- bit if this doesn't fit your assumptions.
1054
1055function get_netsync_connect_command(uri, args)
1056
1057 local argv = nil
1058
1059 if uri["scheme"] == "ssh"
1060 and uri["host"]
1061 and uri["path"] then
1062
1063 argv = { "ssh" }
1064 if uri["user"] then
1065 table.insert(argv, "-l")
1066 table.insert(argv, uri["user"])
1067 end
1068 if uri["port"] then
1069 table.insert(argv, "-p")
1070 table.insert(argv, uri["port"])
1071 end
1072
1073 -- ssh://host/~/dir/file.mtn or
1074 -- ssh://host/~user/dir/file.mtn should be home-relative
1075 if string.find(uri["path"], "^/~") then
1076 uri["path"] = string.sub(uri["path"], 2)
1077 end
1078
1079 table.insert(argv, uri["host"])
1080 end
1081
1082 if uri["scheme"] == "file" and uri["path"] then
1083 argv = { }
1084 end
1085
1086 if uri["scheme"] == "ssh+ux"
1087 and uri["host"]
1088 and uri["path"] then
1089
1090 argv = { "ssh" }
1091 if uri["user"] then
1092 table.insert(argv, "-l")
1093 table.insert(argv, uri["user"])
1094 end
1095 if uri["port"] then
1096 table.insert(argv, "-p")
1097 table.insert(argv, uri["port"])
1098 end
1099
1100 -- ssh://host/~/dir/file.mtn or
1101 -- ssh://host/~user/dir/file.mtn should be home-relative
1102 if string.find(uri["path"], "^/~") then
1103 uri["path"] = string.sub(uri["path"], 2)
1104 end
1105
1106 table.insert(argv, uri["host"])
1107 table.insert(argv, get_remote_unix_socket_command(uri["host"]))
1108 table.insert(argv, "-")
1109 table.insert(argv, "UNIX-CONNECT:" .. uri["path"])
1110 else
1111 -- start remote monotone process
1112 if argv then
1113
1114 table.insert(argv, get_mtn_command(uri["host"]))
1115
1116 if args["debug"] then
1117 table.insert(argv, "--debug")
1118 else
1119 table.insert(argv, "--quiet")
1120 end
1121
1122 table.insert(argv, "--db")
1123 table.insert(argv, uri["path"])
1124 table.insert(argv, "serve")
1125 table.insert(argv, "--stdio")
1126 table.insert(argv, "--no-transport-auth")
1127
1128 end
1129 end
1130 return argv
1131end
1132
1133function use_transport_auth(uri)
1134 if uri["scheme"] == "ssh"
1135 or uri["scheme"] == "ssh+ux"
1136 or uri["scheme"] == "file" then
1137 return false
1138 else
1139 return true
1140 end
1141end
1142
1143function get_mtn_command(host)
1144 return "mtn"
1145end
1146
1147function get_remote_unix_socket_command(host)
1148 return "socat"
1149end
1150
1151do
1152 -- Hook functions are tables containing any of the following 6 items
1153 -- with associated functions:
1154 --
1155 -- startupCorresponds to note_mtn_startup()
1156 -- startCorresponds to note_netsync_start()
1157 -- revision_receivedCorresponds to note_netsync_revision_received()
1158 -- cert_receivedCorresponds to note_netsync_cert_received()
1159 -- pubkey_receivedCorresponds to note_netsync_pubkey_received()
1160 -- endCorresponds to note_netsync_end()
1161 --
1162 -- Those functions take exactly the same arguments as the corresponding
1163 -- global functions, but return a different kind of value, a tuple
1164 -- composed of a return code and a value to be returned back to monotone.
1165 -- The codes are strings:
1166 -- "continue" and "stop"
1167 -- When the code "continue" is returned and there's another notifier, the
1168 -- second value is ignored and the next notifier is called. Otherwise,
1169 -- the second value is returned immediately.
1170 local hook_functions = {}
1171 local supported_items = {
1172 "startup",
1173 "start", "revision_received", "cert_received", "pubkey_received", "end"
1174 }
1175
1176 function _hook_functions_helper(f,...)
1177 local s = "continue"
1178 local v = nil
1179 for _,n in pairs(hook_functions) do
1180 if n[f] then
1181 s,v = n[f](...)
1182 end
1183 if s ~= "continue" then
1184 break
1185 end
1186 end
1187 return v
1188 end
1189 function note_mtn_startup(...)
1190 return _hook_functions_helper("startup",...)
1191 end
1192 function note_netsync_start(...)
1193 return _hook_functions_helper("start",...)
1194 end
1195 function note_netsync_revision_received(...)
1196 return _hook_functions_helper("revision_received",...)
1197 end
1198 function note_netsync_cert_received(...)
1199 return _hook_functions_helper("cert_received",...)
1200 end
1201 function note_netsync_pubkey_received(...)
1202 return _hook_functions_helper("pubkey_received",...)
1203 end
1204 function note_netsync_end(...)
1205 return _hook_functions_helper("end",...)
1206 end
1207
1208 function add_hook_functions(functions, precedence)
1209 if type(functions) ~= "table" or type(precedence) ~= "number" then
1210 return false, "Invalid type"
1211 end
1212 if hook_functions[precedence] then
1213 return false, "Precedence already taken"
1214 end
1215
1216 local unknown_items = ""
1217 local warning = nil
1218 local is_member =
1219 function (s,t)
1220 for k,v in pairs(t) do if s == v then return true end end
1221 return false
1222 end
1223
1224 for n,f in pairs(functions) do
1225 if type(n) == "string" then
1226 if not is_member(n, supported_items) then
1227 if unknown_items ~= "" then
1228 unknown_items = unknown_items .. ","
1229 end
1230 unknown_items = unknown_items .. n
1231 end
1232 if type(f) ~= "function" then
1233 return false, "Value for functions item "..n.." isn't a function"
1234 end
1235 else
1236 warning = "Non-string item keys found in functions table"
1237 end
1238 end
1239
1240 if warning == nil and unknown_items ~= "" then
1241 warning = "Unknown item(s) " .. unknown_items .. " in functions table"
1242 end
1243
1244 hook_functions[precedence] = functions
1245 return true, warning
1246 end
1247 function push_hook_functions(functions)
1248 local n = table.maxn(hook_functions) + 1
1249 return add_hook_functions(functions, n)
1250 end
1251
1252 -- Kept for backward compatibility
1253 function add_netsync_notifier(notifier, precedence)
1254 return add_hook_functions(notifier, precedence)
1255 end
1256 function push_netsync_notifier(notifier)
1257 return push_hook_functions(notifier)
1258 end
1259end

Archive Download this file

Branches

Tags

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