7019 FIX NagVis: Updated to 1.9.11
[check_mk.git] / werk
blob100fe5e67775c72d2aef2e7d992cdd2452176075
1 #!/usr/bin/python
3 import sys, os, time, termios, tty, subprocess
5 # colored output, if stdout is a tty
6 if sys.stdout.isatty():
7 tty_red = '\033[31m'
8 tty_green = '\033[32m'
9 tty_yellow = '\033[33m'
10 tty_blue = '\033[34m'
11 tty_magenta = '\033[35m'
12 tty_cyan = '\033[36m'
13 tty_white = '\033[37m'
14 tty_bgred = '\033[41m'
15 tty_bgyellow = '\033[43m'
16 tty_bgblue = '\033[44m'
17 tty_bgmagenta = '\033[45m'
18 tty_bgcyan = '\033[46m'
19 tty_bgwhite = '\033[47m'
20 tty_bold = '\033[1m'
21 tty_underline = '\033[4m'
22 tty_normal = '\033[0m'
23 def tty_colors(codes):
24 return '\033[%sm' % (';'.join([str(c) for c in codes]))
25 else:
26 tty_red = ''
27 tty_green = ''
28 tty_yellow = ''
29 tty_blue = ''
30 tty_magenta = ''
31 tty_cyan = ''
32 tty_white = ''
33 tty_bgblue = ''
34 tty_bgmagenta = ''
35 tty_bgcyan = ''
36 tty_bold = ''
37 tty_underline = ''
38 tty_normal = ''
39 tty_ok = 'OK'
40 def tty_colors(c):
41 return ""
43 grep_colors = [
44 tty_bold + tty_magenta,
45 tty_bold + tty_cyan,
46 tty_bold + tty_green,
49 def get_tty_size():
50 import termios,struct,fcntl
51 try:
52 ws = struct.pack("HHHH", 0, 0, 0, 0)
53 ws = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, ws)
54 lines, columns, x, y = struct.unpack("HHHH", ws)
55 if lines > 0 and columns > 0:
56 return lines, columns
57 except:
58 pass
59 return (24, 99999)
61 def bail_out(text):
62 sys.stderr.write(text + "\n")
63 sys.exit(1)
65 def goto_werksdir():
66 global g_base_dir
67 g_base_dir = os.path.abspath('.')
68 while not os.path.exists(".werks") and os.path.abspath('.') != '/':
69 os.chdir("..")
71 try:
72 os.chdir(".werks")
73 except:
74 sys.stderr.write("Cannot find directory .werks\n")
75 sys.exit(1)
78 def load_config():
79 global g_last_werk
80 execfile("config", globals(), globals())
81 try:
82 g_last_werk = int(file(".last").read())
83 except:
84 g_last_werk = None
87 def load_werks():
88 global g_werks
89 g_werks = {}
90 check_modified()
91 for entry in os.listdir("."):
92 try:
93 werkid = int(entry)
94 try:
95 g_werks[werkid] = load_werk(werkid)
96 except:
97 sys.stderr.write("SKIPPING INVALID werk %d\n" % werkid)
98 except:
99 continue
101 def save_last_werkid(id):
102 try:
103 file(".last", "w").write("%d\n" % int(id))
104 except:
105 pass
108 def load_current_version():
109 for line in file("../defines.make"):
110 if line.startswith("VERSION"):
111 version = line.split("=", 1)[1].strip()
112 return version
114 bail_out("Failed to read VERSION from defines.make")
117 def check_modified():
118 global g_modified
119 g_modified = set([])
120 for line in os.popen("git status --porcelain"):
121 if line[0] in "AM" and ".werks/" in line:
122 try:
123 id = line.rsplit("/", 1)[-1].strip()
124 g_modified.add(int(id))
125 except:
126 pass
128 def werk_is_modified(werkid):
129 return werkid in g_modified
132 def load_werk(werkid):
133 werk = {
134 "id" : werkid,
135 "state" : "unknown",
136 "title" : "unknown",
137 "component" : "general",
138 "compatible" : "compat",
139 "edition" : "cre",
142 f = file(str(werkid))
143 for line in f:
144 line = line.strip()
145 if line == "":
146 break
147 header, value = line.split(":", 1)
148 werk[header.strip().lower()] = value.strip()
150 description = ""
151 for line in f:
152 description += line
154 werk["description"] = description
155 versions.add(werk["version"])
156 return werk
158 def save_werk(werk):
159 f = file(str(werk["id"]), "w")
160 f.write("Title: %s\n" % werk["title"])
161 for key, val in werk.items():
162 if key not in [ "title", "description", "id" ]:
163 f.write("%s%s: %s\n" % (key[0].upper(), key[1:], val))
164 f.write("\n")
165 f.write(werk["description"])
166 f.close()
167 git_add(werk)
168 save_last_werkid(werk["id"])
170 def change_werk_version(werk_id, new_version):
171 werk = g_werks[werk_id]
172 werk["version"] = new_version
173 save_werk(werk)
174 git_add(werk)
176 def git_add(werk):
177 os.system("git add %d" % werk["id"])
179 def git_commit(werk, custom_files):
180 title = werk["title"]
181 for classid, classname, prefix in classes:
182 if werk["class"] == classid:
183 if prefix:
184 title = "%s %s" % (prefix, title)
186 title = "%04d %s" % (werk['id'], title)
188 if custom_files:
189 files_to_commit = custom_files
190 default_files = [ ".werks" ]
191 for entry in default_files:
192 files_to_commit.append("%s/%s" % (git_top_level(), entry))
194 os.chdir(g_base_dir)
195 os.system("git commit %s -m %s" % (" ".join(files_to_commit),
196 quote_shell_string(title + "\n\n" + werk["description"])))
198 else:
199 if something_in_git_index():
200 dash_a = ''
201 os.system("cd '%s' ; git add .werks" % git_top_level())
202 else:
203 dash_a = '-a'
205 os.system("git commit %s -m %s" % (dash_a, quote_shell_string(title + "\n\n" + werk["description"])))
207 def git_top_level():
208 info = subprocess.Popen(["git", "rev-parse", "--show-toplevel"], stdout=subprocess.PIPE)
209 git_top_level = info.communicate()[0].split()[0]
210 return git_top_level
213 def something_in_git_index():
214 for line in os.popen("git status --porcelain"):
215 if line[0] == 'M':
216 return True
217 return False
220 def quote_shell_string(s):
221 return "'" + s.replace("'", "'\"'\"'") + "'"
224 def next_werk_id():
225 my_werk_ids = get_werk_ids()
226 if not my_werk_ids:
227 bail_out('You have no werk IDS left. You can reserve 10 additional Werk IDS with "./werk ids 10".')
228 return my_werk_ids[0]
230 def add_comment(werk, title, comment):
231 werk["description"] += """
232 %s: %s
233 %s""" % (time.strftime("%F %T"), title, comment)
237 def usage():
238 sys.stdout.write("""Usage: werk COMMAND [ARGS...]
240 where COMMAND is one of:
242 ids [#] - Shows the number of reserved werk IDS. With a number
243 given as parameter the command will reserve new werk IDS.
244 list [-r] [STATE] - list werks (-r: reverse)
245 new - create a new werk
246 show [# #..] - show several werks, or 'all' for all, of leave out for last
247 resolve ID - change a werks state
248 delete #.. - delete werk(s)
249 grep [-v] KW1 KW2... - show werks containing all of the given keywords (-v: verbose)
250 edit [#] - open werk # in editor (or newest werk)
251 blame [#] - show who worked on a werk
252 url # - show the online URL of werk #
254 """)
255 sys.exit(1)
257 def num_color(n, colors, inverse):
258 if inverse:
259 b = 40
260 else:
261 b = 30
263 c = colors[n-1]
264 return tty_colors([b + c, 1])
266 def list_werk(werk):
267 if werk_is_modified(werk["id"]):
268 bold = tty_bold + tty_cyan + "(*) "
269 else:
270 bold = ""
271 lines, cols = get_tty_size()
272 title = werk["title"][:cols - 45]
273 sys.stdout.write("#%04d %-9s %s %3s %-13s %-6s %s%s%s %-8s %s%s%s\n" %
274 (int(werk["id"]),
275 time.strftime("%F", time.localtime(int(werk["date"]))),
276 colored_class(werk["class"], 8),
277 werk["edition"],
278 werk["component"],
279 werk["compatible"],
280 tty_bold, werk["level"], tty_normal,
281 werk["version"],
282 bold, title, tty_normal))
284 def colored_class(classname, digits):
285 if classname == "fix":
286 return tty_bold + tty_red + ("%-" + str(digits) + "s") % classname + tty_normal
287 else:
288 return ("%-" + str(digits) + "s") % classname
290 def show_werk(werk):
291 list_werk(werk)
292 sys.stdout.write("\n%s\n" % werk["description"])
294 def main_list(args, format):
295 werks = g_werks.values()
297 # arguments are tags from state, component and class. Multiple values
298 # in one class are orred. Multiple types are anded.
299 filters = {}
301 sort = lambda a,b: cmp(a['date'], b['date'])
302 reverse = False
303 for a in args:
305 if a == "current":
306 a = g_current_version
308 if a == '-r':
309 reverse = True
310 continue
312 hit = False
313 for tp, values in [
314 ( "edition", editions ),
315 ( "component", all_components()),
316 ( "level", levels ),
317 ( "class", classes ),
318 ( "version", versions ),
319 ( "compatible", compatible ),
321 for v in values:
322 if type(v) == tuple:
323 v = v[0]
324 if v.startswith(a):
325 entries = filters.get(tp, [])
326 entries.append(v)
327 filters[tp] = entries
328 hit = True
329 break
330 if hit:
331 break
332 if not hit:
333 bail_out("No such edition, component, state, class or target version: %s" % a)
335 # Filter
336 newwerks = []
337 for werk in werks:
338 skip = False
339 for tp, entries in filters.items():
340 if werk[tp] not in entries:
341 skip = True
342 break
343 if not skip:
344 newwerks.append(werk)
345 werks = newwerks
347 # Sort
348 if sort:
349 newwerks.sort(sort)
350 if reverse:
351 newwerks.reverse()
353 # Output
354 if format == "console":
355 for werk in werks:
356 list_werk(werk)
357 else:
358 output_csv(werks)
361 # CSV Table has the following columns:
362 # Component;ID;Title;Class;Effort
363 def output_csv(werks):
364 def line(*l):
365 sys.stdout.write('"' + '";"'.join(map(str, l)) + '"\n')
367 nr = 1
368 for entry in components:
369 if len(entry) == 2:
370 name, alias = entry
371 else:
372 name = entry
373 alias = entry
375 line("", "", "", "", "")
377 total_effort = 0
378 for werk in werks:
379 if werk["component"] == name:
380 total_effort += werk_effort(werk)
381 line("", "%d. %s" % (nr, alias), "", total_effort)
382 nr += 1
384 for werk in werks:
385 if werk["component"] == name:
386 line(werk["id"], werk["title"], werk_class(werk), werk_effort(werk))
387 line("", werk["description"].replace("\n", " ").replace('"', "'"), "", "")
389 def werk_class(werk):
390 cl = werk["class"]
391 for entry in classes:
392 if entry == cl:
393 return cl
394 elif type(entry) == tuple and entry[0] == cl:
395 return entry[1]
396 return cl
398 def werk_effort(werk):
399 return int(werk.get("effort", "0"))
401 def main_show(args):
402 ids = args
403 if len(ids) == 0:
404 if g_last_werk == None:
405 bail_out("No last werk known. Please specify id.")
406 ids = [ g_last_werk ]
407 elif ids[0] == 'all':
408 ids = [ id for (id, werk) in g_werks.items() ]
410 for id in ids:
411 if id != ids[0]:
412 sys.stdout.write("-------------------------------------------------------------------------------\n")
413 try:
414 show_werk(g_werks[int(id)])
415 except 1:
416 sys.stderr.write("Skipping invalid werk id '%s'\n" % id)
417 save_last_werkid(ids[-1])
419 def get_input(what, default = ""):
420 sys.stdout.write("%s: " % what)
421 sys.stdout.flush()
422 value = sys.stdin.readline().strip()
423 if value == "":
424 return default
425 else:
426 return value
428 def get_long_input(what):
429 sys.stdout.write("Enter %s. End with CTRL-D.\n" % what)
430 usertext = sys.stdin.read()
431 # remove leading and trailing empty lines
432 while usertext.startswith("\n"):
433 usertext = usertext[1:]
434 while usertext.endswith("\n\n"):
435 usertext = usertext[:-1]
436 return usertext
438 def getch():
439 fd = sys.stdin.fileno()
440 old_settings = termios.tcgetattr(fd)
441 try:
442 tty.setraw(sys.stdin.fileno())
443 ch = sys.stdin.read(1)
444 finally:
445 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
446 if ord(ch) == 3:
447 raise KeyboardInterrupt()
448 return ch
450 def input_choice(what, choices):
451 next_index = 0
452 ctc = {}
453 texts = []
454 for choice in choices:
455 if type(choice) == tuple:
456 choice = choice[0]
458 added = False
460 # Find an identifying character for the input choice. In case all possible
461 # characters are already used start using unique numbers
462 for c in str(choice):
463 if c not in ".-_/" and c not in ctc:
464 ctc[c] = choice
465 texts.append(str(choice).replace(c, tty_bold + c + tty_normal, 1))
466 added = True
467 break
469 if not added:
470 ctc["%s" % next_index] = choice
471 texts.append("%s:%s" % ("%s%d%s" % (tty_bold, next_index, tty_normal), choice))
472 next_index += 1
475 while True:
476 sys.stdout.write("%s (%s): " % (what, ", ".join(texts)))
477 sys.stdout.flush()
478 c = getch()
479 if c in ctc:
480 sys.stdout.write(" %s%s%s\n" % (
481 tty_bold, ctc[c], tty_normal))
482 return ctc[c]
483 else:
484 sys.stdout.write("\n")
487 def get_edition_components(edition):
488 return components + edition_components.get(edition, [])
491 def all_components():
492 c = components
493 for ed_components in edition_components.values():
494 c += ed_components
495 return components
498 def main_new(args):
499 werk = {}
500 werk["id"] = next_werk_id()
501 werk["date"] = int(time.time())
502 werk["version"] = g_current_version
503 werk["title"] = get_input("Title")
504 if werk["title"] == "":
505 sys.stderr.write("Cancelled.\n")
506 sys.exit(0)
507 werk["class"] = input_choice("Class", classes)
508 werk["edition"] = input_choice("Edition", editions)
509 werk["component"] = input_choice("Component", get_edition_components(werk["edition"]))
510 werk["level"] = input_choice("Level", levels)
511 werk["compatible"] = input_choice("Compatible", compatible)
512 werk["description"] = u"\n"
514 g_werks[werk["id"]] = werk
515 save_werk(werk)
516 invalidate_my_werkid(werk["id"])
517 edit_werk(werk["id"], args)
519 sys.stdout.write("Werk saved with id %d.\n" % werk["id"])
521 def get_werk_arg(args):
522 if len(args) == 0:
523 if g_last_werk == None:
524 bail_out("No last werk, please specify id.")
525 id = g_last_werk
526 else:
527 if len(args) != 1:
528 usage()
529 id = int(args[0])
531 werk = g_werks.get(id)
532 if not werk:
533 bail_out("No such werk.\n")
534 save_last_werkid(id)
535 return id
538 def main_blame(args):
539 id = get_werk_arg(args)
540 os.system("git blame %d" % id)
543 def main_url(args):
544 id = get_werk_arg(args)
545 sys.stdout.write(online_url % id + "\n")
548 def main_resolve(args):
549 if len(args) == 0:
550 if g_last_werk == None:
551 bail_out("No last werk, please specify id.")
552 id = g_last_werk
553 else:
554 if len(args) != 1:
555 usage()
556 id = int(args[0])
558 werk = g_werks.get(id)
559 if not werk:
560 bail_out("No such werk.\n")
562 show_werk(werk)
563 state = input_choice("State", states.keys())
565 comment = get_long_input("comment")
566 add_comment(werk, "changed state %s -> %s" % (werk["state"], state), comment)
567 werk["state"] = state
568 save_werk(werk)
570 def main_delete(args):
571 for ids in args:
572 if 0 == os.system("git rm %s" % ids):
573 sys.stdout.write("Deleted werk %s (%s).\n" % (ids, g_werks[int(ids)]["description"]))
575 def grep(line, kw, n):
576 lc = kw.lower()
577 i = line.lower().find(lc)
578 if i == -1:
579 return None
580 else:
581 col = grep_colors[n % len(grep_colors)]
582 return line[0:i] + col + line[i:i+len(kw)] + tty_normal + line[i+len(kw):]
585 def main_grep(args):
586 if '-v' in args:
587 verbose = True
588 args = [ a for a in args if a != '-v' ]
589 else:
590 verbose = False
592 if len(args) == 0:
593 usage()
595 for werk in g_werks.values():
596 one_kw_didnt_match = False
597 title = werk["title"]
598 lines = werk["description"].split("\n")
599 bodylines = set([])
601 # *all* of the keywords must match in order for the
602 # werk to be displayed
603 i = 0
604 for kw in args:
605 i += 1
606 this_kw_matched = False
608 # look for keyword in title
609 match = grep(title, kw, i)
610 if match:
611 werk["title"] = match
612 title = match
613 this_kw_matched = True
615 # look for keyword in description
616 for j, line in enumerate(lines):
617 match = grep(line, kw, i)
618 if match:
619 bodylines.add(j)
620 lines[j] = match
621 this_kw_matched = True
623 if not this_kw_matched:
624 one_kw_didnt_match = True
627 if not one_kw_didnt_match:
628 list_werk(werk)
629 if verbose:
630 for x in sorted(list(bodylines)):
631 sys.stdout.write(" %s\n" % lines[x])
634 def main_edit(args):
635 if len(args) == 0:
636 werkid = int(g_last_werk)
637 if werkid == None:
638 bail_out("No last werk. Please specify id.")
639 else:
640 try:
641 werkid = int(args[0])
642 args = args[1:]
643 except:
644 werkid = int(g_last_werk)
645 if werkid == None:
646 bail_out("No last werk. Please specify id.")
648 edit_werk(werkid, args, commit = False)
649 save_last_werkid(werkid)
652 def edit_werk(werkid, custom_files = [], commit = True):
653 if not os.path.exists(str(werkid)):
654 bail_out("No werk with this id.")
655 editor = os.getenv("EDITOR")
656 if not editor:
657 for p in [ "/usr/bin/editor", "/usr/bin/vim", "/bin/vi" ]:
658 if os.path.exists(p):
659 editor = p
660 break
661 if not editor:
662 bail_out("No editor available (please set EDITOR).\n")
664 if 0 == os.system("bash -c '%s +8 %s'" % (editor, werkid)):
665 load_werks()
666 werk = g_werks[werkid]
667 git_add(g_werks[werkid])
668 if commit:
669 git_commit(werk, custom_files)
672 def main_commit(args):
673 if len(g_modified) == 0:
674 bail_out("No new or modified werk.")
675 else:
676 sys.stdout.write("Commiting:\n")
677 for id in g_modified:
678 list_werk(g_werks[id])
679 if 0 == os.system("git commit -m 'Updated werk entries %s' ." % (
680 ", ".join(["#%04d" % id for id in g_modified]))):
681 sys.stdout.write("--> Successfully committed %d werks.\n" % len(g_modified))
682 else:
683 bail_out("Cannot commit.")
686 def main_pick(args):
687 if len(args) == 0:
688 bail_out("Please specify at least one commit ID to cherry-pick.")
689 if args[0] == '-n':
690 no_commit = True
691 args = args[1:]
692 else:
693 no_commit = False
695 for commit_id in args:
696 werk_cherry_pick(commit_id, no_commit)
699 def werk_cherry_pick(commit_id, no_commit):
700 # Cherry-pick the commit in question from the other branch
701 os.system("git cherry-pick --no-commit '%s'" % commit_id)
703 # Find werks that have been cherry-picked and change their version
704 # to our current version
705 load_werks() # might have changed
706 for line in os.popen("git status --porcelain"):
707 # M .werks/103
708 # M werk
709 status, filename = line.strip().split(None, 1)
710 if filename.startswith(".werks/") and filename[7].isdigit():
711 werk_id = int(filename[7:])
712 change_werk_version(werk_id, g_current_version)
713 sys.stdout.write("Changed version of werk #%04d to %s.\n" % (werk_id, g_current_version))
715 # Commit
716 if not no_commit:
717 os.system("git commit -C '%s'" % commit_id)
719 else:
720 sys.stdout.write("We don't commit yet. Here is the status:\n")
721 sys.stdout.write("Please commit with git commit -C '%s'\n\n" % commit_id)
722 os.system("git status")
725 def get_werk_ids():
726 try:
727 return eval(file('.my_ids', 'r').read())
728 except:
729 return []
732 def invalidate_my_werkid(id):
733 ids = get_werk_ids()
734 ids.remove(id)
735 store_werk_ids(ids)
738 def store_werk_ids(l):
739 file('.my_ids', 'w').write(repr(l) + "\n")
742 def current_branch():
743 return [ l for l in os.popen("git branch") if l.startswith("*") ][0].split()[-1]
745 def main_fetch_ids(args):
746 if not args:
747 sys.stdout.write('You have %d reserved IDs.\n' % (len(get_werk_ids())))
748 sys.exit(0)
749 elif len(args) == 1:
750 num = int(args[0])
751 else:
752 usage()
754 if current_branch() != "master":
755 bail_out("It is not allowed to reserve IDs on any other branch than the master.")
757 # Get the start werk_id to reserve
758 try:
759 first_free = int(eval(file('first_free').read()))
761 # enterprise werks were between 8000 and 8749. Skip over this area for new
762 # reserved werk ids
763 if first_free >= 8000 and first_free < 8780:
764 first_free = 8780
766 # cmk-omd werk were between 7500 and 7680. Skip over this area for new
767 # reserved werk ids
768 if first_free >= 7500 and first_free < 7680:
769 first_free = 7680
770 except:
771 first_free = 0
772 new_first_free = first_free + num
774 # Store the werk_ids to reserve
775 my_ids = get_werk_ids() + range(first_free, first_free + num)
776 store_werk_ids(my_ids)
778 # Store the new reserved werk ids
779 file('first_free', 'w').write(str(new_first_free) + "\n")
781 sys.stdout.write('Reserved %d additional IDs now. You have %d reserved IDs now.\n' %
782 (num, len(my_ids)))
784 if 0 == os.system("git commit -m 'Reserved %d Werk IDS' ." % num):
785 sys.stdout.write("--> Successfully committed reserved werk IDS. Please push it soon!\n")
786 else:
787 bail_out("Cannot commit.")
791 # _ __ ___ __ _(_)_ __
792 # | '_ ` _ \ / _` | | '_ \
793 # | | | | | | (_| | | | | |
794 # |_| |_| |_|\__,_|_|_| |_|
797 # default config
798 editions = []
799 components = []
800 edition_components = {}
801 classes = []
802 levels = []
803 compatible = []
804 online_url = None
806 versions = set([])
807 goto_werksdir()
808 load_config()
809 load_werks()
810 g_current_version = load_current_version()
812 if len(sys.argv) < 2:
813 usage()
815 cmd = sys.argv[1]
816 commands = {
817 "list" : lambda args: main_list(args, "console"),
818 "export" : lambda args: main_list(args, "csv"),
819 "show" : main_show,
820 "new" : main_new,
821 "blame" : main_blame,
822 "delete" : main_delete,
823 "grep" : main_grep,
824 "edit" : main_edit,
825 "ids" : main_fetch_ids,
826 "pick" : main_pick,
827 "cherry-pick" : main_pick,
828 "url" : main_url,
831 hits = []
832 for name, func in commands.items():
833 if name == cmd:
834 hits = [ (name, func) ]
835 break
836 elif name.startswith(cmd):
837 hits.append((name, func))
839 if len(hits) < 1:
840 usage()
842 elif len(hits) > 1:
843 sys.stderr.write("Command '%s' is ambigous. Possible are: %s\n" % \
844 (cmd, ", ".join([ n for (n,f) in hits])))
846 else:
847 hits[0][1](sys.argv[2:])