allow interrupting groovy reconfigure scripts

import difflib.DiffUtils
import groovy.io.FileType
@@ThreadInterrupt
import groovy.transform.ThreadInterrupt
import hudson.AbortException
import hudson.model.View
import java.io.StringBufferInputStream
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.stream.StreamSource
import jenkins.model.Jenkins
import org.apache.xml.serialize.OutputFormat
import org.apache.xml.serialize.XMLSerializer
import org.xml.sax.InputSource

// job types with their prefix and a list of job names
// used to remove obsolete jobs with the same prefix which are not part of the list
job_prefixes_and_names = [:]
@[for job_type in sorted(job_prefixes_and_names.keys())]@
job_prefixes_and_names['@job_type'] = [
    'job_prefix': '@(job_prefixes_and_names[job_type][0])',
    'job_names': [],
]
@{
job_names = sorted(job_prefixes_and_names[job_type][1])
group_size = 1000
}@
// group job names in chunks of @group_size to not exceed groovy limits
@[for i in range(0, len(job_names), group_size)]@
def add_job_prefixes_and_names_@(job_type)_@(int(i / group_size) + 1)(job_names) {
@[for j in range(i, min(i + group_size, len(job_names)))]@
job_names << '@(job_names[j])'
@[end for]@
}
add_job_prefixes_and_names_@(job_type)_@(int(i / group_size) + 1)(job_prefixes_and_names['@job_type'].job_names)
@[end for]@
@[end for]@

println '# BEGIN SECTION: Groovy script - reconfigure'
dry_run = @('true' if dry_run else 'false')
dry_run_suffix = dry_run ? ' (dry run)' : ''

@[if vars().get('expected_num_views')]@
// reconfigure views
println '# BEGIN SUBSECTION: reconfigure @(expected_num_views) views'
created_views = 0
updated_views = 0
skipped_views = 0

view_config_dir = build.getWorkspace().toString() + '/reconfigure_jobs/view_configs'

def view_dir = new File(view_config_dir)
def views = view_dir.listFiles()
views.sort()

if (views.size() != @(expected_num_views)) {
    println "ERROR: Found different number of view configs than expected!! " + views.size() + " is not @(expected_num_views) as expected."
    // fail this build
    throw new AbortException("Wrong number of view configs")
}

views.each{
    found_view = false
    view_name = it.getName()
    view_config = new File(it.path).getText('UTF-8')
    for (v in Jenkins.instance.views) {
        if (v.name != view_name) continue
        found_view = true
        output_stream= new ByteArrayOutputStream()
        v.writeXml(output_stream)
        reader = new StringReader(output_stream.toString())
        source = new InputSource(reader)

        diff = diff_configs(source, view_config)
        if (!diff) {
            println "Skipped view '" + view_name + "' because the config is the same"
            skipped_views += 1
        } else {
            println "Updating view '" + view_name + "'" + dry_run_suffix
            println '    <<<'
            for (line in diff) {
                println '    ' + line
            }
            println '    >>>'

            if (!dry_run) {
                reader = new StringReader(view_config)
                source = new StreamSource(reader)
                v.updateByXml(source)
            }
            updated_views += 1
        }
        break
    }
    if (!found_view) {
        println "Creating view '" + view_name + "'" + dry_run_suffix
        if (!dry_run) {
            stream = new StringBufferInputStream(view_config)
            view = View.createViewFromXML(view_name, stream)
            Jenkins.instance.addView(view)
        }
        created_views += 1
    }
}

println 'Created ' + created_views + ' views, updated ' + updated_views + ' views, skipped ' + skipped_views + ' views' + dry_run_suffix + '.'
println '# END SUBSECTION'

@[end if]@
// reconfigure jobs
println '# BEGIN SUBSECTION: reconfigure @(expected_num_jobs) jobs'
created_jobs = 0
updated_jobs = 0
skipped_jobs = 0

job_config_dir = build.getWorkspace().toString() + '/reconfigure_jobs/job_configs'

def job_dir = new File(job_config_dir)
def jobs = job_dir.listFiles()
jobs.sort()

if (jobs.size() != @(expected_num_jobs)) {
    println "ERROR: Found different number of job configs than expected!! " + jobs.size() + " is not @(expected_num_jobs) as expected."
    // fail this build
    throw new AbortException("Wrong number of job configs")
}

def mergePullRequestData(job_name, current_file, job_config) {
    factory = DocumentBuilderFactory.newInstance()
    builder = factory.newDocumentBuilder()
    document = builder.parse(current_file)

    nodelist = document.getElementsByTagName("triggers")
    if (nodelist.getLength() != 1) return job_config
    trigger_node = nodelist.item(0)

    nodelist = trigger_node.getElementsByTagName("org.jenkinsci.plugins.ghprb.GhprbTrigger")
    if (nodelist.getLength() != 1) return job_config
    ghprb_trigger_node = nodelist.item(0)

    nodelist = ghprb_trigger_node.getElementsByTagName("pullRequests")
    if (nodelist.getLength() != 1) return job_config
    pull_requests_node = nodelist.item(0)

    builder2 = factory.newDocumentBuilder()
    stream = new StringBufferInputStream(job_config)
    document2 = builder2.parse(stream)

    nodelist = document2.getElementsByTagName("triggers")
    if (nodelist.getLength() != 1) return job_config
    trigger_node = nodelist.item(0)

    nodelist = trigger_node.getElementsByTagName("org.jenkinsci.plugins.ghprb.GhprbTrigger")
    if (nodelist.getLength() != 1) return job_config
    ghprb_trigger_node = nodelist.item(0)

    pull_requests_node = document2.importNode(pull_requests_node, true)
    ghprb_trigger_node.appendChild(pull_requests_node)

    println "Merged configuration with existing pull request job '" + job_name + "' to maintain pull request information"

    return format_xml(document2)
}

jobs.each{
    found_project = false
    job_name = it.getName()
    // remove leading serial number
    job_name = job_name[job_name.indexOf(' ') + 1..-1]
    job_config = new File(it.path).getText('UTF-8')
    for (p in Jenkins.instance.allItems) {
        if (p.name != job_name) continue
        found_project = true
        job_config_file = p.getConfigFile()

        if (p.name.substring(1, 5) == 'pr__') {
            job_config = mergePullRequestData(job_name, job_config_file.getFile(), job_config)
        }

        diff = diff_configs(job_config_file.getFile(), job_config)
        if (!diff) {
            println "Skipped job '" + job_name + "' because the config is the same"
            skipped_jobs += 1
        } else {
            println "Updating job '" + job_name + "'" + dry_run_suffix
            println '    <<<'
            for (line in diff) {
                println '    ' + line
            }
            println '    >>>'

            if (!dry_run) {
                reader = new StringReader(job_config)
                source = new StreamSource(reader)
                p.updateByXml(source)
            }
            updated_jobs += 1
        }
        break
    }
    if (!found_project) {
        println "Creating job '" + job_name + "'" + dry_run_suffix
        if (!dry_run) {
            stream = new StringBufferInputStream(job_config)
            Jenkins.instance.createProjectFromXML(job_name, stream)
        }
        created_jobs += 1
    }
}

println 'Created ' + created_jobs + ' jobs, updated ' + updated_jobs + ' jobs, skipped ' + skipped_jobs + ' jobs' + dry_run_suffix + '.'
println 'Rebuilding dependency graph...'
Jenkins.instance.rebuildDependencyGraph()
println '# END SUBSECTION'

// delete obsolete jobs
for (item in job_prefixes_and_names) {
    job_type = item.key
    job_prefix = item.value.job_prefix
    job_names = item.value.job_names
    deleted = 0
    println "# BEGIN SUBSECTION: Groovy script - delete obsolete '" + job_type + "' jobs"
    println "Searching for obsolete jobs starting with '" + job_prefix + "'"
    for (p in Jenkins.instance.allItems) {
        if (!p.name.startsWith(job_prefix)) continue
        if (p.name in job_names) continue
        println "Deleting job '" + p.name + "'" + dry_run_suffix
        if (!dry_run) {
            p.delete()
        }
        deleted += 1
    }
    println 'Deleted ' + deleted + ' jobs' + dry_run_suffix + '.'
    println '# END SUBSECTION'
}

println '# END SECTION'

def diff_configs(current_config, new_config) {

    factory = DocumentBuilderFactory.newInstance()
    builder = factory.newDocumentBuilder()

    current_doc = builder.parse(current_config)

    new_config_stream = new StringBufferInputStream(new_config)
    new_doc = builder.parse(new_config_stream)

    // ignore description which contains a timestamp
    if (current_doc.getElementsByTagName('description')) {
        current_doc.getElementsByTagName('description').item(0).setTextContent('')
    }
    if (new_doc.getElementsByTagName('description')) {
        new_doc.getElementsByTagName('description').item(0).setTextContent('')
    }

    current_string = format_xml(current_doc)
    new_string = format_xml(new_doc)

    if (current_string == new_string) {
        return []
    }

    current_lines = []
    current_string.eachLine { line, count ->
        current_lines << line
    }
    new_lines = []
    new_string.eachLine { line, count ->
        new_lines << line
    }

    patch = DiffUtils.diff(current_lines, new_lines)
    return DiffUtils.generateUnifiedDiff('current config', 'new config', current_lines, patch, 0)
}

def format_xml(doc) {
    format = new OutputFormat(doc)
    format.setIndenting(true)
    format.setIndent(2)
    writer = new StringWriter()
    serializer = new XMLSerializer(writer, format)
    serializer.serialize(doc)
    return writer.toString()
}

import difflib.DiffUtils
import groovy.io.FileType
import hudson.AbortException
import hudson.model.View
import java.io.StringBufferInputStream
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.stream.StreamSource
import jenkins.model.Jenkins
import org.apache.xml.serialize.OutputFormat
import org.apache.xml.serialize.XMLSerializer
import org.xml.sax.InputSource

// job types with their prefix and a list of job names
// used to remove obsolete jobs with the same prefix which are not part of the list
job_prefixes_and_names = [:]
@[for job_type in sorted(job_prefixes_and_names.keys())]@
job_prefixes_and_names['@job_type'] = [
    'job_prefix': '@(job_prefixes_and_names[job_type][0])',
    'job_names': [],
]
@{
job_names = sorted(job_prefixes_and_names[job_type][1])
group_size = 1000
}@
// group job names in chunks of @group_size to not exceed groovy limits
@[for i in range(0, len(job_names), group_size)]@
def add_job_prefixes_and_names_@(job_type)_@(int(i / group_size) + 1)(job_names) {
@[for j in range(i, min(i + group_size, len(job_names)))]@
job_names << '@(job_names[j])'
@[end for]@
}
add_job_prefixes_and_names_@(job_type)_@(int(i / group_size) + 1)(job_prefixes_and_names['@job_type'].job_names)
@[end for]@
@[end for]@

println '# BEGIN SECTION: Groovy script - reconfigure'
dry_run = @('true' if dry_run else 'false')
dry_run_suffix = dry_run ? ' (dry run)' : ''

@[if vars().get('expected_num_views')]@
// reconfigure views
println '# BEGIN SUBSECTION: reconfigure @(expected_num_views) views'
created_views = 0
updated_views = 0
skipped_views = 0

