infocalypse
 
(Arne Babenhauserheide)
2013-08-16: merge bug report

merge bug report

diff --git a/.bugs/bugs b/.bugs/bugs
--- a/.bugs/bugs
+++ b/.bugs/bugs
@@ -1,9 +1,18 @@
 minimize dependencies                                        | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:0fc25f7b84f3e1fb89e9134a28eeabbe76bf054f, time:1372231529.45
+Problems using clone to create repositories.~                | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:210c0e45adba5d500c6c5c00d750bfe43824cd3a, time:1375737210.02
 Add command to reinsert repo list.                           | owner:, open:False, id:2dcc27c850209062080906368530b4b9202271d0, time:1373407233.84
 pull fails, because config.get_wot_identity requests self.defaults['DEFAULT_TRUSTER'] which is not in defaults. | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:31beb672d404944a4655a546b21c95c7baa91002, time:1371735138.39
 cloning from a freenet:// uri does not work right now.       | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:35584408d800bb895d52dcf86cfba12c5b3237dd, time:1373100583.27
+Perform sanity check for multiple repos of the same name.    | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:4160157833020b1a33d2e6b05dc12c848c891d03, time:1375733375.04
+Preserve non-Infocalypse repositories in the repository list. | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:4d63383a07faa29083fb0b61846ab6cc3d3151be, time:1375733323.42
 set the timezone to UTC on cloning a freenet repo to avoid timezone-based attacks. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:4dfc4cc28a7fa69f040776a7138da78ee89ec819, time:1355764180.36
 mime-type problems                                           | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:5916e6e8328e20d8b0276b76b7116dd432730778, time:1353463866.97
                                                              | owner:, open:False, id:65d8d544370f80538e325dae0b6c2da449c5bcfe, time:1373407147.03
