(Steve Dougherty)
2013-07-02: Add partial pull request support; other things. Add partial pull request support; other things. Exit when unable to resolve WoT URI. (Possibly would be better to use ui.abort()) Add listing of WoT dependencies and pull request syntax to readme.
diff --git a/Readme.txt b/Readme.txt --- a/Readme.txt +++ b/Readme.txt @@ -14,3 +14,15 @@ and many other features. And it works transparently: To publish, just clone to freenet: hg clone REPO freenet://USK@/REPO + +It supports WoT too: + + hg clone REPO freenet://wot_nick@public_key_hash/repo + + - Push / pull by name + - Pull requests + +Dependencies for full WoT support: + - PyYAML http://pyyaml.org/wiki/PyYAMLDocumentation + - lib-pyFreenet https://github.com/freenet/lib-pyFreenet-official + - DefusedXML https://pypi.python.org/pypi/defusedxml/ \ No newline at end of file diff --git a/infocalypse/__init__.py b/infocalypse/__init__.py --- a/infocalypse/__init__.py +++ b/infocalypse/__init__.py @@ -390,9 +390,11 @@ cmdtable = { "[options]"), "fn-pull-request": (infocalypse_pull_request, - WOT_OPTS + - FCP_OPTS, - "--wot id@key/repo"), + [('', 'wot', '', 'WoT nick@key/repo to send request ' + 'to')] + + WOT_OPTS + + FCP_OPTS, + "[--truster nick@key] --wot nick@key/repo"), "fn-push": (infocalypse_push, [('', 'uri', '', 'insert URI to push to'), @@ -693,9 +695,16 @@ def freenetclone(orig, *args, **opts): pulluri, pushuri = None, None if isfreenetpath(source): pulluri = freenetpathtouri(ui, source) + + if not pulluri: + raise util.Abort() + if isfreenetpath(dest): pushuri = freenetpathtouri(ui, dest, pull=False) + if not pushuri: + return + # decide which infocalypse command to use. if pulluri and pushuri: action = "copy" diff --git a/infocalypse/commands.py b/infocalypse/commands.py --- a/infocalypse/commands.py +++ b/infocalypse/commands.py @@ -107,9 +107,8 @@ def infocalypse_create(ui_, repo, **opts params['INSERT_URI'] = insert_uri inserted_to = execute_create(ui_, repo, params, stored_cfg) - # TODO: Move into some function. How to separate local success context? - if inserted_to is not None and attributes is not None and \ - stored_cfg.has_wot_identity(stored_cfg.get_request_uri(repo.root)): + if inserted_to and opts['wot']: + # TODO: Imports don't go out of scope, right? import wot wot.update_repo_listing(ui_, attributes['Identity']) @@ -213,10 +212,15 @@ def infocalypse_pull(ui_, repo, **opts): def infocalypse_pull_request(ui, repo, **opts): + import wot if not opts['wot']: - ui.warning("Who do you want to send the pull request to? Set --wot.") + ui.warn("Who do you want to send the pull request to? Set --wot.\n") return + wot_id, repo_name = opts['wot'].split('/', 1) + wot.send_pull_request(ui, repo, get_truster(ui, repo, opts), wot_id, + repo_name) + def infocalypse_push(ui_, repo, **opts): """ Push to an Infocalypse repository in Freenet. """ diff --git a/infocalypse/wot.py b/infocalypse/wot.py --- a/infocalypse/wot.py +++ b/infocalypse/wot.py @@ -7,38 +7,123 @@ import smtplib from base64 import b32encode from fcp.node import base64decode from keys import USK +import yaml +from email.mime.text import MIMEText +import imaplib +FREEMAIL_SMTP_PORT = 4025 +FREEMAIL_IMAP_PORT = 4143 +PULL_REQUEST_PREFIX = "[vcs] " -def send_pull_request(ui, from_identity, to_identity): - local_identity = resolve_local_identity(ui, from_identity) - target_identity = resolve_identity(ui, from_identity, to_identity) - if local_identity is None or target_identity is None: - # Error. +def send_pull_request(ui, repo, from_identifier, to_identifier, to_repo_name): + local_identity = resolve_local_identity(ui, from_identifier) + if local_identity is None: + return + + target_identity = resolve_identity(ui, local_identity['Identity'], + to_identifier) + if target_identity is None: return from_address = to_freemail_address(local_identity) - to_address = to_freemail_address(to_identity) + to_address = to_freemail_address(target_identity) if from_address is None or to_address is None: if from_address is None: - ui.warn("{0} is not using Freemail.\n".format(from_identity[ - 'Nickname'])) + ui.warn("{0}@{1} is not using Freemail.\n".format(local_identity[ + 'Nickname'], local_identity['Identity'])) if to_address is None: - ui.warn("{0} is not using Freemail.\n".format(to_identity[ - 'Nickname'])) + ui.warn("{0}@{1} is not using Freemail.\n".format(target_identity[ + 'Nickname'], target_identity['Identity'])) return - # TODO: Use FCP host; default port. - smtp = smtplib.SMTP() - # TODO: Where to configure Freemail password? - smtp.login(from_address, ) - smtp.sendmail() + # Check that a password is set. + cfg = Config.from_ui(ui) + password = cfg.get_freemail_password(local_identity['Identity']) + if password is None: + ui.warn("{0} does not have a Freemail password set.\n" + "Run hg fn-setupfreemail --truster {0}@{1}\n" + .format(local_identity['Nickname'], local_identity['Identity'])) + return + + to_repo = find_repo(ui, local_identity['Identity'], to_identifier, + to_repo_name) + + # TODO: Frequently doing {0}@{1} ... would a WoTIdentity class make sense? + if to_repo is None: + ui.warn("{0}@{1} has not published a repository named '{2}'.\n" + .format(target_identity['Nickname'], + target_identity['Identifier'], + to_repo_name)) + return + + repo_context = repo['tip'] + # TODO: Will there always be a request URI set in the config? What about + # a path? + from_uri = cfg.get_request_uri(repo.root) + from_branch = repo_context.branch() + + # Use double-quoted scalars so that Unicode can be included. (Nicknames.) + footer = yaml.dump({'request': 'pull', + 'vcs': 'Infocalypse', + 'source': from_uri + '#' + from_branch, + 'target': to_repo}, default_style='"', + explicit_start=True, explicit_end=True, + allow_unicode=True) + + # TODO: Break config sanity check and sending apart so that further + # things can check config, prompt for whatever, then send. + + source_text = ui.edit(""" + +HG: Enter pull request message here. Lines beginning with 'HG:' are removed. +HG: The first line has "{0}" added before it and is the email subject. +HG: The second line should be blank. +HG: Following lines are the body of the message. +HG: Below is the machine-readable footer describing the request. Modifying it +HG: or putting things below it has the potential to cause problems. + +{1} +""".format(PULL_REQUEST_PREFIX, footer), from_identifier) + # TODO: Abort in the case of a blank message? + # Markdown support would be on receiving end. Maybe CLI preview eventually. + # (Would that even work?) + # TODO: Save message and load later in case sending fails. + + # TODO: What if the editor uses different line endings? How to slice + # by-line? + source_lines = source_text.split('\n') + + # Body is third line and after. + msg = MIMEText('\n'.join(source_lines[2:])) + msg['Subject'] = PULL_REQUEST_PREFIX + source_lines[0] + msg['To'] = to_address + msg['From'] = from_address + + smtp = smtplib.SMTP(cfg.defaults['HOST'], FREEMAIL_SMTP_PORT) + smtp.login(from_address, password) + # TODO: Catch exceptions and give nice error messages. + smtp.sendmail(from_address, to_address, msg.as_string()) + + +def receive_pull_requests(ui): + # TODO: Terminology - send/receive different from resolve/read elsewhere. + # TODO: How to find YAML? Look from end backwards for "---\n" then forward + # from there for "...\n"? + # TODO: How to match local repo with the "target" URI? Based on repo list. + + cfg = Config.from_ui(ui) + imap = imaplib.IMAP4(cfg.defaults['HOST'], FREEMAIL_IMAP_PORT) + + type, message_numbers = imap.search(None, "SUBJECT", PULL_REQUEST_PREFIX) + print(type, message_numbers) def update_repo_listing(ui, for_identity): # TODO: WoT property containing edition. Used when requesting. config = Config.from_ui(ui) + # Version number to support possible format changes. root = ET.Element('vcs', {'version': '0'}) # Add request URIs associated with the given identity.