view_config_dir = build.getWorkspace().toString() + '/reconfigure_jobs/view_configs'

def view_dir = new File(view_config_dir)
def views = view_dir.listFiles()
views.sort()

if (views.size() != @(expected_num_views)) {
    println "ERROR: Found different number of view configs than expected!! " + views.size() + " is not @(expected_num_views) as expected."
    // fail this build
    throw new AbortException("Wrong number of view configs")
}

views.each{
    found_view = false
    view_name = it.getName()
    view_config = new File(it.path).getText('UTF-8')
    for (v in Jenkins.instance.views) {
        if (v.name != view_name) continue
        found_view = true
        output_stream= new ByteArrayOutputStream()
        v.writeXml(output_stream)
        reader = new StringReader(output_stream.toString())
        source = new InputSource(reader)

        diff = diff_configs(source, view_config)
        if (!diff) {
            println "Skipped view '" + view_name + "' because the config is the same"
            skipped_views += 1
        } else {
            println "Updating view '" + view_name + "'" + dry_run_suffix
            println '    <<<'
            for (line in diff) {
                println '    ' + line
            }
            println '    >>>'

            if (!dry_run) {
                reader = new StringReader(view_config)
                source = new StreamSource(reader)
                v.updateByXml(source)
            }
            updated_views += 1
        }
        break
    }
    if (!found_view) {
        println "Creating view '" + view_name + "'" + dry_run_suffix
        if (!dry_run) {
            stream = new StringBufferInputStream(view_config)
            view = View.createViewFromXML(view_name, stream)
            Jenkins.instance.addView(view)
        }
        created_views += 1
    }
}

println 'Created ' + created_views + ' views, updated ' + updated_views + ' views, skipped ' + skipped_views + ' views' + dry_run_suffix + '.'
println '# END SUBSECTION'

@[end if]@
// reconfigure jobs
println '# BEGIN SUBSECTION: reconfigure @(expected_num_jobs) jobs'
created_jobs = 0
updated_jobs = 0
skipped_jobs = 0

job_config_dir = build.getWorkspace().toString() + '/reconfigure_jobs/job_configs'

def job_dir = new File(job_config_dir)
def jobs = job_dir.listFiles()
jobs.sort()

if (jobs.size() != @(expected_num_jobs)) {
    println "ERROR: Found different number of job configs than expected!! " + jobs.size() + " is not @(expected_num_jobs) as expected."
    // fail this build
    throw new AbortException("Wrong number of job configs")
}

def mergePullRequestData(job_name, current_file, job_config) {
    factory = DocumentBuilderFactory.newInstance()
    builder = factory.newDocumentBuilder()
    document = builder.parse(current_file)

    nodelist = document.getElementsByTagName("triggers")
    if (nodelist.getLength() != 1) return job_config
    trigger_node = nodelist.item(0)

    nodelist = trigger_node.getElementsByTagName("org.jenkinsci.plugins.ghprb.GhprbTrigger")
    if (nodelist.getLength() != 1) return job_config
    ghprb_trigger_node = nodelist.item(0)

    nodelist = ghprb_trigger_node.getElementsByTagName("pullRequests")
    if (nodelist.getLength() != 1) return job_config
    pull_requests_node = nodelist.item(0)

    builder2 = factory.newDocumentBuilder()
    stream = new StringBufferInputStream(job_config)
    document2 = builder2.parse(stream)

    nodelist = document2.getElementsByTagName("triggers")
    if (nodelist.getLength() != 1) return job_config
    trigger_node = nodelist.item(0)

    nodelist = trigger_node.getElementsByTagName("org.jenkinsci.plugins.ghprb.GhprbTrigger")
    if (nodelist.getLength() != 1) return job_config
    ghprb_trigger_node = nodelist.item(0)

    pull_requests_node = document2.importNode(pull_requests_node, true)
    ghprb_trigger_node.appendChild(pull_requests_node)

    println "Merged configuration with existing pull request job '" + job_name + "' to maintain pull request information"

    return format_xml(document2)
}

jobs.each{
    found_project = false
    job_name = it.getName()
    // remove leading serial number
    job_name = job_name[job_name.indexOf(' ') + 1..-1]
    job_config = new File(it.path).getText('UTF-8')
    for (p in Jenkins.instance.allItems) {
        if (p.name != job_name) continue
        found_project = true
        job_config_file = p.getConfigFile()

        if (p.name.substring(1, 5) == 'pr__') {
            job_config = mergePullRequestData(job_name, job_config_file.getFile(), job_config)
        }

        diff = diff_configs(job_config_file.getFile(), job_config)
        if (!diff) {
            println "Skipped job '" + job_name + "' because the config is the same"
            skipped_jobs += 1
        } else {
            println "Updating job '" + job_name + "'" + dry_run_suffix
            println '    <<<'
            for (line in diff) {
                println '    ' + line
            }
            println '    >>>'

            if (!dry_run) {
                reader = new StringReader(job_config)
                source = new StreamSource(reader)
                p.updateByXml(source)
            }
            updated_jobs += 1
        }
        break
    }
    if (!found_project) {
        println "Creating job '" + job_name + "'" + dry_run_suffix
        if (!dry_run) {
            stream = new StringBufferInputStream(job_config)
            Jenkins.instance.createProjectFromXML(job_name, stream)
        }
        created_jobs += 1
    }
}

println 'Created ' + created_jobs + ' jobs, updated ' + updated_jobs + ' jobs, skipped ' + skipped_jobs + ' jobs' + dry_run_suffix + '.'
println 'Rebuilding dependency graph...'
Jenkins.instance.rebuildDependencyGraph()
println '# END SUBSECTION'

// delete obsolete jobs
for (item in job_prefixes_and_names) {
    job_type = item.key
    job_prefix = item.value.job_prefix
    job_names = item.value.job_names
    deleted = 0
    println "# BEGIN SUBSECTION: Groovy script - delete obsolete '" + job_type + "' jobs"
    println "Searching for obsolete jobs starting with '" + job_prefix + "'"
    for (p in Jenkins.instance.allItems) {
        if (!p.name.startsWith(job_prefix)) continue
        if (p.name in job_names) continue
        println "Deleting job '" + p.name + "'" + dry_run_suffix
        if (!dry_run) {
            p.delete()
        }
        deleted += 1
    }
    println 'Deleted ' + deleted + ' jobs' + dry_run_suffix + '.'
    println '# END SUBSECTION'
}

println '# END SECTION'

def diff_configs(current_config, new_config) {

    factory = DocumentBuilderFactory.newInstance()
    builder = factory.newDocumentBuilder()

    current_doc = builder.parse(current_config)

    new_config_stream = new StringBufferInputStream(new_config)
    new_doc = builder.parse(new_config_stream)

    // ignore description which contains a timestamp
    if (current_doc.getElementsByTagName('description')) {
        current_doc.getElementsByTagName('description').item(0).setTextContent('')
    }
    if (new_doc.getElementsByTagName('description')) {
        new_doc.getElementsByTagName('description').item(0).setTextContent('')
    }

    current_string = format_xml(current_doc)
    new_string = format_xml(new_doc)

    if (current_string == new_string) {
        return []
    }

    current_lines = []
    current_string.eachLine { line, count ->
        current_lines << line
    }
    new_lines = []
    new_string.eachLine { line, count ->
        new_lines << line
    }

    patch = DiffUtils.diff(current_lines, new_lines)
    return DiffUtils.generateUnifiedDiff('current config', 'new config', current_lines, patch, 0)
}

def format_xml(doc) {
    format = new OutputFormat(doc)
    format.setIndenting(true)
    format.setIndent(2)
    writer = new StringWriter()
    serializer = new XMLSerializer(writer, format)
    serializer.serialize(doc)
    return writer.toString()
}


Support more than one upstream RPM repo by default (#867)

This change expands the default pattern when looking for upstream RPM
repositories to sync into building/testing/main.

Co-authored-by: Steven! Ragnarök <54c1a997c2d2cb64bdadd610dcb63136c1bf34f5@users.noreply.github.com>

<project>
  <actions/>
  <description>Generated at @ESCAPE(now_str) from template '@ESCAPE(template_name)'</description>
  <keepDependencies>false</keepDependencies>
  <properties>
@(SNIPPET(
    'property_log-rotator',
    days_to_keep=365 * 3,
    num_to_keep=1000,
))@
@(SNIPPET(
    'property_job-priority',
    priority=20,
))@
@(SNIPPET(
    'property_rebuild-settings',
))@
@(SNIPPET(
    'property_requeue-job',
))@
@(SNIPPET(
    'property_parameters-definition',
    parameters=[
        {
            'type': 'string',
            'name': 'REMOTE_SOURCE_EXPRESSION',
            'description': 'Expression to match pulp remote repositories',
            'default_value': '^ros-upstream-[^-]+-([^-]+-[^-]+-[^-]+(-debug)?)$',
        },
        {
            'type': 'string',
            'name': 'DISTRIBUTION_DEST_EXPRESSION',
            'description': 'Expression to transform source remote to destination pulp distribution',
            'default_value': '^ros-(building|testing|main)-\\1$',
        },
        {
            'type': 'boolean',
            'name': 'EXECUTE_IMPORT',
            'description': 'If this is not true, it will only do a dry run and print the expect import, but not execute.',
            'default_value': 'false',
        },
    ],
))@
@(SNIPPET(
    'property_job-weight',
))@
  </properties>
@(SNIPPET(
    'scm_git',
    url=ros_buildfarm_repository.url,
    branch_name=ros_buildfarm_repository.version or 'master',
    relative_target_dir='ros_buildfarm',
    refspec=None,
))@
  <scmCheckoutRetryCount>2</scmCheckoutRetryCount>
  <assignedNode>building_repository</assignedNode>
  <canRoam>false</canRoam>
  <disabled>false</disabled>
  <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
  <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
  <triggers/>
  <concurrentBuild>false</concurrentBuild>
  <builders>
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'echo "# BEGIN SECTION: mirror repositories to pulp"',
        'export PYTHONPATH=$WORKSPACE/ros_buildfarm:$PYTHONPATH',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/release/rpm/mirror_repo.py' +
        ' --remote-source-expression $REMOTE_SOURCE_EXPRESSION' +
        ' --distribution-dest-expression "^\\g<0>$"',
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: sync packages from mirrors to repos"',
        'if [ "$EXECUTE_IMPORT" != "true" ]; then DRY_RUN_ARG="--dry-run"; fi',
        'export PYTHONPATH=$WORKSPACE/ros_buildfarm:$PYTHONPATH',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/release/rpm/sync_repo.py' +
        ' --distribution-source-expression $REMOTE_SOURCE_EXPRESSION' +
        ' --distribution-dest-expression $DISTRIBUTION_DEST_EXPRESSION' +
        ' $DRY_RUN_ARG',
        'echo "# END SECTION"',
    ]),
))@
  </builders>
  <publishers>
@(SNIPPET(
    'publisher_mailer',
    recipients=recipients,
    dynamic_recipients=[],
    send_to_individuals=False,
))@
  </publishers>
  <buildWrappers>
@(SNIPPET(
    'pulp_credentials',
    credential_id=credential_id,
    dest_credential_id=dest_credential_id,
))@
@(SNIPPET(
    'build-wrapper_timestamper',
))@
  </buildWrappers>
