]> git.e2factory.org Git - e2factory.git/commitdiff
Add source module and refactor e2source config handling.
authorTobias Ulmer <tu@emlix.com>
Tue, 24 Jun 2014 17:23:56 +0000 (19:23 +0200)
committerTobias Ulmer <tu@emlix.com>
Wed, 16 Nov 2016 14:41:18 +0000 (15:41 +0100)
* Introduce a source module with a basic_source class, which all source
  plugins have to implement.
* Move info.sources to source module.
* Turn e2source config tables into defined objects with clear interface.
* Convert all source plugins to class interface, cleaning source
  validation up.
* Improve error checking and detect typos in source configs.
* Add a stringlist module providing a ADT for the common case of dealing
  with arrays of strings.
* Fix files plugin calculation of sourceid when using per-file licences
  entries
* Many small fixes...

Signed-off-by: Tobias Ulmer <tu@emlix.com>
12 files changed:
generic/e2lib.lua
local/Makefile
local/e2-fetch-sources.lua
local/e2-ls-project.lua
local/e2tool.lua
local/scm.lua
local/sl.lua [new file with mode: 0644]
local/source.lua [new file with mode: 0644]
plugins/cvs.lua
plugins/files.lua
plugins/git.lua
plugins/svn.lua

index a66b8680c3a8548dd4a599b24020ad50de635705..7e51d2021e972b53f7b8e11b2b46d99f9432c3d2 100644 (file)
@@ -2458,11 +2458,11 @@ function e2lib.vrfy_dict_exp_keys(t, name, ekeyvec)
     for k,_ in pairs(t) do
         if not lookup[k] then
             if not e then
-                e = err.new("table %s contains unexpected key %q",
-                    name, tostring(k))
+                e = err.new("unexpected key %q in %s",
+                    tostring(k), name)
             else
-                e:append("table %s contains unexpected key %q",
-                    name, tostring(k))
+                e = err.new("unexpected key %q in %s",
+                    tostring(k), name)
             end
         end
     end
index 7da1dd6a98b20a9d6cf0384325220233b8f349da..cd0363c24d73b017ca14bca1d8499ebfc17c1104 100644 (file)
@@ -41,7 +41,8 @@ LOCALLUATOOLS = e2-build e2-dlist e2-dsort e2-fetch-sources \
                e2-build-numbers e2-cf e2-help
 
 LOCALLUALIBS= digest.lua e2build.lua e2tool.lua environment.lua \
-             policy.lua scm.lua licence.lua chroot.lua project.lua
+             policy.lua scm.lua licence.lua chroot.lua project.lua \
+             source.lua sl.lua
 LOCALTOOLS = $(LOCALLUATOOLS)
 
 .PHONY: all install uninstall local install-local doc install-doc
index 9491891dfac411a3b9f95063da3c9a36cd0e39e8..ee52711276a12faf0c111608c80249eaac2302b8 100644 (file)
@@ -37,6 +37,7 @@ local e2tool = require("e2tool")
 local err = require("err")
 local scm = require("scm")
 local chroot = require("chroot")
+local source = require("source")
 
 local function e2_fetch_source(arg)
     local rc, re = e2lib.init()
@@ -138,16 +139,16 @@ local function e2_fetch_source(arg)
         local e = err.new()  -- no message yet, append the summary later on
 
         -- fetch
-        for _, s in pairs(info.sources) do
-            local has_wc = scm.has_working_copy(info, s.name)
-            local wc_avail = scm.working_copy_available(info, s.name)
-            if opts.fetch and sel[s.name] then
+        for sourcename, _ in pairs(source.sources) do
+            local has_wc = scm.has_working_copy(info, sourcename)
+            local wc_avail = scm.working_copy_available(info, sourcename)
+            if opts.fetch and sel[sourcename] then
                 if wc_avail then
                     e2lib.logf(1,
-                    "working copy for %s is already available", s.name)
+                    "working copy for %s is already available", sourcename)
                 else
-                    e2lib.logf(1, "fetching working copy for source %s", s.name)
-                    local rc, re = scm.fetch_source(info, s.name)
+                    e2lib.logf(1, "fetching working copy for source %s", sourcename)
+                    local rc, re = scm.fetch_source(info, sourcename)
                     if not rc then
                         e:cat(re)
                     end
@@ -156,15 +157,15 @@ local function e2_fetch_source(arg)
         end
 
         -- update
-        for _, s in pairs(info.sources) do
-            local has_wc = scm.has_working_copy(info, s.name)
-            local wc_avail = scm.working_copy_available(info, s.name)
-            if opts.update and has_wc and sel[s.name] then
+        for sourcename, _ in pairs(source.sources) do
+            local has_wc = scm.has_working_copy(info, sourcename)
+            local wc_avail = scm.working_copy_available(info, sourcename)
+            if opts.update and has_wc and sel[sourcename] then
                 if not wc_avail then
-                    e2lib.logf(1, "working copy for %s is not available", s.name)
+                    e2lib.logf(1, "working copy for %s is not available", sourcename)
                 else
-                    e2lib.logf(1, "updating working copy for %s", s.name)
-                    local rc, re = scm.update(info, s.name)
+                    e2lib.logf(1, "updating working copy for %s", sourcename)
+                    local rc, re = scm.update(info, sourcename)
                     if not rc then
                         e:cat(re)
                     end
@@ -182,42 +183,42 @@ local function e2_fetch_source(arg)
     local sel = {} -- selected sources
 
     if #arguments > 0 then
-        for _, x in pairs(arguments) do
-            if info.sources[x] and not opts.result then
-                e2lib.logf(3, "is regarded as source: %s", x)
-                sel[x] = x
-            elseif info.results[x] and opts.result then
-                e2lib.logf(3, "is regarded as result: %s", x)
-                local res = info.results[x]
-                for _, s in ipairs(res.sources) do
-                    sel[s] = s
+        for _, srcresname in pairs(arguments) do
+            if source.sources[srcresname] and not opts.result then
+                e2lib.logf(3, "is regarded as source: %s", srcresname)
+                sel[srcresname] = true
+            elseif info.results[srcresname] and opts.result then
+                e2lib.logf(3, "is regarded as result: %s", srcresname)
+                local res = info.results[srcresname]
+                for _, sourcename in ipairs(res.sources) do
+                    sel[sourcename] = true
                 end
             elseif opts.result then
-                error(err.new("is not a result: %s", x))
+                error(err.new("is not a result: %s", srcresname))
             else
-                error(err.new("is not a source: %s", x))
+                error(err.new("is not a source: %s", srcresname))
             end
         end
     elseif opts["all"] then
         -- select all sources
-        for s,src in pairs(info.sources) do
-            sel[s] = s
+        for sourcename, _ in pairs(source.sources) do
+            sel[sourcename] = true
         end
     end
 
     -- select all sources by scm type
-    for s, src in pairs(info.sources) do
-        if select_type[src.type] then
-            sel[s] = s
+    for sourcename, src in pairs(source.sources) do
+        if select_type[src:get_type()] then
+            sel[sourcename] = true
         end
     end
 
 
-    for _, s in pairs(sel) do
-        e2lib.logf(2, "selecting source: %s" , s)
-        local src = info.sources[s]
+    for sourcename, _ in pairs(sel) do
+        e2lib.logf(2, "selecting source: %s" , sourcename)
+        local src = source.sources[sourcename]
         if not src then
-            e:append("selecting invalid source: %s", s)
+            e:append("selecting invalid source: %s", sourcename)
         end
     end
     if e:getcount() > 0 then
index 195f8dcbc11c6b4e9b3e706866f9a9709a297d65..eb822a3303b20dc5086373a90efa904c083719e3 100644 (file)
@@ -41,6 +41,7 @@ local policy = require("policy")
 local scm = require("scm")
 local chroot = require("chroot")
 local project = require("project")
+local source = require("source")
 
 local function e2_ls_project(arg)
     local rc, re = e2lib.init()
@@ -98,16 +99,16 @@ local function e2_ls_project(arg)
 
     local sources = {}
     if opts.all then
-        for s, _ in pairs(info.sources) do
-            table.insert(sources, s)
+        for sourcename, _ in pairs(source.sources) do
+            table.insert(sources, sourcename)
         end
     else
         local yet = {}
         for _, r in pairs(results) do
-            for _, s in ipairs(info.results[r].sources) do
-                if not yet[s] then
-                    table.insert(sources, s)
-                    yet[s] = true
+            for _, sourcename in ipairs(info.results[r].sources) do
+                if not yet[sourcename] then
+                    table.insert(sources, sourcename)
+                    yet[sourcename] = true
                 end
             end
         end
@@ -241,16 +242,15 @@ local function e2_ls_project(arg)
     local s2 = " "
     p1(s1, s2, "src")
     local len = #sources
-    for _, s in pairs(sources) do
-        local src = info.sources[s]
+    for _, sourcename in pairs(sources) do
         len = len - 1
         if len == 0 then
             s2 = " "
         else
             s2 = "|"
         end
-        p2(s1, s2, src.name)
-        local t, re = scm.display(info, src.name)
+        p2(s1, s2, sourcename)
+        local t, re = source.sources[sourcename]:display()
         if not t then
             error(re)
         end
index 2726763943bc1e3669518609861eeefd0898a526..96a4c5e3a8a0c4a9ac3dc5232e88f514c7d12165 100644 (file)
@@ -51,6 +51,7 @@ local transport = require("transport")
 local url = require("url")
 local chroot = require("chroot")
 local project = require("project")
+local source = require("source")
 
 -- Build function table, see end of file for details.
 local e2tool_ftab = {}
@@ -296,9 +297,9 @@ local function check_result(info, resultname)
         e:append("source attribute:")
         e:cat(re)
     else
-        for i,s in ipairs(res.sources) do
-            if not info.sources[s] then
-                e:append("source does not exist: %s", s)
+        for _,sourcename in ipairs(res.sources) do
+            if not source.sources[sourcename] then
+                e:append("source does not exist: %s", sourcename)
             end
         end
     end
@@ -707,68 +708,6 @@ function e2tool.src_res_path_to_name(pathname)
     return pathname:gsub("/", ".")
 end
 
---- Load all source configs. Creates and populates the info.sources dictionary.
--- @param info Info table.
--- @return True on success, false on error.
--- @return Error object on failure.
-local function load_source_configs(info)
-    local rc, re, e
-    local sources, list, path, types
-
-    e = err.new("error loading source configuration")
-    info.sources = {}
-    sources, re = gather_source_paths(info)
-    if not sources then
-        return false, e:cat(re)
-    end
-
-    for _,src in ipairs(sources) do
-        path = e2tool.sourceconfig(src, info.root)
-        types = { "e2source", }
-        rc, re = e2tool.verify_src_res_pathname_valid_chars(src)
-        if not rc then
-            e:append("invalid source file name: %s", src)
-            e:cat(re)
-            return false, e
-        end
-
-        list, re = load_user_config2(info, path, types)
-        if not list then
-            return false, e:cat(re)
-        end
-
-        local name
-        for _,item in ipairs(list) do
-            name = item.data.name
-            item.data.directory = src
-            if not name and #list == 1 then
-                e2lib.warnf("WDEFAULT", "`name' attribute missing in source config.")
-                e2lib.warnf("WDEFAULT", " Defaulting to directory name")
-                item.data.name = e2tool.src_res_path_to_name(src)
-                name = item.data.name
-            end
-
-            if not name then
-                return false, e:append("`name' attribute missing in source config")
-            end
-
-            rc, re = e2tool.verify_src_res_name_valid_chars(name)
-            if not rc then
-                e:append("invalid source name: %s", name)
-                e:cat(re)
-                return false, e
-            end
-
-            if info.sources[name] then
-                return false, e:append("duplicate source: %s", name)
-            end
-
-            info.sources[name] = item.data
-        end
-    end
-    return true
-end
-
 --- Get project-relative directory for a result.
 -- Returns the relative path to the resultdir and optionally a name and prefix
 -- (e.g. prefix/res/name).
@@ -933,43 +872,6 @@ local function load_result_configs(info)
     return true
 end
 
---- check source.
-local function check_source(info, sourcename)
-    local src = info.sources[sourcename]
-    local rc, e, re
-    if not src then
-        e = err.new("no source by that name: %s", sourcename)
-        return false, e
-    end
-    e = err.new("in source: %s", sourcename)
-    if not src.type then
-        e2lib.warnf("WDEFAULT", "in source %s", sourcename)
-        e2lib.warnf("WDEFAULT", " type attribute defaults to `files'")
-        src.type = "files"
-    end
-    rc, re = scm.validate_source(info, sourcename)
-    if not rc then
-        return false, re
-    end
-    return true
-end
-
---- check sources.
-local function check_sources(info)
-    local rc, re
-    local e = err.new("Error while checking sources")
-    for n,s in pairs(info.sources) do
-        rc, re = check_source(info, n)
-        if not rc then
-            e:cat(re)
-        end
-    end
-    if e:getcount() > 1 then
-        return false, e
-    end
-    return true
-end
-
 --- Checks project information for consistancy.
 -- @param info Info table.
 -- @return True on success, false on error.
@@ -977,10 +879,7 @@ end
 local function check_project_info(info)
     local rc, re, e
     e = err.new("error in project configuration")
-    rc, re = check_sources(info)
-    if not rc then
-        return false, e:cat(re)
-    end
+
     rc, re = check_results(info)
     if not rc then
         return false, e:cat(re)
@@ -1110,8 +1009,6 @@ function e2tool.collect_project_info(info, skip_load_config)
         end
     end
 
-    info.sources = {}
-
     -- read environment configuration
     info.env = {}              -- global and result specfic env (deprecated)
     info.env_files = {}   -- a list of environment files
@@ -1141,7 +1038,7 @@ function e2tool.collect_project_info(info, skip_load_config)
     end
 
     -- sources
-    rc, re = load_source_configs(info)
+    rc, re = source.load_source_configs(info)
     if not rc then
         return false, e:cat(re)
     end
@@ -1209,8 +1106,6 @@ function e2tool.collect_project_info(info, skip_load_config)
         end
     end
 
-    --e2tool.add_source_results(info)
-
     -- provide a sorted list of results
     info.results_sorted = {}
     for r,res in pairs(info.results) do
@@ -1218,13 +1113,6 @@ function e2tool.collect_project_info(info, skip_load_config)
     end
     table.sort(info.results_sorted)
 
-    -- provided sorted list of sources
-    info.sources_sorted = {}
-    for s,src in pairs(info.sources) do
-        table.insert(info.sources_sorted, s)
-    end
-    table.sort(info.sources_sorted)
-
     rc, re = policy.init(info)
     if not rc then
         return false, e:cat(re)
@@ -1645,14 +1533,17 @@ function e2tool.pbuildid(info, resultname)
 
     hash.hash_line(hc, r.name)
 
