|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +import ciq_tag |
| 4 | +import click |
| 5 | +import os |
| 6 | +from enum import Enum |
| 7 | +import sys |
| 8 | +import logging |
| 9 | + |
| 10 | +DEFAULT_LOGLEVEL = "INFO" |
| 11 | + |
| 12 | +LOGLEVEL = os.environ.get("LOGS", DEFAULT_LOGLEVEL).upper() |
| 13 | +logger = logging.getLogger(__name__) |
| 14 | +logger.propagate = False |
| 15 | +logger.setLevel(LOGLEVEL) |
| 16 | +log_handler = logging.StreamHandler() |
| 17 | +log_handler.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(funcName)s: %(message)s")) |
| 18 | +logger.addHandler(log_handler) |
| 19 | + |
| 20 | + |
| 21 | +CIQ_TAGS_LIST = ", ".join(c.arg_name for c in ciq_tag.CiqTag) |
| 22 | + |
| 23 | + |
| 24 | +class CmdException(Exception): |
| 25 | + def __init__(self, exit_code, *rest): |
| 26 | + super().__init__(*rest) |
| 27 | + self._exit_code = exit_code |
| 28 | + |
| 29 | + |
| 30 | +def open_input(filename, **rest): |
| 31 | + return sys.stdin if filename == "-" else open(filename, "r", **rest) |
| 32 | + |
| 33 | + |
| 34 | +def open_output(filename, **rest): |
| 35 | + return sys.stdout if filename == "-" else open(filename, "w", **rest) |
| 36 | + |
| 37 | + |
| 38 | +def process_in_out(input, output, result_to_output_map, ciq_msg_method, *method_args_pos, **method_args_key): |
| 39 | + with open_input(input) as in_file: |
| 40 | + input_str = "".join(in_file.readlines()) |
| 41 | + with open_output(output) as out_file: |
| 42 | + msg = ciq_tag.CiqMsg(input_str) |
| 43 | + ret, out = result_to_output_map(msg, ciq_msg_method(msg, *method_args_pos, **method_args_key)) |
| 44 | + if out: |
| 45 | + print(out, file=out_file, end="") |
| 46 | + if ret != 0: |
| 47 | + raise CmdException(ret) |
| 48 | + |
| 49 | + |
| 50 | +def parse_tag(tag_name): |
| 51 | + tag = ciq_tag.CiqTag.get_by_arg_name(tag_name) |
| 52 | + if tag: |
| 53 | + return tag |
| 54 | + else: |
| 55 | + raise CmdException(1, f"Wrong TAG value. Must be one of: {CIQ_TAGS_LIST}") |
| 56 | + |
| 57 | + |
| 58 | +def read_value(value_arg, val_from_file_arg, trim_arg): |
| 59 | + if val_from_file_arg: |
| 60 | + with open_input(value_arg) as inFile: |
| 61 | + value = "".join(inFile.readlines()) |
| 62 | + else: |
| 63 | + value = value_arg |
| 64 | + return value.strip() if trim_arg else value |
| 65 | + |
| 66 | + |
| 67 | +def getter_map(msg, result): |
| 68 | + return (0, result + "\n") if result else (1, "") |
| 69 | + |
| 70 | + |
| 71 | +def setter_map(msg, modified): |
| 72 | + out = msg.get_message() |
| 73 | + return (0, out) if modified else (1, out) |
| 74 | + |
| 75 | + |
| 76 | +def args(*positional, **keyword): |
| 77 | + return (positional, keyword) |
| 78 | + |
| 79 | + |
| 80 | +class ClickDef(Enum): |
| 81 | + TAG = args("tag", type=str) |
| 82 | + |
| 83 | + VALUE = args("value", type=str) |
| 84 | + |
| 85 | + INDEX = args("index", type=int, required=False, default=0) |
| 86 | + |
| 87 | + VAL_FROM_FILE = args( |
| 88 | + "--val-from-file", |
| 89 | + "-f", |
| 90 | + flag_value=True, |
| 91 | + help=""" |
| 92 | +Treat the VALUE argument as a path to a file from which an actual value will be read (useful for |
| 93 | +multi-line formatted texts) |
| 94 | +""", |
| 95 | + ) |
| 96 | + |
| 97 | + TRIM = args( |
| 98 | + "--trim", |
| 99 | + "-t", |
| 100 | + flag_value=(not ciq_tag.DEFAULT_TRIM), |
| 101 | + help=""" |
| 102 | +Trim the value from whitespaces at the beginning and end before inserting to a commit message as a |
| 103 | +tag value. Useful when reading the tag value from a file, which can have trailing newlines |
| 104 | +""", |
| 105 | + ) |
| 106 | + |
| 107 | + INDENT = args( |
| 108 | + "--indent", |
| 109 | + "-t", |
| 110 | + type=int, |
| 111 | + default=ciq_tag.DEFAULT_INDENT, |
| 112 | + help=""" |
| 113 | +When inserting multi-line values indent them by this many spaces. Special value -1 means value |
| 114 | +indenting equal to the width of the tag keyword. |
| 115 | +""", |
| 116 | + ) |
| 117 | + |
| 118 | + DEDENT = args("--dedent", "-T", flag_value=True, help="For the multi-line value remove the indent, if it has any.") |
| 119 | + |
| 120 | + WRAP = args("--wrap", "-w", flag_value=(not ciq_tag.DEFAULT_INDENT), help="Enable value wrapping") |
| 121 | + |
| 122 | + UNWRAP = args("--unwrap", "-W", flag_value=True, help="Unwrap multi-line values to a single line. Implies DEDENT.") |
| 123 | + |
| 124 | + WRAP_WIDTH = args( |
| 125 | + "--wrap-width", |
| 126 | + "-c", |
| 127 | + type=int, |
| 128 | + default=ciq_tag.DEFAULT_WRAP_WIDTH, |
| 129 | + help="If WRAP flag is given wrap the value text to this many columns.", |
| 130 | + ) |
| 131 | + |
| 132 | + def __init__(self, positional, keyword): |
| 133 | + self.positional = positional |
| 134 | + self.keyword = keyword |
| 135 | + |
| 136 | + |
| 137 | +OPTIONS = {} |
| 138 | + |
| 139 | + |
| 140 | +@click.group(context_settings=dict(help_option_names=["-h", "--help"])) |
| 141 | +@click.option( |
| 142 | + "--input", "-i", type=click.Path(), default="-", show_default=True, help="File path to read, or '-' for stdin" |
| 143 | +) |
| 144 | +@click.option( |
| 145 | + "--output", "-o", type=click.Path(), default="-", show_default=True, help="File path to write, or '-' for stdout" |
| 146 | +) |
| 147 | +def cli(input, output): |
| 148 | + OPTIONS["input"] = input |
| 149 | + OPTIONS["output"] = output |
| 150 | + |
| 151 | + |
| 152 | +@cli.command( |
| 153 | + "get", |
| 154 | + help=f""" |
| 155 | +Print to the output (--output) the value of the INDEXth TAG in the commit message given on |
| 156 | +the input (--input). If INDEX is not given assume it's 0, which is the first occurence of |
| 157 | +the TAG. Exit with nonzero if TAG not found. TAG can be one of: {CIQ_TAGS_LIST} |
| 158 | +""", |
| 159 | +) |
| 160 | +@click.argument(*ClickDef.TAG.positional, **ClickDef.TAG.keyword) |
| 161 | +@click.argument(*ClickDef.INDEX.positional, **ClickDef.INDEX.keyword) |
| 162 | +@click.option(*ClickDef.UNWRAP.positional, **ClickDef.UNWRAP.keyword) |
| 163 | +@click.option(*ClickDef.DEDENT.positional, **ClickDef.DEDENT.keyword) |
| 164 | +def command_get(tag, index, unwrap, dedent): |
| 165 | + process_in_out( |
| 166 | + OPTIONS["input"], |
| 167 | + OPTIONS["output"], |
| 168 | + getter_map, |
| 169 | + ciq_tag.CiqMsg.get_tag_value, |
| 170 | + parse_tag(tag), |
| 171 | + index, |
| 172 | + unwrap=unwrap, |
| 173 | + dedent=dedent, |
| 174 | + ) |
| 175 | + |
| 176 | + |
| 177 | +@cli.command( |
| 178 | + "modify", |
| 179 | + help=""" |
| 180 | +Set the value of TAG, in its current place, using the current keyword. Return nonzero if the TAG |
| 181 | +wasn't defined already. |
| 182 | +""", |
| 183 | +) |
| 184 | +@click.argument(*ClickDef.TAG.positional, **ClickDef.TAG.keyword) |
| 185 | +@click.argument(*ClickDef.VALUE.positional, **ClickDef.VALUE.keyword) |
| 186 | +@click.argument(*ClickDef.INDEX.positional, **ClickDef.INDEX.keyword) |
| 187 | +@click.option(*ClickDef.VAL_FROM_FILE.positional, **ClickDef.VAL_FROM_FILE.keyword) |
| 188 | +@click.option(*ClickDef.TRIM.positional, **ClickDef.TRIM.keyword) |
| 189 | +@click.option(*ClickDef.INDENT.positional, **ClickDef.INDENT.keyword) |
| 190 | +@click.option(*ClickDef.WRAP.positional, **ClickDef.WRAP.keyword) |
| 191 | +@click.option(*ClickDef.WRAP_WIDTH.positional, **ClickDef.WRAP_WIDTH.keyword) |
| 192 | +def command_modify(tag, value, index, val_from_file, trim, indent, wrap, wrap_width): |
| 193 | + process_in_out( |
| 194 | + OPTIONS["input"], |
| 195 | + OPTIONS["output"], |
| 196 | + setter_map, |
| 197 | + ciq_tag.CiqMsg.modify_tag_value, |
| 198 | + parse_tag(tag), |
| 199 | + read_value(value, val_from_file, trim), |
| 200 | + index, |
| 201 | + trim=trim, |
| 202 | + indent=indent, |
| 203 | + wrap=wrap, |
| 204 | + wrap_width=wrap_width, |
| 205 | + ) |
| 206 | + |
| 207 | + |
| 208 | +@cli.command( |
| 209 | + "add", |
| 210 | + help=""" |
| 211 | +Add a TAG with VALUE to the commit message. Attempt to locate the proper place to insert the tag then do it |
| 212 | +using the default keyword and value formatting defined by the options. |
| 213 | +""", |
| 214 | +) |
| 215 | +@click.argument(*ClickDef.TAG.positional, **ClickDef.TAG.keyword) |
| 216 | +@click.argument(*ClickDef.VALUE.positional, **ClickDef.VALUE.keyword) |
| 217 | +@click.option(*ClickDef.VAL_FROM_FILE.positional, **ClickDef.VAL_FROM_FILE.keyword) |
| 218 | +@click.option(*ClickDef.TRIM.positional, **ClickDef.TRIM.keyword) |
| 219 | +@click.option(*ClickDef.INDENT.positional, **ClickDef.INDENT.keyword) |
| 220 | +@click.option(*ClickDef.WRAP.positional, **ClickDef.WRAP.keyword) |
| 221 | +@click.option(*ClickDef.WRAP_WIDTH.positional, **ClickDef.WRAP_WIDTH.keyword) |
| 222 | +def command_add(tag, value, val_from_file, trim, indent, wrap, wrap_width): |
| 223 | + process_in_out( |
| 224 | + OPTIONS["input"], |
| 225 | + OPTIONS["output"], |
| 226 | + setter_map, |
| 227 | + ciq_tag.CiqMsg.add_tag, |
| 228 | + parse_tag(tag), |
| 229 | + read_value(value, val_from_file, trim), |
| 230 | + trim=trim, |
| 231 | + indent=indent, |
| 232 | + wrap=wrap, |
| 233 | + wrap_width=wrap_width, |
| 234 | + ) |
| 235 | + |
| 236 | + |
| 237 | +@cli.command( |
| 238 | + "set", |
| 239 | + help=""" |
| 240 | +Attempt to set TAG to the VALUE in place as it would be done with the 'modify' action, using INDEX |
| 241 | +(default 0). If that fails insert it as with the 'add' action. |
| 242 | +""", |
| 243 | +) |
| 244 | +@click.argument(*ClickDef.TAG.positional, **ClickDef.TAG.keyword) |
| 245 | +@click.argument(*ClickDef.VALUE.positional, **ClickDef.VALUE.keyword) |
| 246 | +@click.argument(*ClickDef.INDEX.positional, **ClickDef.INDEX.keyword) |
| 247 | +@click.option(*ClickDef.VAL_FROM_FILE.positional, **ClickDef.VAL_FROM_FILE.keyword) |
| 248 | +@click.option(*ClickDef.TRIM.positional, **ClickDef.TRIM.keyword) |
| 249 | +@click.option(*ClickDef.INDENT.positional, **ClickDef.INDENT.keyword) |
| 250 | +@click.option(*ClickDef.WRAP.positional, **ClickDef.WRAP.keyword) |
| 251 | +@click.option(*ClickDef.WRAP_WIDTH.positional, **ClickDef.WRAP_WIDTH.keyword) |
| 252 | +def command_set(tag, value, index, val_from_file, trim, indent, wrap, wrap_width): |
| 253 | + process_in_out( |
| 254 | + OPTIONS["input"], |
| 255 | + OPTIONS["output"], |
| 256 | + setter_map, |
| 257 | + ciq_tag.CiqMsg.set_tag, |
| 258 | + parse_tag(tag), |
| 259 | + read_value(value, val_from_file, trim), |
| 260 | + index, |
| 261 | + trim=trim, |
| 262 | + indent=indent, |
| 263 | + wrap=wrap, |
| 264 | + wrap_width=wrap_width, |
| 265 | + ) |
| 266 | + |
| 267 | + |
| 268 | +@cli.command( |
| 269 | + "delete", |
| 270 | + help=""" |
| 271 | +Delete a tag from the commit message. Attempt to keep the message formatted nicely. |
| 272 | +""", |
| 273 | +) |
| 274 | +@click.argument(*ClickDef.TAG.positional, **ClickDef.TAG.keyword) |
| 275 | +@click.argument(*ClickDef.INDEX.positional, **ClickDef.INDEX.keyword) |
| 276 | +def command_delete(tag, index): |
| 277 | + process_in_out( |
| 278 | + OPTIONS["input"], |
| 279 | + OPTIONS["output"], |
| 280 | + setter_map, |
| 281 | + ciq_tag.CiqMsg.delete_tag, |
| 282 | + ciq_tag.CiqTag.get_by_arg_name(tag), |
| 283 | + index, |
| 284 | + ) |
| 285 | + |
| 286 | + |
| 287 | +def main(): |
| 288 | + try: |
| 289 | + cli() |
| 290 | + return 0 |
| 291 | + except CmdException as exc: |
| 292 | + logger.error(str(exc)) |
| 293 | + return exc._exit_code |
| 294 | + |
| 295 | + |
| 296 | +if __name__ == "__main__": |
| 297 | + exit(main()) |
0 commit comments