</project>

<project>
  <actions/>
  <description>Generated at @ESCAPE(now_str) from template '@ESCAPE(template_name)'</description>
  <keepDependencies>false</keepDependencies>
  <properties>
@(SNIPPET(
    'property_log-rotator',
    days_to_keep=365 * 3,
    num_to_keep=1000,
))@
@(SNIPPET(
    'property_job-priority',
    priority=20,
))@
@(SNIPPET(
    'property_rebuild-settings',
))@
@(SNIPPET(
    'property_requeue-job',
))@
@(SNIPPET(
    'property_parameters-definition',
    parameters=[
        {
            'type': 'string',
            'name': 'REMOTE_SOURCE_EXPRESSION',
            'description': 'Expression to match pulp remote repositories',
            'default_value': '^ros-bootstrap-([^-]*-[^-]*-[^-]*(-debug)?)$',
        },
        {
            'type': 'string',
            'name': 'DISTRIBUTION_DEST_EXPRESSION',
            'description': 'Expression to transform source remote to destination pulp distribution',
            'default_value': '^ros-(building|testing|main)-\\1$',
        },
        {
            'type': 'boolean',
            'name': 'EXECUTE_IMPORT',
            'description': 'If this is not true, it will only do a dry run and print the expect import, but not execute.',
            'default_value': 'false',
        },
    ],
))@
@(SNIPPET(
    'property_job-weight',
))@
  </properties>
@(SNIPPET(
    'scm_git',
    url=ros_buildfarm_repository.url,
    branch_name=ros_buildfarm_repository.version or 'master',
    relative_target_dir='ros_buildfarm',
    refspec=None,
))@
  <scmCheckoutRetryCount>2</scmCheckoutRetryCount>
  <assignedNode>building_repository</assignedNode>
  <canRoam>false</canRoam>
  <disabled>false</disabled>
  <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
  <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
  <triggers/>
  <concurrentBuild>false</concurrentBuild>
  <builders>
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'echo "# BEGIN SECTION: mirror repositories to pulp"',
        'export PYTHONPATH=$WORKSPACE/ros_buildfarm:$PYTHONPATH',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/release/rpm/mirror_repo.py' +
        ' --remote-source-expression $REMOTE_SOURCE_EXPRESSION' +
        ' --distribution-dest-expression "^\\g<0>$"',
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: sync packages from mirrors to repos"',
        'if [ "$EXECUTE_IMPORT" != "true" ]; then DRY_RUN_ARG="--dry-run"; fi',
        'export PYTHONPATH=$WORKSPACE/ros_buildfarm:$PYTHONPATH',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/release/rpm/sync_repo.py' +
        ' --distribution-source-expression $REMOTE_SOURCE_EXPRESSION' +
        ' --distribution-dest-expression $DISTRIBUTION_DEST_EXPRESSION' +
        ' $DRY_RUN_ARG',
        'echo "# END SECTION"',
    ]),
))@
  </builders>
  <publishers>
@(SNIPPET(
    'publisher_mailer',
    recipients=recipients,
    dynamic_recipients=[],
    send_to_individuals=False,
))@
  </publishers>
  <buildWrappers>
@(SNIPPET(
    'pulp_credentials',
    credential_id=credential_id,
    dest_credential_id=dest_credential_id,
))@
@(SNIPPET(
    'build-wrapper_timestamper',
))@
  </buildWrappers>
</project>


[3.20]  Remove deprecated styles - stopping log noise

Tested against gtk3-3.20.6-1.fc24.x86_64

${
import os
import math

gtk_major, gtk_minor, gtk_patch = map(int, gtk.split('.'))
treeview_pseudo_element = gtk_major >= 3 and gtk_minor >= 16

def my_floor(num):
    return int(math.floor(num))

def my_ceil(num):
    return int(math.ceil(num))

# Should we set the line width in the engine to 2.25, and draw non pixel aligned lines?
# Are these already the correct sizes for the XO?

# These sizes need to be sanity checked ...
if scaling == "100":
    xo = True
    line_width = 2.0        # 2.25px, rounded down
    thick_line_width = 3.5  # 3.5
    subcell_size = 15
    icon_base = 11
    bullet_size = 9.5
    font_height = 24
    default_padding = 6
    toolbutton_padding = 10
    radio_size = 26
    scale_trough_margin = 8
else: # About 72% of the XO size, adjusted so that eg. toolbuttons work
    xo = False
    line_width = 2.0            # 1.62 rounded up
    thick_line_width = 3.0      # 2.52
    subcell_size = 11           # 10.8
    icon_base = 8               # 7.92
    bullet_size = 6.5           # 6.84
    # This is a guess on the font size (Sans 10 at 96 DPI)
    font_height = 17
    default_padding = 4         # 4.32
    toolbutton_padding = 7      # 7.68
    radio_size = 18             # 18.72
    scale_trough_margin = 6     # 5.76

scale_slider_width = my_floor(2 * subcell_size + line_width)
thickness = my_ceil(line_width)

icon_small = icon_base * 3
icon_large = icon_base * 5

disabled_opacity = 0.5

}

* {
    -sugar-focus-line: @white;

    /* 0.05 works good for both the sugar and sugar-xo themes */
    -GtkWidget-cursor-aspect-ratio: 0.05;

    -GtkWidget-scroll-arrow-hlength: $subcell_size;
    -GtkWidget-scroll-arrow-vlength: $subcell_size;

    -GtkRange-activate-slider: 1;

    -GtkScrolledWindow-scrollbar-spacing: 0;

    -GtkExpander-expander-size: $font_height;
    -GtkExpander-expander-spacing: 2;

    -GtkTreeView-expander-size: $font_height;

    -GtkArrow-arrow-size: 1.0;

    -GtkProgressBar-min-horizontal-bar-height: $subcell_size;
    -GtkProgressBar-min-vertical-bar-width: $subcell_size;

    -GtkWidget-text-handle-width: 110px;
    -GtkWidget-text-handle-height: 110px;
}

*:disabled {
    color: @panel_grey;
}

/* Backgrounds and windows */

window {
    background: @panel_grey;
    color: @black;
}

dialog {
    background: @black;
    color: @white;
}

introwindow {
    background: @white;
    color: @black;
}

label, label:disabled {
    background: transparent;
}

/* Buttons */

button {
    ${ border = max(0, my_ceil((3*subcell_size/2.0 - icon_small / 2.0))) }
    padding: $(border)px $(border)px $(border)px $(border)px;
    border-width: $(thickness)px;
    border-color: @button_grey;
    border-style: solid;
    border-radius: $(2*subcell_size)px;
    background: @button_grey;
    color: @white;
}

button:focus {
    border-color: @white;
    color: @white;
}

button:active {
    background: @white;
    color: @black;
}

button:disabled {
    background: transparent;
}

button:active:focus {
    color: @black;
    border-color: @button_grey;
}

/* Toggle buttons */

button:checked {
    background: @white;
    color: @black;
}

/* Entries and views (textview, treeview) */

.view,
textview text {  /* Setting a textview to background white makes it black,
                    unless you only style the text css node */
    border-width: 0px;
    border-style: none;
    border-radius: 0px;
    padding: 0px;
    background: @white;
    color: @black;
}

treeview header button,
treeview header button:hover:active {
    border-radius: 0px;
    background: @button_grey;
    border-width: 0px;
    color: @white;
}

/* FIXME: treeview row css nodes aren't a thing

    $[if treeview_pseudo_element] GtkTreeView row:even
    $[else] GtkTreeView row:nth-child(even)
    $[end if] {
        background: @row_even;
    }

    $[if treeview_pseudo_element] GtkTreeView row:odd
    $[else] GtkTreeView row:nth-child(odd)
    $[end if] {
        background: @row_odd;
    }
*/

entry {
    border-radius: $(2 * subcell_size)px;
    border-width: $(thickness)px;
    border-color: @text_field_grey;
    border-style: solid;
    background: @text_field_grey;
    color: @black;
    ${ entry_ythickness = my_ceil(0.2 * (subcell_size*3.0/2.0 - thickness) + thickness) }
    ${ entry_xthickness = my_ceil(0.32 * (subcell_size*3.0/2.0 - thickness) + thickness) }
    padding: $(2 * max(my_ceil((3*subcell_size - font_height - entry_ythickness*2)/2.0),0))px $(2 * max(subcell_size - entry_xthickness, 0))px $(2 * max(my_floor((3*subcell_size - font_height - entry_ythickness*2)/2.0), 0))px $(2 * max(subcell_size - entry_xthickness, 0))px;
}

entry image {
    /* Default padding is too large and causes toolbar height expansion */
    margin: $(default_padding-1)px;
}

toolitem entry {
    margin: $(subcell_size)px;
}

entry progress {
    border-radius: $(2 * subcell_size)px;
    border-width: $(thickness)px;
    background: @selection_grey;
}

entry:focus {
    background: @white;
}

toolbar entry:focus {
    border-color: @white;
}

entry:disabled {
    background: @button_grey;
    border-color: @button_grey;
}

treeview:selected {
    background: @panel_grey;
    color: @black;
}

entry:selected,
entry:selected:focus {
    background: @selection_grey;
    color: @black;
}

entry:selected,
entry:selected:focus {
    border-color: @selection_grey;
}

selection {
    background: @selected_bg_color;
    color: @selected_fg_color;
}

/* Frames */

frame {
    border-style: solid;
    border-color: @selection_grey;
    border-width: $(thickness)px;
    border-radius: 0;
    padding: $(default_padding)px;
}

frame.journal-preview-box {
    border-color: @button_grey;
}

/* Combo boxes */

combobox {
    color: @white;
}

combobox button.combo arrow {
    -gtk-icon-source: -gtk-icontheme('control-popup-arrow');
    -gtk-icon-transform: scale(0.3);
    min-width: $(subcell_size)px;
    min-height: $(subcell_size)px;
}

/* What does this do?
    combobox .menu.button {
        border-color: transparent;
        border-radius: 0px;
        border-style: none;
        background: transparent;
        color: @white;
    }
*/

combobox menu {
    -GtkWidget-scroll-arrow-hlength: $(my_floor(2.5 * subcell_size));
    -GtkWidget-scroll-arrow-vlength: $(my_floor(2.5 * subcell_size));
}

/* Notebooks */

notebook {
    background: @selection_grey;
    color: @black;
    padding: 0px;
    -GtkWidget-scroll-arrow-hlength: $(my_floor(2.5 * subcell_size));
    -GtkWidget-scroll-arrow-vlength: $(my_floor(2.5 * subcell_size));
}

notebook arrow {
    color: @white;
}

notebook tab {
    background: @selection_grey;
}

/* Setting white color to the tab labels using only the previous rule */
/* doesn't work, so we have to set white color to the GtkLabel, and make */
/* sure the buttons get black color in the next two rules: */
notebook tab label {
    color: @white;
    padding: $(subcell_size)px 0;
}

notebook tab button label {
    color: @black;
}

notebook tab:checked {
    background: @toolbar_grey;
}

notebook tab button {
    border-radius: $(toolbutton_padding)px;
}

/* Browse Widgets */