-    for _,s in ipairs(r.sources) do
-        local src = info.sources[s]
-        local source_set = r.build_mode.source_set()
-        local rc, re, sourceid = scm.sourceid(info, s, source_set)
-        if not rc then
+    for _,sourcename in ipairs(r.sources) do
+        local src, sourceid, sourceset
+
+        src = source.sources[sourcename]
+        sourceset = r.build_mode.source_set()
+        sourceid, re = src:sourceid(sourceset)
+        if not sourceid then
             return false, e:cat(re)
         end
-        hash.hash_line(hc, s)                  -- source name
+
+        hash.hash_line(hc, sourcename)         -- source name
         hash.hash_line(hc, sourceid)           -- sourceid
     end
     for _,d in ipairs(r.depends) do
@@ -1728,11 +1619,16 @@ end
 -- @param resultname string: name of a result
 -- @return table: environment variables valid for the result
 function e2tool.env_by_result(info, resultname)
+    assert(type(info) == "table")
+    assert(type(resultname) == "string" and #resultname > 0)
+
+    local src
     local res = info.results[resultname]
     local env = environment.new()
     env:merge(info.global_env, false)
-    for _, s in ipairs(res.sources) do
-        env:merge(info.sources[s]._env, true)
+    for _, sourcename in ipairs(res.sources) do
+        src = source.sources[sourcename]
+        env:merge(src:get_env(), true)
     end
     env:merge(res._env, true)
     return env
index cb38345d8a3a43325ee0386c1795c6c407626120..a17da0157b61714ff39ac0c7b27238a814874d06 100644 (file)
 ]]
 
 local scm = {}
-local e2lib = require("e2lib")
-local environment = require("environment")
+package.loaded["scm"] = scm
 local err = require("err")
-local licence = require("licence")
 local strict = require("strict")
+local source = require("source")
 
 -- scm modules
 local scms = {}
@@ -87,16 +86,21 @@ function scm.register_interface(name)
     end
 
     local function func(info, sourcename, ...)
-        local src = info.sources[sourcename]
+        assert(info)
+        assert(sourcename)
+
+        local typ
         local rc, re, e
+
         e = err.new("calling scm operation failed")
-        if not scms[src.type] then
-            return false, e:append("no such source type: %s", src.type)
+
+        typ = source.sources[sourcename]:get_type()
+        if not scms[typ] then
+            return false, e:append("no such source type: %s", tostring(typ))
         end
-        local f = scms[src.type][name]
+        local f = scms[typ][name]
         if not f then
-            e:append("%s() is not implemented for source type: %s",
-                name, src.type)
+            e:append("%s() is not implemented for source type: %s", name, typ)
             return false, e
         end
         return f(info, sourcename, ...)
@@ -140,120 +144,6 @@ function scm.register_function(scmtype, name, func)
     return true
 end
 
---- apply default values where possible and a source configuration is
--- incomplete
--- @param info the info table
--- @param sourcename the source name
--- @return bool
--- @return an error object on failure
-local function source_apply_default_licences(info, sourcename)
-  local e = err.new("applying default licences failed.")
-  local src = info.sources[ sourcename ]
-
-  if not src.licences and src.licence then
-    e2lib.warnf("WDEPRECATED", "in source %s:", src.name)
-    e2lib.warnf("WDEPRECATED",
-               " licence attribute is deprecated. Replace by licences.")
-    src.licences = src.licence
-  end
-  if src.licences == nil then
-    e2lib.warnf("WDEFAULT", "in source %s:", src.name)
-    e2lib.warnf("WDEFAULT",
-               " licences attribute missing. Defaulting to empty list.")
-    src.licences = {}
-  elseif type(src.licences) == "string" then
-    e2lib.warnf("WDEPRECATED", "in source %s:", src.name)
-    e2lib.warnf("WDEPRECATED",
-               " licences attribute is not in table format. Converting.")
-    src.licences = { src.licences }
-  end
-
-  if type(src.licences) ~= "table" then
-      e:append("licences attribute is of invalid type")
-      return false, e
-  end
-
-  for i, s in pairs(src.licences) do
-    if type(i) ~= "number" or type(s) ~= "string" then
-      e:append("licences attribute is not a list of strings")
-      return false, e
-    end
-  end
-  for _,l in ipairs(src.licences) do
-    if not licence.licences[l] then
-      e:append("unknown licence: %s", l)
-      return false, e
-    end
-  end
-  return true
-end
-
---- validate generic source configuration, usable by SCM plugins
--- @param info the info table
--- @param sourcename the source name
--- @return bool
--- @return an error object on failure
-function scm.generic_source_validate(info, sourcename)
-    local src = info.sources[sourcename]
-    local rc, re
-    local e
-    if not src then
-        return false, err.new("invalid source: %s", sourcename)
-    end
-    e = err.new("in source %s:", sourcename)
-    rc, re = source_apply_default_licences(info, sourcename)
-    if not rc then
-        return false, e:cat(re)
-    end
-    if not src.type then
-        e:append("source has no `type' attribute")
-    end
-    if src.env and type(src.env) ~= "table" then
-        e:append("source has invalid `env' attribute")
-    else
-        if not src.env then
-            e2lib.warnf("WDEFAULT",
-            "source has no `env' attribute. Defaulting to empty dictionary")
-            src.env = {}
-        end
-        src._env = environment.new()
-        for k,v in pairs(src.env) do
-            if type(k) ~= "string" then
-                e:append("in `env' dictionary: key is not a string: %s", tostring(k))
-            elseif type(v) ~= "string" then
-                e:append("in `env' dictionary: value is not a string: %s", tostring(v))
-            else
-                src._env:set(k, v)
-            end
-        end
-    end
-    if e:getcount() > 1 then
-        return false, e
-    end
-    return true, nil
-end
-
---- apply default values where possible
--- @param info the info table
--- @param sourcename the source name
--- @return bool
--- @return an error object on failure
-function scm.generic_source_default_working(info, sourcename)
-    local src
-
-    src = info.sources[sourcename]
-
-    if not src.working then
-        src.working = e2lib.join("in", sourcename)
-
-        e2lib.warnf("WDEFAULT", "in source %s:", sourcename)
-        e2lib.warnf("WDEFAULT", " `working' attribute defaults to '%s'.",
-            src.working)
-    end
-
-    return true
-end
-
 --- do some consistency checks required before using sources
 -- @param info
 -- @param sourcename string: source name
@@ -274,15 +164,12 @@ function scm.generic_source_check(info, sourcename, require_workingcopy)
     return true, nil
 end
 
-scm.register_interface("sourceid")
-scm.register_interface("validate_source")
 scm.register_interface("toresult")
 scm.register_interface("prepare_source")
 scm.register_interface("fetch_source")
 scm.register_interface("update")
 scm.register_interface("check_workingcopy")
 scm.register_interface("working_copy_available")
-scm.register_interface("display")
 scm.register_interface("has_working_copy")
 
 return strict.lock(scm)
