summaryrefslogtreecommitdiff
path: root/src/xpit_/create.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/xpit_/create.py')
-rw-r--r--src/xpit_/create.py263
1 files changed, 263 insertions, 0 deletions
diff --git a/src/xpit_/create.py b/src/xpit_/create.py
new file mode 100644
index 0000000..30d32fa
--- /dev/null
+++ b/src/xpit_/create.py
@@ -0,0 +1,263 @@
+import os
+import subprocess
+import getpass
+from sys import argv, stdout, stderr
+from datetime import datetime, timezone
+from . import args_helpers, identity, issue, issue_tracker
+
+help_text = """\
+Usage: xpit create [options]
+Examples:
+ xpit create
+ xpit create -u Bob -i secret_bob_alias -r 20260101_120000:20260101_140000 -x
+ xpit create -e_ -t "Startup slow" -l performance,startup -d "The window takes 200ms to open."
+Options:
+ -d, --description <str>
+ The post body.
+ Defaults to 'Description goes here...'
+ -x, --disable-inline-annotations
+ Disable the explanatory ; annotations in newly created posts' headers.
+ -e, --editor <str>
+ Which editor to open the issue in.
+ Pass '_' (underscore) to not open an editor.
+ If not specified, the editor is chosen in the following order:
+ 1. $XPIT_EDITOR
+ 2. $GIT_EDITOR
+ 3. $EDITOR
+ 4. $VISUAL
+ 5.
+ windows: "notepad"
+ other: "vi"
+ -i, --identity <str>
+ Label for keypair used for unique identification and signature.
+ This is used to uniquely identify a user.
+ Keys are stored in '~/.config/xpit/identities/'.
+ Defaults to 'default'.
+ -l, --labels <str>
+ Comma-separated list of labels applicable to this issue.
+ Not compatible with --reply-to.
+ Empty by default.
+ -r, --reply-to <ISSUE_ID>[:LOCAL_ID]
+ Reply to an issue or a specific reply within an issue.
+ -P, --skip-prompt
+ Skip interactive prompts for missing fields (title, description, labels).
+ Only applies when using `-e_` (no automatic editor).
+ -t, --title <str>
+ The issue's title.
+ Not compatible with --reply-to.
+ Defaults to 'Title'.
+ -u, --username <str>
+ The username to use for the issue.
+ If not specified, uses `getpass.getuser()`.
+ -h, --help
+ Print this help text.
+"""
+
+class Args:
+ def __init__(self):
+ self.config_dir: str
+ self.editor: str
+ self.username: str
+ self.identity: str = "default"
+ self.reply_to: str | None = None
+ self.disable_inline_annotations: bool = False
+
+ self.labels: bytes = b""
+ self.skip_prompt: bool = False
+ self.description: bytes = b"Description goes here..."
+ self.title: bytes = b"Title"
+
+ def parse(self) -> bool:
+ flag_value_map = {
+ "help|h": 0,
+ "description|d": 1,
+ "disable-inline-annotations|x": 0,
+ "editor|e": 1,
+ "identity|i": 1,
+ "labels|l": 1,
+ "reply|r": 1,
+ "skip-prompt|P": 0,
+ "title|t|": 1,
+ "username|u": 1,
+ }
+ res = args_helpers.parse_generic(flag_value_map, argv, 2)
+ _editor: str | None = None
+ _username: str | None = getpass.getuser()
+ if res is None:
+ return False
+ if "help" in res:
+ stdout.write(help_text)
+ return False
+ if "description" in res:
+ self.description = res["description"][0].encode("utf-8")
+ if "disable-inline-annotations" in res:
+ self.disable_inline_annotations = True
+ if "editor" in res:
+ _editor = res["editor"][0]
+ if "identity" in res:
+ self.identity = res["identity"][0]
+ if "labels" in res:
+ labels: str = res["labels"][0]
+ issue_tracker.load_config()
+ for label in labels.split(','):
+ if label.encode("utf-8") not in issue_tracker.config[b"allowed_labels"]:
+ stderr.write(
+ f"Label '{label}' not allowed.\nAllowed: "
+ +issue_tracker.config[b"allowed_labels"].decode("utf-8")
+ +'\n')
+ return False
+ self.labels = labels.encode("utf-8")
+ if "reply" in res:
+ self.reply_to = res["reply"][0]
+ if "skip-prompt" in res:
+ self.skip_prompt = True
+ if "title" in res:
+ self.title = res["title"][0].encode("utf-8")
+ if "username" in res:
+ _username = res["username"][0]
+
+ if _username is None:
+ stderr.write("No username.")
+ return False
+ self.username = _username
+
+ if _editor is None:
+ _editor = (
+ os.environ.get("XPIT_EDITOR")
+ or os.environ.get("GIT_EDITOR")
+ or os.environ.get("EDITOR")
+ or os.environ.get("VISUAL")
+ or None
+ )
+ if _editor is None:
+ if os.name == "nt":
+ _editor = "notepad"
+ else:
+ _editor = "vi"
+ self.editor = _editor
+
+ if os.name == "nt":
+ appdata = os.getenv("APPDATA")
+ if appdata is None:
+ stderr.write("Couldn't find $APPDATA directory")
+ return False
+ self.config_dir = os.path.join(appdata, "xpit")
+ else:
+ home = os.getenv("HOME")
+ if home is None:
+ stderr.write("Couldn't find $HOME directory")
+ return False
+ self.config_dir = os.path.join(home, ".config", "xpit")
+
+ return True
+args = Args()
+
+def id_gen() -> str:
+ return datetime.now(timezone.utc).strftime(issue_tracker.config[b"id_format"].decode("utf-8"))
+
+def main() -> int:
+ if len(argv) < 2:
+ stdout.write(help_text)
+ return 1
+
+ if not args.parse():
+ return 1
+
+ identities_dir = os.path.join(args.config_dir, "identities")
+ private_key, public_key = identity.load(identities_dir, args.identity)
+ _ = private_key
+
+ issue_tracker.load_config()
+ if issue_tracker.dir is None:
+ stderr.write("xpit not initialized. Run `xpit init`.\n")
+ return 1
+
+ if args.reply_to is None:
+ issue_id = id_gen()
+ else:
+ issue_id = args.reply_to.split(":", 1)[0]
+ issue_dir = os.path.join(issue_tracker.dir, issue_id)
+ if args.reply_to is None:
+ if os.path.exists(issue_dir):
+ stderr.write(f"Failed to create issue {issue_id}: Already exists.\n")
+ return 1
+ os.mkdir(issue_dir)
+
+ issue_tracker_path = os.path.join(issue_dir, "issue.md")
+ if args.reply_to is not None:
+ if not os.path.isfile(issue_tracker_path):
+ stderr.write(f"Issue {issue_id} not found.\n")
+ return 1
+ with open(issue_tracker_path, "r+b") as f:
+ src = f.read()
+ info, err = issue.parse(src)
+ if err is not None:
+ stderr.write(err.msg)
+ return 1
+ if info.meta[b"status"] != b"open":
+ stderr.write(f"Issue {issue_id} already closed.\n")
+ return 1
+
+ identity_id = identity.id_from_pubkey(public_key)
+ # help_author = b" ; your user-ID"
+ # help_status = b" ; open | closed"
+ allowed_labels = issue_tracker.config[b"allowed_labels"]
+ help_labels = b" ; comma-separated list. Allowed labels are: "+allowed_labels
+ # help_id = b" ; this reply's local ID"
+ # help_reply = b" ; local ID of the post you're replying to"
+ if args.disable_inline_annotations:
+ help_labels = b""
+ if args.editor == "_" and not args.skip_prompt:
+ if args.title is None:
+ args.title = input("Title: ").encode("utf-8")
+ if args.labels is None:
+ args.labels = input("Labels (Comma-separated): ").encode("utf-8")
+ if args.description is None:
+ args.description = input("Description: ").encode("utf-8")
+ with open(issue_tracker_path, "ab") as f:
+ ini_author = b"author=" + args.username.encode("utf-8") + b'#' + identity_id + b'\n'
+ if args.reply_to is None:
+ ini_status = b"status=open\n"
+ ini_labels = b"labels="+args.labels + help_labels + b'\n'
+ ini_add_after_edit = ini_author + ini_status
+ ini_add_now = ini_labels
+ if args.editor == "_":
+ ini_add_now = ini_add_after_edit + ini_add_now
+ msg = b"```ini\n"+ini_add_now+b"""```
+
+# """+args.title+b"""
+
+"""+args.description+b"""
+
+"""
+ else:
+ ini_id = b"id="+id_gen().encode("utf-8")+b'\n'
+ reply_to = args.reply_to.split(":", 2)
+ ini_reply = b""
+ if len(reply_to) == 2:
+ ini_reply = b"reply="+reply_to[1].encode("utf-8")+b'\n'
+ ini_add_after_edit = ini_author + ini_id
+ ini_add_now = ini_reply
+ if args.editor == "_":
+ ini_add_now = ini_add_after_edit + ini_add_now
+ msg = b"---\n```ini\n"+ini_add_now+b"```\n\n"+args.description+b"\n\n"
+ f.write(msg)
+
+ if args.editor != "_":
+ subprocess.call([args.editor, issue_tracker_path])
+ with open(issue_tracker_path, "r+b") as f:
+ src = f.read()
+ src = issue.clean_after_edit(src, ini_add_after_edit)
+ f.seek(0)
+ f.write(src)
+ f.truncate()
+ # TODO check if labels ok. Maybe we want to have some sort of `check` command
+
+ relpath = os.path.relpath(issue_tracker_path, os.getcwd())
+ if args.reply_to is None:
+ stdout.write(f"Created new issue ./{relpath}\n")
+ else:
+ stdout.write(f"Replied to {args.reply_to}\n")
+
+ return 0
+