public inbox for cygwin-apps-cvs@sourceware.org
help / color / mirror / Atom feed
* [calm - Cygwin server-side packaging maintenance script] branch master, updated. 20190530-15-g8118f97
@ 2019-06-07 10:16 jturney
  0 siblings, 0 replies; only message in thread
From: jturney @ 2019-06-07 10:16 UTC (permalink / raw)
  To: cygwin-apps-cvs

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #1: Type: text/plain, Size: 50531 bytes --]




https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=8118f97177a0ffe74681ebe32ca05436eb3c0c78

commit 8118f97177a0ffe74681ebe32ca05436eb3c0c78
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Mon Jun 3 18:15:06 2019 +0100

    Allow a package to appear in arch and noarch
    
    Use the existing package-dict merge() function to combine them,
    reporting non-permitted duplications.
    
    Improve merge() so it can handle a package appearing in more than one of
    the merged-in package-dicts
    
    Handle the merge reporting an error (by returning None) in
    validate_packages()

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=cd973f84cae0ec9598591eda8ffafa0e263fd713

commit cd973f84cae0ec9598591eda8ffafa0e263fd713
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Tue May 28 21:07:37 2019 +0100

    Stop storing relarea path in package
    
    The 'in package list' constraint becomes simply that path relative to
    <arch>/release starts with a package name in the package list
    
    Somewhat unfortunately, items in the movelist don't know what package
    they belong to, which makes things a bit awkward

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=67c8cfe73eb665685f4bc82ba4297d0c48d81e93

commit 67c8cfe73eb665685f4bc82ba4297d0c48d81e93
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Sun Jun 2 14:08:33 2019 +0100

    Store path to tar file in Tar object

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=9d0d2ba77b13b7a95755c631ddda65065a121d50

commit 9d0d2ba77b13b7a95755c631ddda65065a121d50
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Sun Jun 2 13:13:46 2019 +0100

    Add hint object to store path to hint file

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=5fbe4c85050c291b8976675e8cd9b36f004f1672

commit 5fbe4c85050c291b8976675e8cd9b36f004f1672
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Wed May 29 13:18:30 2019 +0100

    Factor out movelist as a separate class
    
    Wrap movelist handling up in an object.  This shouldn't make any
    functional change.

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=a5d2e1a6877e12ad43affb312722a12bff11ff35

commit a5d2e1a6877e12ad43affb312722a12bff11ff35
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Thu May 30 18:49:21 2019 +0100

    Add some test coverage for movelist conflict


Diff:
---
 calm/calm.py                                       |   48 +++++---
 calm/movelist.py                                   |  112 ++++++++++++++++++
 calm/package.py                                    |  115 ++++++++++--------
 calm/pkg2html.py                                   |    6 +-
 calm/uploads.py                                    |   85 ++------------
 test/test_calm.py                                  |   43 ++++++-
 test/testdata/conflict/homedir.expected            |    7 +
 test/testdata/conflict/rel_area.expected           |  124 ++++++++++++++++++++
 test/testdata/conflict/vault.expected              |   12 ++
 .../staleversion/staleversion-230-1-src.tar.xz     |  Bin 0 -> 228 bytes
 .../release/staleversion/staleversion-230-1.hint   |    4 +
 .../release/staleversion/staleversion-230-1.tar.xz |  Bin 0 -> 228 bytes
 test/testdata/uploads/pkglist.expected             |    8 +-
 13 files changed, 405 insertions(+), 159 deletions(-)

diff --git a/calm/calm.py b/calm/calm.py
index 8627d1a..dc63eea 100755
--- a/calm/calm.py
+++ b/calm/calm.py
@@ -66,6 +66,7 @@ import time
 
 from .abeyance_handler import AbeyanceHandler
 from .buffering_smtp_handler import BufferingSMTPHandler
+from .movelist import MoveList
 from . import common_constants
 from . import irk
 from . import maintainers
@@ -116,7 +117,7 @@ def process_relarea(args):
         if stale_to_vault:
             for arch in common_constants.ARCHES + ['noarch', 'src']:
                 logging.info("vaulting %d old package(s) for arch %s" % (len(stale_to_vault[arch]), arch))
-                uploads.move_to_vault(args, stale_to_vault[arch])
+                stale_to_vault[arch].move_to_vault(args)
         else:
             logging.error("error while evaluating stale packages")
             return None
@@ -185,9 +186,7 @@ def process_uploads(args, state):
                     break
 
                 # remove files which are to be removed
-                for p in scan_result[arch].to_vault:
-                    for f in scan_result[arch].to_vault[p]:
-                        package.delete(merged_packages[arch], p, f)
+                scan_result[arch].to_vault.map(lambda p, f: package.delete(merged_packages[arch], p, f))
 
                 # validate the package set
                 logging.debug("validating merged %s package set for maintainer %s" % (arch, name))
@@ -231,18 +230,21 @@ def process_uploads(args, state):
                 # process the move lists
                 if scan_result[arch].to_vault:
                     logging.info("vaulting %d package(s) for arch %s, by request" % (len(scan_result[arch].to_vault), arch))
-                uploads.move_to_vault(args, scan_result[arch].to_vault)
+                scan_result[arch].to_vault.move_to_vault(args)
                 uploads.remove(args, scan_result[arch].remove_success)
                 if scan_result[arch].to_relarea:
                     logging.info("adding %d package(s) for arch %s" % (len(scan_result[arch].to_relarea), arch))
-                uploads.move_to_relarea(m, args, scan_result[arch].to_relarea)
+                scan_result[arch].to_relarea.move_to_relarea(m, args)
+                # XXX: Note that there seems to be a separate process, not run
+                # from cygwin-admin's crontab, which changes the ownership of
+                # files in the release area to cyguser:cygwin
 
             # for each arch
             if args.stale:
                 for arch in common_constants.ARCHES + ['noarch', 'src']:
                     if stale_to_vault[arch]:
                         logging.info("vaulting %d old package(s) for arch %s" % (len(stale_to_vault[arch]), arch))
-                        uploads.move_to_vault(args, stale_to_vault[arch])
+                        stale_to_vault[arch].move_to_vault(args)
 
             # for each arch
             for arch in common_constants.ARCHES:
@@ -288,8 +290,8 @@ def process(args, state):
 
 def remove_stale_packages(args, packages):
     to_vault = {}
-    to_vault['noarch'] = defaultdict(list)
-    to_vault['src'] = defaultdict(list)
+    to_vault['noarch'] = MoveList()
+    to_vault['src'] = MoveList()
 
     for arch in common_constants.ARCHES:
         logging.debug("checking for stale packages for arch %s" % (arch))