BrowseSearchWindow treeview {
    background: @black;
    color: @white;
    border-color: @button_grey;
    border-width: 0 $(thickness)px $(thickness)px $(thickness)px;
    border-style: solid;
}

/* Control panel */

cpanelsectionview {
    background: @white;
    color: @black;
}

/* TODO:  this should only effect the control panel */
controlpanel image:disabled {
    opacity: $(disabled_opacity);
}

/* Alert */

alert {
    background: @black;
    color: @white;
}

alert *:disabled {
    background: @black;
}

button timeouticon label,
button timeouticon label:hover {
    background: @white;
    color: @button_grey;
    border-radius: $(2 * subcell_size)px;
}

button:active timeouticon label {
    background: @toolbar_grey;
    color: @white;
}

/* Tray */

htray, vtray {
    background: @toolbar_grey;
}

htray * , vtray * { background: @toolbar_grey;}

/* Menus and palettes */

palette,
popover {
    border: $(thickness)px solid @button_grey;
    background: @black;
    color: @white;
}

palette .view {
    color: @black;
}

SugarPaletteMenuWidget {
    background: @black;
}

SugarPaletteMenuWidget * {
    color: @white;
}

SugarPaletteWindow SugarGroupBox {
    background: @toolbar_grey;
    color: @white;
}

SugarPaletteWindow SugarGroupBox *:disabled {
    background: @toolbar_grey;
}

menu {
    background: @black;
    color: @white;

    -GtkMenu-scroll-arrow-vlength: $(my_floor(subcell_size/0.7 + 2*thickness));
    /* This means the outline of the submenu overlaps with a palette.
     * However in the case of two normal menus, they are next to each other.
     * It is not possible to be smarter about this, because the style comes from
     * the submenu. */
    -GtkMenu-horizontal-offset :  -$(line_width)px;
    -GtkMenu-vertical-offset   : 0;

    padding: 0px;

    border: $(thickness)px solid @button_grey;
}

menuitem {
    padding: $(subcell_size)px $((subcell_size * 3 - font_height) / 2)px;
}

menuitem:hover,
menuitem:hover {
    background: @button_grey;
}

menuitem separator {
    padding: 0;
}

tooltip {
    background: @black;
    border-style: solid;
    border-width: $(thickness)px;
    border-color: @button_grey;
}

tooltip * {
    color: @white;
}

/* Scrollbars */

scrollbar {
    -GtkScrollbar-has-forward-stepper: false;
    -GtkScrollbar-has-backward-stepper: false;
}

scrollbar trough {
    background: @button_grey;
    border-width: 0px;
}

scrollbar slider {
    background: @white;
    border-radius: $(2*subcell_size)px;
    border-width: $(thickness)px;
    border-color: @button_grey;
    border-style: solid;
}

scrollbar.vertical slider {
    min-width: $(subcell_size)px;
    min-height: $(3*subcell_size)px;
}

scrollbar.vertical.fine-tune slider {
    min-width: $(subcell_size / 2 + thickness)px;
    margin-left: $(subcell_size / 4)px;
    margin-right: $(subcell_size / 4)px;
}

scrollbar.horizontal slider {
    min-height: $(subcell_size)px;
    min-width: $(3*subcell_size)px;
}

scrollbar.horizontal.fine-tune slider {
    min-height: $(subcell_size / 2 + thickness)px;
    margin-top: $(subcell_size / 4)px;
    margin-bottom: $(subcell_size / 4)px;
}

scrollbar slider:active {
    background: @text_field_grey;
}

/* Progress bars */

progressbar progress {
    background: @white;
    border-color: @button_grey;
    border-radius: $(subcell_size)px;
    border-style: solid;
    border-width: $(thickness)px;
}

progressbar trough {
    background: @selection_grey;
    border-style: solid;
    border-radius: $(subcell_size)px;
    border-color: @button_grey;
    border-width: $(thickness)px;
}

toolbar progressbar trough,
palette progressbar trough {
    background: @black;
}

/* Separators */

separator {
    background: @button_grey;
    min-width: $(thickness)px;
    min-height: $(thickness)px;
}

toolbar separator {
    border-left: $(thickness)px solid @button_grey;
    margin-left: $(subcell_size)px;
    margin-right: $(subcell_size)px;
    background: transparent;
}

/* We only want a border through the middle */
palette widget separator.horizontal {
    border-top: $(thickness)px solid @button_grey;
    margin-top: $(subcell_size)px;
    margin-bottom: -$(subcell_size)px;
    background: transparent;
}

/* Tool buttons */

toolbar,
toolbarbox {
    padding: 0;
    background: @toolbar_grey;
    color: @white;
}

/* FIXME: I needed to add the margin in the gtk3.20 port, but that wasn't
          there in the code before.  I ended up having to reduce the border
          to 2px */
toolbutton button,
filtertoolbutton button {
    border-style: none;
    background: transparent;
    border-radius: $(toolbutton_padding)px;
    padding: $(toolbutton_padding - default_padding)px;
    margin: $(default_padding)px;
}

toolbutton button #gtk-toolbar-arrow {
    padding: 0 $(subcell_size + default_padding)px;
}

toolbutton button:disabled,
filtertoolbutton button:disabled {
    opacity: $(disabled_opacity);
}

toolbutton button:hover,
filtertoolbutton button:hover {
    padding: $(toolbutton_padding - 2 - default_padding)px;
    border-width: 2px;
    border-style: solid;
    background-clip: padding-box;
}

/* Edge case of the wifi toolbuttons in the frame */
framewindow toolbutton button:hover {
    padding: $(toolbutton_padding - default_padding)px;
    border-width: 0;
}

toolbutton button:hover:not(:checked),
toolbutton button:hover:not(:checked) * {
    background: @black;
}

toolbutton button:checked,
toolbutton button:checked * {
    background: @button_grey;
    border-radius: $(toolbutton_padding)px;
}

toolbutton button:checked:hover,
toolbutton button:checked:hover * {
    background: @button_grey;
    border-color: @button_grey;
}

toolbutton button:active,
filtertoolbutton button:active,
toolbutton button:active * {
    background: @button_grey;
    color: white;
    border-radius: $(toolbutton_padding)px;
}

palette scrolledwindow {
    background: @black;
}

toolbar combobox button {
    border-radius: $(2*subcell_size)px;
}

/* Scales */

scale highlight {
    background: @white;
    border-style: solid;
    border-color: @button_grey;
    border-width: $(thickness)px;
}

scale trough {
    background: @button_grey;
    border-style: solid;
    border-color: @button_grey;
    border-width: $(thickness)px;
    margin: $(scale_trough_margin)px 0;
}

scale.vertical trough {
    margin: 0 $(scale_trough_margin)px;
}

scale trough:focus {
    border-color: @white;
}

scale trough,
scale highlight {
    border-radius: $(2*subcell_size)px $(2*subcell_size)px;
}

scale slider,
scale slider:active {
    background: transparent;
}

scale slider {
    -gtk-icon-source: url("assets/scale-slider.svg");
    margin: -$(my_floor(subcell_size/2))px;
    min-width: $(2*subcell_size)px;
    min-height: $(2*subcell_size)px;
}

scale slider:active {
    -gtk-icon-source: url("assets/scale-slider-active.svg");
    margin: -$(my_floor(subcell_size/2))px;
    min-width: $(2*subcell_size)px;
    min-height: $(2*subcell_size)px;
}

/* Radio and check buttons */

check,
radio {
    min-width: $(radio_size)px;
    min-height: $(radio_size)px;
    margin: 3px;
}

check:hover {
    background: alpha(@theme_base_color, 0.0);
}

toolbar check,
palette check {
    color: @theme_base_color;
    border: 1px solid @theme_base_color;
}

radio,
radio row:selected,
radio row:selected:focus {
    -gtk-icon-source: url("assets/radio.svg");
    background: alpha(@theme_base_color, 0.0);
}

radio:selected,
radio:selected row:selected,
radio:selected row:selected:focus {
    -gtk-icon-source: url("assets/radio-selected.svg");
}

radio:active,
radio row:selected:active,
radio row:selected:focus:active {
    -gtk-icon-source: url("assets/radio-active.svg");
}

radio:checked,
radio row:selected:checked,
radio row:selected:focus:checked {
   -gtk-icon-source: url("assets/radio-active.svg");
}

radio:active:selected,
radio:selected row:selected:active,
radio:selected row:selected:focus:active {
    -gtk-icon-source: url("assets/radio-active-selected.svg");
}

radio:checked:selected,
radio:selected row:selected:checked,
radio:selected row:selected:focus:checked {
    -gtk-icon-source: url("assets/radio-active-selected.svg");
}

check,
check row:selected,
check row:selected:focus {
    -gtk-icon-source: url("assets/checkbox-unchecked.svg");
    background: alpha(@theme_base_color, 0.0);
}

check:selected,
check:selected row:selected,
check:selected row:selected:focus {
    -gtk-icon-source: url("assets/checkbox-unchecked-selected.svg");
}

check:active,
check row:selected:active,
check row:selected:focus:active {
    -gtk-icon-source: url("assets/checkbox-checked.svg");
}

check:checked,
check row:selected:checked,
check row:selected:focus:checked {
    -gtk-icon-source: url("assets/checkbox-checked.svg");
}

check:active:selected,
check:selected row:selected:active,
check:selected row:selected:focus:active {
    -gtk-icon-source: url("assets/checkbox-checked-selected.svg");
}

check:checked:selected,
check:selected row:selected:checked,
check:selected row:selected:focus:checked {
    background-image: url("assets/checkbox-checked-selected.svg");
    -gtk-icon-source: url("assets/checkbox-checked-selected.svg");
}

/* Linked boxes (eg. combobox entries, spinbuttons) */

box.linked > *:not(:first-child):not(:last-child),
spinbutton :not(entry):not(:first-child):not(:last-child),
popover.touch-selection box.horizontal > *:not(:first-child):not(:last-child) {
    border-radius: 0;
}

box.horizontal.linked > *:first-child:not(:last-child),
spinbutton.horizontal entry,
popover.touch-selection box.horizontal > *:first-child:not(:last-child) {
    border-radius: $(2 * subcell_size)px 0 0 $(2 * subcell_size)px;
}

box.horizontal.linked > *:not(:first-child):last-child,
spinbutton.horizontal  button:last-child,
popover.touch-selection box.horizontal > *:not(:first-child):last-child {
    border-radius: 0 $(2 * subcell_size)px $(2 * subcell_size)px 0;
}

box.vertical.linked > *:first-child:not(:last-child),
spinbutton.vertical button.up {
    border-radius: $(2 * subcell_size)px $(2 * subcell_size)px 0 0;
}

box.vertical.linked > *:not(:first-child):last-child,
spinbutton.vertical button.down {
    border-radius: 0 0 $(2 * subcell_size)px $(2 * subcell_size)px;
}

/* Tool items */

toolitem {
    /* arrow in the toolbarbox */
    color: @white;
}

.palette-down,
.toolbar-down {
    /* outline of the toolbarbutton when palette is expanded */
    border-style: solid;
    border-width: 2px;
    border-color: @button_grey;
}

/* GtkImage */

image {
    background: transparent;
}

/* Sugar Frame Window */

