3 import sys
, os
, time
, termios
, tty
, subprocess
5 # colored output, if stdout is a tty
6 if sys
.stdout
.isatty():
9 tty_yellow
= '\033[33m'
11 tty_magenta
= '\033[35m'
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'
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
]))
47 tty_bold
+ tty_magenta
,
54 import termios
, struct
, fcntl
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:
67 sys
.stderr
.write(text
+ "\n")
73 g_base_dir
= os
.path
.abspath('.')
74 while not os
.path
.exists(".werks") and os
.path
.abspath('.') != '/':
80 sys
.stderr
.write("Cannot find directory .werks\n")
86 execfile("config", globals(), globals())
88 g_last_werk
= int(file(".last").read())
97 for entry
in os
.listdir("."):
101 g_werks
[werkid
] = load_werk(werkid
)
103 sys
.stderr
.write("SKIPPING INVALID werk %d\n" % werkid
)
108 def save_last_werkid(id):
110 file(".last", "w").write("%d\n" % int(id))
115 def load_current_version():
116 for line
in file("../defines.make"):
117 if line
.startswith("VERSION"):
118 version
= line
.split("=", 1)[1].strip()
121 bail_out("Failed to read VERSION from defines.make")
124 def check_modified():
127 for line
in os
.popen("git status --porcelain"):
128 if line
[0] in "AM" and ".werks/" in line
:
130 id = line
.rsplit("/", 1)[-1].strip()
131 g_modified
.add(int(id))
136 def werk_is_modified(werkid
):
137 return werkid
in g_modified
140 def load_werk(werkid
):
145 "component": "general",
146 "compatible": "compat",
150 f
= file(str(werkid
))
155 header
, value
= line
.split(":", 1)
156 werk
[header
.strip().lower()] = value
.strip()
162 werk
["description"] = description
163 versions
.add(werk
["version"])
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
))
174 f
.write(werk
["description"])
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
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
:
196 title
= "%s %s" % (prefix
, title
)
198 title
= "%04d %s" % (werk
['id'], title
)
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
))
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
212 if something_in_git_index():
214 os
.system("cd '%s' ; git add .werks" % git_top_level()) # nosec
218 cmd
= "git commit %s -m %s" % (dash_a
,
219 quote_shell_string(title
+ "\n\n" + werk
["description"]))
220 os
.system(cmd
) # nosec
224 info
= subprocess
.Popen(["git", "rev-parse", "--show-toplevel"], stdout
=subprocess
.PIPE
)
225 git_top_level
= info
.communicate()[0].split()[0]
229 def something_in_git_index():
230 for line
in os
.popen("git status --porcelain"):
236 def quote_shell_string(s
):
237 return "'" + s
.replace("'", "'\"'\"'") + "'"
241 my_werk_ids
= get_werk_ids()
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"] += """
252 %s""" % (time
.strftime("%F %T"), title
, comment
)
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 #
276 def num_color(n
, colors
, inverse
):
283 return tty_colors([b
+ c
, 1])
287 if werk_is_modified(werk
["id"]):
288 bold
= tty_bold
+ tty_cyan
+ "(*) "
291 lines
, cols
= get_tty_size()
292 title
= werk
["title"][:cols
- 45]
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
304 return ("%-" + str(digits
) + "s") % classname
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.
319 sort
= lambda a
, b
: cmp(a
['date'], b
['date'])
324 a
= g_current_version
332 ("edition", editions
),
333 ("component", all_components()),
336 ("version", versions
),
337 ("compatible", compatible
),
343 entries
= filters
.get(tp
, [])
345 filters
[tp
] = entries
351 bail_out("No such edition, component, state, class or target version: %s" % a
)
357 for tp
, entries
in filters
.items():
358 if werk
[tp
] not in entries
:
362 newwerks
.append(werk
)
372 if format
== "console":
379 # CSV Table has the following columns:
380 # Component;ID;Title;Class;Effort
381 def output_csv(werks
):
383 sys
.stdout
.write('"' + '";"'.join(map(str, l
)) + '"\n')
386 for entry
in components
:
393 line("", "", "", "", "")
397 if werk
["component"] == name
:
398 total_effort
+= werk_effort(werk
)
399 line("", "%d. %s" % (nr
, alias
), "", total_effort
)
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
):
410 for entry
in classes
:
413 elif type(entry
) == tuple and entry
[0] == cl
:
418 def werk_effort(werk
):
419 return int(werk
.get("effort", "0"))
425 if g_last_werk
is None:
426 bail_out("No last werk known. Please specify id.")
428 elif ids
[0] == 'all':
429 ids
= [id for (id, werk
) in g_werks
.items()]
434 "-------------------------------------------------------------------------------\n")
436 show_werk(g_werks
[int(id)])
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
)
445 value
= sys
.stdin
.readline().strip()
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]
464 fd
= sys
.stdin
.fileno()
465 old_settings
= termios
.tcgetattr(fd
)
467 tty
.setraw(sys
.stdin
.fileno())
468 ch
= sys
.stdin
.read(1)
470 termios
.tcsetattr(fd
, termios
.TCSADRAIN
, old_settings
)
472 raise KeyboardInterrupt()
476 def input_choice(what
, choices
):
480 for choice
in choices
:
481 if type(choice
) == tuple:
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
:
491 texts
.append(str(choice
).replace(c
, tty_bold
+ c
+ tty_normal
, 1))
496 ctc
["%s" % next_index
] = choice
497 texts
.append("%s:%s" % ("%s%d%s" % (tty_bold
, next_index
, tty_normal
), choice
))
501 sys
.stdout
.write("%s (%s): " % (what
, ", ".join(texts
)))
505 sys
.stdout
.write(" %s%s%s\n" % (tty_bold
, ctc
[c
], tty_normal
))
508 sys
.stdout
.write("\n")
511 def get_edition_components(edition
):
512 return components
+ edition_components
.get(edition
, [])
515 def all_components():
517 for ed_components
in edition_components
.values():
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")
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
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
):
548 if g_last_werk
is None:
549 bail_out("No last werk, please specify id.")
556 werk
= g_werks
.get(id)
558 bail_out("No such werk.\n")
563 def main_blame(args
):
564 id = get_werk_arg(args
)
565 os
.system("git blame %d" % id) # nosec
569 id = get_werk_arg(args
)
570 sys
.stdout
.write(online_url
% id + "\n")
573 def main_resolve(args
):
575 if g_last_werk
is None:
576 bail_out("No last werk, please specify id.")
583 werk
= g_werks
.get(id)
585 bail_out("No such werk.\n")
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
596 def main_delete(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
):
604 i
= line
.lower().find(lc
)
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
):]
615 args
= [a
for a
in args
if a
!= '-v']
622 for werk
in g_werks
.values():
623 one_kw_didnt_match
= False
624 title
= werk
["title"]
625 lines
= werk
["description"].split("\n")
628 # *all* of the keywords must match in order for the
629 # werk to be displayed
633 this_kw_matched
= False
635 # look for keyword in title
636 match
= grep(title
, kw
, i
)
638 werk
["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
)
648 this_kw_matched
= True
650 if not this_kw_matched
:
651 one_kw_didnt_match
= True
653 if not one_kw_didnt_match
:
656 for x
in sorted(list(bodylines
)):
657 sys
.stdout
.write(" %s\n" % lines
[x
])
662 werkid
= int(g_last_werk
)
664 bail_out("No last werk. Please specify id.")
667 werkid
= int(args
[0])
670 werkid
= int(g_last_werk
)
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")
683 for p
in ["/usr/bin/editor", "/usr/bin/vim", "/bin/vi"]:
684 if os
.path
.exists(p
):
688 bail_out("No editor available (please set EDITOR).\n")
690 if 0 == os
.system("bash -c '%s +8 %s'" % (editor
, werkid
)): # nosec
692 werk
= g_werks
[werkid
]
693 git_add(g_werks
[werkid
])
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.")
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
))
710 bail_out("Cannot commit.")
715 bail_out("Please specify at least one commit ID to cherry-pick.")
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
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
)
741 "Changed version of werk #%04d to %s.\n" % (werk_id
, g_current_version
))
745 os
.system("git commit -C '%s'" % commit_id
) # nosec
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")
755 return eval(file('.my_ids', 'r').read())
760 def invalidate_my_werkid(id):
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
):
776 sys
.stdout
.write('You have %d reserved IDs.\n' % (len(get_werk_ids())))
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
788 first_free
= int(eval(file('first_free').read()))
790 # enterprise werks were between 8000 and 8749. Skip over this area for new
792 if first_free
>= 8000 and first_free
< 8780:
795 # cmk-omd werk were between 7500 and 7680. Skip over this area for new
797 if first_free
>= 7500 and first_free
< 7680:
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")
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")
816 bail_out("Cannot commit.")
820 # _ __ ___ __ _(_)_ __
821 # | '_ ` _ \ / _` | | '_ \
822 # | | | | | | (_| | | | | |
823 # |_| |_| |_|\__,_|_|_| |_|
829 edition_components
= {}
839 g_current_version
= load_current_version()
841 if len(sys
.argv
) < 2:
846 "list": lambda args
: main_list(args
, "console"),
847 "export": lambda args
: main_list(args
, "csv"),
851 "delete": main_delete
,
854 "ids": main_fetch_ids
,
856 "cherry-pick": main_pick
,
861 for name
, func
in commands
.items():
863 hits
= [(name
, func
)]
865 elif name
.startswith(cmd
):
866 hits
.append((name
, func
))
872 sys
.stderr
.write("Command '%s' is ambigous. Possible are: %s\n" % \
873 (cmd
, ", ".join([ n
for (n
,f
) in hits
])))
876 hits
[0][1](sys
.argv
[2:])