@@ -298,9 +300,7 @@ def remove_stale_packages(args, packages):
         to_vault[arch] = package.stale_packages(packages[arch])
 
         # remove stale packages from package set
-        for p in to_vault[arch]:
-            for f in to_vault[arch][p]:
-                package.delete(packages[arch], p, f)
+        to_vault[arch].map(lambda p, f: package.delete(packages[arch], p, f))
 
     # if there are no stale packages, we don't have anything to do
     if not any([to_vault[a] for a in to_vault]):
@@ -323,12 +323,19 @@ def remove_stale_packages(args, packages):
     # for each arch.
     #
     # de-duplicate these package moves, as rather awkward workaround for that
-    for path in list(to_vault[common_constants.ARCHES[0]]):
+    moved_list = []
+
+    def dedup(path, f):
         for prefix in ['noarch', 'src']:
             if path.startswith(prefix):
-                to_vault[prefix][path] = to_vault[common_constants.ARCHES[0]][path]
-                for arch in common_constants.ARCHES:
-                    del to_vault[arch][path]
+                to_vault[prefix].add(path, f)
+                moved_list.append(path)
+
+    to_vault[common_constants.ARCHES[0]].map(dedup)
+
+    for path in moved_list:
+        for arch in common_constants.ARCHES:
+            to_vault[arch].remove(path)
 
     return to_vault
 
@@ -340,11 +347,12 @@ def remove_stale_packages(args, packages):
 def report_movelist_conflicts(a, b, reason):
     conflicts = False
 
-    n = uploads.movelist_intersect(a, b)
+    n = MoveList.intersect(a, b)
     if n:
-        for p in n:
-            for f in n[p]:
-                logging.error("%s/%s is both uploaded and %s vaulted" % (p, f, reason))
+        def report_conflict(p, f):
+            logging.error("%s/%s is both uploaded and %s vaulted" % (p, f, reason))
+
+        n.map(report_conflict)
         conflicts = True
 
     return conflicts
diff --git a/calm/movelist.py b/calm/movelist.py
new file mode 100644
index 0000000..8fb682a
--- /dev/null
+++ b/calm/movelist.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2015 Jon Turney
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+import logging
+import os
+
+from collections import defaultdict
+
+
+#
+# movelist class
+#
+
+class MoveList(object):
+    def __init__(self):
+        # a movelist is a dict with relative directory paths for keys and a list
+        # of filenames for each value
+        self.movelist = defaultdict(list)
+
+    def __len__(self):
+        return len(self.movelist)
+
+    def __bool__(self):
+        # empty movelists are false
+        return len(self.movelist) > 0
+
+    def add(self, relpath, f):
+        self.movelist[relpath].append(f)
+
+    def remove(self, relpath):
+        del self.movelist[relpath]
+
+    def _move(self, args, fromdir, todir):
+        for p in sorted(self.movelist):
+            logging.debug("mkdir %s" % os.path.join(todir, p))
+            if not args.dryrun:
+                try:
+                    os.makedirs(os.path.join(todir, p), exist_ok=True)
+                except FileExistsError:
+                    pass
+            logging.debug("move from '%s' to '%s':" % (os.path.join(fromdir, p), os.path.join(todir, p)))
+            for f in sorted(self.movelist[p]):
+                if os.path.exists(os.path.join(fromdir, p, f)):
+                    logging.info("%s" % os.path.join(p, f))
+                    if not args.dryrun:
+                        os.rename(os.path.join(fromdir, p, f), os.path.join(todir, p, f))
+                else:
+                    logging.error("%s can't be moved as it doesn't exist" % (f))
+
+    def move_to_relarea(self, m, args):
+        if self.movelist:
+            logging.info("move from %s's upload area to release area:" % (m.name))
+        self._move(args, m.homedir(), args.rel_area)
+
+    def move_to_vault(self, args):
+        if self.movelist:
+            logging.info("move from release area to vault:")
+        self._move(args, args.rel_area, args.vault)
+
+    # apply a function to all files in the movelists
+    def map(self, function):
+        for p in self.movelist:
+            for f in self.movelist[p]:
+                function(p, f)
+
+    # compute the intersection of a pair of movelists
+    @staticmethod
+    def intersect(a, b):
+        i = MoveList()
+        for p in a.movelist.keys() & b.movelist.keys():
+            pi = set(a.movelist[p]) & set(b.movelist[p])
+            if pi:
+                i.movelist[p] = pi
+        return i
+
+    # copy the files in a movelist
+    def copy(args, fromdir, todir):
+        for p in sorted(self.movelist):
+            logging.debug("mkdir %s" % os.path.join(todir, p))
+            if not args.dryrun:
+                try:
+                    os.makedirs(os.path.join(todir, p), exist_ok=True)
+                except FileExistsError:
+                    pass
+            logging.debug("copy from '%s' to '%s':" % (os.path.join(fromdir, p), os.path.join(todir, p)))
+            for f in sorted(self.movelist[p]):
+                if os.path.exists(os.path.join(fromdir, p, f)):
+                    logging.debug("%s" % (f))
+                    if not args.dryrun:
+                        shutil.copy2(os.path.join(fromdir, p, f), os.path.join(todir, p, f))
+                else:
+                    logging.error("%s can't be copied as it doesn't exist" % (f))
diff --git a/calm/package.py b/calm/package.py
index 76ddb85..838e4fc 100755
--- a/calm/package.py
+++ b/calm/package.py
@@ -39,6 +39,7 @@ import textwrap
 import time
 
 from .version import SetupVersion
+from .movelist import MoveList
 from . import common_constants
 from . import hint
 from . import maintainers
@@ -48,9 +49,9 @@ from . import past_mistakes
 # information we keep about a package
 class Package(object):
     def __init__(self):
-        self.path = ''  # path to package, relative to release area
+        self.pkgpath = ''  # path to package, relative to arch
         self.tars = {}
-        self.hint_files = {}
+        self.hints = {}
         self.is_used_by = set()
         self.version_hints = {}
         self.override_hints = {}