-simpler-wot-uris: name/repo                                  | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:d4f2df3ca2c441e4be389be846634f5a4a08906e, time:1372232568.9
-add support for hg incoming freenet://…                    | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:dbff86d4bba0e38adb81a6e27e7180282222f541, time:1375002083.32
+Add --truster support to built-in commands.                  | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:673a3103a58988552e0ef0b0ab20e8c75c9f6b1f, time:1375736861.8
+add support for hg incoming freenet://…                      | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:dbff86d4bba0e38adb81a6e27e7180282222f541, time:1375002083.32
+Add i18n support for messages.                               | owner:, open:True, id:7760991aef41c6d38be5315f742f6a6f350a0a76, time:1375010635.52
+Fix implied clone destination when cloning from freenet:.    | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:9bd3ef617ba8871d28fbdae2500542e93302c791, time:1375736289.27
+Unit tests involving the node are hard.                      | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:b01a53e59a2096254ecacdcee7673df5323d786e, time:1375737309.25
+Consider reworking repo list XML for conciseness.            | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:b511106032c58ffe7b33d0eb65bac7ec5555575e, time:1375734587.66
+simpler-wot-uris: name/repo                                  | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:d4f2df3ca2c441e4be389be846634f5a4a08906e, time:1372232568.9
+Add a warning that comments and unrecognized entries in ~/.infocalypse are removed. | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:e9236fdd23d44bfbf8565bb1a59317289f324fd6, time:1375735601.35
diff --git a/.bugs/details/210c0e45adba5d500c6c5c00d750bfe43824cd3a.txt b/.bugs/details/210c0e45adba5d500c6c5c00d750bfe43824cd3a.txt
new file mode 100644
--- /dev/null
+++ b/.bugs/details/210c0e45adba5d500c6c5c00d750bfe43824cd3a.txt
@@ -0,0 +1,27 @@
+# Lines starting with '#' and sections without content
+# are not displayed by a call to 'details'
+#
+[paths]
+# Paths related to this bug.
+# suggested format: REPO_PATH:LINENUMBERS
+
+
+[details]
+When using clone . freenet:nickname/reponame to create a repository:
+    - The repository list is not updated. (No request URI in ~/.infocalypse)
+    - "hg push" doesn't work as the default path cannot be reversed to an insert URI and default-push is not set. Set default-push.
+
+[expected]
+# The expected result
+
+
+[actual]
+# What happened instead
+
+
+[reproduce]
+# Reproduction steps
+
+
+[comments]
+# Comments and updates - leave your name
diff --git a/.bugs/details/4160157833020b1a33d2e6b05dc12c848c891d03.txt b/.bugs/details/4160157833020b1a33d2e6b05dc12c848c891d03.txt
new file mode 100644
--- /dev/null
+++ b/.bugs/details/4160157833020b1a33d2e6b05dc12c848c891d03.txt
@@ -0,0 +1,31 @@
+# Lines starting with '#' and sections without content
+# are not displayed by a call to 'details'
+#
+[paths]
+# Paths related to this bug.
+# suggested format: REPO_PATH:LINENUMBERS
+
+
+[details]
+Having multiple directories associated with the same request URI means a pull
+request cannot reasonably give a directory to pull to. (Or should it give all
+matches?)
+
+Having multiple repositories with the same name (in the request URI)
+makes pull requests not work (because it uses only name), and makes it more
+confusing to refer to repositories.
+
+[expected]
+# The expected result
+
+
+[actual]
+# What happened instead
+
+
+[reproduce]
+# Reproduction steps
+
+
+[comments]
+# Comments and updates - leave your name
diff --git a/.bugs/details/673a3103a58988552e0ef0b0ab20e8c75c9f6b1f.txt b/.bugs/details/673a3103a58988552e0ef0b0ab20e8c75c9f6b1f.txt
new file mode 100644
--- /dev/null
+++ b/.bugs/details/673a3103a58988552e0ef0b0ab20e8c75c9f6b1f.txt
@@ -0,0 +1,27 @@
+# Lines starting with '#' and sections without content
+# are not displayed by a call to 'details'
+#
+[paths]
+# Paths related to this bug.
+# suggested format: REPO_PATH:LINENUMBERS
+
+
+[details]
+Currently built-in commands can only use the default truster or the one
+associated with that repository. This is likely sufficient, so this feature
+is low priority.
+
+[expected]
+# The expected result
+
+
+[actual]
+# What happened instead
+
+
+[reproduce]
+# Reproduction steps
+
+
+[comments]
+# Comments and updates - leave your name
diff --git a/.bugs/details/7760991aef41c6d38be5315f742f6a6f350a0a76.txt b/.bugs/details/7760991aef41c6d38be5315f742f6a6f350a0a76.txt
new file mode 100644
--- /dev/null
+++ b/.bugs/details/7760991aef41c6d38be5315f742f6a6f350a0a76.txt
@@ -0,0 +1,28 @@
+# Lines starting with '#' and sections without content
+# are not displayed by a call to 'details'
+#
+[paths]
+# Paths related to this bug.
+# suggested format: REPO_PATH:LINENUMBERS
+
+
+[details]
+# Additional details
+from mercurial.i18n import _
+raise util.Abort(_("destination " + dest + " already exists."))
+Does including a variable in the string allow translation still?
+
+[expected]
+# The expected result
+
+
+[actual]
+# What happened instead
+
+
+[reproduce]
+# Reproduction steps
+
+
+[comments]
+# Comments and updates - leave your name
diff --git a/.bugs/details/9bd3ef617ba8871d28fbdae2500542e93302c791.txt b/.bugs/details/9bd3ef617ba8871d28fbdae2500542e93302c791.txt
new file mode 100644
--- /dev/null
+++ b/.bugs/details/9bd3ef617ba8871d28fbdae2500542e93302c791.txt
@@ -0,0 +1,27 @@
+# Lines starting with '#' and sections without content
+# are not displayed by a call to 'details'
+#
+[paths]
+# Paths related to this bug.
+# suggested format: REPO_PATH:LINENUMBERS
+
+
+[details]
+The implied destination of "hg clone freenet:operhiem1/pyProbe" is "freenet:operhiem1",
+which causes the URI parsing to fail because there's no repository name. In this
+case the destination should be the directory "pyProbe" instead.
+
+[expected]
+# The expected result
+
+
+[actual]
+# What happened instead
+
+
+[reproduce]
+# Reproduction steps
+
+
+[comments]
+# Comments and updates - leave your name
diff --git a/.bugs/details/b01a53e59a2096254ecacdcee7673df5323d786e.txt b/.bugs/details/b01a53e59a2096254ecacdcee7673df5323d786e.txt
new file mode 100644
--- /dev/null
+++ b/.bugs/details/b01a53e59a2096254ecacdcee7673df5323d786e.txt
@@ -0,0 +1,27 @@
+# Lines starting with '#' and sections without content
+# are not displayed by a call to 'details'
+#
+[paths]
+# Paths related to this bug.
+# suggested format: REPO_PATH:LINENUMBERS
+
+
+[details]
+Investigate a mocking framework or something for better frontend tests.
+Currently there's just a very limited amount of backend testing, and tests
+that involve a node over FCP are not feasible.
+
+[expected]
+# The expected result
+
+
+[actual]
+# What happened instead
+
+
+[reproduce]
+# Reproduction steps
+
+
+[comments]
+# Comments and updates - leave your name
diff --git a/.bugs/details/b511106032c58ffe7b33d0eb65bac7ec5555575e.txt b/.bugs/details/b511106032c58ffe7b33d0eb65bac7ec5555575e.txt
new file mode 100644
--- /dev/null
+++ b/.bugs/details/b511106032c58ffe7b33d0eb65bac7ec5555575e.txt
@@ -0,0 +1,27 @@
+# Lines starting with '#' and sections without content
+# are not displayed by a call to 'details'
+#
+[paths]
+# Paths related to this bug.
+# suggested format: REPO_PATH:LINENUMBERS
+
+
+[details]
+For instance, instead of a vcs field in each repository element, have all
+infocalypse repositories in a <infocalypse> element. Maybe also have them
+be <r> elements to be shorter.
+
+[expected]
+# The expected result
+
+
+[actual]
+# What happened instead
+
+
+[reproduce]
+# Reproduction steps
+
+
+[comments]
+# Comments and updates - leave your name
diff --git a/infocalypse/__init__.py b/infocalypse/__init__.py
--- a/infocalypse/__init__.py
+++ b/infocalypse/__init__.py
@@ -568,13 +568,24 @@ extensions.wrapfunction(discovery, 'find
 # wrap the commands
 
 
-def freenetpathtouri(ui, path, pull=True):
+def freenetpathtouri(ui, path, operation, repo=None):
     """
     Return a usable request or insert URI. Expects a freenet:// or freenet:
     protocol to be specified.
 
-    If the key is not a USK it will be resolved as a WoT identity. In this
-    case if the resolution fails, print an error message and return None.
+    If the key is not a USK it will be resolved as a WoT identity.
+
+
+    :param repo: Mercurial localrepository, used to resolve the truster set
+                 for the repository.
+    :param operation: A string name of the operation the URI will be used to
+                      perform. Used to return the appropriate result with
+                      WoT-integrated URI resolution. Valid operations are:
+                       * "pull" - request URI for existing repository.
+                       * "push" - insert URI for existing repository.
+                       * "clone-push" - insert URI for repository that might
+                                        not exist. (Skips looking up
+                                        published name and edition.)
     """
     # TODO: Is this the only URL encoding that may happen? Why not use a more
     # semantically meaningful function?
@@ -588,12 +599,16 @@ def freenetpathtouri(ui, path, pull=True
     # nick to be "USK", but this is a corner case. Using --wot will still work.
     if not path.startswith("USK"):
         import wot
-        if pull:
-            cfg = Config.from_ui(ui)
-            truster = cfg.defaults['DEFAULT_TRUSTER']
+        if operation == "pull":
+            truster = get_truster(ui, repo)
             return wot.resolve_pull_uri(ui, path, truster)
+        elif operation == "push":
+            return wot.resolve_push_uri(ui, path)
+        elif operation == "clone-push":
+            return wot.resolve_push_uri(ui, path, resolve_edition=False)
         else:
-            return wot.resolve_push_uri(ui, path)
+            raise util.Abort("Internal error: invalid operation '{0}' when "
+                             "resolving WoT-integrated URI.".format(operation))
     else:
         return path
 
@@ -615,7 +630,7 @@ def freenetpull(orig, *args, **opts):
     # only act differently, if the target is an infocalypse repo.
     if not isfreenetpath(path):
         return orig(*args, **opts)
-    uri = freenetpathtouri(ui, path)
+    uri = freenetpathtouri(ui, path, "pull", repo)
     opts["uri"] = uri
     opts["aggressive"] = True # always search for the latest revision.
     return infocalypse_pull(ui, repo, **opts)
@@ -652,7 +667,7 @@ def freenetpush(orig, *args, **opts):
     # only act differently, if the target is an infocalypse repo.
     if not isfreenetpath(path):
         return orig(*args, **opts)
-    uri = freenetpathtouri(ui, path, pull=False)
+    uri = parse_repo_path(freenetpathtouri(ui, path, "push", repo))
     if uri is None:
         return
     # if the uri is the short form (USK@/name/#), generate the key and preprocess the uri.
@@ -660,9 +675,9 @@ def freenetpush(orig, *args, **opts):
         ui.status("creating a new key for the repo. For a new repo with an existing key, use clone.\n")
         from sitecmds import genkeypair
         fcphost, fcpport = opts["fcphost"], opts["fcpport"]
-        if fcphost == '':
+        if not fcphost:
             fcphost = '127.0.0.1'
-        if fcpport == 0:
+        if not fcpport:
             fcpport = 9481
             
         # use redundant keys by default, except if explicitely requested otherwise.
@@ -703,13 +718,16 @@ def freenetclone(orig, *args, **opts):
             if dest.endswith(".R1") or dest.endswith(".R0"):
                 dest = dest[:-3]
 
+    # TODO: source holds the "repo" argument, but the naming is confusing in
+    # the context of freenetpathtouri().
     # check whether to create, pull or copy
     pulluri, pushuri = None, None
     if isfreenetpath(source):
-        pulluri = freenetpathtouri(ui, source)
+        pulluri = parse_repo_path(freenetpathtouri(ui, source, "pull"))
 
     if isfreenetpath(dest):
-        pushuri = freenetpathtouri(ui, dest, pull=False)
+        pushuri = parse_repo_path(freenetpathtouri(ui, dest, "clone-push"),
+                                  assume_redundancy=True)
 
     # decide which infocalypse command to use.
     if pulluri and pushuri:
@@ -730,9 +748,9 @@ def freenetclone(orig, *args, **opts):
             ui.status("creating a new key for the repo. To use your default key, call fn-create.\n")
             from sitecmds import genkeypair
             fcphost, fcpport = opts["fcphost"], opts["fcpport"]
-            if fcphost == '':
+            if not fcphost:
                 fcphost = '127.0.0.1'
-            if fcpport == 0:
+            if not fcpport:
                 fcpport = 9481
             
             # use redundant keys by default, except if explicitely requested otherwise.
diff --git a/infocalypse/commands.py b/infocalypse/commands.py
--- a/infocalypse/commands.py
+++ b/infocalypse/commands.py
@@ -18,6 +18,8 @@ from validate import is_hex_string, is_f
 
 import os
 
+from keys import parse_repo_path
+
 
 def set_target_version(ui_, repo, opts, params, msg_fmt):
     """ INTERNAL: Update TARGET_VERSION in params. """
@@ -51,15 +53,19 @@ def infocalypse_create(ui_, repo, **opts
 
     insert_uri = ''
     local_id = None
-    if opts['uri'] != '' and opts['wot'] != '':
+    if opts['uri'] and opts['wot']:
         ui_.warn("Please specify only one of --uri or --wot.\n")
         return
-    elif opts['uri'] != '':
-        insert_uri = opts['uri']
-    elif opts['wot'] != '':
-        # Expecting nick_prefix/repo_name.R<redundancy num>/edition
+    elif opts['uri']:
+        insert_uri = parse_repo_path(opts['uri'])
+    elif opts['wot']:
+        opts['wot'] = parse_repo_path(opts['wot'])
         nick_prefix, repo_name, repo_edition = opts['wot'].split('/', 2)
 
+        if not repo_name.endswith('.R1') and not repo_name.endswith('.R0'):
+            ui_.warn("Warning: Creating repository without redundancy. (R0 or"
+                     " R1)\n")
+
         from wot_id import Local_WoT_ID
 
         ui_.status("Querying WoT for local identities.\n")
@@ -83,8 +89,7 @@ def infocalypse_create(ui_, repo, **opts
         import fcp
         node = fcp.FCPNode()
         vcs_response =\
-            node.fcpPluginMessage(async=False,
-                                  plugin_name="plugins.WebOfTrust.WebOfTrust",
+            node.fcpPluginMessage(plugin_name="plugins.WebOfTrust.WebOfTrust",
                                   plugin_params=msg_params)[0]
 
         if vcs_response['header'] != 'FCPPluginReply' or\
@@ -95,6 +100,7 @@ def infocalypse_create(ui_, repo, **opts
 
     else:
         ui_.warn("Please set the insert key with either --uri or --wot.\n")
+        return
 
     set_target_version(ui_, repo, opts, params,
                        "Only inserting to version(s): %s\n")
@@ -117,19 +123,21 @@ def infocalypse_copy(ui_, repo, **opts):
     """ Copy an Infocalypse repository to a new URI. """
     params, stored_cfg = get_config_info(ui_, opts)
 
-    insert_uri = opts['inserturi']
-    if insert_uri == '':
+    if not opts['inserturi']:
         # REDFLAG: fix parameter definition so that it is required?
         ui_.warn("Please set the insert URI with --inserturi.\n")
         return
+    else:
+        insert_uri = parse_repo_path(opts['inserturi'])
 
-    request_uri = opts['requesturi']
-    if request_uri == '':
+    if not opts['requesturi']:
         request_uri = stored_cfg.get_request_uri(repo.root)
         if not request_uri:
             ui_.warn("There is no stored request URI for this repo.\n"
                      "Please set one with the --requesturi option.\n")
             return
+    else:
+        request_uri = parse_repo_path(opts['requesturi'])
 
     params['INSERT_URI'] = insert_uri
     params['REQUEST_URI'] = request_uri
@@ -140,13 +148,14 @@ def infocalypse_reinsert(ui_, repo, **op
     """ Reinsert the current version of an Infocalypse repository. """
     params, stored_cfg = get_config_info(ui_, opts)
 
-    request_uri = opts['uri']
-    if request_uri == '':
+    if not opts['uri']:
         request_uri = stored_cfg.get_request_uri(repo.root)
         if not request_uri:
             ui_.warn("There is no stored request URI for this repo.\n"
                      "Do a fn-pull from a repository USK and try again.\n")
             return
+    else:
+        request_uri = parse_repo_path(opts['uri'])
 
     level = opts['level']
     if level < 1 or level > 5:
@@ -174,9 +183,11 @@ def infocalypse_pull(ui_, repo, **opts):
      """
     params, stored_cfg = get_config_info(ui_, opts)
 
+    request_uri = ''
+
     if opts['hash']:
         # Use FMS to lookup the uri from the repo hash.
-        if opts['uri'] != '':
+        if opts['uri']:
             ui_.warn("Ignoring --uri because --hash is set!\n")
         if len(opts['hash']) != 1:
             raise util.Abort("Only one --hash value is allowed.")
@@ -185,13 +196,13 @@ def infocalypse_pull(ui_, repo, **opts):
         request_uri = get_uri_from_hash(ui_, repo, params, stored_cfg)
     elif opts['wot']:
         import wot
-        truster = get_truster(ui_, repo, opts)
+        truster = get_truster(ui_, repo, opts['truster'])
 
         request_uri = wot.resolve_pull_uri(ui_, opts['wot'], truster)
-    else:
-        request_uri = opts['uri']
+    elif opts['uri']:
+        request_uri = parse_repo_path(opts['uri'])
 
-    if request_uri == '':
+    if not request_uri:
         request_uri = stored_cfg.get_request_uri(repo.root)
         if not request_uri:
             ui_.warn("There is no stored request URI for this repo.\n"
@@ -211,7 +222,7 @@ def infocalypse_pull_request(ui, repo, *
                          "--wot.\n")
 
     wot_id, repo_name = opts['wot'].split('/', 1)
-    from_identity = get_truster(ui, repo, opts)
+    from_identity = get_truster(ui, repo, opts['truster'])
     to_identity = WoT_ID(wot_id, from_identity)
     wot.send_pull_request(ui, repo, from_identity, to_identity, repo_name)
 
@@ -227,20 +238,22 @@ def infocalypse_check_notifications(ui, 
 
 
 def infocalypse_connect(ui, repo, **opts):
-    import wot
-    wot.connect(ui, repo)
+    import plugin_connect
+    plugin_connect.connect(ui, repo)
 
 
 def infocalypse_push(ui_, repo, **opts):
     """ Push to an Infocalypse repository in Freenet. """
     params, stored_cfg = get_config_info(ui_, opts)
-    insert_uri = opts['uri']
-    if insert_uri == '':
+
+    if not opts['uri']:
         insert_uri = stored_cfg.get_dir_insert_uri(repo.root)
         if not insert_uri:
             ui_.warn("There is no stored insert URI for this repo.\n"
                      "Please set one with the --uri option.\n")
             return
+    else:
+        insert_uri = parse_repo_path(opts['uri'])
 
     set_target_version(ui_, repo, opts, params,
                        "Only pushing to version(s): %s\n")
@@ -270,7 +283,7 @@ def infocalypse_info(ui_, repo, **opts):
     opts['fcpport'] = 0
     params, stored_cfg = get_config_info(ui_, opts)
     request_uri = opts['uri']
-    if request_uri == '':
+    if not request_uri:
         request_uri = stored_cfg.get_request_uri(repo.root)
         if not request_uri:
             ui_.warn("There is no stored request URI for this repo.\n"
@@ -329,7 +342,7 @@ def infocalypse_fmsread(ui_, repo, **opt
     opts['fcpport'] = 0
     params, stored_cfg = get_config_info(ui_, opts)
     request_uri = opts['uri']
-    if request_uri == '':
+    if not request_uri:
         request_uri = stored_cfg.get_request_uri(repo.root)
         if not request_uri:
             ui_.status("There is no stored request URI for this repo.\n")
@@ -381,7 +394,7 @@ def infocalypse_putsite(ui_, repo, **opt
         return
 
     params, stored_cfg = get_config_info(ui_, opts)
-    if opts['key'] != '':  # order important
+    if opts['key']:  # order important
         params['SITE_KEY'] = opts['key']
         if not (params['SITE_KEY'].startswith('SSK') or
                 params['SITE_KEY'] == 'CHK@'):
@@ -431,13 +444,13 @@ def infocalypse_wiki(ui_, repo, **opts):
     if required > 1:
         raise util.Abort("Use either --run, --createconfig, or --apply")
 
-    if opts['apply'] != '':
+    if opts['apply']:
         params, stored_cfg = get_config_info(ui_, opts)
         params['REQUEST_URI'] = opts['apply']
         execute_wiki_apply(ui_, repo, params, stored_cfg)
         return
 
-    if opts['fcphost'] != '' or opts['fcpport'] != 0:
+    if opts['fcphost'] or opts['fcpport']:
         raise util.Abort("--fcphost, --fcpport only for --apply")
 
     # hmmmm.... useless copy?
@@ -496,23 +509,34 @@ def infocalypse_setupfreemail(ui, repo, 
     import wot
     # TODO: Here --truster doesn't make sense. There is no trust involved.
     # TODO: Should this be part of the normal fn-setup?
-    wot.execute_setup_freemail(ui, get_truster(ui, repo, opts))
+    wot.execute_setup_freemail(ui, get_truster(ui, repo, opts['truster']))
 
 
-def get_truster(ui, repo, opts):
+def get_truster(ui, repo=None, truster_identifier=None):
     """
-    Return a local WoT ID - either one that published this repository or the
-    default.
+    Return a local WoT ID.
+
+    Search for a local identity from most to least specific:
+    1. truster_identifier (if given)
+    2. identity that published this respository (if repo is given and an
+                                                 identity is set)
+    3. default truster
+
     :rtype : Local_WoT_ID
     """
     from wot_id import Local_WoT_ID
-    if opts['truster']:
-        return Local_WoT_ID(opts['truster'])
+    if truster_identifier:
+        return Local_WoT_ID(truster_identifier)
     else:
-        cfg = Config().from_ui(ui)
+        cfg = Config.from_ui(ui)
 
-        # Value is identity ID.
-        identity = cfg.get_wot_identity(cfg.get_request_uri(repo.root))
+        # Value is identity ID, so '@' prefix makes it an identifier with an
+        # empty nickname.
+        identity = None
+        if repo:
+            identity = cfg.get_wot_identity(cfg.get_request_uri(repo.root))
+
+        # Either repo is not given or there is no associated identity.
         if not identity:
             identity = cfg.defaults['DEFAULT_TRUSTER']
 
@@ -524,7 +548,7 @@ def get_truster(ui, repo, opts):
 def do_archive_create(ui_, opts, params, stored_cfg):
     """ fn-archive --create."""
     insert_uri = opts['uri']
-    if insert_uri == '':
+    if not insert_uri:
         raise util.Abort("Please set the insert URI with --uri.")
 
     params['INSERT_URI'] = insert_uri
@@ -535,7 +559,7 @@ def do_archive_create(ui_, opts, params,
 def do_archive_push(ui_, opts, params, stored_cfg):
     """ fn-archive --push."""
     insert_uri = opts['uri']
-    if insert_uri == '':
+    if not insert_uri:
         insert_uri = (
             stored_cfg.get_dir_insert_uri(params['ARCHIVE_CACHE_DIR']))
         if not insert_uri:
@@ -553,7 +577,7 @@ def do_archive_pull(ui_, opts, params, s
     """ fn-archive --pull."""
     request_uri = opts['uri']
 
-    if request_uri == '':
+    if not request_uri:
         request_uri = (
             stored_cfg.get_request_uri(params['ARCHIVE_CACHE_DIR']))
         if not request_uri:
diff --git a/infocalypse/config.py b/infocalypse/config.py
--- a/infocalypse/config.py
+++ b/infocalypse/config.py
@@ -128,8 +128,13 @@ class Config:
         # repo id -> publisher WoT identity
         self.wot_identities = {}
         # TODO: Should this be keyed by str(WoT_ID) ?
-        # WoT public key hash -> Freemail password
+        # WoT identity ID -> Freemail password
         self.freemail_passwords = {}
+        # WoT identity ID -> last known repo list edition.
+        # TODO: Once WoT allows setting a property without triggering an
+        # immediate insert, this can move to a WoT property. (Can then query
+        # remote identities! Easier bootstrapping than from edition 0.)
+        self.repo_list_editions = {}
         # fms_id -> (usk_hash, ...) map
         self.fmsread_trust_map = DEFAULT_TRUST.copy()
         self.fmsread_groups = DEFAULT_GROUPS
@@ -269,6 +274,23 @@ class Config:
                              "Run hg fn-setupfreemail --truster {0}\n"
                              .format(wot_identity))
 
+    def set_repo_list_edition(self, wot_identity, edition):
+        """
+        Set the repository list edition for the given WoT identity.
+        :type wot_identity: WoT_ID
+        """
+        self.repo_list_editions[wot_identity.identity_id] = edition
+
+    def get_repo_list_edition(self, wot_identity):
+        """
+        Return the repository list edition associated with the given WoT
+        identity. Return 0 if one is not set.
+        """
+        if wot_identity.identity_id in self.repo_list_editions:
+            return self.repo_list_editions[wot_identity.identity_id]
+        else:
+            return 0
+
     # Hmmm... really nescessary?
     def get_dir_insert_uri(self, repo_dir):
         """ Return the insert USK for repo_dir or None. """
@@ -383,6 +405,11 @@ class Config:
                 cfg.freemail_passwords[wot_id] = parser.get(
                     'freemail_passwords', wot_id)
 
+        if parser.has_section('repo_list_editions'):
+            for wot_id in parser.options('repo_list_editions'):
+                cfg.repo_list_editions[wot_id] = int(parser.get(
+                    'repo_list_editions', wot_id))
+
         # ignored = fms_id|usk_hash|usk_hash|...
         if parser.has_section('fmsread_trust_map'):
             cfg.fmsread_trust_map.clear() # Wipe defaults.
@@ -467,6 +494,10 @@ class Config:
         for wot_id in cfg.freemail_passwords:
             parser.set('freemail_passwords', wot_id, cfg.freemail_passwords[
                 wot_id])
+        parser.add_section('repo_list_editions')
+        for wot_id in cfg.repo_list_editions:
+            parser.set('repo_list_editions', wot_id, cfg.repo_list_editions[
+                wot_id])
         parser.add_section('fmsread_trust_map')
         for index, fms_id in enumerate(cfg.fmsread_trust_map):
             entry = cfg.fmsread_trust_map[fms_id]
diff --git a/infocalypse/devnotes.txt b/infocalypse/devnotes.txt
--- a/infocalypse/devnotes.txt
+++ b/infocalypse/devnotes.txt
@@ -9,14 +9,8 @@ WoT identifier -
                  Web of Trust identifier. This is MyNickname@public-key-hash.
                  Often abbreviated "wot_id". Not to be confused with a WoT
                  identity.
-WoT identity -
+WoT identity ID -
               name given to the public key hash when returned by WoT.
-              Can also refer to a dictionary containing the Identity and
-              other attributes such as a request URI and contexts.
-
-              TODO: Apparently in the WoT code this is actually called
-              "identity ID". "ID" -> "identification" ~ "identifier",
-              right? What does that leave to call "nick@public key hash"?
 
 ------------------------------------------------------------
 Dev log:
diff --git a/infocalypse/doc/gsoc-2013-midterm-test.org b/infocalypse/doc/gsoc-2013-midterm-test.org
new file mode 100644
--- /dev/null
+++ b/infocalypse/doc/gsoc-2013-midterm-test.org
@@ -0,0 +1,54 @@
+#+title: Infocalypse GSoC midterm test
+
+* Intro
+
+Testing Infocalypse Features from the midterm plans. Firstoff only personal notes, pending an LCWoT rebuild.
+
+* Setup
+
+Get infocalypse:
+
+#+BEGIN_SRC 
+hg -R ~/infocalypse_wiki_hacking/ up f5800540e7f6
+#+END_SRC
+
+* Getting a repo via WoT ID
+
+#+BEGIN_SRC sh
+hg clone freenet://ArneBab/tech
+#+END_SRC
+
+: Abbruch: No such identity 'ArneBab'
+
+hm, did not work.
+
+#+BEGIN_SRC sh
+hg clone freenet://ArneBab@6~ZDYdvAgMoUfG6M5Kwi7SQqyS-gTcyFeaNN1Pf3FvY/tech
+#+END_SRC
+
+: Abbruch: No such identity 'ArneBab@6~ZDYdvAgMoUfG6M5Kwi7SQqyS-gTcyFeaNN1Pf3FvY'
+
+#+BEGIN_SRC sh
+hg fn-pull --wot ArneBab@6~ZDYdvAgMoUfG6M5Kwi7SQqyS-gTcyFeaNN1Pf3FvY/tech.R1/0
+#+END_SRC
+
+: Abbruch: '@' is ambiguous.
+: 
+: _mgrThread: No incoming message from node
+: _mgrThread: Testing for client req
+: Unhandled exception in thread started by 
+: sys.excepthook is missing
+: lost sys.stderr
+: shutdown: entered
+
+#+BEGIN_SRC sh
+hg fn-pull --aggressive --uri USK@6~ZDYdvAgMoUfG6M5Kwi7SQqyS-gTcyFeaNN1Pf3FvY,OSOT4OEeg4xyYnwcGECZUX6~lnmYrZsz05Km7G7bvOQ,AQACAAE/tech.R1/87
+#+END_SRC
+
+Works.
+
+#+BEGIN_SRC sh
+hg pull freenet://USK@6~ZDYdvAgMoUfG6M5Kwi7SQqyS-gTcyFeaNN1Pf3FvY,OSOT4OEeg4xyYnwcGECZUX6~lnmYrZsz05Km7G7bvOQ,AQACAAE/tech.R1/87
+#+END_SRC
+
+Works.
diff --git a/infocalypse/keys.py b/infocalypse/keys.py
--- a/infocalypse/keys.py
+++ b/infocalypse/keys.py
@@ -1,4 +1,5 @@
 from string import split
+from mercurial import util
 
 
 class USK:
@@ -17,6 +18,28 @@ class USK:
         elif self.key.startswith('freenet://'):
             self.key = self.key[len('freenet://'):]
 
+    def get_repo_name(self):
+        """
+        Return name with the redundancy level, if any, removed.
+
+        # TODO: tests. Use in detecting duplicate names. (Also
+        # determining repo names from URI.)
+
+        >>> USK('USK@.../name/5').get_repo_name()
+        'name'
+        >>> USK('USK@.../name.R1/5').get_repo_name()
+        'name'
+        >>> USK('USK@.../name.R0/5').get_repo_name()
+        'name'
+        >>> USK('USK@.../name.something/5').get_repo_name()
+        'name.something'
+        >>> USK('USK@.../name.R2/5').get_repo_name()
+        'name.R2'
+        """
+        if self.name.endswith('.R1') or self.name.endswith('.R0'):
+            return self.name[:-3]
+        return self.name
+
     def clone(self):
         return USK(str(self))
 
@@ -25,3 +48,50 @@ class USK:
 
     def __repr__(self):
         return "USK('%s')" % str(self)
+
+
+# Method instead of class because the existing code expects keys to be strings.
+# TODO: Would assuming edition / redundancy be better suited as arguments to
+# the USK __init__()? WoT paths are not USKs though. Once again RepoPath
+# might be nice. It would especially avoid repeated string operations to work
+# with redundancy level.
+def parse_repo_path(path, assume_redundancy=False):
+    """
+    Return the given path to a repo - either USK or WoT path -
+    assuming if unspecified:
+    * edition 0
+    * optionally, not by default (assume_redundancy) R1 redundancy
+
+    >>> parse_repo_path('USK@.../name')
+    'USK@.../name/0'
+    >>> parse_repo_path('USK@.../name/')
+    'USK@.../name/0'
+    >>> parse_repo_path('USK@.../name', assume_redundancy=True)
+    'USK@.../name.R1/0'
+    >>> parse_repo_path('USK@.../name.R0/5', assume_redundancy=True)
+    'USK@.../name.R0/5'
+    >>> parse_repo_path('not a key')
+    Traceback (most recent call last):
+        ...
+    Abort: Cannot parse 'not a key' as repository path.
+    """
+    parts = path.split('/')
+
+    if len(parts) == 2:
+        # Assuming USK@..,/name: '/edition' omitted.
+        parts.append('0')
+
+    if not len(parts) == 3:
+        raise util.Abort("Cannot parse '{0}' as repository path.".format(path))
+
+    if not parts[2]:
+        # Assuming USK@../name/: 'edition' omitted
+        parts[2] = '0'
+
+    if assume_redundancy:
+        # Assuming USK@.../repo_name/edition
+        repo_name = parts[1]
+        if not repo_name.endswith('.R1') and not repo_name.endswith('.R0'):
+            parts[1] += '.R1'
+
+    return '/'.join(parts)
diff --git a/infocalypse/plugin_connect.py b/infocalypse/plugin_connect.py
new file mode 100644
--- /dev/null
+++ b/infocalypse/plugin_connect.py
@@ -0,0 +1,112 @@
+from signal import signal, SIGINT
+from time import sleep
+import fcp
+import threading
+from mercurial import util
+import sys
+
+PLUGIN_NAME = "org.freenetproject.plugin.dvcs_webui.main.Plugin"
+
+def connect(ui, repo):
+    node = fcp.FCPNode()
+
+    ui.status("Connecting.\n")
+
+    # TODO: Would it be worthwhile to have a wrapper that includes PLUGIN_NAME?
+    # TODO: Where to document the spec? devnotes.txt? How to format?
+    hi_there = node.fcpPluginMessage(plugin_name=PLUGIN_NAME,
+                                     plugin_params={'Message': 'Hello',
+                                                    'VoidQuery': 'true'})[0]
+
+    if hi_there['header'] == 'Error':
+        raise util.Abort("The DVCS web UI plugin is not loaded.")
+
+    if hi_there['Replies.Message'] == 'Error':
+        # TODO: Debugging
+        print hi_there
+        raise util.Abort("Another VCS instance is already connected.")
+
+    session_token = hi_there['Replies.SessionToken']
+
+    ui.status("Connected.\n")
+
+    def disconnect(signum, frame):
+        ui.status("Disconnecting.\n")
+        node.fcpPluginMessage(plugin_name=PLUGIN_NAME,
+                              plugin_params=
+                              {'Message': 'Disconnect',
+                               'SessionToken': session_token})
+        sys.exit()
+
+    # Send Disconnect on interrupt instead of waiting on timeout.
+    signal(SIGINT, disconnect)
+
+    def ping():
+        # Loop with delay.
+        while True:
+            pong = node.fcpPluginMessage(plugin_name=PLUGIN_NAME,
+                                         plugin_params=
+                                         {'Message': 'Ping',
+                                          'SessionToken': session_token})[0]
+            if pong['Replies.Message'] == 'Error':
+                raise util.Abort(pong['Replies.Description'])
+            elif pong['Replies.Message'] != 'Pong':
+                ui.warn("Got unrecognized Ping reply '{0}'.\n".format(pong[
+                        'Replies.Message']))
+
+            # Wait for less than timeout threshold. In testing responses take
+            # a little over a second.
+            sleep(3.5)
+
+    # Start self-perpetuating pinging in the background.
+    t = threading.Timer(0.0, ping)
+    # Daemon threads do not hold up the process exiting. Allows prompt
+    # response to - for instance - SIGTERM.
+    t.daemon = True
+    t.start()
+
+    while True:
+        query_identifier = node._getUniqueId()
+        # The event-querying is single-threaded, which makes things slow as
+        # everything waits on the completion of the current operation.
+        # Asynchronous code would require changes on the plugin side but
+        # potentially have much lower latency.
+        # TODO: Can wrap away PLUGIN_NAME, SessionToken, and QueryIdentifier?
+        command = node.fcpPluginMessage(plugin_name=PLUGIN_NAME,
+                                        plugin_params=
+                                        {'Message': 'Ready',
+                                         'SessionToken': session_token,
+                                         'QueryIdentifier': query_identifier})[0]
+
+        response = command['Replies.Message']
+        if response == 'Error':
+            raise util.Abort(command['Replies.Description'])
+
+        if response not in handlers:
+            raise util.Abort("Unsupported query '{0}'\n")
+
+        # Handlers are indexed by the query message name, take the query
+        # message, and return (result_name, plugin_params).
+        result_name, plugin_params = handlers[response](command)
+
+        plugin_params['Message'] = result_name
+        plugin_params['QueryIdentifier'] = query_identifier
+        plugin_params['SessionToken'] = session_token
+
+        ack = node.fcpPluginMessage(plugin_name=PLUGIN_NAME,
+                                    plugin_params=plugin_params)[0]
+
+        if ack['Replies.Message'] != "Ack":
+            raise util.Abort("Received unexpected message instead of result "
+                             "acknowledgement:\n{0}\n".format(ack))
+
+
+# Handlers return two items: result message name, message-specific parameters.
+# The sending code handles the plugin name, required parameters and plugin name.
+
+
+def VoidQuery(query):
+    return "VoidResult", {}
+
+# TODO: Perhaps look up method by name directly?
+handlers = {'VoidQuery': VoidQuery}
diff --git a/infocalypse/wot.py b/infocalypse/wot.py
--- a/infocalypse/wot.py
+++ b/infocalypse/wot.py
@@ -8,77 +8,20 @@ from keys import USK
 import yaml
 from email.mime.text import MIMEText
 import imaplib
-import threading
 from wot_id import Local_WoT_ID, WoT_ID
 
 FREEMAIL_SMTP_PORT = 4025
 FREEMAIL_IMAP_PORT = 4143
-# TODO: Is whitespace in the search key illegal?
-VCS_TOKEN = "[vcs] "
-PLUGIN_NAME = "org.freenetproject.plugin.infocalypse_webui.main.InfocalypsePlugin"
-
-
-def connect(ui, repo):
-    node = fcp.FCPNode()
-
-    # TODO: Should I be using this? Looks internal. The identifier needs to
-    # be consistent though.
-    fcp_id = node._getUniqueId()
-
-    ui.status("Connecting as '%s'.\n" % fcp_id)
-
-    def ping():
-        pong = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, id=fcp_id,
-                                     plugin_params={'Message': 'Ping'})[0]
-        if pong['Replies.Message'] == 'Error':
-            raise util.Abort(pong['Replies.Description'])
-        # Must be faster than the timeout threshold. (5 seconds)
-        threading.Timer(4.0, ping).start()
-
-    # Start self-perpetuating pinging in the background.
-    t = threading.Timer(0.0, ping)
-    # Daemon threads do not hold up the process exiting. Allows prompt
-    # response to - for instance - SIGTERM.
-    t.daemon = True
-    t.start()
-
-    while True:
-        sequenceID = node._getUniqueId()
-        # The event-querying is single-threaded, which makes things slow as
-        # everything waits on the completion of the current operation.
-        # Asynchronous code would require changes on the plugin side but
-        # potentially have much lower latency.
-        command = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, id=fcp_id,
-                                        plugin_params=
-                                        {'Message': 'ClearToSend',
-                                         'SequenceID': sequenceID})[0]
-        # TODO: Look up handlers in a dictionary.
-        print command
-
-        # Reload the config each time - it may have changed between messages.
-        cfg = Config.from_ui(ui)
-
-        response = command['Replies.Message']
-        if response == 'Error':
-            raise util.Abort(command['Replies.Description'])
-        elif response == 'ListLocalRepos':
-            params = {'Message': 'RepoList',
-                      'SequenceID': sequenceID}
-
-            # Request USKs are keyed by repo path.
-            repo_index = 0
-            for path in cfg.request_usks.iterkeys():
-                params['Repo%s' % repo_index] = path
-                repo_index += 1
-
-            ack = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, id=fcp_id,
-                                        plugin_params=params)[0]
-            print ack
+VCS_TOKEN = "[vcs]"
+# "infocalypse" is lower case in case it is used somewhere mixed case can
+# cause problems like a filesystem path. Used for machine-readable VCS name.
+VCS_NAME = "infocalypse"
 
 
 def send_pull_request(ui, repo, from_identity, to_identity, to_repo_name):
     """
-
+    Prompt for a pull request message, and send a pull request from
+    from_identity to to_identity for the repository to_repo_name.
 
     :type to_identity: WoT_ID
     :type from_identity: Local_WoT_ID
@@ -87,7 +30,7 @@ def send_pull_request(ui, repo, from_ide
     to_address = require_freemail(to_identity)
 
     cfg = Config.from_ui(ui)
-    password = cfg.get_freemail_password(from_identity.identity_id)
+    password = cfg.get_freemail_password(from_identity)
 
     to_repo = find_repo(ui, to_identity, to_repo_name)
 
@@ -103,10 +46,8 @@ def send_pull_request(ui, repo, from_ide
     from_branch = repo_context.branch()
 
     # Use double-quoted scalars so that Unicode can be included. (Nicknames.)
-    # "infocalypse" is lower case in case it is used somewhere mixed case can
-    # cause problems like a filesystem path.
     footer = yaml.dump({'request': 'pull',
-                        'vcs': 'infocalypse',
+                        'vcs': VCS_NAME,
                         'source': from_uri + '#' + from_branch,
                         'target': to_repo}, default_style='"',
                        explicit_start=True, explicit_end=True,
@@ -119,8 +60,8 @@ def send_pull_request(ui, repo, from_ide
 
 HG: Enter pull request message here. Lines beginning with 'HG:' are removed.
 HG: The first line has "{0}" added before it in transit and is the subject.
-HG: The second line should be blank.
-HG: Following lines are the body of the message.
+HG: The second line is ignored.
+HG: Subsequent lines are the body of the message.
 """.format(VCS_TOKEN), str(from_identity))
     # TODO: Save message and load later in case sending fails.
 
@@ -133,7 +74,7 @@ HG: Following lines are the body of the 
 
     # Body is third line and after.
     msg = MIMEText('\n'.join(source_lines[2:]) + footer)
-    msg['Subject'] = VCS_TOKEN + source_lines[0]
+    msg['Subject'] = VCS_TOKEN + ' ' + source_lines[0]
     msg['To'] = to_address
     msg['From'] = from_address
 
@@ -147,6 +88,8 @@ HG: Following lines are the body of the 
 
 def check_notifications(ui, local_identity):
     """
+    Check Freemail for local_identity and print information on any VCS
+    messages received.
 
     :type local_identity: Local_WoT_ID
     """
@@ -203,9 +146,14 @@ def check_notifications(ui, local_identi
 
 
 def read_message_yaml(ui, from_address, subject, body):
+    """
+    Print information about the given message.
+    """
     # Get consistent line endings.
     body = '\n'.join(body.splitlines())
     yaml_start = body.rfind('---\n')
+    # The .join() does not add a trailing newline, and the end token might be
+    # the last line.
     end_token = '...'
     yaml_end = body.rfind(end_token)
 
@@ -238,9 +186,7 @@ def read_message_yaml(ui, from_address, 
                   " formatted. Details:\n%s\n" % (subject, e))
         return
 
-    # "infocalypse" is lower case in case it is used somewhere mixed case can
-    # cause problems like a filesystem path.
-    if request['vcs'] != 'infocalypse':
+    if request['vcs'] != VCS_NAME:
         ui.status("Notification '%s' is for '%s', not Infocalypse.\n"
                   % (subject, request['vcs']))
         return
@@ -270,6 +216,7 @@ def require_freemail(wot_identity):
     Return the given identity's Freemail address.
     Abort with an error message if the given identity does not have a
     Freemail address / context.
+
     :type wot_identity: WoT_ID
     """
     if not wot_identity.freemail_address:
@@ -279,19 +226,20 @@ def require_freemail(wot_identity):
 
 
 def update_repo_listing(ui, for_identity):
-    # TODO: WoT property containing edition. Used when requesting.
-    # Version number to support possible format changes.
     """
+    Insert list of repositories published by the given identity.
 
     :type for_identity: Local_WoT_ID
     """
+    # TODO: WoT property containing repo list edition. Used when requesting.
+    # Version number to support possible format changes.
     root = ET.Element('vcs', {'version': '0'})
 
     ui.status("Updating repo listing for '%s'\n" % for_identity)
 
     for request_uri in build_repo_list(ui, for_identity):
         repo = ET.SubElement(root, 'repository', {
-            'vcs': 'Infocalypse',
+            'vcs': VCS_NAME,
         })
         repo.text = request_uri
 
@@ -303,8 +251,10 @@ def update_repo_listing(ui, for_identity
     # TODO: Somehow store the edition, perhaps in ~/.infocalypse. WoT
     # properties are apparently not appropriate.
 
+    cfg = Config.from_ui(ui)
+
     insert_uri.name = 'vcs'
-    insert_uri.edition = '0'
+    insert_uri.edition = cfg.get_repo_list_edition(for_identity)
 
     ui.status("Inserting with URI:\n{0}\n".format(insert_uri))
     uri = node.put(uri=str(insert_uri), mimetype='application/xml',
@@ -314,6 +264,8 @@ def update_repo_listing(ui, for_identity
         ui.warn("Failed to update repository listing.")
     else:
         ui.status("Updated repository listing:\n{0}\n".format(uri))
+        cfg.set_repo_list_edition(for_identity, USK(uri).edition)
+        Config.to_file(cfg)
 
 
 def build_repo_list(ui, for_identity):
@@ -342,6 +294,7 @@ def find_repo(ui, identity, repo_name):
     identity matching the given identifier.
     Raise util.Abort if unable to read repo listing or a repo by that name
     does not exist.
+
     :type identity: WoT_ID
     """
     listing = read_repo_listing(ui, identity)
@@ -357,44 +310,99 @@ def read_repo_listing(ui, identity):
     """
     Read a repo listing for a given identity.
     Return a dictionary of repository request URIs keyed by name.
+
     :type identity: WoT_ID
     """
+    cfg = Config.from_ui(ui)
     uri = identity.request_uri.clone()
     uri.name = 'vcs'
-    uri.edition = 0
+    uri.edition = cfg.get_repo_list_edition(identity)
 
     # TODO: Set and read vcs edition property.
-    node = fcp.FCPNode()
-    ui.status("Fetching {0}\n".format(uri))
-    # TODO: What exception can this throw on failure? Catch it,
-    # print its description, and return None.
-    mime_type, repo_xml, msg = node.get(str(uri), priority=1,
-                                        followRedirect=True)
+    ui.status("Fetching.\n")
+    mime_type, repo_xml, msg = fetch_edition(uri)
+    ui.status("Fetched {0}.\n".format(uri))
 
-    ui.status("Parsing.\n")
+    cfg.set_repo_list_edition(identity, uri.edition)
+    Config.to_file(cfg)
+
     repositories = {}
+    ambiguous = []
     root = fromstring(repo_xml)
     for repository in root.iterfind('repository'):
-        if repository.get('vcs') == 'Infocalypse':
-            uri = repository.text
-            # Expecting key/reponame.R<num>/edition
-            name = uri.split('/')[1].split('.')[0]
-            ui.status("Found repository \"{0}\" at {1}\n".format(name, uri))
-            repositories[name] = uri
+        if repository.get('vcs') == VCS_NAME:
+            uri = USK(repository.text)
+            name = uri.get_repo_name()
+            if name not in repositories:
+                repositories[name] = uri
+            else:
+                existing = repositories[name]
+                if uri.key == existing.key and uri.name == existing.name:
+                    # Different edition of same key and complete name.
+                    # Use the latest edition.
+                    if uri.edition > existing.edition:
+                        repositories[name] = uri
+                else:
+                    # Different key or complete name. Later remove and give
+                    # warning.
+                    ambiguous.append(name)
+
+    for name in ambiguous:
+        # Same repo name but different key or exact name.
+        ui.warn("\"{0}\" refers ambiguously to multiple paths. Ignoring.\n"
+                .format(name))
+        del repositories[name]
+
+    # TODO: Would it make sense to mention those for which multiple editions
+    # are specified? It has no practical impact from this perspective,
+    # and these problems should be pointed out (or prevented) for local repo
+    # lists.
+
+    for name in repositories.iterkeys():
+        ui.status("Found repository \"{0}\".\n".format(name))
+
+    # Convert values from USKs to strings - USKs are not expected elsewhere.
+    for key in repositories.keys():
+        repositories[key] = str(repositories[key])
 
     return repositories
 
 
+def fetch_edition(uri):
+    """
+    Fetch a USK uri, following redirects. Change the uri edition to the one
+    fetched.
+    :type uri: USK
+    """
+    node = fcp.FCPNode()
+    # Following a redirect automatically does not provide the edition used,
+    # so manually following redirects is required.
+    # TODO: Is there ever legitimately more than one redirect?
+    try:
+        return node.get(str(uri), priority=1)
+    except fcp.FCPGetFailed, e:
+        # Error code 27 is permanent redirect: there's a newer edition of
+        # the USK.
+        # https://wiki.freenetproject.org/FCPv2/GetFailed#Fetch_Error_Codes
+        if not e.info['Code'] == 27:
+            raise
+
+        uri.edition = USK(e.info['RedirectURI']).edition
+
+        return node.get(str(uri), priority=1)
+
+
 def resolve_pull_uri(ui, path, truster):
         """
         Return a pull URI for the given path.
         Print an error message and abort on failure.
-        :type truster: Local_WoT_ID
+
         TODO: Is it appropriate to outline possible errors?
         Possible failures are being unable to fetch a repo list for the given
         identity, which may be a fetch failure or being unable to find the
         identity, and not finding the requested repo in the list.
 
+        :type truster: Local_WoT_ID
         :param ui: For feedback.
         :param path: path describing a repo. nick@key/reponame
         :param truster: identity whose trust list to use.
@@ -411,11 +419,16 @@ def resolve_pull_uri(ui, path, truster):
         return find_repo(ui, identity, repo_name)
 
 
-def resolve_push_uri(ui, path):
+def resolve_push_uri(ui, path, resolve_edition=True):
     """
     Return a push URI for the given path.
     Raise util.Abort if unable to resolve identity or repository.
 
+    :param resolve_edition: Defaults to True. If False, skips resolving the
+                            repository, uses the edition number 0. and does
+                            not modify the repository name. This is useful
+                            for finding a push URI for a repository that does
+                            not already exist.
     :param ui: For feedback.
     :param path: path describing a repo - nick@key/repo_name,
     where the identity is a local one. (Such that the insert URI is known.)
@@ -425,26 +438,33 @@ def resolve_push_uri(ui, path):
 
     local_id = Local_WoT_ID(wot_id)
 
-    insert_uri = local_id.insert_uri
+    if resolve_edition:
+        # TODO: find_repo should make it clearer that it returns a request URI,
+        # and return a USK.
+        repo = find_repo(ui, local_id, repo_name)
 
-    # TODO: find_repo should make it clearer that it returns a request URI,
-    # and return a USK.
-    repo = find_repo(ui, local_id, repo_name)
+        # Request URI
+        repo_uri = USK(repo)
 
-    # Request URI
-    repo_uri = USK(repo)
+        # Maintains name, edition.
+        repo_uri.key = local_id.insert_uri.key
 
-    # Maintains name, edition.
-    repo_uri.key = insert_uri.key
+        return str(repo_uri)
+    else:
+        repo_uri = local_id.insert_uri.clone()
 
-    return str(repo_uri)
+        repo_uri.name = repo_name
+        repo_uri.edition = 0
 
-# Support for querying WoT for own identities and identities meeting various
-# criteria.
-# TODO: "cmds" suffix to module name to fit fms, arc, inf?
+        return str(repo_uri)
 
 
 def execute_setup_wot(ui_, local_id):
+    """
+    Set WoT-related defaults.
+
+    :type local_id: Local_WoT_ID
+    """
     cfg = Config.from_ui(ui_)
 
     ui_.status("Setting default truster to {0}.\n".format(local_id))
@@ -456,6 +476,8 @@ def execute_setup_wot(ui_, local_id):
 def execute_setup_freemail(ui, local_id):
     """
     Prompt for, test, and set a Freemail password for the identity.
+
+    :type local_id: Local_WoT_ID
     """
     address = require_freemail(local_id)
 
diff --git a/infocalypse/wot_id.py b/infocalypse/wot_id.py
--- a/infocalypse/wot_id.py
+++ b/infocalypse/wot_id.py
@@ -37,7 +37,7 @@ class WoT_ID(object):
         # (and only) identity described by an unspecified message, in which case
         # it queries WoT to produce one.
         if not message:
-            message = get_identity(wot_identifier, truster)
+            message = _get_identity(wot_identifier, truster)
 
         def get_attribute(attribute):
             return message['Replies.{0}{1}'.format(attribute, id_num)]
@@ -107,21 +107,29 @@ class Local_WoT_ID(WoT_ID):
     """
 
     def __init__(self, wot_identifier):
-        id_num, message = get_local_identity(wot_identifier)
+        """
+        Create a WoT_ID for a local identity matching the identifier.
+
+        :type wot_identifier: str
+        """
+        id_num, message = _get_local_identity(wot_identifier)
 
         self.insert_uri = USK(message['Replies.InsertURI{0}'.format(id_num)])
 
         WoT_ID.__init__(self, None, None, id_num=id_num, message=message)
 
 
-def get_identity(wot_identifier, truster):
+def _get_identity(wot_identifier, truster):
     """
     Internal.
 
     Return an FCP reply from WoT for an identity on the truster's trust list
     matching the identifier. Abort if anything but exactly one match is found.
+
+    :type wot_identifier: str
+    :type truster: Local_WoT_ID
     """
-    nickname_prefix, key_prefix = parse_name(wot_identifier)
+    nickname_prefix, key_prefix = _parse_name(wot_identifier)
     # TODO: Support different FCP IP / port.
     node = fcp.FCPNode()
 
@@ -131,71 +139,72 @@ def get_identity(wot_identifier, truster
     # TODO: Should this manually ensure an identity has a vcs context
     # otherwise?
 
-    # LCWoT can have * to allow a wildcard match, but a wildcard alone is
-    # not allowed. See Lucine Term Modifiers documentation. The nickname
-    # uses this syntax but the ID is inherently startswith().
-    params = {'Message': 'GetIdentitiesByPartialNickname',
-              'Truster': truster.identity_id,
-              'PartialNickname':
-              nickname_prefix + '*' if nickname_prefix else '',
-              'PartialID': key_prefix,
-              'MaxIdentities': 2,
-              'Context': 'vcs'}
+    # GetIdentitiesByPartialNickname does not support empty nicknames.
+    if nickname_prefix:
+        params = {'Message': 'GetIdentitiesByPartialNickname',
+                  'Truster': truster.identity_id,
+                  'PartialNickname':
+                  nickname_prefix + '*',
+                  'PartialID': key_prefix,
+                  'MaxIdentities': 2,
+                  'Context': 'vcs'}
 
-    response = \
-        node.fcpPluginMessage(async=False,
-                              plugin_name="plugins.WebOfTrust.WebOfTrust",
-                              plugin_params=params)[0]
+        response = \
+            node.fcpPluginMessage(plugin_name="plugins.WebOfTrust.WebOfTrust",
+                                  plugin_params=params)[0]
 
-    if response['header'] != 'FCPPluginReply' or \
-            'Replies.Message' not in response:
-        raise util.Abort('Unexpected reply. Got {0}\n'.format(response))
-    elif response['Replies.Message'] == 'Identities':
-        matches = response['Replies.IdentitiesMatched']
-        if matches == 0:
-            raise util.Abort("No identities match '{0}'\n".format(
-                wot_identifier))
-        elif matches == 1:
-            return response
-        else:
-            raise util.Abort("'{0}' is ambiguous.\n".format(wot_identifier))
+        if response['header'] != 'FCPPluginReply' or \
+                'Replies.Message' not in response:
+            raise util.Abort('Unexpected reply. Got {0}\n'.format(response))
+        elif response['Replies.Message'] == 'Identities':
+            matches = response['Replies.IdentitiesMatched']
+            if matches == 0:
+                raise util.Abort("No identities match '{0}'\n".format(
+                    wot_identifier))
+            elif matches == 1:
+                return response
+            else:
+                raise util.Abort("'{0}' matches more than one identity.\n"
+                                 .format(wot_identifier))
 
-    # Partial matching not supported, or unknown truster. The only
-    # difference in the errors is human-readable, so just try the exact
-    # match.
-    assert response['Replies.Message'] == 'Error'
+        # Partial matching not supported, or unknown truster. The only
+        # difference in the errors is human-readable, so try the exact match.
+        assert response['Replies.Message'] == 'Error'
 
     # key_prefix must be a complete key for the lookup to succeed.
     params = {'Message': 'GetIdentity',
-              'Truster': truster,
+              'Truster': truster.identity_id,
               'Identity': key_prefix}
     response = \
-        node.fcpPluginMessage(async=False,
-                              plugin_name="plugins.WebOfTrust.WebOfTrust",
+        node.fcpPluginMessage(plugin_name="plugins.WebOfTrust.WebOfTrust",
                               plugin_params=params)[0]
 
     if response['Replies.Message'] == 'Error':
         # Searching by exact public key hash, not matching.
-        raise util.Abort("No such identity '{0}'.\n".format(wot_identifier))
+        raise util.Abort("No identity has the complete public key hash '{0}'. "
+                         "({1}) To flexibly match by partial nickname and key "
+                         "use LCWoT for now.\n".format(key_prefix,
+                                                       wot_identifier))
 
     # There should be only one result.
     # Depends on https://bugs.freenetproject.org/view.php?id=5729
     return response
 
 
-def get_local_identity(wot_identifier):
+def _get_local_identity(wot_identifier):
     """
     Internal.
 
     Return (id_number, FCP reply) from WoT for a local identity matching the
     identifier. Abort if anything but exactly one match is found.
+
+    :type wot_identifier: str
     """
-    nickname_prefix, key_prefix = parse_name(wot_identifier)
+    nickname_prefix, key_prefix = _parse_name(wot_identifier)
 
     node = fcp.FCPNode()
     response = \
-        node.fcpPluginMessage(async=False,
-                              plugin_name="plugins.WebOfTrust.WebOfTrust",
+        node.fcpPluginMessage(plugin_name="plugins.WebOfTrust.WebOfTrust",
                               plugin_params={'Message':
                                              'GetOwnIdentities'})[0]
 
@@ -244,8 +253,10 @@ def get_local_identity(wot_identifier):
     return id_num, response
 
 
-def parse_name(wot_identifier):
+def _parse_name(wot_identifier):
     """
+    Internal.
+
     Parse identifier of the forms: nick
                                    nick@key
                                    @key