diff --git a/local/sl.lua b/local/sl.lua
new file mode 100644 (file)
index 0000000..cc9b87e
--- /dev/null
@@ -0,0 +1,276 @@
+--- Universal string list. Handy for storing result-, licence-, source names.
+-- @module local.sl
+
+-- Copyright (C) 2014 emlix GmbH, see file AUTHORS
+--
+-- This file is part of e2factory, the emlix embedded build system.
+-- For more information see http://www.e2factory.org
+--
+-- e2factory is a registered trademark of emlix GmbH.
+--
+-- e2factory is free software: you can redistribute it and/or modify it under
+-- the terms of the GNU General Public License as published by the
+-- Free Software Foundation, either version 3 of the License, or (at your
+-- option) any later version.
+--
+-- This program is distributed in the hope that it will be useful, but WITHOUT
+-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+-- FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+-- more details.
+
+local sl = {}
+local class = require("class")
+local err = require("err")
+local e2lib = require("e2lib")
+local strict = require("strict")
+
+-- ----------------------------------------------------------------------------
+-- There is plenty of optimization potential here, however the string lists
+-- are usually very small: 0 - 100 entries. Don't waste time.
+-- ----------------------------------------------------------------------------
+
+--- Class "sl" for keeping string lists.
+-- Trying to use string list with anything but strings throws an exception.
+sl.sl = class("sl")
+
+--- Initialize string list [sl:new()]. Merge and unique can't be set both.
+-- @param merge Whether entries are to be merged, defaults to false (boolean).
+-- @param unique Whether inserting duplicate entries raises errors,
+--               defaults to false (boolean).
+function sl.sl:initialize(merge, unique)
+    assert(merge == nil or type(merge) == "boolean")
+    assert(unique == nil or type(unique) == "boolean")
+    assert(not (merge and unique))
+
+    self._merge = merge or false
+    self._unique = unique or false
+    self._list = {}
+end
+
+--- Insert an entry into the string list.
+-- @param entry The entry.
+-- @return True on success, false when the entry is not unique.
+function sl.sl:insert(entry)
+    assert(type(entry) == "string")
+
+    if self._merge then
+        if self:lookup(entry) then
+            return true
+        end
+    elseif self._unique then
+        if self:lookup(entry) then
+            return false
+        end
+    end
+    table.insert(self._list, entry)
+    return true
+end
+
+--- Remove *all* matching entries from the string list.
+-- @param entry The entry.
+-- @return True when one or more entries were removed, false otherwise.
+function sl.sl:remove(entry)
+    assert(type(entry) == "string")
+    local changed, i
+
+    changed = false
+    i = 1
+    while self._list[i] do
+        if self._list[i] == entry then
+            table.remove(self._list, i)
+            changed = true
+        else
+            i = i+1
+        end
+    end
+
+    return changed
+end
+
+--- Check whether entry is in string list.
+-- @param entry The search entry.
+-- @return True if entry is in the string list, false otherwise.
+function sl.sl:lookup(entry)
+    assert(type(entry) == "string")
+
+    for k, v in ipairs(self._list) do
+        if v == entry then
+            return true
+        end
+    end
+
+    return false
+end
+
+--- Return the number of entries in the string list.
+-- @return Number of entries, 0 if empty.
+function sl.sl:size()
+    return #self._list
+end
+
+--- Iterate through the string list in insertion order.
+-- @return Iterator function.
+function sl.sl:iter_inserted()
+
+    local i = 0
+
+    return function()
+        i = i + 1
+        return self._list[i]
+    end
+end
+
+--- Iterate through the string list in alphabetical order.
+-- @return Iterator function.
+function sl.sl:iter_sorted()
+    local t = {}
+    local i = 0
+
+    for _,v in ipairs(self._list) do
+        table.insert(t, v)
+    end
+    table.sort(t)
+
+    return function()
+        i = i + 1
+        return t[i]
+    end
+end
+
+--- Create in independent string list copy.
+-- @return New string list object.
+function sl.sl:copy()
+    local c = sl.sl:new(self._merge, self._unique)
+    for e in self:iter_inserted() do
+        assert(c:insert(e))
+    end
+    assert(self:size() == c:size())
+    return c
+end
+
+--- Concatenate the string list in alphabetical order.
+-- @param sep Separator, defaults to empty string.
+-- @return Concatenated string.
+function sl.sl:concat_sorted(sep)
+    assert(sep == nil or type(sep) == "string")
+    local first = true
+    local cat = ""
+    sep = sep or ""
+
+    for e in self:iter_sorted() do
+        if first then
+            cat = e
+            first = false
+        else
+            cat = cat..sep..e
+        end
+    end
+
+    return cat
+end
+
+--- Return string list entries as an array, in insertion order.
+-- @return Array in insertion order.
+function sl.sl:totable_inserted()
+    local t = {}
+    for _,v in ipairs(self._list) do
+        table.insert(t, v)
+    end
+    return t
+end
+
+--- Return string list entries as an array, in insertion order.
+-- @return Array in insertion order.
+function sl.sl:totable_sorted()
+    return table.sort(self:totable_inserted())
+end
+
+--[[
+local function selftest()
+    local s1 = sl.sl:new()
+
+    assert(s1:size() == 0)
+    assert(s1.class.name == "sl")
+
+    s1:insert("ccc")
+    s1:insert("bbb")
+    s1:insert("aaa")
+    s1:insert("aaa")
+
+    assert(s1:size() == 4)
+
+    local c = 1
+    for entry in s1:iter_inserted() do
+        assert(c <= s1:size() and c > 0)
+        if c == 1 then assert(entry == "ccc") end
+        if c == 2 then assert(entry == "bbb") end
+        if c == 3 then assert(entry == "aaa") end
+        if c == 4 then assert(entry == "aaa") end
+        c = c+1
+    end
+
+    assert(s1:lookup("foo") == false)
+    assert(s1:lookup("bbb") == true)
+
+    s1:insert("xxx")
+    assert(s1:size() == 5)
+    c = 1
+    for entry in s1:iter_sorted() do
+        assert(c <= s1:size() and c > 0)
+        if c == 1 then assert(entry == "aaa") end
+        if c == 2 then assert(entry == "aaa") end
+        if c == 3 then assert(entry == "bbb") end
+        if c == 4 then assert(entry == "ccc") end
+        if c == 5 then assert(entry == "xxx") end
+        c = c+1
+    end
+
+    assert(s1:remove("doesnotexist") == false)
+    assert(s1:remove("aaa") == true)
+    assert(s1:size() == 3)
+    c = 1
+    for entry in s1:iter_sorted() do
+        assert(c <= s1:size() and c > 0)
+        --e2lib.logf(1, "entry=%s", entry)
+        if c == 1 then assert(entry == "bbb") end
+        if c == 2 then assert(entry == "ccc") end
+        if c == 3 then assert(entry == "xxx") end
+        c = c+1
+    end
+
+    assert(s1:concat_sorted() == "bbbcccxxx")
+    assert(s1:concat_sorted("y") == "bbbycccyxxx")
+
+    local s2 = sl.sl:new(false, true)
+
+    c = false
+    for _,v in ipairs({"bbb", "aaa", "xxx", "foo", "bla", "bar", "xxx"}) do
+        if not s2:insert(v) then
+            c = true
+            assert(v == "xxx")
+        end
+    end
+    assert(c == true)
+
+    local s3 = sl.sl:new(true, false)
+
+    for _,v in ipairs({"bbb", "aaa", "xxx", "foo", "bar", "bar", "xxx", "y"}) do
+        assert(s3:insert(v) == true)
+    end
+    assert(s3:size() == 6)
+
+    local s4 = sl.sl:new()
+    s4:insert("")
+    s4:insert("")
+    s4:insert("")
+
+    assert(s4:concat_sorted() == "")
+    assert(s4:concat_sorted("x") == "xx")
+end
+
+selftest()
+--]]
+
+return strict.lock(sl)
+
+-- vim:sw=4:sts=4:et:
diff --git a/local/source.lua b/local/source.lua
new file mode 100644 (file)
index 0000000..7b79ecc
--- /dev/null
@@ -0,0 +1,433 @@
+--- Source base class. Implements the base source class and config loader.
+-- @module local.source
+
+-- Copyright (C) 2007-2014 emlix GmbH, see file AUTHORS
+--
+-- This file is part of e2factory, the emlix embedded build system.
+-- For more information see http://www.e2factory.org
+--
+-- e2factory is a registered trademark of emlix GmbH.
+--
+-- e2factory is free software: you can redistribute it and/or modify it under
+-- the terms of the GNU General Public License as published by the
+-- Free Software Foundation, either version 3 of the License, or (at your
+-- option) any later version.
+--
+-- This program is distributed in the hope that it will be useful, but WITHOUT
+-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+-- FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+-- more details.
+
+local source = {}
+package.loaded["source"] = source
+
+local cache = require("cache")
+local class = require("class")
+local e2lib = require("e2lib")
+local e2tool = require("e2tool")
+local environment = require("environment")
+local err = require("err")
+local licence = require("licence")
+local sl = require("sl")
+local strict = require("strict")
+
+--- Dictionary indexed by source type, derived source class.
+local source_types = {}
+
+--- Source base class.
+source.basic_source = class("basic_source")
+
+--- Source base constructor. Assert error on invalid input.
+-- @param rawsrc Source config dict containing at least "name" and "type"
+-- attributes.
+function source.basic_source:initialize(rawsrc)
+    assert(type(rawsrc) == "table")
+    assert(type(rawsrc.name) == "string" and rawsrc.name ~= "")
+    assert(type(rawsrc.type) == "string" and rawsrc.type ~= "")
+
+    self._name = rawsrc.name
+    self._type = rawsrc.type
+    self._licences = false
+    self._env = false
+end
+
+--- Get name.
+-- @return Name of source (Ex: group.result).
+function source.basic_source:get_name()
+    assert(type(self._name) == "string")
+    return self._name
+end
+
+--- Get name as directory path.
+-- @return Path of source (ex: group/result).
+function source.basic_source:get_name_as_path()
+    assert(type(self._name) == "string")
+    local p = e2tool.src_res_name_to_path(self._name)
+    assert(type(p) == "string")
+    return p
+end
+
+--- Get type of source.
+-- @return Type of source (ex: "files", "git", ...)
+function source.basic_source:get_type()
+    return self._type
+end
+
+--- Set licence array.
+-- @param licences String list of licence names (sl).
+function source.basic_source:set_licences(licences)
+    assert(type(licences) == "table" and licences.class.name == "sl")
+    self._licences = licences:copy()
+end
+
+--- Get licence array. Must be set before calling get_licences(). Note that
+-- this returns all licences used in a source. Some sources may have more
+-- detailed licensing information which can be accessed by other means.
+-- @return String list of licence names (sl).
+function source.basic_source:get_licences()
+    assert(type(self._licences) == "table")
+    return self._licences:copy()
+end
+
+--- Set env object.
+-- @param env Env object.
+function source.basic_source:set_env(env)
+    assert(type(env) == "table")
+    self._env = env
+end
+
+--- Get env object. Must be set before calling get_env().
+-- @return Env object.
+function source.basic_source:get_env()
+    assert(type(self._env) == "table")
+    return self._env
+end
+
+--- Abstract sourceid method. Every child class must overwrite this
+-- method with an implementation. Calling this method throws an error.
+-- @param sourceset Source set (ex: "tag", "branch", ...)
+-- @return Sourceid string (usually a hash value) or false on error.
+-- @return Error object on failure.
+function source.basic_source:sourceid(sourceset)
+    error(err.new("called sourceid() of source base class, type %s name %s",
+        self._type, self._name))
+end
+
+--- Abstract display method. Every child class must overwrite this
+-- method with an implementation. Calling this method throws an error.
+-- @return Array of strings containing free form information about source.
+function source.basic_source:display()
+    error(err.new("called display() of source base class, type %s name %s",
+        self._type, self._name))
+end
+
+--- Dictionary holding all source objects indexed by their name.
+source.sources = {}
+--- Array holding all source objects in alphabetical order.
+source.sources_sorted = {}
+
+--- Gather source paths.
+-- @param info Info table.
+-- @param basedir Nil or directory from where to start scanning for more
+--                sources. Only for recursion.
+-- @param sources Nil or table of source paths. Only for recursion.
+-- @return Table with source paths, or false on error.
+-- @return Error object on failure.
+local function gather_source_paths(info, basedir, sources)
+    local rc, re
+    local currdir, sdir, sconfig, s
+    sources = sources or {}
+
+    currdir = e2tool.sourcedir(basedir, info.root)
+    for entry, re in e2lib.directory(currdir) do
+        if not entry then
+            return false, re
+        end
+
+        if basedir then
+            entry = e2lib.join(basedir, entry)
+        end
+
+        sdir = e2tool.sourcedir(entry, info.root)
+        sconfig = e2tool.sourceconfig(entry, info.root)
+        s = e2lib.stat(sdir, false)
+        if s.type == "directory" then
+            if e2lib.exists(sconfig) then
+                table.insert(sources, entry)
+            else
+                -- try sub directory
+                rc, re = gather_source_paths(info, entry, sources)
+                if not rc then
+                    return false, re
+                end
+            end
+        end
+    end
+
+    return sources
+end
+
+--- Search, load and verify all source configs. On success, all sources
+--available as objects in source.sources[] etc.
+-- @param info Info table
+-- @return True on success, false on error.
+-- @return Error object on failure.
+function source.load_source_configs(info)
+    local rc, re, e
+    local g, rawsrc, loadcnt, configs, path, src
+
+    e = err.new("error loading source configuration")
+
+    configs, re = gather_source_paths(info)
+    if not configs then
+        return false, e:cat(re)
+    end
+
+    for _,cfg in ipairs(configs) do
+        rc, re = e2tool.verify_src_res_pathname_valid_chars(cfg)
+        if not rc then
+            e:append("invalid source file name: %s", cfg)
+            e:cat(re)
+            return false, e
+        end
+
+        rawsrc = nil
+        loadcnt = 0
+        g = {
+            e2source = function(data) rawsrc = data loadcnt = loadcnt + 1 end,
+            env = info.env,
+            string = e2lib.safe_string_table(),
+        }
+
+        path = e2tool.sourceconfig(cfg, info.root)
+        rc, re = e2lib.dofile2(path, g)
+        if not rc then
+            return false, e:cat(re)
+        end
+
+        if type(rawsrc) ~= "table" then
+            return false, e:append("source %q is missing an e2source table", cfg)
+        end
+
+        if loadcnt > 1 then
+            return false, e:append("duplicate source config in %q", cfg)
+        end
+
+        if not rawsrc.name then
+            e2lib.warnf("WDEFAULT", "`name' attribute missing in source config.")
+            e2lib.warnf("WDEFAULT", " Defaulting to directory name")
+            rawsrc.name = e2tool.src_res_path_to_name(cfg)
+        end
+
+        if rawsrc.name ~= e2tool.src_res_path_to_name(cfg) then
+            return false, e:append(
+                "source name %q must match source directory name %q",
+                rawsrc.name, e2tool.src_res_path_to_name(cfg))
+        end
+
+        rc, re = e2tool.verify_src_res_name_valid_chars(rawsrc.name)
+        if not rc then
+            e:append("invalid source name: %s", rawsrc.name)
+            e:cat(re)
+            return false, e
+        end
+
+        if source.sources[rawsrc.name] then
+            return false, e:append("duplicate source: %s", rawsrc.name)
+        end
+
+        -- source with no type field is treated as file source
+        if not rawsrc.type then
+            rawsrc.type = "files"
+            e2lib.warnf("WDEFAULT", "in source %s", rawsrc.name)
+            e2lib.warnf("WDEFAULT", " type attribute defaults to `files'")
+        end
+
+        if not source_types[rawsrc.type] then
+            return false,
+                e:append("don't know how to handle %q source", rawsrc.type)
+        end
+
+        src = source_types[rawsrc.type]
+
+        -- src:new(rawsrc)
+        rc, re = e2lib.trycall(src.new, src, rawsrc)
+        if not rc then
+            e = err.new("error in source %q", rawsrc.name)
+            return false, e:cat(re)
+        end
+
+        src = re
+        assert(type(src) == "table")
+        source.sources[src:get_name()] = src
+    end
+
+    for sourcename,_ in pairs(source.sources) do
+        table.insert(source.sources_sorted, sourcename)
+    end
+    table.sort(source.sources_sorted)
+
+    return true
+end
+
+--- Register a source class. A type can only be registered once.
+-- @param typ Source type name.
+-- @param source_class Class derived from basic_source.
+-- @return True on success, false on error.
+-- @return Error object on failure.
+function source.register_source_class(typ, source_class)
+    assert(type(typ) == "string" and typ ~= "")
+    assert(type(source_class) == "table")
+
+    if source_types[typ] then
+        return false, err.new("source %q already registered", typ)
+    end
+
+    source_types[typ] = source_class
+
+    return true
+end
+
+--- Validate licences attribute in rawsrc and set licences in src if successful.
+-- @param rawsrc e2source config table
+-- @param src Object of class basic_source.
+-- @return True on success, false on error.
+-- @return Error object on failure.
+function source.generic_source_validate_licences(rawsrc, src)
+    assert(type(rawsrc) == "table" and rawsrc.name and rawsrc.type)
+    assert(type(src) == "table")
+
+    local rc, re, licences
+
+    licences = sl.sl:new(false, true --[[unique]])
+
+    --[[if not rawsrc.licences and rawsrc.licence then
+        e2lib.warnf("WDEPRECATED", "in source %s:", src.name)
+        e2lib.warnf("WDEPRECATED",
+        " licence attribute is deprecated. Replace by licences.")
+        src.licences = src.licence
+    end]]
+
+    if rawsrc.licences == nil then
+        e2lib.warnf("WDEFAULT", "in source %s:", rawsrc.name)
+        e2lib.warnf("WDEFAULT",
+            " licences attribute missing. Defaulting to empty list.")
+        rawsrc.licences = {}
+    elseif type(rawsrc.licences) == "string" then
+        e2lib.warnf("WDEPRECATED", "in source %s:", rawsrc.name)
+        e2lib.warnf("WDEPRECATED",
+            " licences attribute is not in table format. Converting.")
+        rawsrc.licences = { rawsrc.licences }
+    end
+
+    if type(rawsrc.licences) ~= "table" then
+        return false, err.new("licences attribute must be a table")
+    end
+
+    rc, re = e2lib.vrfy_listofstrings(rawsrc.licences, "licences attribute",
+        true, false)
+    if not rc then
+        return false, re
+    end
+
+    for _,licencename in ipairs(rawsrc.licences) do
+        if not licence.licences[licencename] then
+            return false, err.new("unknown licence: %s", licencename)
+        end
+        assert(licences:insert(licencename))
+    end
+
+    src:set_licences(licences)
+
+    return true
+end
+
+--- Helper to validate and set env in src.
+-- @param rawsrc e2source config table.
+-- @param src Source object.
+-- @return True on success, false on error.
+-- @return Error object on failure.
+function source.generic_source_validate_env(rawsrc, src)
+    assert(type(rawsrc) == "table" and rawsrc.name and rawsrc.type)
+    assert(type(src) == "table")
+
+    local newenv = environment.new()
+
+    if rawsrc.env ~= nil and type(rawsrc.env) ~= "table" then
+        return false, err.new("source has invalid `env' attribute")
+    end
+
+    if not rawsrc.env then
+        e2lib.warnf("WDEFAULT",
+            "source has no `env' attribute. Defaulting to empty dictionary")
+        rawsrc.env = {}
+    end
+
+    for k, v in pairs(rawsrc.env) do
+        if type(k) ~= "string" then
+            return false, err.new(
+                "in `env' dictionary: key is not a string: %s", tostring(k))
+        elseif type(v) ~= "string" then
+            return false, err.new(
+                "in `env' dictionary: value is not a string: %s", tostring(v))
+        else
+            newenv:set(k, v)
+        end
+    end
+
+    src:set_env(newenv)
+
+    return true
+end
+
+--- Helper to validate server.
+-- @param rawsrc e2source config table
+-- @param ismandatory Whether rawsrc containing a server attr is mandatory.
+-- @return True on success, false on error.
+-- @return Error object on failure.
+function source.generic_source_validate_server(rawsrc, ismandatory)
+    assert(type(rawsrc) == "table" and rawsrc.name and rawsrc.type)
+    assert(type(ismandatory) == "boolean")
+
+    local info = e2tool.info()
+
+    if ismandatory and rawsrc.server == nil then
+        return false, err.new("source has no `server' attribute")
+    end
+
+    if rawsrc.server ~= nil and type(rawsrc.server) ~= "string" then
+        return false, err.new("'server' attribute must be a string")
+    end
+
+    if rawsrc.server and (not cache.valid_server(info.cache, rawsrc.server)) then
+        return false, err.new("invalid server: %s", rawsrc.server)
+    end
+
+    return true
+end
+
+--- Helper to validate working attribute.
+-- @param rawsrc e2source config table
+-- @return True on success, false on error.
+-- @return Error object on failure.
+function source.generic_source_validate_working(rawsrc)
+    assert(type(rawsrc) == "table" and rawsrc.name and rawsrc.type)
+
+    if rawsrc.working ~= nil and not type(rawsrc.working) == "string" then
+        return false, err.new("'working' attribute must be a string")
+    end
+
+    if rawsrc.working == nil then
+        rawsrc.working = e2lib.join("in", rawsrc.name)
+
+        e2lib.warnf("WDEFAULT", "in source %s:", rawsrc.name)
+        e2lib.warnf("WDEFAULT", " `working' attribute defaults to '%s'.",
+            rawsrc.working)
+    end
+
+    return true
+end
+
+return strict.lock(source)
+
+-- vim:sw=4:sts=4:et:
index 944b5e89fe0e54fedcd6d45bb349b58acba749ff..d0eedacd0147127943a439127a35b91f339782bc 100644 (file)
@@ -30,7 +30,9 @@
 
 local cvs = {}
 local cache = require("cache")
+local class = require("class")
 local e2lib = require("e2lib")
+local e2tool = require("e2tool")
 local eio = require("eio")
 local err = require("err")
 local hash = require("hash")
@@ -39,13 +41,30 @@ local scm = require("scm")
 local strict = require("strict")
 local tools = require("tools")
 local url = require("url")
+local source = require("source")
 
 plugin_descriptor = {
     description = "CVS SCM Plugin",
-    init = function (ctx) scm.register("cvs", cvs) return true end,
+    init = function (ctx)
+        local rc, re
+
+        rc, re = source.register_source_class("cvs", cvs.cvs_source)
+        if not rc then
+            return false, re
+        end
+
+        rc, re = scm.register("cvs", cvs)
+        if not rc then
+            return false, re
+        end
+
+        return true
+    end,
     exit = function (ctx) return true end,
 }
 
+cvs.cvs_source = class("cvs_source", source.basic_source)
+
 local function cvs_tool(argv, workdir)
     local rc, re, cvscmd, cvsflags, rsh
 
@@ -77,65 +96,196 @@ local function cvs_tool(argv, workdir)
     return e2lib.callcmd_log(cvscmd, workdir, { CVS_RSH=rsh })
 end
 