@@ -59,7 +60,7 @@ class Package(object):
 
     def __repr__(self):
         return "Package('%s', %s, %s, %s, %s)" % (
-            self.path,
+            self.pkgpath,
             pprint.pformat(self.tars),
             pprint.pformat(self.version_hints),
             pprint.pformat(self.override_hints),
@@ -72,32 +73,44 @@ class Package(object):
 # information we keep about a tar file
 class Tar(object):
     def __init__(self):
+        self.path = None  # path to tar, relative to release area
+        self.fn = None    # filename
         self.sha512 = ''
         self.size = 0
         self.is_empty = False
         self.is_used = False
 
     def __repr__(self):
-        return "Tar('%s', %d, %s)" % (self.sha512, self.size, self.is_empty)
+        return "Tar('%s', '%s', '%s', %d, %s)" % (self.fn, self.path, self.sha512, self.size, self.is_empty)
+
+
+# information we keep about a hint file
+class Hint(object):
+    def __init__(self):
+        self.path = None  # path to hint, relative to release area
+        self.fn = None    # filename of hint
+        self.hints = {}   # XXX: duplicates version_hints, for the moment
 
 
 #
 # read a packages from a directory hierarchy
 #
 def read_packages(rel_area, arch):
-    packages = defaultdict(Package)
+    packages = {}
 
     # <arch>/ noarch/ and src/ directories are considered
     for root in ['noarch', 'src', arch]:
+        packages[root] = defaultdict(Package)
+
         releasedir = os.path.join(rel_area, root)
         logging.debug('reading packages from %s' % releasedir)
 
         for (dirpath, subdirs, files) in os.walk(releasedir, followlinks=True):
-            read_package(packages, rel_area, dirpath, files)
+            read_package(packages[root], rel_area, dirpath, files)
 
-        logging.debug("%d packages read" % len(packages))
+        logging.debug("%d packages read" % len(packages[root]))
 
-    return packages
+    return merge({}, *packages.values())
 
 
 # helper function to compute sha512 for a particular file
@@ -175,9 +188,10 @@ def read_package(packages, basedir, dirpath, files, remove=[]):
             return True
 
         # check for duplicate package names at different paths
+        (_, _, pkgpath) = relpath.split(os.sep, 2)
         if p in packages:
             logging.error("duplicate package name at paths %s and %s" %
-                          (dirpath, packages[p].path))
+                          (relpath, packages[p].pkgpath))
             return True
 
         # determine version overrides
@@ -267,6 +281,8 @@ def read_package(packages, basedir, dirpath, files, remove=[]):
             if not f.endswith('.hint'):
                 # collect the attributes for each tar file
                 t = Tar()
+                t.path = relpath
+                t.fn = f
                 t.size = os.path.getsize(os.path.join(dirpath, f))
                 t.is_empty = tarfile_is_empty(os.path.join(dirpath, f))
                 t.mtime = os.path.getmtime(os.path.join(dirpath, f))
@@ -281,7 +297,7 @@ def read_package(packages, basedir, dirpath, files, remove=[]):
 
         # determine hints for each version we've encountered
         version_hints = {}
-        hint_files = {}
+        hints = {}
         actual_tars = {}
         for vr in vr_list:
             hint_fn = '%s-%s.hint' % (p, vr)
@@ -304,8 +320,13 @@ def read_package(packages, basedir, dirpath, files, remove=[]):
             else:
                 ovr = vr
 
+            hintobj = Hint()
+            hintobj.path = relpath
+            hintobj.fn = hint_fn
+            hintobj.hints = pvr_hint
+
             version_hints[ovr] = pvr_hint
-            hint_files[ovr] = hint_fn
+            hints[ovr] = hintobj
             actual_tars[ovr] = tars[vr]
 
         # ignore dotfiles
@@ -322,8 +343,8 @@ def read_package(packages, basedir, dirpath, files, remove=[]):
         packages[p].version_hints = version_hints
         packages[p].override_hints = override_hints
         packages[p].tars = actual_tars
-        packages[p].hint_files = hint_files
-        packages[p].path = relpath
+        packages[p].hints = hints
+        packages[p].pkgpath = pkgpath
         packages[p].skip = any(['skip' in version_hints[vr] for vr in version_hints])
 
     elif (relpath.count(os.path.sep) > 1):
@@ -391,6 +412,9 @@ def sort_key(k):
 def validate_packages(args, packages):
     error = False
 
+    if not packages:
+        return False
+
     for p in sorted(packages.keys()):
         logging.log(5, "validating package '%s'" % (p))
         has_requires = False
@@ -789,7 +813,7 @@ def validate_package_maintainers(args, packages):
         # ignore obsolete packages
         if any(['_obsolete' in packages[p].version_hints[vr].get('category', '') for vr in packages[p].version_hints]):
             continue
-        if not is_in_package_list(packages[p].path, all_packages):
+        if not is_in_package_list(packages[p].pkgpath, all_packages):
             logging.error("package '%s' is not in the package list" % (p))
             error = True
 
@@ -975,10 +999,10 @@ def write_setup_ini(args, packages, arch):
 
 # helper function to output details for a particular tar file
 def tar_line(p, category, v, f):
-    t = p.vermap[v][category]
-    fn = os.path.join(p.path, t)
-    sha512 = p.tar(v, category).sha512
-    size = p.tar(v, category).size
+    to = p.tar(v, category)
+    fn = os.path.join(to.path, to.fn)
+    sha512 = to.sha512
+    size = to.size
     print("%s: %s %d %s" % (category, fn, size, sha512), file=f)
 
 
@@ -1054,10 +1078,6 @@ def write_repo_json(args, packages, f):
 # - we combine the list of tarfiles, duplicates are not permitted
 # - we use the hints from b, and warn if they are different to the hints for a
 #
-# (XXX: this implementation possibly assumes that a package is at most in a and
-# one of b, which is currently true, but it could be written with more
-# generality)
-#
 def merge(a, *l):
     # start with a copy of a
     c = copy.deepcopy(a)
@@ -1065,13 +1085,13 @@ def merge(a, *l):
     for b in l:
         for p in b:
             # if the package is in b but not in a, add it to the copy
-            if p not in a:
+            if p not in c:
                 c[p] = b[p]
             # else, if the package is both in a and b, we have to do a merge
             else:
                 # package must exist at same relative path
-                if a[p].path != b[p].path:
-                    logging.error("package '%s' is at paths %s and %s" % (p, a[p].path, b[p].path))
+                if c[p].pkgpath != b[p].pkgpath:
+                    logging.error("package '%s' is at paths %s and %s" % (p, c[p].path, b[p].path))
                     return None
                 else:
                     for vr in b[p].tars:
@@ -1087,13 +1107,12 @@ def merge(a, *l):
 
                     # hints from b override hints from a, but warn if they have
                     # changed
-                    c[p].version_hints = a[p].version_hints
                     for vr in b[p].version_hints:
                         c[p].version_hints[vr] = b[p].version_hints[vr]
