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'
23 def tty_colors(codes
):
24 return '\033[%sm' % (';'.join([str(c
) for c
in codes
]))
44 tty_bold
+ tty_magenta
,
50 import termios
,struct
,fcntl
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:
62 sys
.stderr
.write(text
+ "\n")
67 g_base_dir
= os
.path
.abspath('.')
68 while not os
.path
.exists(".werks") and os
.path
.abspath('.') != '/':
74 sys
.stderr
.write("Cannot find directory .werks\n")
80 execfile("config", globals(), globals())
82 g_last_werk
= int(file(".last").read())
91 for entry
in os
.listdir("."):
95 g_werks
[werkid
] = load_werk(werkid
)
97 sys
.stderr
.write("SKIPPING INVALID werk %d\n" % werkid
)
101 def save_last_werkid(id):
103 file(".last", "w").write("%d\n" % int(id))
108 def load_current_version():
109 for line
in file("../defines.make"):
110 if line
.startswith("VERSION"):
111 version
= line
.split("=", 1)[1].strip()
114 bail_out("Failed to read VERSION from defines.make")
117 def check_modified():
120 for line
in os
.popen("git status --porcelain"):
121 if line
[0] in "AM" and ".werks/" in line
:
123 id = line
.rsplit("/", 1)[-1].strip()
124 g_modified
.add(int(id))
128 def werk_is_modified(werkid
):
129 return werkid
in g_modified
132 def load_werk(werkid
):
137 "component" : "general",
138 "compatible" : "compat",
142 f
= file(str(werkid
))
147 header
, value
= line
.split(":", 1)
148 werk
[header
.strip().lower()] = value
.strip()
154 werk
["description"] = description
155 versions
.add(werk
["version"])
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
))
165 f
.write(werk
["description"])
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
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
:
184 title
= "%s %s" % (prefix
, title
)
186 title
= "%04d %s" % (werk
['id'], title
)
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
))
195 os
.system("git commit %s -m %s" % (" ".join(files_to_commit
),
196 quote_shell_string(title
+ "\n\n" + werk
["description"])))
199 if something_in_git_index():
201 os
.system("cd '%s' ; git add .werks" % git_top_level())
205 os
.system("git commit %s -m %s" % (dash_a
, quote_shell_string(title
+ "\n\n" + werk
["description"])))
208 info
= subprocess
.Popen(["git", "rev-parse", "--show-toplevel"], stdout
=subprocess
.PIPE
)
209 git_top_level
= info
.communicate()[0].split()[0]
213 def something_in_git_index():
214 for line
in os
.popen("git status --porcelain"):
220 def quote_shell_string(s
):
221 return "'" + s
.replace("'", "'\"'\"'") + "'"
225 my_werk_ids
= get_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"] += """
233 %s""" % (time
.strftime("%F %T"), title
, comment
)
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 #
257 def num_color(n
, colors
, inverse
):
264 return tty_colors([b
+ c
, 1])
267 if werk_is_modified(werk
["id"]):
268 bold
= tty_bold
+ tty_cyan
+ "(*) "
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" %
275 time
.strftime("%F", time
.localtime(int(werk
["date"]))),
276 colored_class(werk
["class"], 8),
280 tty_bold
, werk
["level"], tty_normal
,
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
288 return ("%-" + str(digits
) + "s") % classname
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.
301 sort
= lambda a
,b
: cmp(a
['date'], b
['date'])
306 a
= g_current_version
314 ( "edition", editions
),
315 ( "component", all_components()),
317 ( "class", classes
),
318 ( "version", versions
),
319 ( "compatible", compatible
),
325 entries
= filters
.get(tp
, [])
327 filters
[tp
] = entries
333 bail_out("No such edition, component, state, class or target version: %s" % a
)
339 for tp
, entries
in filters
.items():
340 if werk
[tp
] not in entries
:
344 newwerks
.append(werk
)
354 if format
== "console":
361 # CSV Table has the following columns:
362 # Component;ID;Title;Class;Effort
363 def output_csv(werks
):
365 sys
.stdout
.write('"' + '";"'.join(map(str, l
)) + '"\n')
368 for entry
in components
:
375 line("", "", "", "", "")
379 if werk
["component"] == name
:
380 total_effort
+= werk_effort(werk
)
381 line("", "%d. %s" % (nr
, alias
), "", total_effort
)
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
):
391 for entry
in classes
:
394 elif type(entry
) == tuple and entry
[0] == cl
:
398 def werk_effort(werk
):
399 return int(werk
.get("effort", "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() ]
412 sys
.stdout
.write("-------------------------------------------------------------------------------\n")
414 show_werk(g_werks
[int(id)])
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
)
422 value
= sys
.stdin
.readline().strip()
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]
439 fd
= sys
.stdin
.fileno()
440 old_settings
= termios
.tcgetattr(fd
)
442 tty
.setraw(sys
.stdin
.fileno())
443 ch
= sys
.stdin
.read(1)
445 termios
.tcsetattr(fd
, termios
.TCSADRAIN
, old_settings
)
447 raise KeyboardInterrupt()
450 def input_choice(what
, choices
):
454 for choice
in choices
:
455 if type(choice
) == tuple:
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
:
465 texts
.append(str(choice
).replace(c
, tty_bold
+ c
+ tty_normal
, 1))
470 ctc
["%s" % next_index
] = choice
471 texts
.append("%s:%s" % ("%s%d%s" % (tty_bold
, next_index
, tty_normal
), choice
))
476 sys
.stdout
.write("%s (%s): " % (what
, ", ".join(texts
)))
480 sys
.stdout
.write(" %s%s%s\n" % (
481 tty_bold
, ctc
[c
], tty_normal
))
484 sys
.stdout
.write("\n")
487 def get_edition_components(edition
):
488 return components
+ edition_components
.get(edition
, [])
491 def all_components():
493 for ed_components
in edition_components
.values():
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")
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
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
):
523 if g_last_werk
== None:
524 bail_out("No last werk, please specify id.")
531 werk
= g_werks
.get(id)
533 bail_out("No such werk.\n")
538 def main_blame(args
):
539 id = get_werk_arg(args
)
540 os
.system("git blame %d" % id)
544 id = get_werk_arg(args
)
545 sys
.stdout
.write(online_url
% id + "\n")
548 def main_resolve(args
):
550 if g_last_werk
== None:
551 bail_out("No last werk, please specify id.")
558 werk
= g_werks
.get(id)
560 bail_out("No such werk.\n")
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
570 def main_delete(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
):
577 i
= line
.lower().find(lc
)
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
):]
588 args
= [ a
for a
in args
if a
!= '-v' ]
595 for werk
in g_werks
.values():
596 one_kw_didnt_match
= False
597 title
= werk
["title"]
598 lines
= werk
["description"].split("\n")
601 # *all* of the keywords must match in order for the
602 # werk to be displayed
606 this_kw_matched
= False
608 # look for keyword in title
609 match
= grep(title
, kw
, i
)
611 werk
["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
)
621 this_kw_matched
= True
623 if not this_kw_matched
:
624 one_kw_didnt_match
= True
627 if not one_kw_didnt_match
:
630 for x
in sorted(list(bodylines
)):
631 sys
.stdout
.write(" %s\n" % lines
[x
])
636 werkid
= int(g_last_werk
)
638 bail_out("No last werk. Please specify id.")
641 werkid
= int(args
[0])
644 werkid
= int(g_last_werk
)
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")
657 for p
in [ "/usr/bin/editor", "/usr/bin/vim", "/bin/vi" ]:
658 if os
.path
.exists(p
):
662 bail_out("No editor available (please set EDITOR).\n")
664 if 0 == os
.system("bash -c '%s +8 %s'" % (editor
, werkid
)):
666 werk
= g_werks
[werkid
]
667 git_add(g_werks
[werkid
])
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.")
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
))
683 bail_out("Cannot commit.")
688 bail_out("Please specify at least one commit ID to cherry-pick.")
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"):
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
))
717 os
.system("git commit -C '%s'" % commit_id
)
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")
727 return eval(file('.my_ids', 'r').read())
732 def invalidate_my_werkid(id):
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
):
747 sys
.stdout
.write('You have %d reserved IDs.\n' % (len(get_werk_ids())))
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
759 first_free
= int(eval(file('first_free').read()))
761 # enterprise werks were between 8000 and 8749. Skip over this area for new
763 if first_free
>= 8000 and first_free
< 8780:
766 # cmk-omd werk were between 7500 and 7680. Skip over this area for new
768 if first_free
>= 7500 and first_free
< 7680:
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' %
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")
787 bail_out("Cannot commit.")
791 # _ __ ___ __ _(_)_ __
792 # | '_ ` _ \ / _` | | '_ \
793 # | | | | | | (_| | | | | |
794 # |_| |_| |_|\__,_|_|_| |_|
800 edition_components
= {}
810 g_current_version
= load_current_version()
812 if len(sys
.argv
) < 2:
817 "list" : lambda args
: main_list(args
, "console"),
818 "export" : lambda args
: main_list(args
, "csv"),
821 "blame" : main_blame
,
822 "delete" : main_delete
,
825 "ids" : main_fetch_ids
,
827 "cherry-pick" : main_pick
,
832 for name
, func
in commands
.items():
834 hits
= [ (name
, func
) ]
836 elif name
.startswith(cmd
):
837 hits
.append((name
, func
))
843 sys
.stderr
.write("Command '%s' is ambigous. Possible are: %s\n" % \
844 (cmd
, ", ".join([ n
for (n
,f
) in hits
])))
847 hits
[0][1](sys
.argv
[2:])