---- validate source configuration, log errors to the debug log
--- @param info the info table
--- @param sourcename the source name
--- @return bool
-function cvs.validate_source(info, sourcename)
-    local rc, re = scm.generic_source_validate(info, sourcename)
+
+function cvs.cvs_source:initialize(rawsrc)
+    assert(type(rawsrc) == "table")
+    assert(type(rawsrc.name) == "string" and #rawsrc.name > 0)
+    assert(type(rawsrc.type) == "string" and rawsrc.type == "cvs")
+
+    local rc, re
+
+    source.basic_source.initialize(self, rawsrc)
+
+    self._branch = false
+    self._cvsroot = false
+    self._module = false
+    self._server = false
+    self._tag = false
+    self._working = false
+    self._sourceids = {
+        ["working-copy"] = "working-copy",
+    }
+
+    rc, re = e2lib.vrfy_dict_exp_keys(rawsrc, "e2source", {
+        "branch",
+        "cvsroot",
+        "env",
+        "licences",
+        "module",
+        "name",
+        "server",
+        "tag",
+        "type",
+        "working",
+    })
     if not rc then
-        -- error in generic configuration. Don't try to go on.
-        return false, re
+        error(re)
     end
-    local src = info.sources[ sourcename ]
-    if not src.sourceid then
-        src.sourceid = {}
+
+    rc, re = source.generic_source_validate_licences(rawsrc, self)
+    if not rc then
+        error(re)
     end
-    local e = err.new("in source %s:", sourcename)
-    rc, re = scm.generic_source_default_working(info, sourcename)
+
+    rc, re = source.generic_source_validate_env(rawsrc, self)
     if not rc then
-        return false, e:cat(re)
+        error(re)
     end
-    e:setcount(0)
-    -- XXX should move the default value out of the validate function
-    if not src.server then
-        e:append("source has no `server' attribute")
+
+    rc, re = source.generic_source_validate_server(rawsrc, true)
+    if not rc then
+        error(re)
     end
-    if not src.licences then
-        e:append("source has no `licences' attribute")
+    self._server = rawsrc.server
+
+    rc, re = source.generic_source_validate_working(rawsrc)
+    if not rc then
+        error(re)
     end
-    if not src.cvsroot then
+    self._working = rawsrc.working
+
+    if rawsrc.cvsroot == nil then
         e2lib.warnf("WDEFAULT", "in source %s:", sourcename)
         e2lib.warnf("WDEFAULT",
         " source has no `cvsroot' attribute, defaulting to the server path")
-        src.cvsroot = "."
-    end
-    if not src.cvsroot then
-        e:append("source has no `cvsroot' attribute")
-    end
-    if src.remote then
-        e:append("source has `remote' attribute, not allowed for cvs sources")
+        self._cvsroot = "."
+    elseif type(rawsrc.cvsroot) == "string" then
+        self._cvsroot = rawsrc.cvsroot
+    else
+        error(err.new("'cvsroot' must be a string"))
     end
-    if not src.branch then
-        e:append("source has no `branch' attribute")
+
+    for _,attr in ipairs({ "branch", "module", "tag" }) do
+        if rawsrc[attr] == nil then
+            error(err.new("source has no `%s' attribute", attr))
+        elseif type(rawsrc[attr]) ~= "string" then
+            error(err.new("'%s' must be a string", attr))
+        elseif rawsrc[attr] == "" then
+            error(err.new("'%s' may not be empty", attr))
+        end
     end
-    if type(src.tag) ~= "string" then
-        e:append("source has no `tag' attribute or tag attribute has wrong type")
+    self._branch = rawsrc.branch
+    self._module = rawsrc.module
+    self._tag = rawsrc.tag
+end
+
+function cvs.cvs_source:get_working()
+    assert(type(self._working) == "string")
+
+    return self._working
+end
+
+function cvs.cvs_source:get_module()
+    assert(type(self._module) == "string")
+
+    return self._module
+end
+
+function cvs.cvs_source:get_branch()
+    assert(type(self._branch) == "string")
+
+    return self._branch
+end
+
+function cvs.cvs_source:get_tag()
+    assert(type(self._tag) == "string")
+
+    return self._tag
+end
+
+function cvs.cvs_source:get_server()
+    assert(type(self._server) == "string")
+
+    return self._server
+end
+
+function cvs.cvs_source:get_cvsroot()
+    assert(type(self._cvsroot) == "string")
+
+    return self._cvsroot
+end
+
+function cvs.cvs_source:sourceid(sourceset)
+    assert(type(sourceset) == "string" and #sourceset > 0)
+
+    local rc, re, hc, lid, info, licences
+
+    if self._sourceids[sourceset] then
+        return self._sourceids[sourceset]
     end
-    if not src.module then
-        e:append("source has no `module' attribute")
+
+    info = e2tool.info()
+    assert(type(info) == "table")
+
+    hc = hash.hash_start()
+    hash.hash_line(hc, self._name)
+    hash.hash_line(hc, self._type)
+    hash.hash_line(hc, self._env:id())
+    licences = self:get_licences()
+    for licencename in licences:iter_sorted() do
+        lid, re = licence.licences[licencename]:licenceid(info)
+        if not lid then
+            return false, re
+        end
+        hash.hash_line(hc, lid)
     end
-    if not src.working then
-        e:append("source has no `working' attribute")
+    -- cvs specific
+    if sourceset == "tag" and self._tag ~= "^" then
+        -- we rely on tags being unique with cvs
+        hash.hash_line(hc, self._tag)
+    else
+        -- the old function took a hash of the CVS/Entries file, but
+        -- forgot the subdirecties' CVS/Entries files. We might
+        -- reimplement that once...
+        return false, err.new("cannot calculate sourceid for source set %s",
+            sourceset)
     end
-    local rc, re = tools.check_tool("cvs")
-    if not rc then
-        e:cat(re)
+    hash.hash_line(hc, self._server)
+    hash.hash_line(hc, self._cvsroot)
+    hash.hash_line(hc, self._module)
+
+    self._sourceids[sourceset] = hash.hash_finish(hc)
+
+    return self._sourceids[sourceset]
+end
+
+function cvs.cvs_source:display()
+    local licences
+    local d = {}
+
+    self:sourceid("tag")
+    self:sourceid("branch")
+
+    table.insert(d, string.format("type       = %s", self:get_type()))
+    table.insert(d, string.format("branch     = %s", self._branch))
+    table.insert(d, string.format("tag        = %s", self._tag))
+    table.insert(d, string.format("server     = %s", self._server))
+    table.insert(d, string.format("cvsroot    = %s", self._cvsroot))
+    table.insert(d, string.format("module     = %s", self._module))
+    table.insert(d, string.format("working    = %s", self._working))
+
+    licences = self:get_licences()
+    for licencename in licences:iter_sorted() do
+        table.insert(d, string.format("licence    = %s", licencename))
     end
-    if e:getcount() > 0 then
-        return false, e
+
+    for sourceset, sid in pairs(self._sourceids) do
+        if sid then
+            table.insert(d, string.format("sourceid [%s] = %s", sourceset, sid))
+        end
     end
-    return true, nil
+
+    return d
 end
 
 --- Build the cvsroot string.
@@ -146,9 +296,9 @@ end
 local function mkcvsroot(info, sourcename)
     local cvsroot, src, surl, u, re
 
-    src = info.sources[sourcename]
+    src = source.sources[sourcename]
 
-    surl, re = cache.remote_url(info.cache, src.server, src.cvsroot)
+    surl, re = cache.remote_url(info.cache, src:get_server(), src:get_cvsroot())
     if not surl then
         return false, e:cat(re)
     end
@@ -176,7 +326,7 @@ function cvs.fetch_source(info, sourcename)
     local rc, re, e, src, cvsroot, workdir, argv
 
     e = err.new("fetching source failed: %s", sourcename)
-    src = info.sources[sourcename]
+    src = source.sources[sourcename]
 
     cvsroot, re = mkcvsroot(info, sourcename)
     if not cvsroot then
@@ -185,23 +335,23 @@ function cvs.fetch_source(info, sourcename)
 
     -- split the working directory into dirname and basename as some cvs clients
     -- don't like slashes (e.g. in/foo) in their checkout -d<path> argument
-    workdir = e2lib.dirname(e2lib.join(info.root, src.working))
+    workdir = e2lib.dirname(e2lib.join(info.root, src:get_working()))
 
     argv = {
         "-d", cvsroot,
         "checkout",
         "-R",
-        "-d", e2lib.basename(src.working),
+        "-d", e2lib.basename(src:get_working()),
     }
 
     -- always fetch the configured branch, as we don't know the build mode here.
     -- HEAD has special meaning to cvs
-    if src.branch ~= "HEAD" then
+    if src:get_branch() ~= "HEAD" then
         table.insert(argv, "-r")
-        table.insert(argv, src.branch)
+        table.insert(argv, src:get_branch())
     end
 
-    table.insert(argv, src.module)
+    table.insert(argv, src:get_module())
 
     rc, re = cvs_tool(argv, workdir)
     if not rc or rc ~= 0 then
@@ -210,44 +360,44 @@ function cvs.fetch_source(info, sourcename)
     return true
 end
 
-function cvs.prepare_source(info, sourcename, source_set, buildpath)
+function cvs.prepare_source(info, sourcename, sourceset, buildpath)
     local rc, re, e, src, cvsroot, argv
 
     e = err.new("cvs.prepare_source failed")
-    src = info.sources[sourcename]
+    src = source.sources[sourcename]
 
     cvsroot, re = mkcvsroot(info, sourcename)
     if not cvsroot then
         return false, re
     end
 
-    if source_set == "tag" or source_set == "branch" then
+    if sourceset == "tag" or sourceset == "branch" then
         argv = {
             "-d", cvsroot,
             "export", "-R",
-            "-d", src.name,
+            "-d", src:get_name(),
             "-r",
         }
 
-        if source_set == "branch" or
-            (source_set == "lazytag" and src.tag == "^") then
-            table.insert(argv, src.branch)
-        elseif (source_set == "tag" or source_set == "lazytag") and
-            src.tag ~= "^" then
-            table.insert(argv, src.tag)
+        if sourceset == "branch" or
+            (sourceset == "lazytag" and src:get_tag() == "^") then
+            table.insert(argv, src:get_branch())
+        elseif (sourceset == "tag" or sourceset == "lazytag") and
+            src:get_tag() ~= "^" then
+            table.insert(argv, src:get_tag())
         else
             return false, e:cat(err.new("source set not allowed"))
         end
 
-        table.insert(argv, src.module)
+        table.insert(argv, src:get_module())
 
         rc, re = cvs_tool(argv, buildpath)
         if not rc or rc ~= 0 then
             return false, e:cat(re)
         end
-    elseif source_set == "working-copy" then
-        rc, re = e2lib.cp(e2lib.join(info.root, src.working),
-            e2lib.join(buildpath, src.name), true)
+    elseif sourceset == "working-copy" then
+        rc, re = e2lib.cp(e2lib.join(info.root, src:get_working()),
+            e2lib.join(buildpath, src:get_name()), true)
         if not rc then
             return false, e:cat(re)
         end
@@ -261,9 +411,9 @@ function cvs.update(info, sourcename)
     local rc, re, e, src, workdir, argv
 
     e = err.new("updating source '%s' failed", sourcename)
-    src = info.sources[sourcename]
+    src = source.sources[sourcename]
 
-    workdir = e2lib.join(info.root, src.working)
+    workdir = e2lib.join(info.root, src:get_working())
 
     argv = { "update", "-R" }
     rc, re = cvs_tool(argv, workdir)
@@ -271,12 +421,12 @@ function cvs.update(info, sourcename)
         return false, e:cat(re)
     end
 
-    return true, nil
+    return true
 end
 
 function cvs.working_copy_available(info, sourcename)
-    local src = info.sources[sourcename]
-    local dir = string.format("%s/%s", info.root, src.working)
+    local src = source.sources[sourcename]
+    local dir = e2lib.join(info.root, src:get_working())
     return e2lib.isdir(dir)
 end
 
@@ -284,80 +434,6 @@ function cvs.has_working_copy(info, sourcename)
     return true
 end
 
---- create a table of lines for display
--- @param info the info structure
--- @param sourcename string
--- @return a table, nil on error
--- @return an error object on failure
-function cvs.display(info, sourcename)
-    local src = info.sources[sourcename]
-    local rc, re
-    local display = {}
-
-    display[1] = string.format("type       = %s", src.type)
-    display[2] = string.format("branch     = %s", src.branch)
-    display[3] = string.format("tag        = %s", src.tag)
-    display[4] = string.format("server     = %s", src.server)
-    display[5] = string.format("cvsroot    = %s", src.cvsroot)
-    display[6] = string.format("module     = %s", src.module)
-    display[7] = string.format("working    = %s", src.working)
-    local i = 8
-    for _,l in ipairs(src.licences) do
-        display[i] = string.format("licence    = %s", l)
-        i = i + 1
-    end
-    for k,v in pairs(src.sourceid) do
-        if v then
-            display[i] = string.format("sourceid [%s] = %s", k, v)
-            i = i + 1
-        end
-    end
-    return display, nil
-end
-
-function cvs.sourceid(info, sourcename, source_set)
-    local src = info.sources[sourcename]
-    local rc, re, lid
-
-    if source_set == "working-copy" then
-        src.sourceid[source_set] = "working-copy"
-    end
-    if src.sourceid[source_set] then
-        return true, nil, src.sourceid[source_set]
-    end
-    local e = err.new("calculating sourceid failed for source %s",
-    sourcename)
-    local hc = hash.hash_start()
-    hash.hash_line(hc, src.name)
-    hash.hash_line(hc, src.type)
-    hash.hash_line(hc, src._env:id())
-    for _,ln in ipairs(src.licences) do
-        lid, re = licence.licences[ln]:licenceid(info)
-        if not lid then
-            return false, e:cat(re)
-        end
-        hash.hash_line(hc, lid)
-    end
-    -- cvs specific
-    if source_set == "tag" and src.tag ~= "^" then
-        -- we rely on tags being unique with cvs
-        hash.hash_line(hc, src.tag)
-    else
-        -- the old function took a hash of the CVS/Entries file, but
-        -- forgot the subdirecties' CVS/Entries files. We might
-        -- reimplement that once...
-        e:append("cannot calculate sourceid for source set %s",
-        source_set)
-        return false, e
-    end
-    hash.hash_line(hc, src.server)
-    hash.hash_line(hc, src.cvsroot)
-    hash.hash_line(hc, src.module)
-    -- skip src.working
-    src.sourceid[source_set] = hash.hash_finish(hc)
-    return true, nil, src.sourceid[source_set]
-end
-
 function cvs.toresult(info, sourcename, sourceset, directory)
     -- <directory>/source/<sourcename>.tar.gz
     -- <directory>/makefile
@@ -368,7 +444,7 @@ function cvs.toresult(info, sourcename, sourceset, directory)
     if not rc then
         return false, e:cat(re)
     end
-    local src = info.sources[sourcename]
+    local src = source.sources[sourcename]
     -- write makefile
     local makefile = "Makefile"
     local source = "source"
@@ -400,7 +476,7 @@ function cvs.toresult(info, sourcename, sourceset, directory)
         return false, e:cat(re)
     end
     -- create a tarball in the final location
-    local archive = string.format("%s.tar.gz", src.name)
+    local archive = string.format("%s.tar.gz", src:get_name())
     rc, re = e2lib.tar({ "-C", tmpdir ,"-czf", sourcedir .. "/" .. archive,
     sourcename })
     if not rc then
@@ -409,7 +485,9 @@ function cvs.toresult(info, sourcename, sourceset, directory)
     -- write licences
     local destdir = string.format("%s/licences", directory)
     local fname = string.format("%s/%s.licences", destdir, archive)
-    local licence_list = table.concat(src.licences, "\n") .. "\n"
+    local licenses = src:get_licences()
+    local licence_list = licenses:concat_sorted("\n").."\n"
+
     rc, re = e2lib.mkdir_recursive(destdir)
     if not rc then
         return false, e:cat(re)
index 89c33b3589dcc428b9aeee98d36f295982b94c4c..7c062a3bc2b01f455a8e8e9f1a23774fb440ce12 100644 (file)
 
 local files = {}
 local cache = require("cache")
+local class = require("class")
 local e2lib = require("e2lib")
 local e2tool = require("e2tool")
 local eio = require("eio")
+local environment = require("environment")
 local err = require("err")
 local hash = require("hash")
+local licence = require("licence")
 local scm = require("scm")
+local sl = require("sl")
+local source = require("source")
 local strict = require("strict")
 local tools = require("tools")
-local licence = require("licence")
+
 
 plugin_descriptor = {
     description = "Files SCM Plugin",
-    init = function (ctx) scm.register("files", files) return true end,
+    init = function (ctx)
+        local rc, re
+
+        rc, re = source.register_source_class("files", files.files_source)
+        if not rc then
+            return false, re
+        end
+
+        rc, re = scm.register("files", files)
+        if not rc then
+            return false, re
+        end
+
+        return true
+    end,
     exit = function (ctx) return true end,
 }
 
---- validate source configuration, log errors to the debug log
--- @param info the info table
--- @param sourcename the source name
--- @return bool
-function files.validate_source(info, sourcename)
-    local rc1 = true   -- the return value
-    local rc, e = scm.generic_source_validate(info, sourcename)
+files.files_source = class("files_source", source.basic_source)
+
+function files.files_source:initialize(rawsrc)
+    assert(type(rawsrc) == "table")
+    assert(type(rawsrc.name) == "string" and #rawsrc.name > 0)
+    assert(type(rawsrc.type) == "string" and rawsrc.type == "files")
+
+    local rc, re, e, info
+
+    source.basic_source.initialize(self, rawsrc)
+
+    self._files = {}
+    self._sourceid = false
+
+    rc, re = e2lib.vrfy_dict_exp_keys(rawsrc, "e2source config", {
+        "env",
+        "file",
+        "licences",
+        "name",
+        "server",
+        "type",
+    })
+    if not rc then
+        error(re)
+    end
+
+    rc, re = source.generic_source_validate_licences(rawsrc, self)
+    if not rc then
+        error(re)
+    end
+
+    rc, re = source.generic_source_validate_env(rawsrc, self)
+    if not rc then
+        error(re)
+    end
+
+    rc, re = source.generic_source_validate_server(rawsrc, false)
     if not rc then
-        return false, e
+        error(re)
     end
-    e = err.new("in source %s:", sourcename)
-    e:setcount(0)
-    local src = info.sources[ sourcename ]
-    if type(src.file) ~= "table" then
-        return false, e:cat(err.new("source has no valid `file' attribute"))
+
+    if type(rawsrc.file) ~= "table" then
+        error(err.new("`file' attribute must be a table"))
     end
 
-    for _,f in pairs(src.file) do
+    e = err.new("error in file list of source")
+    for _,f in ipairs(rawsrc.file) do
         if type(f) ~= "table" then
-            e:append("%s: source has invalid file entry in `file' attribute",
-            sourcename)
-            break
-        end
-        -- catch deprecated configuration
-        if f.name then
-            e:append("source has file entry with `name' attribute")
+            error(e:append("`file' attribute must be a table"))
         end
-        if (not f.licences) and src.licences then
-            f.licences = src.licences
-        end
-        if (not f.server) and src.server then
-            f.server = src.server
-        end
-        if not f.licences then
-            e:append("source has file entry without `licences' attribute")
+
+        rc, re = e2lib.vrfy_dict_exp_keys(f, "e2source config",
+        {
+            "copy",
+            "licences",
+            "location",
+            "patch",
+            "server",
+            "sha1",
+            "unpack",
+        })
+        if not rc then
+            error(e:cat(re))
         end
-        for _,l in ipairs(f.licences) do
-            if not licence.licences[l] then
-                e:append("invalid licence assigned to file: %s", l)
-            end
+
+
+        if (not f.server) and rawsrc.server then
+            f.server = rawsrc.server
         end
+
+        info = e2tool.info()
+
         if not f.server then
-            e:append("source has file entry without `server' attribute")
+            error(e:append("file entry without `server' attribute"))
         end
         if f.server and (not cache.valid_server(info.cache, f.server)) then
-            e:append("invalid server: %s", f.server)
+            error(e:append("invalid server: %s", f.server))
         end
         if not f.location then
-            e:append("source has file entry without `location' attribute")
+            error(e:append("file entry without `location' attribute"))
         end
         if f.server ~= info.root_server_name and not f.sha1 then
-            e:append("source has file entry for remote file without `sha1` "..
-            "attribute")
+            error(e:append("file entry for remote file without "..
+            "`sha1` attribute"))
         end
 
         local attrcnt = 0
@@ -107,33 +157,189 @@ function files.validate_source(info, sourcename)
                 attrcnt = attrcnt + 1
 
                 if type(f[attr]) ~= "string" then
-                    e:append("'%s' in file entry of source must be a string", attr)
-                    break
+                    error(e:append(
+                        "'%s' in file entry of source must be a string", attr))
                 end
 
             end
         end
 
         if attrcnt == 0 then
-            e:append("source has file entry without `unpack, copy or patch' " ..
-                "attribute")
+            error(e:append("file entry without "..
+                "unpack, copy or patch attribute"))
         elseif attrcnt > 1 then
-            e:append("source has file entry with conflicting unpack, copy or"..
-                " patch attributes")
+            error(e:append("file entry with conflicting "..
+                "unpack, copy or patch attributes"))
         end
 
-        if f.checksum_file then
-            e2lib.warnf("WDEPRECATED", "in source %s:", sourcename)
-            e2lib.warnf("WDEPRECATED",
-            " checksum_file attribute is deprecated and no longer used")
-            f.checksum_file = nil
+        assert(type(f.location) == "string" and f.location ~= "")
+        assert(type(f.server) == "string" and f.server ~= "")
+        assert(f.sha1 == nil or (type(f.sha1) == "string" and #f.sha1 == 40))
+
+        -- per file licences --
+        local laerr = string.format("%s:%s licences attribute",
+            f.server, f.location)
+        local llist, licences
+
+        if f.licences == nil then
+            f.licences = self:get_licences():copy()
+        elseif type(f.licences == "table") then
+            rc, re = e2lib.vrfy_listofstrings(f.licences, laerr, true, false)
+            if not rc then
+                error(e:cat(re))
+            end
+
+            licences = self:get_licences()
+            llist = sl.sl:new(false, true)
+
+            for _,licencename in ipairs(f.licences) do
+                if not licence.licences[licencename] then
+                    error(e:append("%s has unknown licence: %q",
+                        laerr, licencename))
+                end
+
+                -- Make sure the _licences list contains every licence in the
+                -- entire source. Duplicates are rejected by unique string list.
+                licences:insert(licencename)
+                assert(llist:insert(licencename))
+            end
+
+            self:set_licences(licences)
+            f.licences = llist
+        else
+            error(e:append("%s must be a table", laerr))
+        end
+
+        if f.unpack then
+            assert(type(f.unpack) == "string")
+
+            table.insert(self._files, {
+                location=f.location,
+                server=f.server,
+                sha1=f.sha1,
+                unpack=f.unpack,
+                licences=f.licences,
+            })
+        elseif f.copy then
+            assert(type(f.copy) == "string")
+
+            table.insert(self._files, {
+                location=f.location,
+                server=f.server,
+                sha1=f.sha1,
+                copy=f.copy,
+                licences=f.licences,
+            })
+        elseif f.patch then
+            assert(type(f.patch) == "string")
+
+            table.insert(self._files, {
+                location=f.location,
+                server=f.server,
+                sha1=f.sha1,
+                patch=f.patch,
+                licences=f.licences,
+            })
+        else
+            assert("internal error" == true)
         end
     end
+end
+
+function files.files_source:file_iter()
+    local i = 0
+
+    return function ()
+        i = i + 1
+
+        if self._files[i] then
+            -- return a copy so nobody can mess with the internals
+            local f = {
+                location = self._files[i].location,
+                server = self._files[i].server,
+                sha1 = self._files[i].sha1,
+                licences = self._files[i].licences:copy()
+            }
+            for _,attr in ipairs({ "copy", "unpack", "patch" }) do
+                if self._files[i][attr] then
+                    f[attr] = self._files[i][attr]
+                    break
+                end
+            end
+            return f
+        end
 
-    if e:getcount() > 0 then
-        return false, e
+        return nil
     end
-    return true, nil
+end
+
+function files.files_source:sourceid(sourceset --[[always ignored for files]])
+    local hc, info, licences
+
+    if self._sourceid then
+        return self._sourceid
+    end
+
+    info = e2tool.info()
+    assert(info)
+
+    hc = hash.hash_start()
+    hash.hash_line(hc, self._name)
+    hash.hash_line(hc, self._type)
+    hash.hash_line(hc, self._env:id())
+
+    for f in self:file_iter() do
+        local fileid, re = e2tool.fileid(info, f)
+        if not fileid then
+            return false, re
+        end
+        hash.hash_line(hc, fileid)
+        hash.hash_line(hc, f.location)
+        hash.hash_line(hc, f.server)
+        hash.hash_line(hc, tostring(f.unpack))
+        hash.hash_line(hc, tostring(f.patch))
+        hash.hash_line(hc, tostring(f.copy))
+
+        -- per file licence list
+        for licencename in f.licences:iter_sorted() do
+            local lid, re = licence.licences[licencename]:licenceid(info)
+            if not lid then
+                return false, re
+            end
+            hash.hash_line(hc, lid)
+        end
+    end
+
+    self._sourceid = hash.hash_finish(hc)
+
+    return self._sourceid
+end
+
+--- create a table of lines for display
+-- @return a table
+function files.files_source:display()
+    local s, sid, d, licences
+
+    self:sourceid()
+
+    d = {}
+    table.insert(d, string.format("type       = %s", self:get_type()))
+
+    for f in self:file_iter() do
+        s = string.format("file       = %s:%s", f.server, f.location)
+        table.insert(d, s)
+    end
+
+    licences = self:get_licences()
+    for licencename in licences:iter_sorted() do
+        table.insert(d, string.format("licence    = %s", licencename))
+    end
+
+    if self._sourceid then
+        table.insert(d, string.format("sourceid   = %s", self._sourceid))
+    end
+
+    return d
 end
 
 --- cache files for a source
@@ -142,25 +348,25 @@ end
 -- @return bool
 -- @return nil, an error string on error
 function files.cache_source(info, sourcename)
-    local rc, e
-    local s = info.sources[sourcename]
+    local rc, re
+    local src = source.sources[sourcename]
+
     -- cache all files for this source
-    for i,f in pairs(s.file) do
+    for f in src:file_iter() do
         e2lib.logf(4, "files.cache_source: caching file %s:%s", f.server,
             f.location)
         local flags = { cache = true }
         if f.server ~= info.root_server_name then
-            local rc, e = cache.cache_file(info.cache, f.server,
-                f.location, flags)
+            rc, re = cache.cache_file(info.cache, f.server, f.location, flags)
             if not rc then
-                return false, e
+                return false, re
             end
         else
             e2lib.logf(4, "not caching %s:%s (stored locally)", f.server,
                 f.location)
         end
     end
-    return true, nil
+    return true
 end
 
 function files.fetch_source(info, sourcename)
@@ -337,8 +543,9 @@ function files.prepare_source(info, sourcename, sourceset, buildpath)
     local rc, re
     local e = err.new("error preparing source: %s", sourcename)
     local symlink = nil
-    local s = info.sources[sourcename]
-    for _,file in ipairs(info.sources[sourcename].file) do
+    local src = source.sources[sourcename]
+
+    for file in src:file_iter() do
         if file.sha1 then
             rc, re = e2tool.verify_hash(info, file.server, file.location, file.sha1)
             if not rc then
@@ -438,74 +645,6 @@ function files.prepare_source(info, sourcename, sourceset, buildpath)
     return true, nil
 end
 
---- create a table of lines for display
--- @param info the info structure
--- @param sourcename string
--- @return a table, nil on error
--- @return an error string on failure
-function files.display(info, sourcename)
-    local src = info.sources[sourcename]
-    local display = {}
-
-    display[1] = string.format("type       = %s", src.type)
-    local i = 2
-    for _,f in pairs(src.file) do
-        display[i] = string.format("file       = %s:%s", f.server, f.location)
-        i = i + 1
-    end
-    for _,l in ipairs(src.licences) do
-        display[i] = string.format("licence    = %s", l)
-        i = i + 1
-    end
-    if src.sourceid then
-        display[i] = string.format("sourceid   = %s", src.sourceid)
-        i = i + 1
-    end
-    return display
-end
-
---- calculate an id for a source
--- @param info
--- @param sourcename
--- @param sourceset
--- @return string: the source id, nil on error
--- @return an error string on error
-function files.sourceid(info, sourcename, sourceset)
-    local rc, re
-    local e = err.new("error calculating sourceid for source: %s",
-        sourcename)
-    local src = info.sources[sourcename]
-    if src.sourceid then
-        return true, nil, src.sourceid
-    end
-    -- sourceset is ignored for files sources
-    local hc = hash.hash_start()
-    hash.hash_line(hc, src.name)
-    hash.hash_line(hc, src.type)
-    hash.hash_line(hc, src._env:id())
-    for _,ln in ipairs(src.licences) do
-        local lid, re = licence.licences[ln]:licenceid(info)
-        if not lid then
-            return false, re
-        end
-        hash.hash_line(hc, lid)
-    end
-    for _,f in ipairs(src.file) do
-        local fileid, re = e2tool.fileid(info, f)
-        if not fileid then
-            return false, e:cat(re)
-        end
-        hash.hash_line(hc, fileid)
-        hash.hash_line(hc, f.location)
-        hash.hash_line(hc, f.server)
-        hash.hash_line(hc, tostring(f.unpack))
-        hash.hash_line(hc, tostring(f.patch))
-        hash.hash_line(hc, tostring(f.copy))
-    end
-    src.sourceid = hash.hash_finish(hc)
-    return true, nil, src.sourceid
-end
-
 --- Create a source result containing the generated Makefile and files
 -- belonging to the source, for use with collect_project.
 -- Result refers to a collection of files to recreate an e2source for
@@ -519,12 +658,12 @@ end
 function files.toresult(info, sourcename, sourceset, directory)
     local rc, re, out
     local e = err.new("converting result failed")
-    local s = info.sources[sourcename]
+    local src = source.sources[sourcename]
     local source = "source"     -- directory to store source files in
     local makefile = e2lib.join(directory, "Makefile")
 
     out = { ".PHONY: place\n\nplace:\n" }
-    for _,file in ipairs(s.file) do
+    for file in src:file_iter() do
         e2lib.logf(4, "export file: %s", file.location)
         local destdir = string.format("%s/%s", directory, source)
         local destname = nil
@@ -615,8 +754,8 @@ function files.toresult(info, sourcename, sourceset, directory)
         -- write licences
         local destdir = string.format("%s/licences", directory)
         local fname = string.format("%s/%s.licences", destdir,
-        e2lib.basename(file.location))
-        local licence_list = table.concat(file.licences, "\n") .. "\n"
+            e2lib.basename(file.location))
+        local licence_list = file.licences:concat_sorted("\n") .. "\n"
         rc, re = e2lib.mkdir_recursive(destdir)
         if not rc then
             return false, e:cat(re)
index d8fc00f2f109d0915b3c00b6ea1faa7abbf076f5..5b6144e8608f0c14bd5efb5a1bc84ff5257055a4 100644 (file)
@@ -30,6 +30,7 @@
 
 local git = {}
 local cache = require("cache")
+local class = require("class")
 local e2lib = require("e2lib")
 local e2option = require("e2option")
 local e2tool = require("e2tool")
@@ -42,6 +43,7 @@ local scm = require("scm")
 local strict = require("strict")
 local tools = require("tools")
 local url = require("url")
+local source = require("source")
 
 --- Initialize git plugin.
 -- @param ctx Plugin context. See plugin module.
@@ -50,6 +52,11 @@ local url = require("url")
 local function git_plugin_init(ctx)
     local rc, re
 
+    rc, re = source.register_source_class("git", git.git_source)
+    if not rc then
+        return false, re
+    end
+
     rc, re = scm.register("git", git)
     if not rc then
         return false, re
@@ -75,6 +82,191 @@ plugin_descriptor = {
     exit = function (ctx) return true end,
 }
 
+git.git_source = class("git_source", source.basic_source)
+
+function git.git_source:initialize(rawsrc)
+    assert(type(rawsrc) == "table")
+    assert(type(rawsrc.name) == "string" and rawsrc.name ~= "")
+    assert(type(rawsrc.type) == "string" and rawsrc.type ~= "")
+
+    local rc, re
+
+    source.basic_source.initialize(self, rawsrc)
+
+    self._server = false
+    self._location = false
+    self._tag = false
+    self._branch = false
+    self._working = false
+    self._sourceids = {
+        ["working-copy"] = "working-copy",
+    }
+    self._commitids = {}
+
+    rc, re = e2lib.vrfy_dict_exp_keys(rawsrc, "e2source", {
+        "branch",
+        "env",
+        "licences",
+        "location",
+        "name",
+        "server",
+        "tag",
+        "type",
+        "working",
+    })
+    if not rc then
+        error(re)
+    end
+
+    rc, re = source.generic_source_validate_licences(rawsrc, self)
+    if not rc then
+        error(re)
+    end
+
+    rc, re = source.generic_source_validate_env(rawsrc, self)
+    if not rc then
+        error(re)
+    end
+
+    rc, re = source.generic_source_validate_server(rawsrc, true)
+    if not rc then
+        error(re)
+    end
+    self._server = rawsrc.server
+
+    rc, re = source.generic_source_validate_working(rawsrc)
+    if not rc then
+        error(re)
+    end
+    self._working = rawsrc.working
+
+    for _,attr in ipairs({ "branch", "location", "tag" }) do
+        if rawsrc[attr] == nil then
+            error(err.new("source has no `%s' attribute", attr))
+        elseif type(rawsrc[attr]) ~= "string" then
+            error(err.new("'%s' must be a string", attr))
+        elseif rawsrc[attr] == "" then
+            error(err.new("'%s' may not be empty", attr))
+        end
+    end
+
+    self._branch = rawsrc.branch
+    self._location = rawsrc.location
+    self._tag = rawsrc.tag
+end
+
+function git.git_source:get_server()
+    assert(type(self._server) == "string")
+
+    return self._server
+end
+
+function git.git_source:get_location()
+    assert(type(self._location) == "string")
+
+    return self._location
+end
+
+function git.git_source:get_working()
+    assert(type(self._working) == "string")
+
+    return self._working
+end
+
+function git.git_source:get_branch()
+    assert(type(self._branch) == "string")
+
+    return self._branch
+end
+
+
+function git.git_source:get_tag()
+    assert(type(self._tag) == "string")
+
+    return self._tag
+end
+
+function git.git_source:sourceid(sourceset)
+    assert(type(sourceset) == "string" and #sourceset > 0,
+        "sourceset arg invalid")
+
+    local rc, re, info, id, hc, licences
+
+    if self._sourceids[sourceset] then
+        return self._sourceids[sourceset]
+    end
+
+    info = e2tool.info()
+    assert(info)
+
+    rc, re, id = git.git_commit_id(info, self._name, sourceset,
+        e2option.opts["check-remote"])
+    if not rc then
+        return false, re
+    end
+
+    hc = hash.hash_start()
+    hash.hash_line(hc, self._name)
+    hash.hash_line(hc, self._type)
+    hash.hash_line(hc, self._env:id())
+
+    licences = self:get_licences()
+    for licencename in licences:iter_sorted() do
+        local lid, re = licence.licences[licencename]:licenceid(info)
+        if not lid then
+            return false, re
+        end
+        hash.hash_line(hc, lid)
+    end
+
+    hash.hash_line(hc, self._server)
+    hash.hash_line(hc, self._location)
+    hash.hash_line(hc, self._working)
+    hash.hash_line(hc, id)
+    self._commitids[sourceset] = id
+    self._sourceids[sourceset] = hash.hash_finish(hc)
+
+    return self._sourceids[sourceset]
+end
+
+function git.git_source:display()
+    local rev_tag, rev_branch, licences
+
+    -- try to calculte the sourceid, but do not care if it fails.
+    -- working copy might be unavailable
+    self:sourceid("tag")
+    self:sourceid("branch")
+
+    rev_tag = ""
+    rev_branch = ""
+    if self._commitids["tag"] then
+        rev_tag = string.format("[%s...]", self._commitids["tag"]:sub(1,8))
+    end
+    if self._commitids["branch"] then
+        rev_branch = string.format("[%s...]", self._commitids["branch"]:sub(1,8))
+    end
+    local d = {}
+    table.insert(d, string.format("type       = %s", self:get_type()))
+    table.insert(d, string.format("branch     = %-15s %s", self._branch, rev_branch))
+    table.insert(d, string.format("tag        = %-15s %s", self._tag, rev_tag))
+    table.insert(d, string.format("server     = %s", self._server))
+    table.insert(d, string.format("location   = %s", self._location))
+    table.insert(d, string.format("working    = %s", self._working))
+
+    licences = self:get_licences()
+    for licencename in licences:iter_sorted() do
+        table.insert(d, string.format("licence    = %s", licencename))
+    end
+
+    for sourceset, sid in pairs(self._sourceids) do
+        if sid then
+            table.insert(d, string.format("sourceid [%s] = %s", sourceset, sid))
+        end
+    end
+
+    return d
+end
+
 --- Return the git commit ID of the specified source configuration. Specific to
 -- sources of type git, useful for writing plugins.
 -- @param info Info table.
@@ -88,7 +280,7 @@ function git.git_commit_id(info, sourcename, sourceset, check_remote)
     local rc, re, e, src, id, fr, gitdir, ref
 
     e = err.new("getting commit ID failed for source: %s", sourcename)
-    src = info.sources[sourcename]
+    src = source.sources[sourcename]
 
     rc, re = scm.working_copy_available(info, sourcename)
     if not rc then
@@ -100,17 +292,17 @@ function git.git_commit_id(info, sourcename, sourceset, check_remote)
         return false, e:cat(re)
     end
 
-    gitdir = e2lib.join(info.root, src.working, ".git")
+    gitdir = e2lib.join(info.root, src:get_working(), ".git")
 
-    if sourceset == "branch" or (sourceset == "lazytag" and src.tag == "^") then
-        ref = string.format("refs/heads/%s", src.branch)
+    if sourceset == "branch" or (sourceset == "lazytag" and src:get_tag() == "^") then
+        ref = string.format("refs/heads/%s", src:get_branch())
 
         rc, re, id = generic_git.lookup_id(gitdir, false, ref)
         if not rc then
             return false, e:cat(re)
         end
-    elseif sourceset == "tag" or (sourceset == "lazytag" and src.tag ~= "^") then
-        ref = string.format("refs/tags/%s", src.tag)
+    elseif sourceset == "tag" or (sourceset == "lazytag" and src:get_tag() ~= "^") then
+        ref = string.format("refs/tags/%s", src:get_tag())
 
         rc, re, id = generic_git.lookup_id(gitdir, false, ref)
         if not rc then
@@ -118,7 +310,7 @@ function git.git_commit_id(info, sourcename, sourceset, check_remote)
         end
 
         if id and check_remote then
-            rc, re = generic_git.verify_remote_tag(gitdir, src.tag)
+            rc, re = generic_git.verify_remote_tag(gitdir, src:get_tag())
             if not rc then
                 return false, e:cat(re)
             end
@@ -129,62 +321,13 @@ function git.git_commit_id(info, sourcename, sourceset, check_remote)
 
     if not id then
         re = err.new("can't get git commit ID for ref %q from repository %q",
-            ref, src.working)
+            ref, src:get_working())
         return false, e:cat(re)
     end
 
     return true, nil, id
 end
 
---- validate source configuration, log errors to the debug log
--- @param info the info table
--- @param sourcename the source name
--- @return bool
--- @return an error object on error
-function git.validate_source(info, sourcename)
-    local rc, re = scm.generic_source_validate(info, sourcename)
-    if not rc then
-        -- error in generic configuration. Don't try to go on.
-        return false, re
-    end
-    local src = info.sources[ sourcename ]
-    local e = err.new("in source %s:", sourcename)
-    rc, re = scm.generic_source_default_working(info, sourcename)
-    if not rc then
-        return false, e:cat(re)
-    end
-    e:setcount(0)
-    -- catch deprecated attributes
-    if src.remote then
-        e:append("source has deprecated `remote' attribute")
-    end
-    if not src.server then
-        e:append("source has no `server' attribute")
-    end
-    if src.server and (not cache.valid_server(info.cache, src.server)) then
-        e:append("invalid server: %s", src.server)
-    end
-    if not src.licences then
-        e:append("source has no `licences' attribute")
-    end
-    if not src.branch then
-        e:append("source has no `branch' attribute")
-    end
-    if type(src.tag) ~= "string" then
-        e:append("source has no `tag' attribute or tag attribute has wrong type")
-    end
-    if not src.location then
-        e:append("source has no `location' attribute")
-    end
-    if not src.working then
-        e:append("source has no `working' attribute")
-    end
-    if e:getcount() > 0 then
-        return false, e
-    end
-    return true, nil
-end
-
 --- update a working copy
 -- @param info the info structure
 -- @param sourcename string
@@ -193,7 +336,7 @@ end
 function git.update(info, sourcename)
     local e, rc, re, src, gitwc, gitdir, argv, id, branch, remote
 
-    src = info.sources[sourcename]
+    src = source.sources[sourcename]
     e = err.new("updating source '%s' failed", sourcename)
 
     rc, re = scm.working_copy_available(info, sourcename)
@@ -201,9 +344,9 @@ function git.update(info, sourcename)
         return false, e:cat(re)
     end
 
-    e2lib.logf(2, "updating %s [%s]", src.working, src.branch)
+    e2lib.logf(2, "updating %s [%s]", src:get_working(), src:get_branch())
 
-    gitwc  = e2lib.join(info.root, src.working)
+    gitwc  = e2lib.join(info.root, src:get_working())
     gitdir = e2lib.join(gitwc, ".git")
 
     argv = generic_git.git_new_argv(gitdir, gitwc, "fetch")
@@ -234,20 +377,20 @@ function git.update(info, sourcename)
         return true
     end
 
-    if branch ~= "refs/heads/" .. src.branch then
+    if branch ~= "refs/heads/" .. src:get_branch() then
         e2lib.warnf("WOTHER", "not on configured branch. Skipping.")
         return true
     end
 
     rc, re, remote = generic_git.git_config(
-        gitdir, "branch."..src.branch.."remote")
+        gitdir, "branch."..src:get_branch().."remote")
     if not rc or string.len(remote) == 0  then
         e2lib.warnf("WOTHER", "no remote configured for branch %q. Skipping.",
-            src.branch)
+            src:get_branch())
         return true
     end
 
-    branch = remote .. "/" .. src.branch
+    branch = remote .. "/" .. src:get_branch()
     argv = generic_git.git_new_argv(gitdir, gitwc, "merge", "--ff-only", branch)
     rc, re = generic_git.git(argv)
     if not rc then
@@ -265,33 +408,34 @@ end
 function git.fetch_source(info, sourcename)
     local e, rc, re, src, git_dir, work_tree, id
 
-    src = info.sources[sourcename]
+    src = source.sources[sourcename]
     e = err.new("fetching source failed: %s", sourcename)
 
-    work_tree = e2lib.join(info.root, src.working)
+    work_tree = e2lib.join(info.root, src:get_working())
     git_dir = e2lib.join(work_tree, ".git")
 
-    e2lib.logf(2, "cloning %s:%s [%s]", src.server, src.location, src.branch)
+    e2lib.logf(2, "cloning %s:%s [%s]", src:get_server(), src:get_location(),
+        src:get_branch())
 
-    rc, re = generic_git.git_clone_from_server(info.cache, src.server,
-        src.location, work_tree, false --[[always checkout]])
+    rc, re = generic_git.git_clone_from_server(info.cache, src:get_server(),
+        src:get_location(), work_tree, false --[[always checkout]])
     if not rc then
         return false, e:cat(re)
     end
 
     rc, re, id = generic_git.lookup_id(git_dir, false,
-        "refs/heads/" .. src.branch)
+        "refs/heads/" .. src:get_branch())
     if not rc then
         return false, e:cat(re)
     elseif not id then