framewindow {
    background: @toolbar_grey;
}

/* Sugar Canvas icon */

canvasicon {
    border-color: transparent;
    border-radius: $(4 * thickness)px;
    border-width: 2px;
    border-style: solid;
    background: transparent;
}

canvasicon:hover {
    border-color: @zoom_views_prelight;
    background: @zoom_views_prelight;
}

canvasicon.toggle-press {
    border-color: @zoom_views_active;
    background: @zoom_views_active;
}

/* Sugar CellRenderer Icons */

.cell.sugar-icon-cell {
    background: transparent;
    border-radius: $(4 * thickness)px;
    border-color: @white;
    border-width: $(thickness)px;
    border-style: solid;
}

.cell.sugar-icon-cell:hover {
    background: @zoom_views_prelight;
}

.cell.sugar-icon-cell:active {
    background: @zoom_views_active;
}

/* CellRendererProgress */

.cell.progressbar, .cell.trough {
    border-style: solid;
    border-width: $(thickness)px;
    border-color: @selection_grey;
    border-radius: $(2*subcell_size)px;
}

.cell.progressbar {
    background: @white;
}

.cell.trough {
    background: @text_field_grey;
}

/* Text cursor handles */

cursor-handle.top,
cursor-handle.bottom {
    background: transparent;
    border-radius: 10px;
    border-width: $(thickness)px;
    border-color: @panel_grey;
    border-style: solid;
}

cursor-handle.bottom {
    -gtk-icon-source: url("assets/cursor-handle-bottom.svg");
}

cursor-handle.top {
    -gtk-icon-source: url("assets/cursor-handle-top.svg");
}

cursor-handle.insertion-cursor {
    -gtk-icon-source: url("assets/cursor-handle-insertion.svg");
}

/* Application specific properties */

EvView {
    background: @panel_grey;
}

EvView:selected,
ApDocView:selected {
    background: @selection_grey;
}

${
import os
import math

gtk_major, gtk_minor, gtk_patch = map(int, gtk.split('.'))
treeview_pseudo_element = gtk_major >= 3 and gtk_minor >= 16

def my_floor(num):
    return int(math.floor(num))

def my_ceil(num):
    return int(math.ceil(num))

# Should we set the line width in the engine to 2.25, and draw non pixel aligned lines?
# Are these already the correct sizes for the XO?

# These sizes need to be sanity checked ...
if scaling == "100":
    xo = True
    line_width = 2.0        # 2.25px, rounded down
    thick_line_width = 3.5  # 3.5
    subcell_size = 15
    icon_base = 11
    bullet_size = 9.5
    font_height = 24
    default_padding = 6
    toolbutton_padding = 10
    radio_size = 26
    scale_trough_margin = 8
else: # About 72% of the XO size, adjusted so that eg. toolbuttons work
    xo = False
    line_width = 2.0            # 1.62 rounded up
    thick_line_width = 3.0      # 2.52
    subcell_size = 11           # 10.8
    icon_base = 8               # 7.92
    bullet_size = 6.5           # 6.84
    # This is a guess on the font size (Sans 10 at 96 DPI)
    font_height = 17
    default_padding = 4         # 4.32
    toolbutton_padding = 7      # 7.68
    radio_size = 18             # 18.72
    scale_trough_margin = 6     # 5.76

scale_slider_width = my_floor(2 * subcell_size + line_width)
thickness = my_ceil(line_width)

icon_small = icon_base * 3
icon_large = icon_base * 5

disabled_opacity = 0.5

}

* {
    -sugar-focus-line: @white;

    /* 0.05 works good for both the sugar and sugar-xo themes */
    -GtkWidget-cursor-aspect-ratio: 0.05;

    -GtkWidget-wide-separators: true;
    -GtkWidget-separator-height: $thickness;
    -GtkWidget-separator-width: $thickness;

    -GtkWidget-scroll-arrow-hlength: $subcell_size;
    -GtkWidget-scroll-arrow-vlength: $subcell_size;

    -GtkRange-activate-slider: 1;

    -GtkScrolledWindow-scrollbar-spacing: 0;

    -GtkExpander-expander-size: $font_height;
    -GtkExpander-expander-spacing: 2;

    -GtkTreeView-expander-size: $font_height;

    -GtkArrow-arrow-size: 1.0;

    -GtkToolbar-space-size: $(2*subcell_size);

    -GtkProgressBar-min-horizontal-bar-height: $subcell_size;
    -GtkProgressBar-min-vertical-bar-width: $subcell_size;

    -GtkCheckButton-indicator-size: $radio_size;
    -GtkCheckButton-indicator-spacing: 3;

    -GtkWidget-text-handle-width: 110px;
    -GtkWidget-text-handle-height: 110px;
}

*:disabled {
    color: @panel_grey;
}

/* Backgrounds and windows */

window {
    background: @panel_grey;
    color: @black;
}

dialog {
    background: @black;
    color: @white;
}

introwindow {
    background: @white;
    color: @black;
}

label, label:disabled {
    background: transparent;
}

/* Buttons */

button {
    ${ border = max(0, my_ceil((3*subcell_size/2.0 - icon_small / 2.0))) }
    padding: $(border)px $(border)px $(border)px $(border)px;
    border-width: $(thickness)px;
    border-color: @button_grey;
    border-style: solid;
    border-radius: $(2*subcell_size)px;
    background: @button_grey;
    color: @white;
}

button:focus {
    border-color: @white;
    color: @white;
}

button:active {
    background: @white;
    color: @black;
}

button:disabled {
    background: transparent;
}

button:active:focus {
    color: @black;
    border-color: @button_grey;
}

/* Toggle buttons */

button:checked {
    background: @white;
    color: @black;
}

/* Entries and views (textview, treeview) */

.view,
textview text {  /* Setting a textview to background white makes it black,
                    unless you only style the text css node */
    border-width: 0px;
    border-style: none;
    border-radius: 0px;
    padding: 0px;
    background: @white;
    color: @black;
}

treeview header button,
treeview header button:hover:active {
    border-radius: 0px;
    background: @button_grey;
    border-width: 0px;
    color: @white;
}

/* FIXME: treeview row css nodes aren't a thing

    $[if treeview_pseudo_element] GtkTreeView row:even
    $[else] GtkTreeView row:nth-child(even)
    $[end if] {
        background: @row_even;
    }

    $[if treeview_pseudo_element] GtkTreeView row:odd
    $[else] GtkTreeView row:nth-child(odd)
    $[end if] {
        background: @row_odd;
    }
*/

entry {
    border-radius: $(2 * subcell_size)px;
    border-width: $(thickness)px;
    border-color: @text_field_grey;
    border-style: solid;
    background: @text_field_grey;
    color: @black;
    ${ entry_ythickness = my_ceil(0.2 * (subcell_size*3.0/2.0 - thickness) + thickness) }
    ${ entry_xthickness = my_ceil(0.32 * (subcell_size*3.0/2.0 - thickness) + thickness) }
    padding: $(2 * max(my_ceil((3*subcell_size - font_height - entry_ythickness*2)/2.0),0))px $(2 * max(subcell_size - entry_xthickness, 0))px $(2 * max(my_floor((3*subcell_size - font_height - entry_ythickness*2)/2.0), 0))px $(2 * max(subcell_size - entry_xthickness, 0))px;
}

entry image {
    /* Default padding is too large and causes toolbar height expansion */
    margin: $(default_padding-1)px;
}

toolitem entry {
    margin: $(subcell_size)px;
}

entry progress {
    border-radius: $(2 * subcell_size)px;
    border-width: $(thickness)px;
    background: @selection_grey;
}

entry:focus {
    background: @white;
}

toolbar entry:focus {
    border-color: @white;
}

entry:disabled {
    background: @button_grey;
    border-color: @button_grey;
}

treeview:selected {
    background: @panel_grey;
    color: @black;
}

entry:selected,
entry:selected:focus {
    background: @selection_grey;
    color: @black;
}

entry:selected,
entry:selected:focus {
    border-color: @selection_grey;
}

selection {
    background: @selected_bg_color;
    color: @selected_fg_color;
}

/* Frames */

frame {
    border-style: solid;
    border-color: @selection_grey;
    border-width: $(thickness)px;
    border-radius: 0;
    padding: $(default_padding)px;
}

frame.journal-preview-box {
    border-color: @button_grey;
}

/* Combo boxes */

combobox {
    color: @white;
}

combobox button.combo arrow {
    -gtk-icon-source: -gtk-icontheme('control-popup-arrow');
    -gtk-icon-transform: scale(0.3);
    min-width: $(subcell_size)px;
    min-height: $(subcell_size)px;
}

/* What does this do?
    combobox .menu.button {
        border-color: transparent;
        border-radius: 0px;
        border-style: none;
        background: transparent;
        color: @white;
    }
*/

combobox menu {
    -GtkWidget-scroll-arrow-hlength: $(my_floor(2.5 * subcell_size));
    -GtkWidget-scroll-arrow-vlength: $(my_floor(2.5 * subcell_size));
}

/* Notebooks */

notebook {
    background: @selection_grey;
    color: @black;
    padding: 0px;
    -GtkNotebook-tab-overlap: -2;
    -GtkNotebook-tab-curvature: $default_padding;
    -GtkWidget-scroll-arrow-hlength: $(my_floor(2.5 * subcell_size));
    -GtkWidget-scroll-arrow-vlength: $(my_floor(2.5 * subcell_size));
}

notebook arrow {
    color: @white;
}

notebook tab {
    background: @selection_grey;
}

/* Setting white color to the tab labels using only the previous rule */
/* doesn't work, so we have to set white color to the GtkLabel, and make */
/* sure the buttons get black color in the next two rules: */
notebook tab label {
    color: @white;
    padding: $(subcell_size)px 0;
}

notebook tab button label {
    color: @black;
}

notebook tab:checked {
    background: @toolbar_grey;
}

notebook tab button {
    border-radius: $(toolbutton_padding)px;
}

/* Browse Widgets */

BrowseSearchWindow treeview {
    background: @black;
    color: @white;
    border-color: @button_grey;
    border-width: 0 $(thickness)px $(thickness)px $(thickness)px;
    border-style: solid;
}

/* Control panel */

cpanelsectionview {
    background: @white;
    color: @black;
}

/* TODO:  this should only effect the control panel */
controlpanel image:disabled {
    opacity: $(disabled_opacity);
}

/* Alert */

alert {
    background: @black;
    color: @white;
}

alert *:disabled {
    background: @black;
}

button timeouticon label,
button timeouticon label:hover {
    background: @white;
    color: @button_grey;
    border-radius: $(2 * subcell_size)px;
}

button:active timeouticon label {
    background: @toolbar_grey;
    color: @white;
}

/* Tray */

htray, vtray {
    background: @toolbar_grey;
}

htray * , vtray * { background: @toolbar_grey;}

/* Menus and palettes */

palette,
popover {
    border: $(thickness)px solid @button_grey;
    background: @black;
    color: @white;
}

palette .view {
    color: @black;
}

SugarPaletteMenuWidget {
    background: @black;
}

SugarPaletteMenuWidget * {
    color: @white;
}

SugarPaletteWindow SugarGroupBox {
    background: @toolbar_grey;
    color: @white;
}