-                        if vr in a[p].version_hints:
-                            if a[p].version_hints[vr] != b[p].version_hints[vr]:
+                        if vr in c[p].version_hints:
+                            if c[p].version_hints[vr] != b[p].version_hints[vr]:
                                 diff = '\n'.join(difflib.ndiff(
-                                    pprint.pformat(a[p].version_hints[vr]).splitlines(),
+                                    pprint.pformat(c[p].version_hints[vr]).splitlines(),
                                     pprint.pformat(b[p].version_hints[vr]).splitlines()))
 
                                 logging.warning("package '%s' version '%s' hints changed\n%s" % (p, vr, diff))
@@ -1102,10 +1121,10 @@ def merge(a, *l):
                     c[p].override_hints.update(b[p].override_hints)
 
                     # merge hint file lists
-                    c[p].hint_files.update(b[p].hint_files)
+                    c[p].hints.update(b[p].hints)
 
                     # skip if both a and b are skip
-                    c[p].skip = a[p].skip and b[p].skip
+                    c[p].skip = c[p].skip and b[p].skip
 
     return c
 
@@ -1115,8 +1134,9 @@ def merge(a, *l):
 #
 
 def delete(packages, path, fn):
+    (_, _, pkgpath) = path.split(os.sep, 2)
     for p in packages:
-        if packages[p].path == path:
+        if packages[p].pkgpath == pkgpath:
             for vr in packages[p].tars:
                 for t in packages[p].tars[vr]:
                     if t == fn:
@@ -1127,14 +1147,14 @@ def delete(packages, path, fn):
                 if not packages[p].tars[vr]:
                     packages[p].vermap.pop(vr, None)
 
-            for h in packages[p].hint_files:
-                if packages[p].hint_files[h] == fn:
-                    del packages[p].hint_files[h]
+            for h in packages[p].hints:
+                if packages[p].hints[h].fn == fn:
+                    del packages[p].hints[h]
                     break
 
 
 #
-# verify that the package ppath is in the list of packages plist
+# verify that the package path starts with a package in the list of packages
 #
 # (This means that a maintainer can upload a package with any name, provided the
 # path contains one allowed for that maintainer)
@@ -1147,20 +1167,10 @@ def delete(packages, path, fn):
 # arbitrary package upload.
 #
 
-def package_list_re(plist):
-    if getattr(package_list_re, "_plist", []) != plist:
-        pattern = '|'.join(map(lambda p: r'/' + re.escape(p) + r'(?:/|$)', plist))
-        package_list_re._regex = re.compile(pattern, re.IGNORECASE)
-        package_list_re._plist = plist
-
-    return package_list_re._regex
-
 
 def is_in_package_list(ppath, plist):
-    if package_list_re(plist).search(ppath):
-        return True
-
-    return False
+    superpackage = ppath.split(os.sep, 1)[0]
+    return superpackage in plist
 
 
 #
@@ -1231,14 +1241,15 @@ def stale_packages(packages):
                 mark_package_fresh(packages, pn, v)
 
     # build a move list of stale versions
-    stale = defaultdict(list)
+    stale = MoveList()
     for pn, po in packages.items():
         for v in sorted(po.vermap.keys(), key=lambda v: SetupVersion(v)):
             all_stale = True
             for category in ['source', 'install']:
                 if category in po.vermap[v]:
                     if not getattr(po.tar(v, category), 'fresh', False):
-                        stale[po.path].append(po.vermap[v][category])
+                        to = po.tar(v, category)
+                        stale.add(to.path, to.fn)
                         logging.debug("package '%s' version '%s' %s is stale" % (pn, v, category))
                     else:
                         all_stale = False
@@ -1246,8 +1257,8 @@ def stale_packages(packages):
             # if there's a pvr.hint without a fresh source or install of the
             # same version, move it as well
             if all_stale:
-                if v in po.hint_files:
-                    stale[po.path].append(po.hint_files[v])
+                if v in po.hints:
+                    stale.add(po.hints[v].path, po.hints[v].fn)
                     logging.debug("package '%s' version '%s' hint is stale" % (pn, v))
 
         # clean up freshness mark
diff --git a/calm/pkg2html.py b/calm/pkg2html.py
index c12d811..9370092 100755
--- a/calm/pkg2html.py
+++ b/calm/pkg2html.py
@@ -333,8 +333,8 @@ def write_arch_listing(args, packages, arch):
         listings = os.listdir(dir)
         listings.remove('.htaccess')
 
-        for t in itertools.chain.from_iterable([packages[p].tars[vr] for vr in packages[p].tars]):
-            fver = re.sub(r'\.tar.*$', '', t)
+        for tn, to in itertools.chain.from_iterable([packages[p].tars[vr].items() for vr in packages[p].tars]):
+            fver = re.sub(r'\.tar.*$', '', tn)
             listing = os.path.join(dir, fver)
 
             # ... if it doesn't already exist, or force
