GUI CSS: Refactored view styles to nested structure meeting sasslint requirements...
[check_mk.git] / werk
blob89597016cf9c34ad7f62bf80790e9486117acaa0
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'
24 def tty_colors(codes):
25 return '\033[%sm' % (';'.join([str(c) for c in codes]))
26 else:
27 tty_red = ''
28 tty_green = ''
29 tty_yellow = ''
30 tty_blue = ''
31 tty_magenta = ''
32 tty_cyan = ''
33 tty_white = ''
34 tty_bgblue = ''
35 tty_bgmagenta = ''
36 tty_bgcyan = ''
37 tty_bold = ''
38 tty_underline = ''
39 tty_normal = ''
40 tty_ok = 'OK'
42 def tty_colors(c):
43 return ""
46 grep_colors = [
47 tty_bold + tty_magenta,
48 tty_bold + tty_cyan,
49 tty_bold + tty_green,
53 def get_tty_size():
54 import termios, struct, fcntl
55 try:
56 ws = struct.pack("HHHH", 0, 0, 0, 0)
57 ws = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, ws)
58 lines, columns, x, y = struct.unpack("HHHH", ws)
59 if lines > 0 and columns > 0:
60 return lines, columns
61 except:
62 pass
63 return (24, 99999)
66 def bail_out(text):
67 sys.stderr.write(text + "\n")
68 sys.exit(1)
71 def goto_werksdir():
72 global g_base_dir
73 g_base_dir = os.path.abspath('.')
74 while not os.path.exists(".werks") and os.path.abspath('.') != '/':
75 os.chdir("..")
77 try:
78 os.chdir(".werks")
79 except:
80 sys.stderr.write("Cannot find directory .werks\n")
81 sys.exit(1)
84 def load_config():
85 global g_last_werk
86 execfile("config", globals(), globals())
87 try:
88 g_last_werk = int(file(".last").read())
89 except:
90 g_last_werk = None
93 def load_werks():
94 global g_werks
95 g_werks = {}
96 check_modified()
97 for entry in os.listdir("."):
98 try:
99 werkid = int(entry)
100 try:
101 g_werks[werkid] = load_werk(werkid)
102 except:
103 sys.stderr.write("SKIPPING INVALID werk %d\n" % werkid)
104 except:
105 continue
108 def save_last_werkid(id):
109 try:
110 file(".last", "w").write("%d\n" % int(id))
111 except:
112 pass
115 def load_current_version():
116 for line in file("../defines.make"):
117 if line.startswith("VERSION"):
118 version = line.split("=", 1)[1].strip()
119 return version
121 bail_out("Failed to read VERSION from defines.make")
124 def check_modified():
125 global g_modified
126 g_modified = set([])
127 for line in os.popen("git status --porcelain"):
128 if line[0] in "AM" and ".werks/" in line:
129 try:
130 id = line.rsplit("/", 1)[-1].strip()
131 g_modified.add(int(id))
132 except:
133 pass
136 def werk_is_modified(werkid):
137 return werkid in g_modified
140 def load_werk(werkid):
141 werk = {
142 "id": werkid,
143 "state": "unknown",
144 "title": "unknown",
145 "component": "general",
146 "compatible": "compat",
147 "edition": "cre",
150 f = file(str(werkid))
151 for line in f:
152 line = line.strip()
153 if line == "":
154 break
155 header, value = line.split(":", 1)
156 werk[header.strip().lower()] = value.strip()
158 description = ""
159 for line in f:
160 description += line
162 werk["description"] = description
163 versions.add(werk["version"])
164 return werk
167 def save_werk(werk):
168 f = file(str(werk["id"]), "w")
169 f.write("Title: %s\n" % werk["title"])
170 for key, val in werk.items():
171 if key not in ["title", "description", "id"]:
172 f.write("%s%s: %s\n" % (key[0].upper(), key[1:], val))
173 f.write("\n")
174 f.write(werk["description"])
175 f.close()
176 git_add(werk)
177 save_last_werkid(werk["id"])
180 def change_werk_version(werk_id, new_version):
181 werk = g_werks[werk_id]
182 werk["version"] = new_version
183 save_werk(werk)
184 git_add(werk)
187 def git_add(werk):
188 os.system("git add %d" % werk["id"]) # nosec
191 def git_commit(werk, custom_files):
192 title = werk["title"]
193 for classid, classname, prefix in classes:
194 if werk["class"] == classid:
195 if prefix:
196 title = "%s %s" % (prefix, title)
198 title = "%04d %s" % (werk['id'], title)
200 if custom_files:
201 files_to_commit = custom_files
202 default_files = [".werks"]
203 for entry in default_files:
204 files_to_commit.append("%s/%s" % (git_top_level(), entry))
206 os.chdir(g_base_dir)
207 cmd = "git commit %s -m %s" % (" ".join(files_to_commit),
208 quote_shell_string(title + "\n\n" + werk["description"]))
209 os.system(cmd) # nosec
211 else:
212 if something_in_git_index():
213 dash_a = ''
214 os.system("cd '%s' ; git add .werks" % git_top_level()) # nosec
215 else:
216 dash_a = '-a'
218 cmd = "git commit %s -m %s" % (dash_a,
219 quote_shell_string(title + "\n\n" + werk["description"]))
220 os.system(cmd) # nosec
223 def git_top_level():
224 info = subprocess.Popen(["git", "rev-parse", "--show-toplevel"], stdout=subprocess.PIPE)
225 git_top_level = info.communicate()[0].split()[0]
226 return git_top_level
229 def something_in_git_index():
230 for line in os.popen("git status --porcelain"):
231 if line[0] == 'M':
232 return True
233 return False
236 def quote_shell_string(s):
237 return "'" + s.replace("'", "'\"'\"'") + "'"
240 def next_werk_id():
241 my_werk_ids = get_werk_ids()
242 if not my_werk_ids:
243 bail_out(
244 'You have no werk IDS left. You can reserve 10 additional Werk IDS with "./werk ids 10".'
246 return my_werk_ids[0]
249 def add_comment(werk, title, comment):
250 werk["description"] += """
251 %s: %s
252 %s""" % (time.strftime("%F %T"), title, comment)
255 def usage():
256 sys.stdout.write("""Usage: werk COMMAND [ARGS...]
258 where COMMAND is one of:
260 ids [#] - Shows the number of reserved werk IDS. With a number
261 given as parameter the command will reserve new werk IDS.
262 list [-r] [STATE] - list werks (-r: reverse)
263 new - create a new werk
264 show [# #..] - show several werks, or 'all' for all, of leave out for last
265 resolve ID - change a werks state
266 delete #.. - delete werk(s)
267 grep [-v] KW1 KW2... - show werks containing all of the given keywords (-v: verbose)
268 edit [#] - open werk # in editor (or newest werk)
269 blame [#] - show who worked on a werk
270 url # - show the online URL of werk #
272 """)
273 sys.exit(1)
276 def num_color(n, colors, inverse):
277 if inverse:
278 b = 40
279 else:
280 b = 30
282 c = colors[n - 1]
283 return tty_colors([b + c, 1])
286 def list_werk(werk):
287 if werk_is_modified(werk["id"]):
288 bold = tty_bold + tty_cyan + "(*) "
289 else:
290 bold = ""
291 lines, cols = get_tty_size()
292 title = werk["title"][:cols - 45]
293 sys.stdout.write(
294 "#%04d %-9s %s %3s %-13s %-6s %s%s%s %-8s %s%s%s\n" %
295 (int(werk["id"]), time.strftime("%F", time.localtime(int(werk["date"]))),
296 colored_class(werk["class"], 8), werk["edition"], werk["component"], werk["compatible"],
297 tty_bold, werk["level"], tty_normal, werk["version"], bold, title, tty_normal))
300 def colored_class(classname, digits):
301 if classname == "fix":
302 return tty_bold + tty_red + ("%-" + str(digits) + "s") % classname + tty_normal
303 else:
304 return ("%-" + str(digits) + "s") % classname
307 def show_werk(werk):
308 list_werk(werk)
309 sys.stdout.write("\n%s\n" % werk["description"])
312 def main_list(args, format):
313 werks = g_werks.values()
315 # arguments are tags from state, component and class. Multiple values
316 # in one class are orred. Multiple types are anded.
317 filters = {}
319 sort = lambda a, b: cmp(a['date'], b['date'])
320 reverse = False
321 for a in args:
323 if a == "current":
324 a = g_current_version
326 if a == '-r':
327 reverse = True
328 continue
330 hit = False
331 for tp, values in [
332 ("edition", editions),
333 ("component", all_components()),
334 ("level", levels),
335 ("class", classes),
336 ("version", versions),
337 ("compatible", compatible),
339 for v in values:
340 if type(v) == tuple:
341 v = v[0]
342 if v.startswith(a):
343 entries = filters.get(tp, [])
344 entries.append(v)
345 filters[tp] = entries
346 hit = True
347 break
348 if hit:
349 break
350 if not hit:
351 bail_out("No such edition, component, state, class or target version: %s" % a)
353 # Filter
354 newwerks = []
355 for werk in werks:
356 skip = False
357 for tp, entries in filters.items():
358 if werk[tp] not in entries:
359 skip = True
360 break
361 if not skip:
362 newwerks.append(werk)
363 werks = newwerks
365 # Sort
366 if sort:
367 newwerks.sort(sort)
368 if reverse:
369 newwerks.reverse()
371 # Output
372 if format == "console":
373 for werk in werks:
374 list_werk(werk)
375 else:
376 output_csv(werks)
379 # CSV Table has the following columns:
380 # Component;ID;Title;Class;Effort
381 def output_csv(werks):
382 def line(*l):
383 sys.stdout.write('"' + '";"'.join(map(str, l)) + '"\n')
385 nr = 1
386 for entry in components:
387 if len(entry) == 2:
388 name, alias = entry
389 else:
390 name = entry
391 alias = entry
393 line("", "", "", "", "")
395 total_effort = 0
396 for werk in werks:
397 if werk["component"] == name:
398 total_effort += werk_effort(werk)
399 line("", "%d. %s" % (nr, alias), "", total_effort)
400 nr += 1
402 for werk in werks:
403 if werk["component"] == name:
404 line(werk["id"], werk["title"], werk_class(werk), werk_effort(werk))
405 line("", werk["description"].replace("\n", " ").replace('"', "'"), "", "")
408 def werk_class(werk):
409 cl = werk["class"]
410 for entry in classes:
411 if entry == cl:
412 return cl
413 elif type(entry) == tuple and entry[0] == cl:
414 return entry[1]
415 return cl
418 def werk_effort(werk):
419 return int(werk.get("effort", "0"))
422 def main_show(args):
423 ids = args
424 if len(ids) == 0:
425 if g_last_werk is None:
426 bail_out("No last werk known. Please specify id.")
427 ids = [g_last_werk]
428 elif ids[0] == 'all':
429 ids = [id for (id, werk) in g_werks.items()]
431 for id in ids:
432 if id != ids[0]:
433 sys.stdout.write(
434 "-------------------------------------------------------------------------------\n")
435 try:
436 show_werk(g_werks[int(id)])
437 except 1:
438 sys.stderr.write("Skipping invalid werk id '%s'\n" % id)
439 save_last_werkid(ids[-1])
442 def get_input(what, default=""):
443 sys.stdout.write("%s: " % what)
444 sys.stdout.flush()
445 value = sys.stdin.readline().strip()
446 if value == "":
447 return default
448 else:
449 return value
452 def get_long_input(what):
453 sys.stdout.write("Enter %s. End with CTRL-D.\n" % what)
454 usertext = sys.stdin.read()
455 # remove leading and trailing empty lines
456 while usertext.startswith("\n"):
457 usertext = usertext[1:]
458 while usertext.endswith("\n\n"):
459 usertext = usertext[:-1]
460 return usertext
463 def getch():
464 fd = sys.stdin.fileno()
465 old_settings = termios.tcgetattr(fd)
466 try:
467 tty.setraw(sys.stdin.fileno())
468 ch = sys.stdin.read(1)
469 finally:
470 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
471 if ord(ch) == 3:
472 raise KeyboardInterrupt()
473 return ch
476 def input_choice(what, choices):
477 next_index = 0
478 ctc = {}
479 texts = []
480 for choice in choices:
481 if type(choice) == tuple:
482 choice = choice[0]
484 added = False
486 # Find an identifying character for the input choice. In case all possible
487 # characters are already used start using unique numbers
488 for c in str(choice):
489 if c not in ".-_/" and c not in ctc:
490 ctc[c] = choice
491 texts.append(str(choice).replace(c, tty_bold + c + tty_normal, 1))
492 added = True
493 break
495 if not added:
496 ctc["%s" % next_index] = choice
497 texts.append("%s:%s" % ("%s%d%s" % (tty_bold, next_index, tty_normal), choice))
498 next_index += 1
500 while True:
501 sys.stdout.write("%s (%s): " % (what, ", ".join(texts)))
502 sys.stdout.flush()
503 c = getch()
504 if c in ctc:
505 sys.stdout.write(" %s%s%s\n" % (tty_bold, ctc[c], tty_normal))
506 return ctc[c]
507 else:
508 sys.stdout.write("\n")
511 def get_edition_components(edition):
512 return components + edition_components.get(edition, [])
515 def all_components():
516 c = components
517 for ed_components in edition_components.values():
518 c += ed_components
519 return components
522 def main_new(args):
523 werk = {}
524 werk["id"] = next_werk_id()
525 werk["date"] = int(time.time())
526 werk["version"] = g_current_version
527 werk["title"] = get_input("Title")
528 if werk["title"] == "":
529 sys.stderr.write("Cancelled.\n")
530 sys.exit(0)
531 werk["class"] = input_choice("Class", classes)
532 werk["edition"] = input_choice("Edition", editions)
533 werk["component"] = input_choice("Component", get_edition_components(werk["edition"]))
534 werk["level"] = input_choice("Level", levels)
535 werk["compatible"] = input_choice("Compatible", compatible)
536 werk["description"] = u"\n"
538 g_werks[werk["id"]] = werk
539 save_werk(werk)
540 invalidate_my_werkid(werk["id"])
541 edit_werk(werk["id"], args)
543 sys.stdout.write("Werk saved with id %d.\n" % werk["id"])
546 def get_werk_arg(args):
547 if len(args) == 0:
548 if g_last_werk is None:
549 bail_out("No last werk, please specify id.")
550 id = g_last_werk
551 else:
552 if len(args) != 1:
553 usage()
554 id = int(args[0])
556 werk = g_werks.get(id)
557 if not werk:
558 bail_out("No such werk.\n")
559 save_last_werkid(id)
560 return id
563 def main_blame(args):
564 id = get_werk_arg(args)
565 os.system("git blame %d" % id) # nosec
568 def main_url(args):
569 id = get_werk_arg(args)
570 sys.stdout.write(online_url % id + "\n")
573 def main_resolve(args):
574 if len(args) == 0:
575 if g_last_werk is None:
576 bail_out("No last werk, please specify id.")
577 id = g_last_werk
578 else:
579 if len(args) != 1:
580 usage()
581 id = int(args[0])
583 werk = g_werks.get(id)
584 if not werk:
585 bail_out("No such werk.\n")
587 show_werk(werk)
588 state = input_choice("State", states.keys())
590 comment = get_long_input("comment")
591 add_comment(werk, "changed state %s -> %s" % (werk["state"], state), comment)
592 werk["state"] = state
593 save_werk(werk)
596 def main_delete(args):
597 for ids in args:
598 if 0 == os.system("git rm %s" % ids): # nosec
599 sys.stdout.write("Deleted werk %s (%s).\n" % (ids, g_werks[int(ids)]["description"]))
602 def grep(line, kw, n):
603 lc = kw.lower()
604 i = line.lower().find(lc)
605 if i == -1:
606 return None
607 else:
608 col = grep_colors[n % len(grep_colors)]
609 return line[0:i] + col + line[i:i + len(kw)] + tty_normal + line[i + len(kw):]
612 def main_grep(args):
613 if '-v' in args:
614 verbose = True
615 args = [a for a in args if a != '-v']
616 else:
617 verbose = False
619 if len(args) == 0:
620 usage()
622 for werk in g_werks.values():
623 one_kw_didnt_match = False
624 title = werk["title"]
625 lines = werk["description"].split("\n")
626 bodylines = set([])
628 # *all* of the keywords must match in order for the
629 # werk to be displayed
630 i = 0
631 for kw in args:
632 i += 1
633 this_kw_matched = False
635 # look for keyword in title
636 match = grep(title, kw, i)
637 if match:
638 werk["title"] = match
639 title = match
640 this_kw_matched = True
642 # look for keyword in description
643 for j, line in enumerate(lines):
644 match = grep(line, kw, i)
645 if match:
646 bodylines.add(j)
647 lines[j] = match
648 this_kw_matched = True
650 if not this_kw_matched:
651 one_kw_didnt_match = True
653 if not one_kw_didnt_match:
654 list_werk(werk)
655 if verbose:
656 for x in sorted(list(bodylines)):
657 sys.stdout.write(" %s\n" % lines[x])
660 def main_edit(args):
661 if len(args) == 0:
662 werkid = int(g_last_werk)
663 if werkid is None:
664 bail_out("No last werk. Please specify id.")
665 else:
666 try:
667 werkid = int(args[0])
668 args = args[1:]
669 except:
670 werkid = int(g_last_werk)
671 if werkid is None:
672 bail_out("No last werk. Please specify id.")
674 edit_werk(werkid, args, commit=False)
675 save_last_werkid(werkid)
678 def edit_werk(werkid, custom_files=[], commit=True):
679 if not os.path.exists(str(werkid)):
680 bail_out("No werk with this id.")
681 editor = os.getenv("EDITOR")
682 if not editor:
683 for p in ["/usr/bin/editor", "/usr/bin/vim", "/bin/vi"]:
684 if os.path.exists(p):
685 editor = p
686 break
687 if not editor:
688 bail_out("No editor available (please set EDITOR).\n")
690 if 0 == os.system("bash -c '%s +8 %s'" % (editor, werkid)): # nosec
691 load_werks()
692 werk = g_werks[werkid]
693 git_add(g_werks[werkid])
694 if commit:
695 git_commit(werk, custom_files)
698 def main_commit(args):
699 if len(g_modified) == 0:
700 bail_out("No new or modified werk.")
701 else:
702 sys.stdout.write("Commiting:\n")
703 for id in g_modified:
704 list_werk(g_werks[id])
705 cmd = "git commit -m 'Updated werk entries %s' ." % (", ".join(
706 ["#%04d" % id for id in g_modified]))
707 if 0 == os.system(cmd): # nosec
708 sys.stdout.write("--> Successfully committed %d werks.\n" % len(g_modified))
709 else:
710 bail_out("Cannot commit.")
713 def main_pick(args):
714 if len(args) == 0:
715 bail_out("Please specify at least one commit ID to cherry-pick.")
716 if args[0] == '-n':
717 no_commit = True
718 args = args[1:]
719 else:
720 no_commit = False
722 for commit_id in args:
723 werk_cherry_pick(commit_id, no_commit)
726 def werk_cherry_pick(commit_id, no_commit):
727 # Cherry-pick the commit in question from the other branch
728 os.system("git cherry-pick --no-commit '%s'" % commit_id) # nosec
730 # Find werks that have been cherry-picked and change their version
731 # to our current version
732 load_werks() # might have changed
733 for line in os.popen("git status --porcelain"): # nosec
734 # M .werks/103
735 # M werk
736 status, filename = line.strip().split(None, 1)
737 if filename.startswith(".werks/") and filename[7].isdigit():
738 werk_id = int(filename[7:])
739 change_werk_version(werk_id, g_current_version)
740 sys.stdout.write(
741 "Changed version of werk #%04d to %s.\n" % (werk_id, g_current_version))
743 # Commit
744 if not no_commit:
745 os.system("git commit -C '%s'" % commit_id) # nosec
747 else:
748 sys.stdout.write("We don't commit yet. Here is the status:\n")
749 sys.stdout.write("Please commit with git commit -C '%s'\n\n" % commit_id)
750 os.system("git status")
753 def get_werk_ids():
754 try:
755 return eval(file('.my_ids', 'r').read())
756 except:
757 return []
760 def invalidate_my_werkid(id):
761 ids = get_werk_ids()
762 ids.remove(id)
763 store_werk_ids(ids)
766 def store_werk_ids(l):
767 file('.my_ids', 'w').write(repr(l) + "\n")
770 def current_branch():
771 return [l for l in os.popen("git branch") if l.startswith("*")][0].split()[-1]
774 def main_fetch_ids(args):
775 if not args:
776 sys.stdout.write('You have %d reserved IDs.\n' % (len(get_werk_ids())))
777 sys.exit(0)
778 elif len(args) == 1:
779 num = int(args[0])
780 else:
781 usage()
783 if current_branch() != "master":
784 bail_out("It is not allowed to reserve IDs on any other branch than the master.")
786 # Get the start werk_id to reserve
787 try:
788 first_free = int(eval(file('first_free').read()))
790 # enterprise werks were between 8000 and 8749. Skip over this area for new
791 # reserved werk ids
792 if first_free >= 8000 and first_free < 8780:
793 first_free = 8780
795 # cmk-omd werk were between 7500 and 7680. Skip over this area for new
796 # reserved werk ids
797 if first_free >= 7500 and first_free < 7680:
798 first_free = 7680
799 except:
800 first_free = 0
801 new_first_free = first_free + num
803 # Store the werk_ids to reserve
804 my_ids = get_werk_ids() + range(first_free, first_free + num)
805 store_werk_ids(my_ids)
807 # Store the new reserved werk ids
808 file('first_free', 'w').write(str(new_first_free) + "\n")
810 sys.stdout.write(
811 'Reserved %d additional IDs now. You have %d reserved IDs now.\n' % (num, len(my_ids)))
813 if 0 == os.system("git commit -m 'Reserved %d Werk IDS' ." % num): # nosec
814 sys.stdout.write("--> Successfully committed reserved werk IDS. Please push it soon!\n")
815 else:
816 bail_out("Cannot commit.")
820 # _ __ ___ __ _(_)_ __
821 # | '_ ` _ \ / _` | | '_ \
822 # | | | | | | (_| | | | | |
823 # |_| |_| |_|\__,_|_|_| |_|
826 # default config
827 editions = []
828 components = []
829 edition_components = {}
830 classes = []
831 levels = []
832 compatible = []
833 online_url = None
835 versions = set([])
836 goto_werksdir()
837 load_config()
838 load_werks()
839 g_current_version = load_current_version()
841 if len(sys.argv) < 2:
842 usage()
844 cmd = sys.argv[1]
845 commands = {
846 "list": lambda args: main_list(args, "console"),
847 "export": lambda args: main_list(args, "csv"),
848 "show": main_show,
849 "new": main_new,
850 "blame": main_blame,
851 "delete": main_delete,
852 "grep": main_grep,
853 "edit": main_edit,
854 "ids": main_fetch_ids,
855 "pick": main_pick,
856 "cherry-pick": main_pick,
857 "url": main_url,
860 hits = []
861 for name, func in commands.items():
862 if name == cmd:
863 hits = [(name, func)]
864 break
865 elif name.startswith(cmd):
866 hits.append((name, func))
868 if len(hits) < 1:
869 usage()
871 elif len(hits) > 1:
872 sys.stderr.write("Command '%s' is ambigous. Possible are: %s\n" % \
873 (cmd, ", ".join([ n for (n,f) in hits])))
875 else:
876 hits[0][1](sys.argv[2:])