SugarPaletteWindow SugarGroupBox *:disabled {
    background: @toolbar_grey;
}

menu {
    background: @black;
    color: @white;

    -GtkMenu-scroll-arrow-vlength: $(my_floor(subcell_size/0.7 + 2*thickness));
    /* This means the outline of the submenu overlaps with a palette.
     * However in the case of two normal menus, they are next to each other.
     * It is not possible to be smarter about this, because the style comes from
     * the submenu. */
    -GtkMenu-horizontal-offset :  -$(line_width)px;
    -GtkMenu-vertical-offset   : 0;

    padding: 0px;

    border: $(thickness)px solid @button_grey;
}

menuitem {
    padding: $(subcell_size)px $((subcell_size * 3 - font_height) / 2)px;
}

menuitem:hover,
menuitem:hover {
    background: @button_grey;
}

menuitem separator {
    padding: 0;
}

tooltip {
    background: @black;
    border-style: solid;
    border-width: $(thickness)px;
    border-color: @button_grey;
}

tooltip * {
    color: @white;
}

/* Scrollbars */

scrollbar {
    -GtkScrollbar-has-forward-stepper: false;
    -GtkScrollbar-has-backward-stepper: false;
}

scrollbar trough {
    background: @button_grey;
    border-width: 0px;
}

scrollbar slider {
    background: @white;
    border-radius: $(2*subcell_size)px;
    border-width: $(thickness)px;
    border-color: @button_grey;
    border-style: solid;
}

scrollbar.vertical slider {
    min-width: $(subcell_size)px;
    min-height: $(3*subcell_size)px;
}

scrollbar.vertical.fine-tune slider {
    min-width: $(subcell_size / 2 + thickness)px;
    margin-left: $(subcell_size / 4)px;
    margin-right: $(subcell_size / 4)px;
}

scrollbar.horizontal slider {
    min-height: $(subcell_size)px;
    min-width: $(3*subcell_size)px;
}

scrollbar.horizontal.fine-tune slider {
    min-height: $(subcell_size / 2 + thickness)px;
    margin-top: $(subcell_size / 4)px;
    margin-bottom: $(subcell_size / 4)px;
}

scrollbar slider:active {
    background: @text_field_grey;
}

/* Progress bars */

progressbar progress {
    background: @white;
    border-color: @button_grey;
    border-radius: $(subcell_size)px;
    border-style: solid;
    border-width: $(thickness)px;
}

progressbar trough {
    background: @selection_grey;
    border-style: solid;
    border-radius: $(subcell_size)px;
    border-color: @button_grey;
    border-width: $(thickness)px;
}

toolbar progressbar trough,
palette progressbar trough {
    background: @black;
}

/* Separators */

separator {
    background-color: @button_grey;
}

/* If we set the background for these, we get about 20px high
   separator.  But we only want a border through the middle */
palette widget separator {
    border-top: $(thickness)px solid @button_grey;
    margin-top: $(subcell_size)px;
    margin-bottom: -$(subcell_size)px;
    background: transparent;
}

/* Tool buttons */

toolbar,
toolbarbox {
    padding: 0;
    background: @toolbar_grey;
    color: @white;
}

/* FIXME: I needed to add the margin in the gtk3.20 port, but that wasn't
          there in the code before.  I ended up having to reduce the border
          to 2px */
toolbutton button,
filtertoolbutton button {
    border-style: none;
    background: transparent;
    border-radius: $(toolbutton_padding)px;
    padding: $(toolbutton_padding - default_padding)px;
    margin: $(default_padding)px;
}

toolbutton button #gtk-toolbar-arrow {
    padding: 0 $(subcell_size + default_padding)px;
}

toolbutton button:disabled,
filtertoolbutton button:disabled {
    opacity: $(disabled_opacity);
}

toolbutton button:hover,
filtertoolbutton button:hover {
    padding: $(toolbutton_padding - 2 - default_padding)px;
    border-width: 2px;
    border-style: solid;
    background-clip: padding-box;
}

/* Edge case of the wifi toolbuttons in the frame */
framewindow toolbutton button:hover {
    padding: $(toolbutton_padding - default_padding)px;
    border-width: 0;
}

toolbutton button:hover:not(:checked),
toolbutton button:hover:not(:checked) * {
    background: @black;
}

toolbutton button:checked,
toolbutton button:checked * {
    background: @button_grey;
    border-radius: $(toolbutton_padding)px;
}

toolbutton button:checked:hover,
toolbutton button:checked:hover * {
    background: @button_grey;
    border-color: @button_grey;
}

toolbutton button:active,
filtertoolbutton button:active,
toolbutton button:active * {
    background: @button_grey;
    color: white;
    border-radius: $(toolbutton_padding)px;
}

palette scrolledwindow {
    background: @black;
}

toolbar combobox button {
    border-radius: $(2*subcell_size)px;
}

/* Scales */

scale {
    -GtkScale-slider-length: $scale_slider_width;
    -GtkRange-slider-width: $scale_slider_width;
}

scale highlight {
    background: @white;
    border-style: solid;
    border-color: @button_grey;
    border-width: $(thickness)px;
}

scale trough {
    background: @button_grey;
    border-style: solid;
    border-color: @button_grey;
    border-width: $(thickness)px;
    margin: $(scale_trough_margin)px 0;
}

scale.vertical trough {
    margin: 0 $(scale_trough_margin)px;
}

scale trough:focus {
    border-color: @white;
}

scale trough,
scale highlight {
    border-radius: $(2*subcell_size)px $(2*subcell_size)px;
}

scale slider,
scale slider:active {
    background: transparent;
}

scale slider {
    -gtk-icon-source: url("assets/scale-slider.svg");
    margin: -$(my_floor(subcell_size/2))px;
    min-width: $(2*subcell_size)px;
    min-height: $(2*subcell_size)px;
}

scale slider:active {
    -gtk-icon-source: url("assets/scale-slider-active.svg");
    margin: -$(my_floor(subcell_size/2))px;
    min-width: $(2*subcell_size)px;
    min-height: $(2*subcell_size)px;
}

/* Radio and check buttons */

check:hover {
    background: alpha(@theme_base_color, 0.0);
}

toolbar check,
palette check {
    color: @theme_base_color;
    border: 1px solid @theme_base_color;
}

radio,
radio row:selected,
radio row:selected:focus {
    -gtk-icon-source: url("assets/radio.svg");
    background: alpha(@theme_base_color, 0.0);
}

radio:selected,
radio:selected row:selected,
radio:selected row:selected:focus {
    -gtk-icon-source: url("assets/radio-selected.svg");
}

radio:active,
radio row:selected:active,
radio row:selected:focus:active {
    -gtk-icon-source: url("assets/radio-active.svg");
}

radio:checked,
radio row:selected:checked,
radio row:selected:focus:checked {
   -gtk-icon-source: url("assets/radio-active.svg");
}

radio:active:selected,
radio:selected row:selected:active,
radio:selected row:selected:focus:active {
    -gtk-icon-source: url("assets/radio-active-selected.svg");
}

radio:checked:selected,
radio:selected row:selected:checked,
radio:selected row:selected:focus:checked {
    -gtk-icon-source: url("assets/radio-active-selected.svg");
}

check,
check row:selected,
check row:selected:focus {
    -gtk-icon-source: url("assets/checkbox-unchecked.svg");
    background: alpha(@theme_base_color, 0.0);
}

check:selected,
check:selected row:selected,
check:selected row:selected:focus {
    -gtk-icon-source: url("assets/checkbox-unchecked-selected.svg");
}

check:active,
check row:selected:active,
check row:selected:focus:active {
    -gtk-icon-source: url("assets/checkbox-checked.svg");
}

check:checked,
check row:selected:checked,
check row:selected:focus:checked {
    -gtk-icon-source: url("assets/checkbox-checked.svg");
}

check:active:selected,
check:selected row:selected:active,
check:selected row:selected:focus:active {
    -gtk-icon-source: url("assets/checkbox-checked-selected.svg");
}

check:checked:selected,
check:selected row:selected:checked,
check:selected row:selected:focus:checked {
    background-image: url("assets/checkbox-checked-selected.svg");
    -gtk-icon-source: url("assets/checkbox-checked-selected.svg");
}

/* Linked boxes (eg. combobox entries, spinbuttons) */

box.linked > *:not(:first-child):not(:last-child),
spinbutton :not(entry):not(:first-child):not(:last-child),
popover.touch-selection box.horizontal > *:not(:first-child):not(:last-child) {
    border-radius: 0;
}

box.horizontal.linked > *:first-child:not(:last-child),
spinbutton.horizontal entry,
popover.touch-selection box.horizontal > *:first-child:not(:last-child) {
    border-radius: $(2 * subcell_size)px 0 0 $(2 * subcell_size)px;
}

box.horizontal.linked > *:not(:first-child):last-child,
spinbutton.horizontal  button:last-child,
popover.touch-selection box.horizontal > *:not(:first-child):last-child {
    border-radius: 0 $(2 * subcell_size)px $(2 * subcell_size)px 0;
}

box.vertical.linked > *:first-child:not(:last-child),
spinbutton.vertical button.up {
    border-radius: $(2 * subcell_size)px $(2 * subcell_size)px 0 0;
}

box.vertical.linked > *:not(:first-child):last-child,
spinbutton.vertical button.down {
    border-radius: 0 0 $(2 * subcell_size)px $(2 * subcell_size)px;
}

/* Tool items */

toolitem {
    /* arrow in the toolbarbox */
    color: @white;
}

.palette-down,
.toolbar-down {
    /* outline of the toolbarbutton when palette is expanded */
    border-style: solid;
    border-width: 2px;
    border-color: @button_grey;
}

/* GtkImage */

image {
    background: transparent;
}

/* Sugar Frame Window */

framewindow {
    background: @toolbar_grey;
}

/* Sugar Canvas icon */

canvasicon {
    border-color: transparent;
    border-radius: $(4 * thickness)px;
    border-width: 2px;
    border-style: solid;
    background: transparent;
}

canvasicon:hover {
    border-color: @zoom_views_prelight;
    background: @zoom_views_prelight;
}

canvasicon.toggle-press {
    border-color: @zoom_views_active;
    background: @zoom_views_active;
}

/* Sugar CellRenderer Icons */

.cell.sugar-icon-cell {
    background: transparent;
    border-radius: $(4 * thickness)px;
    border-color: @white;
    border-width: $(thickness)px;
    border-style: solid;
}

.cell.sugar-icon-cell:hover {
    background: @zoom_views_prelight;
}

.cell.sugar-icon-cell:active {
    background: @zoom_views_active;
}

/* CellRendererProgress */

.cell.progressbar, .cell.trough {
    border-style: solid;
    border-width: $(thickness)px;
    border-color: @selection_grey;
    border-radius: $(2*subcell_size)px;
}

.cell.progressbar {
    background: @white;
}

.cell.trough {
    background: @text_field_grey;
}

/* Text cursor handles */

cursor-handle.top,
cursor-handle.bottom {
    background: transparent;
    border-radius: 10px;
    border-width: $(thickness)px;
    border-color: @panel_grey;
    border-style: solid;
}

cursor-handle.bottom {
    -gtk-icon-source: url("assets/cursor-handle-bottom.svg");
}