-        rc, re = generic_git.git_branch_new1(work_tree, true, src.branch,
-            "origin/" .. src.branch)
+        rc, re = generic_git.git_branch_new1(work_tree, true, src:get_branch(),
+            "origin/" .. src:get_branch())
         if not rc then
             return false, e:cat(re)
         end
 
         rc, re = generic_git.git_checkout1(work_tree,
-            "refs/heads/" .. src.branch)
+            "refs/heads/" .. src:get_branch())
         if not rc then
             return false, e:cat(re)
         end
@@ -308,16 +452,16 @@ end
 -- @return bool
 -- @return nil on success, an error string on error
 function git.prepare_source(info, sourcename, sourceset, buildpath)
-    local src = info.sources[ sourcename ]
+    local src = source.sources[sourcename]
     local rc, re, e
     local e = err.new("preparing git sources failed")
     rc, re = scm.generic_source_check(info, sourcename, true)
     if not rc then
         return false, e:cat(re)
     end
-    local gitdir = e2lib.join(info.root, src.working, ".git")
+    local gitdir = e2lib.join(info.root, src:get_working(), ".git")
     if sourceset == "branch" or
-        (sourceset == "lazytag" and src.tag == "^") then
+        (sourceset == "lazytag" and src:get_tag() == "^") then
         local argv, work_tree
 
         rc, re = git.git_commit_id(info, sourcename, sourceset)