@@ -365,7 +365,7 @@ def write_arch_listing(args, packages, arch):
                                                  <h1>%s</h1>
                                                  <pre>''' % (header, header)), file=f)
 
-                        tf = os.path.join(args.rel_area, packages[p].path, t)
+                        tf = os.path.join(args.rel_area, to.path, to.fn)
                         if not os.path.exists(tf):
                             # this shouldn't happen with a full mirror
                             logging.error("tarfile %s not found" % (tf))
diff --git a/calm/uploads.py b/calm/uploads.py
index 5b765cb..587aa9b 100644
--- a/calm/uploads.py
+++ b/calm/uploads.py
@@ -34,6 +34,7 @@ import shutil
 import tarfile
 import time
 
+from .movelist import MoveList
 from . import common_constants
 from . import package
 
@@ -54,8 +55,8 @@ def scan(m, all_packages, arch, args):
     basedir = os.path.join(m.homedir(), arch)
 
     packages = defaultdict(package.Package)
-    move = defaultdict(list)
-    vault = defaultdict(list)
+    move = MoveList()
+    vault = MoveList()
     remove = []
     remove_success = []
     error = False
@@ -129,12 +130,13 @@ def scan(m, all_packages, arch, args):
             continue
 
         # package doesn't appear in package list at all
-        if not package.is_in_package_list(relpath, all_packages):
+        (_, _, pkgpath) = relpath.split(os.sep, 2)
+        if not package.is_in_package_list(pkgpath, all_packages):
             logging.error("package '%s' is not in the package list" % dirpath)
             continue
 
         # only process packages for which we are listed as a maintainer
-        if not package.is_in_package_list(relpath, m.pkgs):
+        if not package.is_in_package_list(pkgpath, m.pkgs):
             logging.warning("package '%s' is not in the package list for maintainer %s" % (dirpath, m.name))
             continue
 
@@ -196,7 +198,7 @@ def scan(m, all_packages, arch, args):
                     logging.error("remove file %s is not empty" % fn)
                     error = True
                 else:
-                    vault[relpath].append(f[1:])
+                    vault.add(relpath, f[1:])
                     remove_success.append(fn)
                     removed_files.append(f[1:])
                 files.remove(f)
@@ -249,9 +251,9 @@ def scan(m, all_packages, arch, args):
                         logging.debug("different %s is already in release area" % fn)
                     # we always consider .hint files as needing to be moved, as
                     # we currently can't have a valid package without one
-                    move[relpath].append(f)
+                    move.add(relpath, f)
             else:
-                move[relpath].append(f)
+                move.add(relpath, f)
 
         # read and validate package
         if files:
@@ -286,72 +288,3 @@ def remove(args, remove):
                 os.unlink(f)
             except FileNotFoundError:
                 logging.error("%s can't be deleted as it doesn't exist" % (f))
-
-
-#
-#
-#
-
-def move(args, movelist, fromdir, todir):
-    for p in sorted(movelist):
-        logging.debug("mkdir %s" % os.path.join(todir, p))
-        if not args.dryrun:
-            try:
-                os.makedirs(os.path.join(todir, p), exist_ok=True)
-            except FileExistsError:
-                pass
-        logging.debug("move from '%s' to '%s':" % (os.path.join(fromdir, p), os.path.join(todir, p)))
-        for f in sorted(movelist[p]):
-            if os.path.exists(os.path.join(fromdir, p, f)):
-                logging.info("%s" % os.path.join(p, f))
-                if not args.dryrun:
-                    os.rename(os.path.join(fromdir, p, f), os.path.join(todir, p, f))
-            else:
-                logging.error("%s can't be moved as it doesn't exist" % (f))
-
-
-def move_to_relarea(m, args, movelist):
-    if movelist:
-        logging.info("move from %s's upload area to release area:" % (m.name))
-    move(args, movelist, m.homedir(), args.rel_area)
-    # XXX: Note that there seems to be a separate process, not run from
-    # cygwin-admin's crontab, which changes the ownership of files in the
-    # release area to cyguser:cygwin
-
-
-def move_to_vault(args, movelist):
-    if movelist:
-        logging.info("move from release area to vault:")
-    move(args, movelist, args.rel_area, args.vault)
-
-
-# compute the intersection of a pair of movelists
-def movelist_intersect(a, b):
-    i = defaultdict(list)
-    for p in a.keys() & b.keys():
-        pi = set(a[p]) & set(b[p])
-        if pi:
-            i[p] = pi
-    return i
-
-
-#
-#
-#
-
-def copy(args, movelist, fromdir, todir):
-    for p in sorted(movelist):
-        logging.debug("mkdir %s" % os.path.join(todir, p))
-        if not args.dryrun:
-            try:
-                os.makedirs(os.path.join(todir, p), exist_ok=True)
-            except FileExistsError:
-                pass
-        logging.debug("copy from '%s' to '%s':" % (os.path.join(fromdir, p), os.path.join(todir, p)))
-        for f in sorted(movelist[p]):
-            if os.path.exists(os.path.join(fromdir, p, f)):
-                logging.debug("%s" % (f))
-                if not args.dryrun:
-                    shutil.copy2(os.path.join(fromdir, p, f), os.path.join(todir, p, f))
-            else:
-                logging.error("%s can't be copied as it doesn't exist" % (f))
diff --git a/test/test_calm.py b/test/test_calm.py
index a44517b..569c81c 100755
--- a/test/test_calm.py
+++ b/test/test_calm.py
@@ -333,8 +333,8 @@ class CalmTest(unittest.TestCase):
         shutil.rmtree(test_root)
 
         self.assertEqual(scan_result.error, False)
-        compare_with_expected_file(self, 'testdata/uploads', dict(scan_result.to_relarea), 'move')
-        self.assertCountEqual(scan_result.to_vault, {'x86/release/testpackage': ['x86/release/testpackage/testpackage-0.1-1.tar.bz2']})
+        compare_with_expected_file(self, 'testdata/uploads', dict(scan_result.to_relarea.movelist), 'move')
+        self.assertCountEqual(scan_result.to_vault.movelist, {'x86/release/testpackage': ['x86/release/testpackage/testpackage-0.1-1.tar.bz2']})
         self.assertCountEqual(scan_result.remove_always, [f for (f, t) in ready_fns])
         self.assertEqual(scan_result.remove_success, ['testdata/homes/Blooey McFooey/x86/release/testpackage/-testpackage-0.1-1-src.tar.bz2', 'testdata/homes/Blooey McFooey/x86/release/testpackage/-testpackage-0.1-1.tar.bz2'])
         with pprint_patch():
@@ -366,6 +366,37 @@ class CalmTest(unittest.TestCase):
 
         # XXX: delete a needed package, and check validate fails
 
+    def test_process_uploads_conflict(self):
+        args = types.SimpleNamespace()
+
+        for d in ['rel_area', 'homedir', 'vault']:
+            setattr(args, d, tempfile.mktemp())
+            logging.info('%s = %s', d, getattr(args, d))
+
+        shutil.copytree('testdata/relarea', getattr(args, 'rel_area'))
+        shutil.copytree('testdata/homes.conflict', getattr(args, 'homedir'))
+
+        setattr(args, 'dryrun', False)
+        setattr(args, 'email', None)
+        setattr(args, 'force', False)
+        setattr(args, 'pkglist', 'testdata/pkglist/cygwin-pkg-maint')
+        setattr(args, 'stale', True)
+
+        # set appropriate !ready
+        m_homedir = os.path.join(getattr(args, 'homedir'), 'Blooey McFooey')
+        os.system('touch "%s"' % (os.path.join(m_homedir, 'x86', 'release', 'staleversion', '!ready')))
+
+        state = calm.calm.CalmState()
+        state.packages = calm.calm.process_relarea(args)
+        state.packages = calm.calm.process_uploads(args, state)
+        self.assertTrue(state.packages)
+
+        for d in ['rel_area', 'homedir', 'vault']:
+            with self.subTest(directory=d):
+                dirlist = capture_dirtree(getattr(args, d))
+                compare_with_expected_file(self, 'testdata/conflict', dirlist, d)
+                shutil.rmtree(getattr(args, d))
+
     def test_process(self):
         self.maxDiff = None
 
@@ -436,6 +467,7 @@ class CalmTest(unittest.TestCase):
         # (git doesn't store timestamps, so they will all be dated the time of checkout)
         relarea_x86 = os.path.join('testdata', 'relarea', 'x86', 'release')
         relarea_noarch = os.path.join('testdata', 'relarea', 'noarch', 'release')
+        home_conflict = os.path.join('testdata', 'homes.conflict', 'Blooey McFooey', 'x86', 'release')
         touches = [(os.path.join(relarea_x86, 'cygwin', 'cygwin-2.2.0-1.tar.xz'), '2016-11-01'),
                    (os.path.join(relarea_x86, 'cygwin', 'cygwin-2.2.0-1-src.tar.xz'), '2016-11-01'),
                    (os.path.join(relarea_x86, 'cygwin', 'cygwin-2.2.1-1.tar.xz'), '2016-11-02'),
@@ -464,9 +496,12 @@ class CalmTest(unittest.TestCase):
                    (os.path.join(relarea_x86, 'keychain', 'keychain-2.6.8-1.tar.bz2'), '2016-11-02'),
                    (os.path.join(relarea_x86, 'keychain', 'keychain-2.6.8-1-src.tar.bz2'), '2016-11-02'),
                    (os.path.join(relarea_noarch, 'perl-Net-SMTP-SSL', 'perl-Net-SMTP-SSL-1.03-1.tar.xz'), '2016-11-01'),
-                   (os.path.join(relarea_noarch, 'perl-Net-SMTP-SSL', 'perl-Net-SMTP-SSL-1.03-1-src.tar.xz'), '2016-11-01')]
+                   (os.path.join(relarea_noarch, 'perl-Net-SMTP-SSL', 'perl-Net-SMTP-SSL-1.03-1-src.tar.xz'), '2016-11-01'),
+                   (os.path.join(home_conflict, 'staleversion', 'staleversion-230-1.hint'), '2017-04-06'),
+                   (os.path.join(home_conflict, 'staleversion', 'staleversion-230-1.tar.xz'), '2017-04-06'),
+                   (os.path.join(home_conflict, 'staleversion', 'staleversion-230-1-src.tar.xz'), '2017-04-06')]
         for (f, t) in touches:
-            os.system('touch %s -d %s' % (f, t))
+            os.system('touch "%s" -d %s' % (f, t))
 
         # ensure !reminder-timestamp is created for uploads
         home = os.path.join('testdata', 'homes', 'Blooey McFooey')
diff --git a/test/testdata/conflict/homedir.expected b/test/testdata/conflict/homedir.expected
new file mode 100644
index 0000000..729c6b9
--- /dev/null
+++ b/test/testdata/conflict/homedir.expected
@@ -0,0 +1,7 @@
+{'.': [],
+ 'Blooey McFooey': [],
+ 'Blooey McFooey/x86': [],
+ 'Blooey McFooey/x86/release': [],
+ 'Blooey McFooey/x86/release/staleversion': ['staleversion-230-1-src.tar.xz',
+                                             'staleversion-230-1.hint',
+                                             'staleversion-230-1.tar.xz']}
diff --git a/test/testdata/conflict/rel_area.expected b/test/testdata/conflict/rel_area.expected
new file mode 100644
index 0000000..a23f44d
--- /dev/null
+++ b/test/testdata/conflict/rel_area.expected
@@ -0,0 +1,124 @@
+{'.': [],
+ 'noarch': ['sha512.sum'],
+ 'noarch/release': ['sha512.sum'],
+ 'noarch/release/obs-a': ['obs-a-1.0-1-src.tar.xz', 'obs-a-1.0-1.hint', 'obs-a-1.0-1.tar.xz', 'sha512.sum'],
+ 'noarch/release/obs-b': ['obs-b-1.0-1-src.tar.xz', 'obs-b-1.0-1.hint', 'obs-b-1.0-1.tar.xz', 'sha512.sum'],
+ 'noarch/release/perl-Net-SMTP-SSL': ['perl-Net-SMTP-SSL-1.03-1-src.tar.xz',
+                                      'perl-Net-SMTP-SSL-1.03-1.hint',
+                                      'perl-Net-SMTP-SSL-1.03-1.tar.xz',
+                                      'sha512.sum'],
+ 'noarch/release/test-c': ['sha512.sum', 'test-c-1.0-1-src.tar.xz', 'test-c-1.0-1.hint', 'test-c-1.0-1.tar.xz'],
+ 'noarch/release/test-d': ['sha512.sum', 'test-d-1.0-1-src.tar.xz', 'test-d-1.0-1.hint', 'test-d-1.0-1.tar.xz'],
+ 'noarch/release/test-e': ['sha512.sum', 'test-e-1.0-1-src.tar.xz', 'test-e-1.0-1.hint', 'test-e-1.0-1.tar.xz'],
+ 'x86': ['sha512.sum'],
+ 'x86/release': ['sha512.sum'],
+ 'x86/release/arc': ['arc-4.32.7-10-src.tar.bz2', 'arc-4.32.7-10.hint', 'arc-4.32.7-10.tar.bz2'],
+ 'x86/release/base-cygwin': ['base-cygwin-3.6-1.hint',
+                             'base-cygwin-3.6-1.tar.xz',
+                             'base-cygwin-3.8-1.hint',
+                             'base-cygwin-3.8-1.tar.xz',
+                             'sha512.sum'],
+ 'x86/release/corrupt': ['corrupt-2.0.0-1-src.tar.xz', 'corrupt-2.0.0-1.hint', 'corrupt-2.0.0-1.tar.xz', 'sha512.sum'],
+ 'x86/release/cygwin': ['.this-should-be-ignored',
+                        'cygwin-2.2.0-1-src.tar.xz',
+                        'cygwin-2.2.0-1.hint',
+                        'cygwin-2.2.0-1.tar.xz',
+                        'cygwin-2.2.1-1-src.tar.xz',
+                        'cygwin-2.2.1-1.hint',
+                        'cygwin-2.2.1-1.tar.xz',
+                        'cygwin-2.3.0-0.3-src.tar.xz',
+                        'cygwin-2.3.0-0.3.hint',
+                        'cygwin-2.3.0-0.3.tar.xz',
+                        'override.hint',
+                        'sha512.sum'],
+ 'x86/release/cygwin/cygwin-debuginfo': ['cygwin-debuginfo-2.2.0-1.hint',
+                                         'cygwin-debuginfo-2.2.0-1.tar.xz',
+                                         'cygwin-debuginfo-2.2.1-1.hint',
+                                         'cygwin-debuginfo-2.2.1-1.tar.xz',
+                                         'cygwin-debuginfo-2.3.0-0.3.hint',
+                                         'cygwin-debuginfo-2.3.0-0.3.tar.xz',
+                                         'override.hint',
+                                         'sha512.sum'],
+ 'x86/release/cygwin/cygwin-devel': ['cygwin-devel-2.2.0-1.hint',
+                                     'cygwin-devel-2.2.0-1.tar.xz',
+                                     'cygwin-devel-2.2.1-1.hint',
+                                     'cygwin-devel-2.2.1-1.tar.xz',
+                                     'cygwin-devel-2.3.0-0.3.hint',
+                                     'cygwin-devel-2.3.0-0.3.tar.xz',
+                                     'override.hint',
+                                     'sha512.sum'],
+ 'x86/release/invalid': ['invalid-0.hint', 'sha512.sum'],
+ 'x86/release/keychain': ['keychain-2.6.8-1-src.tar.bz2',
+                          'keychain-2.6.8-1.hint',
+                          'keychain-2.6.8-1.tar.bz2',
+                          'keychain-2.7.1-1-src.tar.bz2',
+                          'keychain-2.7.1-1.hint',
+                          'keychain-2.7.1-1.tar.bz2',
+                          'sha512.sum'],
+ 'x86/release/libspiro': ['libspiro-20071029-1.hint', 'sha512.sum'],
+ 'x86/release/libspiro/libspiro-devel': ['libspiro-devel-20071029-1.hint', 'sha512.sum'],
+ 'x86/release/libspiro/libspiro0': ['libspiro0-20071029-1.hint', 'sha512.sum'],
+ 'x86/release/libtextcat': ['libtextcat-2.2-2-src.tar.bz2',
+                            'libtextcat-2.2-2.hint',
+                            'libtextcat-2.2-2.tar.bz2',
+                            'sha512.sum'],
+ 'x86/release/libtextcat/libtextcat-devel': ['libtextcat-devel-2.2-2.hint',
+                                             'libtextcat-devel-2.2-2.tar.bz2',
+                                             'sha512.sum'],
+ 'x86/release/libtextcat/libtextcat0': ['libtextcat0-2.2-2.hint', 'libtextcat0-2.2-2.tar.bz2', 'sha512.sum'],
+ 'x86/release/mDNSResponder': ['mDNSResponder-379.32.1-1-src.tar.bz2',
+                               'mDNSResponder-379.32.1-1.hint',
+                               'mDNSResponder-379.32.1-1.tar.bz2',
+                               'sha512.sum'],
+ 'x86/release/mDNSResponder/libdns_sd-devel': ['libdns_sd-devel-379.32.1-1.hint',
+                                               'libdns_sd-devel-379.32.1-1.tar.bz2',
+                                               'sha512.sum'],
+ 'x86/release/mDNSResponder/libdns_sd1': ['libdns_sd1-379.32.1-1.hint', 'libdns_sd1-379.32.1-1.tar.bz2', 'sha512.sum'],
+ 'x86/release/mingw64-i686-binutils': ['mingw64-i686-binutils-2.29.1.787c9873-1.hint', 'sha512.sum'],
+ 'x86/release/mingw64-i686-binutils/mingw64-i686-binutils-debuginfo': ['mingw64-i686-binutils-debuginfo-2.29.1.787c9873-1.hint',
+                                                                       'sha512.sum'],
+ 'x86/release/openssh': ['openssh-7.2p2-1-src.tar.xz', 'openssh-7.2p2-1.hint', 'openssh-7.2p2-1.tar.xz', 'sha512.sum'],
+ 'x86/release/per-version': ['override.hint',
+                             'per-version-4.0-1-src.tar.xz',
+                             'per-version-4.0-1.hint',
+                             'per-version-4.0-1.tar.xz',
+                             'per-version-4.8-1-src.tar.xz',
+                             'per-version-4.8-1.hint',
+                             'per-version-4.8-1.tar.xz',
+                             'sha512.sum'],
+ 'x86/release/per-version-incomplete': ['override.hint',
+                                        'per-version-incomplete-36-1-src.tar.xz',
+                                        'per-version-incomplete-36-1.hint',
+                                        'per-version-incomplete-36-1.tar.xz',
+                                        'per-version-incomplete-39-1-src.tar.xz',
+                                        'per-version-incomplete-39-1.tar.xz',
+                                        'sha512.sum'],
+ 'x86/release/per-version-replacement-hint-only': ['per-version-replacement-hint-only-1.0-1-src.tar.xz',
+                                                   'per-version-replacement-hint-only-1.0-1.hint',
+                                                   'per-version-replacement-hint-only-1.0-1.tar.xz',
+                                                   'sha512.sum'],
+ 'x86/release/proj': ['proj-4.8.0-1.hint', 'sha512.sum'],
+ 'x86/release/proj/libproj-devel': ['libproj-devel-4.8.0-1.hint', 'sha512.sum'],
+ 'x86/release/proj/libproj1': ['libproj1-4.8.0-1.hint', 'sha512.sum'],
+ 'x86/release/rpm-doc': ['rpm-doc-4.1-2-src.tar.bz2',
+                         'rpm-doc-4.1-2.hint',
+                         'rpm-doc-4.1-2.tar.bz2',
+                         'rpm-doc-999-1.hint',
+                         'rpm-doc-999-1.tar.bz2',
+                         'sha512.sum'],
+ 'x86/release/splint': ['sha512.sum', 'splint-3.1.2-1.hint'],
+ 'x86/release/staleversion': ['override.hint',
+                              'sha512.sum',
+                              'staleversion-243-0-src.tar.xz',
+                              'staleversion-243-0.hint',
+                              'staleversion-243-0.tar.xz',
+                              'staleversion-250-0-src.tar.xz',
+                              'staleversion-250-0.hint',
+                              'staleversion-250-0.tar.xz',
+                              'staleversion-260-0-src.tar.xz',
+                              'staleversion-260-0.hint',
+                              'staleversion-260-0.tar.xz'],
+ 'x86/release/testpackage': ['sha512.sum',
+                             'testpackage-0.1-1-src.tar.bz2',
+                             'testpackage-0.1-1.hint',
+                             'testpackage-0.1-1.tar.bz2']}
diff --git a/test/testdata/conflict/vault.expected b/test/testdata/conflict/vault.expected
new file mode 100644
index 0000000..6f418e2
--- /dev/null
+++ b/test/testdata/conflict/vault.expected
@@ -0,0 +1,12 @@
+{'.': [],
+ 'x86': [],
+ 'x86/release': [],
+ 'x86/release/staleversion': ['staleversion-240-1-src.tar.xz',
+                              'staleversion-240-1.hint',
+                              'staleversion-240-1.tar.xz',
+                              'staleversion-242-0-src.tar.xz',
+                              'staleversion-242-0.hint',
+                              'staleversion-242-0.tar.xz',
+                              'staleversion-251-0-src.tar.xz',
+                              'staleversion-251-0.hint',
+                              'staleversion-251-0.tar.xz']}
diff --git a/test/testdata/homes.conflict/Blooey McFooey/x86/release/staleversion/staleversion-230-1-src.tar.xz b/test/testdata/homes.conflict/Blooey McFooey/x86/release/staleversion/staleversion-230-1-src.tar.xz
new file mode 100644
index 0000000..0e6f1e8
Binary files /dev/null and b/test/testdata/homes.conflict/Blooey McFooey/x86/release/staleversion/staleversion-230-1-src.tar.xz differ
diff --git a/test/testdata/homes.conflict/Blooey McFooey/x86/release/staleversion/staleversion-230-1.hint b/test/testdata/homes.conflict/Blooey McFooey/x86/release/staleversion/staleversion-230-1.hint
new file mode 100644
index 0000000..7f7f48a
--- /dev/null
+++ b/test/testdata/homes.conflict/Blooey McFooey/x86/release/staleversion/staleversion-230-1.hint	
@@ -0,0 +1,4 @@
+sdesc: "Test package for stale version removal"
+ldesc: "Test package for stale version removal"
+category: Shells Base
+requires:
diff --git a/test/testdata/homes.conflict/Blooey McFooey/x86/release/staleversion/staleversion-230-1.tar.xz b/test/testdata/homes.conflict/Blooey McFooey/x86/release/staleversion/staleversion-230-1.tar.xz
new file mode 100644
index 0000000..0e6f1e8
Binary files /dev/null and b/test/testdata/homes.conflict/Blooey McFooey/x86/release/staleversion/staleversion-230-1.tar.xz differ
diff --git a/test/testdata/uploads/pkglist.expected b/test/testdata/uploads/pkglist.expected
index 8498ce9..a9d9bfd 100644
--- a/test/testdata/uploads/pkglist.expected
+++ b/test/testdata/uploads/pkglist.expected
@@ -1,5 +1,5 @@
-{'testpackage': Package('x86/release/testpackage', {'1.0-1': {'testpackage-1.0-1-src.tar.bz2': Tar('aff488008bee3486e25b539fe6ccd1397bd3c5c0ba2ee2cf34af279554baa195af7493ee51d6f8510735c9a2ea54436d776a71e768165716762aec286abbbf83', 195, False),
-           'testpackage-1.0-1.tar.bz2': Tar('aff488008bee3486e25b539fe6ccd1397bd3c5c0ba2ee2cf34af279554baa195af7493ee51d6f8510735c9a2ea54436d776a71e768165716762aec286abbbf83', 195, False)}}, {'1.0-1': {'sdesc': '"A test package"',
+{'testpackage': Package('testpackage', {'1.0-1': {'testpackage-1.0-1-src.tar.bz2': Tar('testpackage-1.0-1-src.tar.bz2', 'x86/release/testpackage', 'aff488008bee3486e25b539fe6ccd1397bd3c5c0ba2ee2cf34af279554baa195af7493ee51d6f8510735c9a2ea54436d776a71e768165716762aec286abbbf83', 195, False),
+           'testpackage-1.0-1.tar.bz2': Tar('testpackage-1.0-1.tar.bz2', 'x86/release/testpackage', 'aff488008bee3486e25b539fe6ccd1397bd3c5c0ba2ee2cf34af279554baa195af7493ee51d6f8510735c9a2ea54436d776a71e768165716762aec286abbbf83', 195, False)}}, {'1.0-1': {'sdesc': '"A test package"',
            'ldesc': '"A test package\n'
                     "It's description might contains some unicode junk\n"
                     'Like it’s you’re Markup Language™ Nokogiri’s tool―that '
@@ -7,10 +7,10 @@
            'category': 'Devel',
            'requires': 'cygwin',
            'depends': 'cygwin'}}, {}, False),
- 'testpackage-subpackage': Package('x86/release/testpackage/testpackage-subpackage', {'1.0-1': {'testpackage-subpackage-1.0-1.tar.bz2': Tar('aff488008bee3486e25b539fe6ccd1397bd3c5c0ba2ee2cf34af279554baa195af7493ee51d6f8510735c9a2ea54436d776a71e768165716762aec286abbbf83', 195, False)}}, {'1.0-1': {'sdesc': '"A test subpackage"',
+ 'testpackage-subpackage': Package('testpackage/testpackage-subpackage', {'1.0-1': {'testpackage-subpackage-1.0-1.tar.bz2': Tar('testpackage-subpackage-1.0-1.tar.bz2', 'x86/release/testpackage/testpackage-subpackage', 'aff488008bee3486e25b539fe6ccd1397bd3c5c0ba2ee2cf34af279554baa195af7493ee51d6f8510735c9a2ea54436d776a71e768165716762aec286abbbf83', 195, False)}}, {'1.0-1': {'sdesc': '"A test subpackage"',
            'ldesc': '"A test subpackage"',
            'category': 'Devel',
            'external-source': 'testpackage'}}, {}, False),
- 'testpackage2-subpackage': Package('x86/release/testpackage2/testpackage2-subpackage', {'1.0-1': {'testpackage2-subpackage-1.0-1.tar.bz2': Tar('6de201dfed1d45412509c65deb34690dc2d09c6aafccfe491fd2f440f92842b9c755b61dc7bcdd4cc0c9f18cf46c2b3a1241e99c4c2a33fff5555e7b2f0b6348', 14, True)}}, {'1.0-1': {'sdesc': '"A test subpackage 2"',
+ 'testpackage2-subpackage': Package('testpackage2/testpackage2-subpackage', {'1.0-1': {'testpackage2-subpackage-1.0-1.tar.bz2': Tar('testpackage2-subpackage-1.0-1.tar.bz2', 'x86/release/testpackage2/testpackage2-subpackage', '6de201dfed1d45412509c65deb34690dc2d09c6aafccfe491fd2f440f92842b9c755b61dc7bcdd4cc0c9f18cf46c2b3a1241e99c4c2a33fff5555e7b2f0b6348', 14, True)}}, {'1.0-1': {'sdesc': '"A test subpackage 2"',
            'ldesc': '"A test subpackage 2"',
            'category': 'Devel'}}, {}, False)}


^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2019-06-07 10:16 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2019-06-07 10:16 [calm - Cygwin server-side packaging maintenance script] branch master, updated. 20190530-15-g8118f97 jturney

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).