cursor-handle.top {
    -gtk-icon-source: url("assets/cursor-handle-top.svg");
}

cursor-handle.insertion-cursor {
    -gtk-icon-source: url("assets/cursor-handle-insertion.svg");
}

/* Application specific properties */

EvView {
    background: @panel_grey;
}

EvView:selected,
ApDocView:selected {
    background: @selection_grey;
}


cleanups

cleanups

Herd.AssetContainerComponent = Ember.Component.extend
  action: 'generateChild'
  classNames: ['asset-container']
  bgImage: true
  asset: null
  child: null
  transform: null
  t: null
  n: null
  suffix: null

  combinedName: ~>
    name = if @suffix
      [@n,@suffix].join '-'
    else
      @n
    name

  +computed child asset
  isImage: ->
    return @asset?.type == 'Herd::Image' unless @child
    @child.type == 'Herd::Image' and !@bgImage

  +computed child asset
  isVideo: ->
    return @asset?.type == 'Herd::Video' unless @child
    @child.type == 'Herd::Video'

  +computed child.fileName
  assetUrl: ->
    if @child
      if Ember.empty @child.fileName
        return "https://d13yacurqjgara.cloudfront.net/users/82092/screenshots/1073359/spinner.gif"
      else
        return "#{@child?.url}?b=#{@child.updatedAt.getTime()}"

    else if @asset and (@t or @combinedName)
      @child = @asset if @asset.assetableId == 0
      @child = @asset.n @combinedName if !@child and @combinedName
      @child = @asset.t @t unless @child

      if @child?.fileName and @child?.url
        return @assetUrl()

      else if !@child
        @child = @assetManager.pushRequest @asset.store.createRecord 'asset',
          parentAsset: @asset
          transform:  @transform || @asset.store.createRecord 'transform',
            name: @combinedName
            options: @t
            assetableType: @asset.assetableType
    null

  processChildElements: ->
    console.log @futureChild
    debugger

  actions:
    metaUpdate: ->
      @asset.save()

Herd.AssetContainerComponent = Ember.Component.extend
  action: 'generateChild'
  classNames: ['asset-container']
  bgImage: true
  asset: null
  child: null
  transform: null
  t: null
  n: null
  suffix: null

  combinedName: ~>
    name = if @suffix
      [@n,@suffix].join '-'
    else
      @n
    console.log 'combinedName: ', name, @asset.assetableType, @asset.assetableId
    name

  +computed child asset
  isImage: ->
    return @asset?.type == 'Herd::Image' unless @child
    @child.type == 'Herd::Image' and !@bgImage

  +computed child asset
  isVideo: ->
    return @asset?.type == 'Herd::Video' unless @child
    @child.type == 'Herd::Video'

  +computed child.fileName
  assetUrl: ->
    if @child
      if Ember.empty @child.fileName
        return "https://d13yacurqjgara.cloudfront.net/users/82092/screenshots/1073359/spinner.gif"
      else
        return "#{@child?.url}?b=#{@child.updatedAt.getTime()}"

    else if @asset and (@t or @combinedName)
      @child = @asset if @asset.assetableId == 0
      @child = @asset.n @combinedName if !@child and @combinedName
      @child = @asset.t @t unless @child

      if @child?.fileName and @child?.url
        return "#{@child?.url}?b=#{@child.updatedAt.getTime()}"
      else if !@child

        @child = @assetManager.pushRequest @asset.store.createRecord 'asset',
          parentAsset: @asset
          transform:  @transform || @asset.store.createRecord 'transform',
            name: @combinedName
            options: @t
            assetableType: @asset.assetableType

        console.log('jus pushRequest', @child)

        #return "https://d13yacurqjgara.cloudfront.net/users/82092/screenshots/1073359/spinner.gif"
    else
      null

  processChildElements: ->
    console.log @futureChild
    debugger

  actions:
    metaUpdate: ->
      @asset.save()


Minor change: apparently, had been using add command for removing fields.  Meant that we could never actually remove a field (would just set its value to undefined).  That is fixed now.

/**
 Note: this file should only be imported once.  It maintains a map of
 presences hosted by this entity.  Associated with each presence are
 two things:

   1: A list of other presence's in the world that want to know more
   about this presence (as indicated by their having sent a
   subscription message); and

   2: A feature vector (really an object with fields and values) that
   contains information that the user wants to push to all listening presences.

will use transaction model to write changes to object.

 */

std.featureObject = {};

system.require('featureChangeTransaction.em');

(function()
{
    var presencesToFeatureObjectMap = {};
    var TIME_TO_ACK_UPDATE = 10;

    /**
     Whenever we create a new presence, just add an entry for it
     into feature object map.
     */
    function ourPresenceConnected(newPres)
    {
        presencesToFeatureObjectMap[newPres.toString()]  =
            {
                data: {},
                dataCommitMap: {},
                subscribers: {},
                updateCount: 0
            };
    }
    system.onPresenceConnected(ourPresenceConnected);

    /*
     Note: may want to add a conditional subscribe.;
     Subscribe me if I am like this;
     */

    /**
     Whenever we get a subscription message, reply with our feature
     data (and add subscriber for future events) or tell the other
     side that query failed.
     */
    function handleFeatureObjectSubscribe(msg,subscriber)
    {
        var selfIndex = system.self.toString();
        if (selfIndex in presencesToFeatureObjectMap)
        {
            var replyObj = {
                success: true,
                data: presencesToFeatureObjectMap[selfIndex].data
                };

            msg.makeReply(replyObj) >> [];

            presencesToFeatureObjectMap[selfIndex].subscribers[subscriber.toString()] = subscriber;
        }
        else
        {
            var replyObj = {
                success: false,
                data: udefined
            };
            msg.makeReply(replyObj) >> [];
        }
    }
    //see ProxManager.beginFeatureSubscribe (in proxManager.em) to see
    //what format the feature object subscription request message
    //should take.
    handleFeatureObjectSubscribe << {'presFeatureSubRequest'::};

    /**
     Whenever we get an unsubscription message, remove the presence
     from our subscribers, and notify them that they have been
     unsubscribed of their own volition.  Do not need to ack request.
     */
    function handleFeatureObjectRequestUnsubscribe(msg,subscriber)
    {
        if (!(system.self.toString() in presencesToFeatureObjectMap))
        {
            throw new Error('Error when received a request to unsubscribe.  ' +
                            'No record of receiver in featureObjectMap.');
        }

        var featureObjer = presencesToFeatureObjectMap[system.self.toString()];

        //actually remove the subscriber from map.
        if (subscriber.toString() in featureObjer.subscribers)
            delete featureObjer.subscribers[subscriber.toString()];
    }

    //see ProxManager.killSubscription for what message is sent when
    //requesting unsubscription.  (In proxManager.em.)
    handleFeatureObjectRequestUnsubscribe << {'presFeatureReqUnsub' ::};

    /**
     If we drop an update message without acking it, then after some
     time, we'll get unsubscribed.  If we had just missed that
     message, and then logged back in, this unsubscription would be
     incorrect, and we'd get into a poor state.  As a result, if we
     receive an unsubscription notice, we just resubscribe.
     */
    function resubscribeAfterUnsubscribe(msg,sender)
    {
        var subscriptionMsg = {
          'featureObjectSubscribeRequest' : true
        };

        subscriptionMsg >> sender >> [];
    }
    resubscribeAfterUnsubscribe <<{'featureObjectUnsubscribe'::};

    std.featureObject.addField = function(index,data,pres)
    {
        singleCommit(
            std.featureObject.Action.ADD,index,data,pres);
    };

    std.featureObject.removeField = function(index,pres)
    {
        singleCommit(
            std.featureObject.Action.REMOVE,index,undefined,pres);
    };

    std.featureObject.changeField = function(index,data,pres)
    {
        singleCommit(
            std.featureObject.Action.CHANGE,index,data,pres);
    };

    function singleCommit(type, index, data,pres)
    {
        if (typeof(pres) == 'undefined')
        {
            if (typeof(system.self) == 'undefined')
            {
                throw new Error ('Error changing feaure field: ' +
                                 'unknown presence.');
            }
            pres = system.self;
        }

        var committer = std.featureObject.beginFeatureTransaction(pres);
        if (type == std.featureObject.Action.ADD)
            committer.addField(index,data);
        else if (type == std.featureObject.Action.REMOVE)
            committer.removeField(index);
        else if (type == std.featureObject.Action.CHANGE)
            committer.changeField(index,data);
        else
        {
            throw new Error('Unrecognized commit type in singleCommit ' +
                            'of featureObject.');
        }

        committer.commit();
    };

    /**
     @param {presence} presToChange (optional)
     */
    std.featureObject.beginFeatureTransaction = function(presToChange)
    {
        if (typeof(presToChange) == 'undefined')
        {
            if (tyepeof(system.self) == 'undefined')
            {
                throw new Error ('Error in featureObject.  ' +
                                 'Have no presence to bind feature ' +
                                 'changes to.');
            }
            presToChange = system.self;
        }

        if (!(presToChange.toString() in presencesToFeatureObjectMap ))
        {
            throw new Error('Error in beginFeatureTransaction.  Invalid '+
                            'presence to bind changes to.');
        }
        var featureObject = presencesToFeatureObjectMap[presToChange.toString()];
        return new std.featureObject.FeatureChangeTransaction(
            presToChange,featureObject.updateCount);
    };

    /**
     @param {std.featureObject.FeatureChangeTransaction} featChangeTrans
     @throws Error if changes to feature vector are stamped after
     latest update to vector.
     */
    std.featureObject.commitFeatureChanges =function(featChangeTrans)
    {
        if (!(featChangeTrans.pres.toString() in presencesToFeatureObjectMap))
        {
            throw new Error('Incorrect presence detected ' +
                            'when committing feature changes.');
        }

        var featElement = presencesToFeatureObjectMap[featChangeTrans.pres.toString()];

        //checking that all existing elements in data map
        //have not been modified since commit began.
        var addedDataMap ={};

        for (var s in featChangeTrans.actions)
        {
            var act = featChangeTrans.actions[s];

            if (act.type == std.featureObject.Action.ADD)
                addedDataMap[act.index] = true;

            if (act.index in featElement.dataCommitMap)
            {
                if (featElement.dataCommitMap[act.index] >
                    featChangeTrans.commitNum)
                {
                    throw new Error('Aborting feature transaction: ' +
                                    'detected out of date data.');
                }
            }
            else if (!act.index in addedDataMap)
            {
                throw new Error ('Aborting feature transaction: ' +
                                 'attempting operation on removed'+
                                 ' or non-existent data.');
            }

            if ((act.type != std.featureObject.Action.ADD)    &&
                (act.type != std.featureObject.Action.REMOVE) &&
                (act.type != std.featureObject.Action.CHANGE))
            {
                throw new Error('Unknown action type when commiting ' +
                                'feature.  Aborting.');
            }
        }

        //we know that the commit will go through.  We'll label all
        //data in commit as having been modified at updateCount.
        ++featElement.updateCount;

        //If gotten to this point, know we're not operating on stale
        //data.  Go ahead and commit all changes.
        for (var s in featChangeTrans.actions)
        {
            var act = featChangeTrans.actions[s];
            if ((act.type == std.featureObject.Action.ADD) ||
                (act.type == std.featureObject.Action.CHANGE))
            {
                featElement.data[act.index] = act.data;
                featElement.dataCommitMap[act.index] = featElement.updateCount;
            }
            else if (act.type == std.featureObject.Action.REMOVE)
            {
                if (act.index in featElement.data)
                {
                    delete featElement.data[act.index];
                    delete featElement.dataCommitMap[act.index];
                }
            }
        }
        sendFeaturesToSubscribers(featElement,featChangeTrans.pres);
    };

    /**
     If subscriber does not ack feature data after some time, then we
     unsubscribe them.
     */
    function sendFeaturesToSubscribers(featElement,pres)
    {

        //see handleFeatureObjUpdates in proxManager.em for format of
        //featureObject update messages.
        var updateMsg =
            {
                featureObjUpdate: featElement.data
            };

        //for now, ship full vector to all subscribing
        for (var s in featElement.subscribers)
        {
            pres #
                updateMsg >>
                featElement.subscribers[s] >>
                [function(){}, TIME_TO_ACK_UPDATE,
                 //if don't receive an ack, then unsubscribe the subscriber.
                 function()
                 {
                     if (s in featElement.subscribers)
                     {
                         var unsubscribeNotice =
                             {
                                 featureObjectUnsubscribe:''
                             };

                         //tell them they're being unsubscribed
                         pres  #
                             unsubscribeNotice >>
                             featElement.subscribers[s] >>[];

                         //actually remove them from subscribers
                         delete featElement.subscribers[s];
                     }
                 }];
        }
    }
})();