@@ -332,7 +476,7 @@ function git.prepare_source(info, sourcename, sourceset, buildpath)
         end
 
         argv = generic_git.git_new_argv(gitdir, work_tree, "checkout", "-f")
-        table.insert(argv, "refs/heads/" .. src.branch)
+        table.insert(argv, "refs/heads/" .. src:get_branch())
         table.insert(argv, "--")
 
         rc, re = generic_git.git(argv)
@@ -340,7 +484,7 @@ function git.prepare_source(info, sourcename, sourceset, buildpath)
             return false, e:cat(re)
         end
     elseif sourceset == "tag" or
-        (sourceset == "lazytag" and src.tag ~= "^") then
+        (sourceset == "lazytag" and src:get_tag() ~= "^") then
         local argv, work_tree
 
         rc, re = git.git_commit_id(info, sourcename, sourceset)
@@ -355,7 +499,7 @@ function git.prepare_source(info, sourcename, sourceset, buildpath)
         end
 
         argv = generic_git.git_new_argv(gitdir, work_tree, "checkout", "-f")
-        table.insert(argv, "refs/tags/" .. src.tag)
+        table.insert(argv, "refs/tags/" .. src:get_tag())
         table.insert(argv, "--")
 
         rc, re = generic_git.git(argv)
@@ -365,7 +509,7 @@ function git.prepare_source(info, sourcename, sourceset, buildpath)
     elseif sourceset == "working-copy" then
         local working, destdir, empty
 