/**
 Note: this file should only be imported once.  It maintains a map of
 presences hosted by this entity.  Associated with each presence are
 two things:

   1: A list of other presence's in the world that want to know more
   about this presence (as indicated by their having sent a
   subscription message); and

   2: A feature vector (really an object with fields and values) that
   contains information that the user wants to push to all listening presences.

will use transaction model to write changes to object.

 */

std.featureObject = {};

system.require('featureChangeTransaction.em');

(function()
{
    var presencesToFeatureObjectMap = {};
    var TIME_TO_ACK_UPDATE = 10;

    /**
     Whenever we create a new presence, just add an entry for it
     into feature object map.
     */
    function ourPresenceConnected(newPres)
    {
        presencesToFeatureObjectMap[newPres.toString()]  =
            {
                data: {},
                dataCommitMap: {},
                subscribers: {},
                updateCount: 0
            };
    }
    system.onPresenceConnected(ourPresenceConnected);

    /*
     Note: may want to add a conditional subscribe.;
     Subscribe me if I am like this;
     */

    /**
     Whenever we get a subscription message, reply with our feature
     data (and add subscriber for future events) or tell the other
     side that query failed.
     */
    function handleFeatureObjectSubscribe(msg,subscriber)
    {
        var selfIndex = system.self.toString();
        if (selfIndex in presencesToFeatureObjectMap)
        {
            var replyObj = {
                success: true,
                data: presencesToFeatureObjectMap[selfIndex].data
                };

            msg.makeReply(replyObj) >> [];

            presencesToFeatureObjectMap[selfIndex].subscribers[subscriber.toString()] = subscriber;
        }
        else
        {
            var replyObj = {
                success: false,
                data: udefined
            };
            msg.makeReply(replyObj) >> [];
        }
    }
    //see ProxManager.beginFeatureSubscribe (in proxManager.em) to see
    //what format the feature object subscription request message
    //should take.
    handleFeatureObjectSubscribe << {'presFeatureSubRequest'::};

    /**
     Whenever we get an unsubscription message, remove the presence
     from our subscribers, and notify them that they have been
     unsubscribed of their own volition.  Do not need to ack request.
     */
    function handleFeatureObjectRequestUnsubscribe(msg,subscriber)
    {
        if (!(system.self.toString() in presencesToFeatureObjectMap))
        {
            throw new Error('Error when received a request to unsubscribe.  ' +
                            'No record of receiver in featureObjectMap.');
        }

        var featureObjer = presencesToFeatureObjectMap[system.self.toString()];

        //actually remove the subscriber from map.
        if (subscriber.toString() in featureObjer.subscribers)
            delete featureObjer.subscribers[subscriber.toString()];
    }

    //see ProxManager.killSubscription for what message is sent when
    //requesting unsubscription.  (In proxManager.em.)
    handleFeatureObjectRequestUnsubscribe << {'presFeatureReqUnsub' ::};

    /**
     If we drop an update message without acking it, then after some
     time, we'll get unsubscribed.  If we had just missed that
     message, and then logged back in, this unsubscription would be
     incorrect, and we'd get into a poor state.  As a result, if we
     receive an unsubscription notice, we just resubscribe.
     */
    function resubscribeAfterUnsubscribe(msg,sender)
    {
        var subscriptionMsg = {
          'featureObjectSubscribeRequest' : true
        };

        subscriptionMsg >> sender >> [];
    }
    resubscribeAfterUnsubscribe <<{'featureObjectUnsubscribe'::};

    std.featureObject.addField = function(index,data,pres)
    {
        singleCommit(
            std.featureObject.Action.ADD,index,data,pres);
    };

    std.featureObject.removeField = function(index,pres)
    {
        singleCommit(
            std.featureObject.Action.ADD,index,undefined,pres);
    };

    std.featureObject.changeField = function(index,data,pres)
    {
        singleCommit(
            std.featureObject.Action.CHANGE,index,data,pres);
    };

    function singleCommit(type, index, data,pres)
    {
        if (typeof(pres) == 'undefined')
        {
            if (typeof(system.self) == 'undefined')
            {
                throw new Error ('Error changing feaure field: ' +
                                 'unknown presence.');
            }
            pres = system.self;
        }

        var committer = std.featureObject.beginFeatureTransaction(pres);
        if (type == std.featureObject.Action.ADD)
            committer.addField(index,data);
        else if (type == std.featureObject.Action.REMOVE)
            committer.removeField(index);
        else if (type == std.featureObject.Action.CHANGE)
            committer.changeField(index,data);
        else
        {
            throw new Error('Unrecognized commit type in singleCommit ' +
                            'of featureObject.');
        }

        committer.commit();
    };

    /**
     @param {presence} presToChange (optional)
     */
    std.featureObject.beginFeatureTransaction = function(presToChange)
    {
        if (typeof(presToChange) == 'undefined')
        {
            if (tyepeof(system.self) == 'undefined')
            {
                throw new Error ('Error in featureObject.  ' +
                                 'Have no presence to bind feature ' +
                                 'changes to.');
            }
            presToChange = system.self;
        }

        if (!(presToChange.toString() in presencesToFeatureObjectMap ))
        {
            throw new Error('Error in beginFeatureTransaction.  Invalid '+
                            'presence to bind changes to.');
        }
        var featureObject = presencesToFeatureObjectMap[presToChange.toString()];
        return new std.featureObject.FeatureChangeTransaction(
            presToChange,featureObject.updateCount);
    };

    /**
     @param {std.featureObject.FeatureChangeTransaction} featChangeTrans
     @throws Error if changes to feature vector are stamped after
     latest update to vector.
     */
    std.featureObject.commitFeatureChanges =function(featChangeTrans)
    {
        if (!(featChangeTrans.pres.toString() in presencesToFeatureObjectMap))
        {
            throw new Error('Incorrect presence detected ' +
                            'when committing feature changes.');
        }

        var featElement = presencesToFeatureObjectMap[featChangeTrans.pres.toString()];

        //checking that all existing elements in data map
        //have not been modified since commit began.
        var addedDataMap ={};

        for (var s in featChangeTrans.actions)
        {
            var act = featChangeTrans.actions[s];

            if (act.type == std.featureObject.Action.ADD)
                addedDataMap[act.index] = true;

            if (act.index in featElement.dataCommitMap)
            {
                if (featElement.dataCommitMap[act.index] >
                    featChangeTrans.commitNum)
                {
                    throw new Error('Aborting feature transaction: ' +
                                    'detected out of date data.');
                }
            }
            else if (!act.index in addedDataMap)
            {
                throw new Error ('Aborting feature transaction: ' +
                                 'attempting operation on removed'+
                                 ' or non-existent data.');
            }

            if ((act.type != std.featureObject.Action.ADD)    &&
                (act.type != std.featureObject.Action.REMOVE) &&
                (act.type != std.featureObject.Action.CHANGE))
            {
                throw new Error('Unknown action type when commiting ' +
                                'feature.  Aborting.');
            }
        }

        //we know that the commit will go through.  We'll label all
        //data in commit as having been modified at updateCount.
        ++featElement.updateCount;

        //If gotten to this point, know we're not operating on stale
        //data.  Go ahead and commit all changes.
        for (var s in featChangeTrans.actions)
        {
            var act = featChangeTrans.actions[s];
            if ((act.type == std.featureObject.Action.ADD) ||
                (act.type == std.featureObject.Action.CHANGE))
            {
                featElement.data[act.index] = act.data;
                featElement.dataCommitMap[act.index] = featElement.updateCount;
            }
            else if (act.type == std.featureObject.Action.REMOVE)
            {
                if (act.index in featElement.data)
                {
                    delete featElement.data[act.index];
                    delete featElement.dataCommitMap[act.index];
                }
            }
        }
        sendFeaturesToSubscribers(featElement,featChangeTrans.pres);
    };

    /**
     If subscriber does not ack feature data after some time, then we
     unsubscribe them.
     */
    function sendFeaturesToSubscribers(featElement,pres)
    {

        //see handleFeatureObjUpdates in proxManager.em for format of
        //featureObject update messages.
        var updateMsg =
            {
                featureObjUpdate: featElement.data
            };

        //for now, ship full vector to all subscribing
        for (var s in featElement.subscribers)
        {
            pres #
                updateMsg >>
                featElement.subscribers[s] >>
                [function(){}, TIME_TO_ACK_UPDATE,
                 //if don't receive an ack, then unsubscribe the subscriber.
                 function()
                 {
                     if (s in featElement.subscribers)
                     {
                         var unsubscribeNotice =
                             {
                                 featureObjectUnsubscribe:''
                             };

                         //tell them they're being unsubscribed
                         pres  #
                             unsubscribeNotice >>
                             featElement.subscribers[s] >>[];

                         //actually remove them from subscribers
                         delete featElement.subscribers[s];
                     }
                 }];
        }
    }
})();


remove redundant tag in  description setter snippet

    <hudson.plugins.descriptionsetter.DescriptionSetterPublisher plugin="description-setter@@1.9">
      <regexp>@regexp</regexp>
      <regexpForFailed>@regexp_for_failed</regexpForFailed>
      <setForMatrix>false</setForMatrix>
    </hudson.plugins.descriptionsetter.DescriptionSetterPublisher>

    <hudson.plugins.descriptionsetter.DescriptionSetterPublisher plugin="description-setter@@1.9">
      <regexp>@regexp</regexp>
      <regexpForFailed/>
      <regexpForFailed>@regexp_for_failed</regexpForFailed>
      <setForMatrix>false</setForMatrix>
    </hudson.plugins.descriptionsetter.DescriptionSetterPublisher>