-        working = e2lib.join(info.root, src.working)
+        working = e2lib.join(info.root, src:get_working())
         destdir = e2lib.join(buildpath, sourcename)
 
         rc, re = e2lib.mkdir_recursive(destdir)
@@ -392,7 +536,7 @@ function git.prepare_source(info, sourcename, sourceset, buildpath)
         end
 
         if empty then
-            e2lib.warnf("WOTHER", "in result: %s", src.name)
+            e2lib.warnf("WOTHER", "in result: %s", sourcename)
             e2lib.warnf("WOTHER", "working copy seems empty")
         end
     else
@@ -402,19 +546,17 @@ function git.prepare_source(info, sourcename, sourceset, buildpath)
     return true
 end
 
---- check if a working copy for a git repository is available
+--- Check if a working copy for a git repository is available
 -- @param info the info structure
 -- @param sourcename string
--- @return bool
--- @return sometimes an error string, when ret. false. XXX interface cleanup.
+-- @return True if available, false otherwise.
 function git.working_copy_available(info, sourcename)
-    local src = info.sources[sourcename]
-    local rc, re
-    local e = err.new("checking if working copy is available for source %s",
-    sourcename)
-    local gitwc = e2lib.join(info.root, src.working)
-    local rc = e2lib.isdir(gitwc)
-    return rc, nil
+    local rc
+    local src = source.sources[sourcename]
+    local gitwc = e2lib.join(info.root, src:get_working())
+
+    rc = e2lib.isdir(gitwc)
+    return rc
 end
 
 function git.has_working_copy(info, sname)
@@ -450,13 +592,14 @@ end
 -- @return a table, nil on error
 -- @return an error string on failure
 function git.display(info, sourcename)
-    local src = info.sources[sourcename]
+    error("called git.display")
+    local src = source.sources[sourcename]
     local rc, re
     local e = err.new("display source information failed")
     -- try to calculte the sourceid, but do not care if it fails.
     -- working copy might be unavailable
-    scm.sourceid(info, sourcename, "tag")
-    scm.sourceid(info, sourcename, "branch")
+    src:sourceid("tag")
+    src:sourceid("branch")
     local rev_tag = ""
     local rev_branch = ""
     if src.commitid["tag"] then
@@ -466,12 +609,12 @@ function git.display(info, sourcename)
         rev_branch = string.format("[%s...]", src.commitid["branch"]:sub(1,8))
     end
     local display = {}
-    display[1] = string.format("type       = %s", src.type)
-    display[2] = string.format("branch     = %-15s %s", src.branch, rev_branch)
-    display[3] = string.format("tag        = %-15s %s", src.tag, rev_tag)
-    display[4] = string.format("server     = %s", src.server)
-    display[5] = string.format("location   = %s", src.location)
-    display[6] = string.format("working    = %s", src.working)
+    display[1] = string.format("type       = %s", src:get_type())
+    display[2] = string.format("branch     = %-15s %s", src:get_branch(), rev_branch)
+    display[3] = string.format("tag        = %-15s %s", src:get_tag(), rev_tag)
+    display[4] = string.format("server     = %s", src:get_server())
+    display[5] = string.format("location   = %s", src:get_location())
+    display[6] = string.format("working    = %s", src:get_working())
     local i = 8
     for _,l in ipairs(src.licences) do
         display[i] = string.format("licence    = %s", l)
@@ -489,53 +632,6 @@ function git.display(info, sourcename)
     return display
 end
 
---- calculate an id for a source
--- @param info
--- @param sourcename
--- @param sourceset
--- @return string: the sourceid, or nil
--- @return an error string
-function git.sourceid(info, sourcename, sourceset)
-    local src = info.sources[sourcename]
-    local rc, re, e, id
-    if not src.sourceid then
-        src.sourceid = {}
-        src.sourceid["working-copy"] = "working-copy"
-        src.commitid = {}
-    end
-    if src.sourceid[sourceset] then
-        return true, nil, src.sourceid[sourceset]
-    end
-
-    rc, re, id = git.git_commit_id(info, sourcename, sourceset,
-        e2option.opts["check-remote"])
-    if not rc then
-        return false, re
-    end
-
-    src.commitid[sourceset] = id
-    local hc = hash.hash_start()
-    hash.hash_line(hc, src.name)
-    hash.hash_line(hc, src.type)
-    hash.hash_line(hc, src._env:id())
-    for _,ln in ipairs(src.licences) do
-        local lid, re = licence.licences[ln]:licenceid(info)
-        if not lid then
-            return false, re
-        end
-        hash.hash_line(hc, lid)
-    end
-    -- git specific
-    --hash.hash_line(hc, src.branch)
-    --hash.hash_line(hc, src.tag)
-    hash.hash_line(hc, src.server)
-    hash.hash_line(hc, src.location)
-    hash.hash_line(hc, src.working)
-    hash.hash_line(hc, src.commitid[sourceset])
-    src.sourceid[sourceset] = hash.hash_finish(hc)
-    return true, nil, src.sourceid[sourceset]
-end
-
 function git.toresult(info, sourcename, sourceset, directory)
     local rc, re, argv
     local e = err.new("converting result")
@@ -543,11 +639,11 @@ function git.toresult(info, sourcename, sourceset, directory)
     if not rc then
         return false, e:cat(re)
     end
-    local src = info.sources[sourcename]
+    local src = source.sources[sourcename]
     local makefile = "Makefile"
     local source = "source"
     local sourcedir = e2lib.join(directory, source)
-    local archive = string.format("%s.tar.gz", src.name)
+    local archive = string.format("%s.tar.gz", src:get_name())
     local cmd = nil
 
     rc, re = e2lib.mkdir_recursive(sourcedir)
@@ -558,7 +654,7 @@ function git.toresult(info, sourcename, sourceset, directory)
     if sourceset == "tag" or sourceset == "branch" then
         local ref, tmpfn
 
-        ref, re = generic_git.sourceset2ref(sourceset, src.branch, src.tag)
+        ref, re = generic_git.sourceset2ref(sourceset, src:get_branch(), src:get_tag())
         if not ref then
             return false, e:cat(re)
         end
@@ -568,7 +664,7 @@ function git.toresult(info, sourcename, sourceset, directory)
             return false, e:cat(re)
         end
 
-        argv = generic_git.git_new_argv(nil, e2lib.join(info.root, src.working))
+        argv = generic_git.git_new_argv(nil, e2lib.join(info.root, src:get_working()))
         table.insert(argv, "archive")
         table.insert(argv, "--format=tar") -- older versions don't have "tar.gz"
         table.insert(argv, string.format("--prefix=%s/", sourcename))
@@ -592,7 +688,7 @@ function git.toresult(info, sourcename, sourceset, directory)
         end
     elseif sourceset == "working-copy" then
         argv = {
-            "-C", e2lib.join(info.root, src.working),
+            "-C", e2lib.join(info.root, src:get_working()),
             string.format("--transform=s,^./,./%s/,", sourcename),
             "--exclude=.git",
             "-czf",
@@ -620,7 +716,8 @@ function git.toresult(info, sourcename, sourceset, directory)
     -- write licences
     local destdir = e2lib.join(directory, "licences")
     local fname = string.format("%s/%s.licences", destdir, archive)
-    local licence_list = table.concat(src.licences, "\n") .. "\n"
+    local licences = src:get_licences()
+    local licence_list = licences:concat_sorted("\n") .. "\n"
     rc, re = e2lib.mkdir_recursive(destdir)
     if not rc then
         return false, e:cat(re)
@@ -644,24 +741,24 @@ function git.check_workingcopy(info, sourcename)
     end
 
     -- check if branch exists
-    local src = info.sources[sourcename]
-    local gitdir = e2lib.join(info.root, src.working, ".git")
-    local ref = string.format("refs/heads/%s", src.branch)
+    local src = source.sources[sourcename]
+    local gitdir = e2lib.join(info.root, src:get_working(), ".git")
+    local ref = string.format("refs/heads/%s", src:get_branch())
     local id
 
     rc, re, id = generic_git.lookup_id(gitdir, false, ref)
     if not rc then
         return false, e:cat(re)
     elseif not id then
-        return false, e:cat(err.new("branch %q does not exist", src.branch))
+        return false, e:cat(err.new("branch %q does not exist", src:get_branch()))
     end
 
     -- git config branch.<branch>.remote == "origin"
     local query, expect, res
-    query = string.format("branch.%s.remote", src.branch)
+    query = string.format("branch.%s.remote", src:get_branch())
     res, re = generic_git.git_config(gitdir, query)
     if not res then
-        e:append("remote is not configured for branch \"%s\"", src.branch)
+        e:append("remote is not configured for branch \"%s\"", src:get_branch())
         return false, e
     elseif res ~= "origin" then
         e:append("%s is not \"origin\"", query)
@@ -670,7 +767,7 @@ function git.check_workingcopy(info, sourcename)
 
     -- git config remote.origin.url == server:location
     query = string.format("remote.origin.url")
-    expect, re = git_url(info.cache, src.server, src.location)
+    expect, re = git_url(info.cache, src:get_server(), src:get_location())
     if not expect then
         return false, e:cat(re)
     end
index 9ab8771958b99a05541ecae1a624014066948ab5..b914e4413983a2ae8960921ee2cea03c9fe11dad 100644 (file)
@@ -30,7 +30,9 @@
 
 local svn = {}
 local cache = require("cache")
+local class = require("class")
 local e2lib = require("e2lib")
+local e2tool = require("e2tool")
 local eio = require("eio")
 local err = require("err")
 local hash = require("hash")
@@ -39,13 +41,30 @@ local scm = require("scm")
 local strict = require("strict")
 local tools = require("tools")
 local url = require("url")
+local source = require("source")
 
 plugin_descriptor = {
     description = "SVN SCM Plugin",
-    init = function (ctx) scm.register("svn", svn) return true end,
+    init = function (ctx)
+        local rc, re
+
+        rc, re = source.register_source_class("svn", svn.svn_source)
+        if not rc then
+            return false, re
+        end
+
+        rc, re = scm.register("svn", svn)
+        if not rc then
+            return false, re
+        end
+
+        return true
+    end,
     exit = function (ctx) return true end,
 }
 
+svn.svn_source = class("svn_source", source.basic_source)
+
 --- translate url into subversion url
 -- @param u table: url table
 -- @return string: subversion style url
@@ -143,12 +162,222 @@ local function svn_tool(argv, workdir)
     return true, nil, table.concat(out)
 end
 
+function svn.svn_source:initialize(rawsrc)
+    assert(type(rawsrc) == "table")
+    assert(type(rawsrc.name) == "string" and #rawsrc.name > 0)
+    assert(type(rawsrc.type) == "string" and rawsrc.type == "svn")
+
+    local rc, re
+
+    source.basic_source.initialize(self, rawsrc)
+
+    self._server = false
+    self._location = false
+    self._tag = false
+    self._branch = false
+    self._working = false
+    self._workingcopy_subdir = false
+    self._sourceids = {
+        ["working-copy"] = "working-copy"
+    }
+
+    rc, re = e2lib.vrfy_dict_exp_keys(rawsrc, "e2source", {
+        "branch",
+        "env",
+        "licences",
+        "location",
+        "name",
+        "server",
+        "tag",
+        "type",
+        "working",
+        "workingcopy_subdir",
+    })
+
+    if not rc then
+        error(re)
+    end
+
+    rc, re = source.generic_source_validate_licences(rawsrc, self)
+    if not rc then
+        error(re)
+    end
+
+    rc, re = source.generic_source_validate_env(rawsrc, self)
+    if not rc then
+        error(re)
+    end
+
+    rc, re = source.generic_source_validate_server(rawsrc, true)
+    if not rc then
+        error(re)
+    end
+    self._server = rawsrc.server
+
+    rc, re = source.generic_source_validate_working(rawsrc)
+    if not rc then
+        error(re)
+    end
+    self._working = rawsrc.working
+
+    -- workingcopy_subdir is optional and defaults to the branch
+    -- make sure branch is checked first to avoid confusing error
+    if rawsrc.workingcopy_subdir == nil then
+        rawsrc.workingcopy_subdir = rawsrc.branch
+    end
+
+    for _,attr in ipairs({"branch", "location", "tag", "workingcopy_subdir"}) do
+        if rawsrc[attr] == nil then
+            error(err.new("source has no `%s' attribute", attr))
+        elseif type(rawsrc[attr]) ~= "string" then
+            error(err.new("'%s' must be a string", attr))
+        elseif rawsrc[attr] == "" then
+            error(err.new("'%s' may not be empty", attr))
+        end
+    end
+
+    self._branch = rawsrc.branch
+    self._location = rawsrc.location
+    self._tag = rawsrc.tag
+    self._workingcopy_subdir = rawsrc.workingcopy_subdir
+end
+
+function svn.svn_source:get_working()
+    assert(type(self._working) == "string")
+    return self._working
+end
+
+function svn.svn_source:get_workingcopy_subdir()
+    assert(type(self._workingcopy_subdir) == "string")
+    return self._workingcopy_subdir
+end
+
+function svn.svn_source:get_server()
+    assert(type(self._server) == "string")
+    return self._server
+end
+
+function svn.svn_source:get_location()
+    assert(type(self._location) == "string")
+    return self._location
+end
+
+function svn.svn_source:get_branch()
+    assert(type(self._branch) == "string")
+    return self._branch
+end
+
+function svn.svn_source:get_tag()
+    assert(type(self._tag) == "string")
+    return self._tag
+end
+
+function svn.svn_source:sourceid(sourceset)
+    assert(type(sourceset) == "string" and #sourceset > 0)
+
+    local rc, re
+    local hc, surl, svnurl, argv, out, svnrev, lid, svnrev, info, licences
+
+    if self._sourceids[sourceset] then
+        return self._sourceids[sourceset]
+    end
+
+    hc = hash.hash_start()
+    hash.hash_line(hc, self._name)
+    hash.hash_line(hc, self._type)
+    hash.hash_line(hc, self._env:id())
+
+    info = e2tool.info()
+    assert(type(info) == "table")
+
+    licences = self:get_licences()
+    for licencename in licences:iter_sorted() do
+        lid, re = licence.licences[licencename]:licenceid(info)
+        if not lid then
+            return false, re
+        end
+        hash.hash_line(hc, lid)
+    end
+
+    surl, re = cache.remote_url(info.cache, self._server, self._location)
+    if not surl then
+        return false, re
+    end
+
+    svnurl, re = mksvnurl(surl)
+    if not svnurl then
+        return false, re
+    end
+
+    hash.hash_line(hc, self._server)
+    hash.hash_line(hc, self._location)
+
+    if sourceset == "tag" then
+        hash.hash_line(hc, self._tag)
+        argv = { "info", svnurl.."/"..self._tag }
+    elseif sourceset == "branch" then
+        hash.hash_line(hc, self._branch)
+        argv = { "info", svnurl.."/"..self._branch }
+    elseif sourceset == "lazytag" then
+        return false, err.new("svn source does not support lazytag mode")
+    else
+        return false,
+            err.new("svn sourceid can't handle sourceset %q", sourceset)
+    end
+
+    rc, re, out = svn_tool(argv)
+    if not rc then
+        return false,
+            err.new("retrieving revision for tag or branch failed"):cat(re)
+    end
+
+    svnrev = string.match(out, "Last Changed Rev: (%d+)")
+    if not svnrev or string.len(svnrev) == 0 then
+        return false, err.new("could not find SVN revision")
+    end
+    hash.hash_line(hc, svnrev)
+
+    self._sourceids[sourceset] = hash.hash_finish(hc)
+
+    return self._sourceids[sourceset]
+end
+
+function svn.svn_source:display()
+    local d, licences
+
+    -- try to calculte the sourceid, but do not care if it fails.
+    -- working copy might be unavailable
+    self:sourceid("tag")
+    self:sourceid("branch")
+
+    d = {}
+    table.insert(d, string.format("type       = %s", self:get_type()))
+    table.insert(d, string.format("server     = %s", self._server))
+    table.insert(d, string.format("remote     = %s", self._location))
+    table.insert(d, string.format("branch     = %s", self._branch))
+    table.insert(d, string.format("tag        = %s", self._tag))
+    table.insert(d, string.format("working    = %s", self._working))
+
+    licences = self:get_licences()
+    for licencename in licences:iter_sorted() do
+        table.insert(d, string.format("licence    = %s", licencename))
+    end
+
+    for sourceset, sid in pairs(self._sourceids) do
+        if sid then
+            table.insert(d, string.format("sourceid [%s] = %s", sourceset, sid))
+        end
+    end
+
+    return d
+end
+
 function svn.fetch_source(info, sourcename)
     local rc, re
     local e = err.new("fetching source failed: %s", sourcename)
-    local src = info.sources[sourcename]
-    local location = src.location
-    local server = src.server
+    local src = source.sources[sourcename]
+    local location = src:get_location()
+    local server = src:get_server()
     local surl, re = cache.remote_url(info.cache, server, location)
     if not surl then
         return false, e:cat(re)
@@ -158,21 +387,21 @@ function svn.fetch_source(info, sourcename)
         return false, e:cat(re)
     end
 
-    local argv = { "checkout", svnurl, info.root .. "/" .. src.working }
+    local argv = { "checkout", svnurl, info.root .. "/" .. src:get_working() }
 
     rc, re = svn_tool(argv)
     if not rc then
         return false, e:cat(re)
     end
-    return true, nil
+    return true
 end
 
-function svn.prepare_source(info, sourcename, source_set, build_path)
+function svn.prepare_source(info, sourcename, sourceset, build_path)
     local rc, re
     local e = err.new("svn.prepare_source failed")
-    local src = info.sources[ sourcename ]
-    local location = src.location
-    local server = src.server
+    local src = source.sources[sourcename]
+    local location = src:get_location()
+    local server = src:get_server()
     local surl, re = cache.remote_url(info.cache, server, location)
     if not surl then
         return false, e:cat(re)
@@ -181,12 +410,12 @@ function svn.prepare_source(info, sourcename, source_set, build_path)
     if not svnurl then
         return false, e:cat(re)
     end
-    if source_set == "tag" or source_set == "branch" then
+    if sourceset == "tag" or sourceset == "branch" then
         local rev
-        if source_set == "tag" then
-            rev = src.tag
-        else -- source_set == "branch"
-            rev = src.branch
+        if sourceset == "tag" then
+            rev = src:get_tag()
+        else -- sourceset == "branch"
+            rev = src:get_branch()
         end
         local argv = { "export", svnurl .. "/" .. rev,
         build_path .. "/" .. sourcename }
@@ -194,10 +423,11 @@ function svn.prepare_source(info, sourcename, source_set, build_path)
         if not rc then
             return false, e:cat(re)
         end
-    elseif source_set == "working-copy" then
+    elseif sourceset == "working-copy" then
         -- cp -R info.root/src.working/src.workingcopy_subdir build_path
-        local s = e2lib.join(info.root, src.working, src.workingcopy_subdir)
-        local d = e2lib.join(build_path, src.name)
+        local s = e2lib.join(info.root, src:get_working(),
+            src:get_workingcopy_subdir())
+        local d = e2lib.join(build_path, src:get_name())
         rc, re = e2lib.cp(s, d, true)
         if not rc then
             return false, e:cat(re)
@@ -210,8 +440,9 @@ end
 
 function svn.working_copy_available(info, sourcename)
     local rc, re
-    local src = info.sources[sourcename]
-    local dir = e2lib.join(info.root, src.working)
+    local src = source.sources[sourcename]
+
+    local dir = e2lib.join(info.root, src:get_working())
     return e2lib.isdir(dir)
 end
 
@@ -220,127 +451,30 @@ function svn.check_workingcopy(info, sourcename)
     local e = err.new("checking working copy failed")
     e:append("in source %s (svn configuration):", sourcename)
     e:setcount(0)
-    local src = info.sources[sourcename]
+    local src = source.sources[sourcename]
     if e:getcount() > 0 then
         return false, e
     end
     -- check if the configured branch and tag exist
     local d
-    d = e2lib.join(info.root, src.working, src.branch)
+    d = e2lib.join(info.root, src:get_working(), src:get_branch())
     if not e2lib.isdir(d) then
-        e:append("branch does not exist: %s", src.branch)
+        e:append("branch does not exist: %s", src:get_branch())
     end
-    d = e2lib.join(info.root, src.working, src.tag)
+    d = e2lib.join(info.root, src:get_working(), src:get_tag())
     if not e2lib.isdir(d) then
-        e:append("tag does not exist: %s", src.tag)
+        e:append("tag does not exist: %s", src:get_tag())
     end
     if e:getcount() > 0 then
         return false, e
     end
-    return true, nil
+    return true
 end
 
 function svn.has_working_copy(info, sname)
     return true
 end
 
---- create a table of lines for display
--- @param info the info structure
--- @param sourcename string
--- @return a table, nil on error
--- @return an error string on failure
-function svn.display(info, sourcename)
-    local src = info.sources[sourcename]
-    local rc, e
-    local display = {}
-    display[1] = string.format("type       = %s", src.type)
-    display[2] = string.format("server     = %s", src.server)
-    display[3] = string.format("remote     = %s", src.location)
-    display[4] = string.format("branch     = %s", src.branch)
-    display[5] = string.format("tag        = %s", src.tag)
-    display[6] = string.format("working    = %s", src.working)
-    local i = 7
-    for _,l in pairs(src.licences) do
-        display[i] = string.format("licence    = %s", l)
-        i = i + 1
-    end
-    return display
-end
-
---- calculate an id for a source
--- @param info
--- @param sourcename
--- @param source_set
-function svn.sourceid(info, sourcename, source_set)
-    local src = info.sources[sourcename]
-    local rc, re
-    local hc, surl, svnurl, argv, out, svnrev, lid
-
-    if not src.sourceid then
-        src.sourceid = {}
-    end
-
-    src.sourceid["working-copy"] = "working-copy"
-    if src.sourceid[source_set] then
-        return true, nil, src.sourceid[source_set]
-    end
-
-    hc = hash.hash_start()
-    hash.hash_line(hc, src.name)
-    hash.hash_line(hc, src.type)
-    hash.hash_line(hc, src._env:id())
-    for _,ln in pairs(src.licences) do
-        lid, re = licence.licences[ln]:licenceid(info)
-        if not lid then
-            return false, re
-        end
-        hash.hash_line(hc, lid)
-    end
-
-    -- svn specific
-    surl, re = cache.remote_url(info.cache, src.server, src.location)
-    if not surl then
-        return false, re
-    end
-
-    svnurl, re = mksvnurl(surl)
-    if not svnurl then
-        return false, re
-    end
-
-    hash.hash_line(hc, src.server)
-    hash.hash_line(hc, src.location)
-
-    if source_set == "tag" then
-        hash.hash_line(hc, src.tag)
-        argv = { "info", svnurl.."/"..src.tag }
-    elseif source_set == "branch" then
-        hash.hash_line(hc, src.branch)
-        argv = { "info", svnurl.."/"..src.branch }
-    elseif source_set == "lazytag" then
-        return false, err.new("svn source does not support lazytag mode")
-    else
-        return false,
-            err.new("svn sourceid can't handle source_set %q", source_set)
-    end
-
-    rc, re, out = svn_tool(argv)
-    if not rc then
-        return false,
-            err.new("retrieving revision for tag or branch failed"):cat(re)
-    end
-
-    svnrev = string.match(out, "Last Changed Rev: (%d+)")
-    if not svnrev or string.len(svnrev) == 0 then
-        return false, err.new("could not find SVN revision")
-    end
-    hash.hash_line(hc, svnrev)
-
-    src.sourceid[source_set] = hash.hash_finish(hc)
-
-    return true, nil, src.sourceid[source_set]
-end
-
 function svn.toresult(info, sourcename, sourceset, directory)
     -- <directory>/source/<sourcename>.tar.gz
     -- <directory>/makefile
@@ -351,7 +485,7 @@ function svn.toresult(info, sourcename, sourceset, directory)
     if not rc then
         return false, e:cat(re)
     end
-    local src = info.sources[sourcename]
+    local src = source.sources[sourcename]
     -- write makefile
     local makefile = "Makefile"
     local source = "source"
@@ -378,7 +512,7 @@ function svn.toresult(info, sourcename, sourceset, directory)
         return false, e:cat(re)
     end
     -- create a tarball in the final location
-    local archive = string.format("%s.tar.gz", src.name)
+    local archive = string.format("%s.tar.gz", src:get_name())
     rc, re = e2lib.tar({ "-C", tmpdir ,"-czf", sourcedir .. "/" .. archive,
     sourcename })
     if not rc then
@@ -387,7 +521,8 @@ function svn.toresult(info, sourcename, sourceset, directory)
     -- write licences
     local destdir = e2lib.join(directory, "licences")
     local fname = string.format("%s/%s.licences", destdir, archive)
-    local licence_list = table.concat(src.licences, "\n") .. "\n"
+    local licences = src:get_licences()
+    local licence_list = licences:concat_sorted("\n") .. "\n"
     rc, re = e2lib.mkdir_recursive(destdir)
     if not rc then
         return false, e:cat(re)
@@ -403,8 +538,8 @@ end
 function svn.update(info, sourcename)
     local rc, re
     local e = err.new("updating source '%s' failed", sourcename)
-    local src = info.sources[ sourcename ]
-    local workdir = e2lib.join(info.root, src.working)
+    local src = source.sources[sourcename]
+    local workdir = e2lib.join(info.root, src:get_working())
     rc, re = svn_tool({ "update" }, workdir)
     if not rc then
         return false, e:cat(re)
@@ -412,63 +547,6 @@ function svn.update(info, sourcename)
     return true
 end
 
---- validate source configuration, log errors to the debug log
--- @param info the info table
--- @param sourcename the source name
--- @return bool
-function svn.validate_source(info, sourcename)
-    local rc, re = scm.generic_source_validate(info, sourcename)
-    if not rc then
-        -- error in generic configuration. Don't try to go on.
-        return false, re
-    end
-    local src = info.sources[ sourcename ]
-    if not src.sourceid then
-        src.sourceid = {}
-    end
-    local e = err.new("in source %s:", sourcename)
-    rc, re = scm.generic_source_default_working(info, sourcename)
-    if not rc then
-        return false, e:cat(re)
-    end
-    e:setcount(0)
-    if not src.server then
-        e:append("source has no `server' attribute")
-    end
-    if not src.licences then
-        e:append("source has no `licences' attribute")
-    end
-    if not src.location then
-        e:append("source has no `location' attribute")
-    end
-    if src.remote then
-        e:append("source has `remote' attribute, not allowed for svn sources")
-    end
-    if not src.branch then
-        e:append("source has no `branch' attribute")
-    end
-    if type(src.tag) ~= "string" then
-        e:append("source has no `tag' attribute or tag attribute has wrong type")
-    end
-    if type(src.workingcopy_subdir) ~= "string" then
-        e2lib.warnf("WDEFAULT", "in source %s", sourcename)
-        e2lib.warnf("WDEFAULT",
-        " workingcopy_subdir defaults to the branch: %s", src.branch)
-        src.workingcopy_subdir = src.branch
-    end
-    if not src.working then
-        e:append("source has no `working' attribute")
-    end
-    local rc, re = tools.check_tool("svn")
-    if not rc then
-        e:cat(re)
-    end
-    if e:getcount() > 0 then
-        return false, e
-    end
-    return true, nil
-end
-
 strict.lock(svn)
 
 -- vim:sw=4:sts=4:et: