Merge remote-tracking branch 'eclipse/main' into eclipse-main

This commit is contained in:
Axel Uhl
2026-08-29 09:05:45 +02:00
11 changed files with 690 additions and 41 deletions
+180 -22
View File
@@ -1,6 +1,42 @@
name: release
on:
push:
# NOTE: introducing a "tags:" filter makes GitHub treat any Git ref kind that
# is NOT filtered as "undefined", and the workflow then stops running for it.
# So branches MUST be listed explicitly here; without "branches: ['**']" the
# tag filter below would silently disable all branch-push builds. '**' (not
# '*') is required to match branch names containing slashes (e.g. releases/x).
branches:
- '**'
# Reviewed-fork-PR tags. A maintainer fetches a fork PR head, reviews the
# exact SHA, then pushes one of these tags at that SHA. Because a tag push is
# a push event in this (base) repo, the run executes in trusted context WITH
# secrets — the only way to run secret-bearing validation on fork-authored
# code. '*-reviewed-for-compile' runs a compile-only build with NO secrets
# (cheap sanity check); '*-reviewed-for-build' runs the full build with tests
# and secrets. Safety rests on the maintainer having read that exact SHA
# before pushing the tag. See the "Determine build mode" step below.
tags:
- '*-reviewed-for-compile'
- '*-reviewed-for-build'
# paths-ignore is not evaluated for tag pushes (GitHub rule), so it only
# scopes branch pushes, exactly as before.
paths-ignore:
- 'wiki/**'
- '.github/workflows/*'
- '**/*.md'
- 'README.md'
- 'docs/**'
# Auto-build submitted PRs. A same-repo PR (branch pushed by a committer) runs
# the full build with tests and secrets — same trust as a direct branch push.
# A fork PR runs compile-only with NO secrets (GitHub withholds secrets from
# fork pull_request runs at the event level); the secret-bearing full build of
# fork code stays gated behind a maintainer pushing a *-reviewed-for-build tag.
# No branches: filter here — for pull_request that would filter by BASE branch;
# omitting it targets PRs into any branch. Draft PRs are excluded by the build
# job's if:. See the "Determine build mode" step for the fork/same-repo split.
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths-ignore:
- 'wiki/**'
- '.github/workflows/*'
@@ -30,12 +66,20 @@ jobs:
outputs:
should_run: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- id: filter
uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3.0.3
uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
with:
# Under the default 'some' quantifier a negated rule still counts as a
# match (picomatch treats '!README.md' as "anything but README.md"),
# so a changed README.md would set code=true. 'some-with-excludes'
# (v4+) makes the '!' rules act as real exclusions: code is true only
# when a changed file matches the positive '**' rule AND none of the
# excludes.
predicate-quantifier: 'some-with-excludes'
filters: |
code:
- '**'
- '!wiki/**'
- '!.github/workflows/*'
- '!**/*.md'
@@ -44,13 +88,36 @@ jobs:
build:
needs: changes
if: ${{ needs.changes.outputs.should_run == 'true' || github.event_name == 'workflow_dispatch' }}
if: >-
${{ (needs.changes.outputs.should_run == 'true' || github.event_name == 'workflow_dispatch')
&& (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
permissions:
contents: write
checks: write
actions: read
runs-on: ${{ github.event.inputs.runner_cpus != '' && format('ubuntu-latest-{0}cpu', github.event.inputs.runner_cpus) || 'ubuntu-latest' }}
outputs:
skip_tests: ${{ steps.mode.outputs.skip_tests }}
steps:
- name: Determine build mode
id: mode
shell: bash
# skip_tests=true means "compile only, no tests, no secrets". It is true
# for a workflow_dispatch with skip_tests=true, for a push of a
# *-reviewed-for-compile tag, OR for a fork-originated pull_request (fork
# runs get no secrets, so we compile without tests; the secret-bearing
# full build of fork code is gated behind a *-reviewed-for-build tag).
# Everything else (branches, same-repo PRs, *-reviewed-for-build tags)
# runs the full build with tests and secrets.
run: |
skip=false
if [[ "${{ github.event.inputs.skip_tests }}" == "true" ]]; then skip=true; fi
if [[ "${{ github.ref }}" == refs/tags/*-reviewed-for-compile ]]; then skip=true; fi
if [[ "${{ github.event_name }}" == "pull_request" \
&& "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then
skip=true
fi
echo "skip_tests=$skip" >> "$GITHUB_OUTPUT"
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@v1.3.1
with:
@@ -91,23 +158,23 @@ jobs:
echo Installed firefox versions: ${{ steps.setup-firefox.outputs.firefox-version }}
${{ steps.setup-firefox.outputs.firefox-path }} --version
- name: Check out the repository to the runner
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: '500'
- name: Install JDK 8
uses: actions/setup-java@v4
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '8'
mvn-toolchain-id: 'JavaSE-1.8'
- name: Install JDK 17
uses: actions/setup-java@v4
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
mvn-toolchain-id: 'JavaSE-17'
- name: Install JDK 25
uses: actions/setup-java@v4
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '25'
@@ -129,30 +196,36 @@ jobs:
- shell: bash
env: # Or as an environment variable
TMP: /mnt/tmp
AWS_S3_TEST_S3ACCESSID: ${{ secrets.AWS_S3_TEST_S3ACCESSID }}
AWS_S3_TEST_S3ACCESSKEY: ${{ secrets.AWS_S3_TEST_S3ACCESSKEY }}
GEONAMES_ORG_USERNAMES: ${{ secrets.GEONAMES_ORG_USERNAMES }}
GOOGLE_MAPS_AUTHENTICATION_PARAMS: ${{ secrets.GOOGLE_MAPS_AUTHENTICATION_PARAMS }}
# These five are consumed only at TEST runtime. For a compile-only run
# (skip_tests) they resolve to '' so the real secret values are never
# placed in the process environment where reviewed-but-fork-authored
# code could read them. The full build (skip_tests false) gets them all.
AWS_S3_TEST_S3ACCESSID: ${{ steps.mode.outputs.skip_tests == 'true' && '' || secrets.AWS_S3_TEST_S3ACCESSID }}
AWS_S3_TEST_S3ACCESSKEY: ${{ steps.mode.outputs.skip_tests == 'true' && '' || secrets.AWS_S3_TEST_S3ACCESSKEY }}
GEONAMES_ORG_USERNAMES: ${{ steps.mode.outputs.skip_tests == 'true' && '' || secrets.GEONAMES_ORG_USERNAMES }}
GOOGLE_MAPS_AUTHENTICATION_PARAMS: ${{ steps.mode.outputs.skip_tests == 'true' && '' || secrets.GOOGLE_MAPS_AUTHENTICATION_PARAMS }}
POLAR_DATA_BASE_URL: ${{ vars.POLAR_DATA_BASE_URL }}
POLAR_DATA_BEARER_TOKEN: ${{ secrets.POLAR_DATA_BEARER_TOKEN }}
POLAR_DATA_BEARER_TOKEN: ${{ steps.mode.outputs.skip_tests == 'true' && '' || secrets.POLAR_DATA_BEARER_TOKEN }}
APP_PARAMETERS: "-Daws.region=eu-west-1"
JAVA8_HOME: ${{env.JAVA_HOME_8_X64}}
JAVA17_HOME: ${{env.JAVA_HOME_17_X64}}
run: |
./configuration/buildAndUpdateProduct.sh -x ${{ github.event.inputs.runner_cpus == '' && '4' || github.event.inputs.runner_cpus }} ${{ github.event.inputs.skip_tests == 'true' && '-t' || '' }} ${{ github.event.inputs.local_target_platform == 'true' && '-v' || '' }} build 2>&1
./configuration/buildAndUpdateProduct.sh -x ${{ github.event.inputs.runner_cpus == '' && '4' || github.event.inputs.runner_cpus }} ${{ steps.mode.outputs.skip_tests == 'true' && '-t' || '' }} ${{ github.event.inputs.local_target_platform == 'true' && '-v' || '' }} build 2>&1
- name: show disk stats
if: always()
run: df -h
- name: Upload build log
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: build.log
path: build.log
retention-days: 90
- name: Collect Test Reports
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
if: always()
uses: dorny/test-reporter@a6ddd83ac95ff4586f5d3aceeb314d9a1841db95 # v3.0.0
# Skip for compile-only runs: no tests ran, so there are no TEST-*.xml
# and fail-on-error would otherwise error on zero matches.
if: ${{ always() && steps.mode.outputs.skip_tests != 'true' }}
with:
name: Maven Tests
path: '**/TEST-*.xml'
@@ -170,7 +243,7 @@ jobs:
run: |
echo "SIMPLE_VERSION_INFO=$( basename dist/* )" >>${GITHUB_ENV}
- name: Upload test results
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: test-results
@@ -178,7 +251,7 @@ jobs:
**/surefire-reports/**
retention-days: 90
- name: Upload distribution artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/docker-24' || github.ref == 'refs/heads/docker-25' || startsWith(github.ref, 'refs/heads/releases/') }}
with:
name: ${{ env.SIMPLE_VERSION_INFO }}
@@ -232,7 +305,92 @@ jobs:
echo "Identified CI job: ${JOB}"
curl -u "${{ vars.CI_JOB_USERNAME }}:${{ secrets.CI_JOB_PASSWORD}}" ${{ vars.CI_BASE_URL }}/job/${JOB}/build?token=${{ secrets.CI_JOB_TOKEN }}
build-gate:
# build-gate is the REQUIRED status check in the branch ruleset (context
# "build-gate"). It must be SATISFIED only for a genuine full build (tests
# actually ran and passed) or a legitimate trivial-change skip; every other
# case — most importantly a compile-only fork PR — must leave the PR
# un-mergeable WITHOUT a red X.
#
# WHY THIS IS A POSTED COMMIT STATUS, NOT A JOB NAMED build-gate:
# GitHub Actions auto-creates a check-run for every JOB, and fills its
# conclusion from the job's fate — including "skipped" when the job's if: is
# false. A rule set / branch protection treats a "skipped" required check as
# SATISFIED (mergeable). So a job named build-gate can NEVER represent "absent"
# — a skipped job still materializes a skipped=passed check-run. That is the
# exact bug that let compile-only fork PRs #53/#54 merge without tests.
#
# A COMMIT STATUS (legacy Statuses API) is different: it exists only if we
# explicitly POST it. A skipped STEP posts nothing (steps are not check-runs).
# The ruleset matches the required context "build-gate" by NAME across both
# APIs, so a posted commit status with context=build-gate satisfies it exactly
# as the old check-run did — no ruleset change needed.
#
# Hence: this job is named post-build-gate-status (NOT build-gate) so its own
# auto check-run carries a context the ruleset ignores. Its single step POSTs a
# commit status context=build-gate ONLY when: the change was trivial (build
# skipped, no relevant changes) OR a full build ran WITH tests and succeeded
# -> state=success; OR a full build ran WITH tests and FAILED
# (build.result==failure, skip_tests==false) -> state=failure (red X, so a
# committer's own broken build is loud). In every OTHER case it posts NOTHING,
# so no build-gate context exists for the head SHA and the PR stays blocked at
# "Expected" with no red X. That "nothing" covers: any compile-only run,
# succeeded OR failed (fork PR, *-reviewed-for-compile, workflow_dispatch
# skip_tests) — no tests ran, so there is nothing meaningful to fail on; AND a
# CANCELLED trusted build — cancellation is indeterminate (tests might have
# passed had it finished), so "failure" would be misleading; re-run to get a
# real verdict. Merge qualification for fork code comes only from a maintainer
# pushing a *-reviewed-for-build tag (full trusted build with tests), which
# posts success/failure. compile-gate (advisory) still shows compile pass/fail.
#
# The status is posted against the PR HEAD sha on pull_request events
# (github.sha there is the ephemeral refs/pull/N/merge commit, which the
# ruleset does not evaluate) and github.sha otherwise. needs:[changes, build]
# + if: always() so the trivial-change path (build skipped) is still evaluated.
post-build-gate-status:
permissions:
statuses: write
needs: [changes, build]
if: always()
runs-on: ubuntu-latest
steps:
- name: Post build-gate commit status
env:
GH_TOKEN: ${{ github.token }}
BUILD_RESULT: ${{ needs.build.result }}
SKIP_TESTS: ${{ needs.build.outputs.skip_tests }}
SHOULD_RUN: ${{ needs.changes.outputs.should_run }}
GATE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
STATE=""
if [[ "$BUILD_RESULT" == "success" && "$SKIP_TESTS" != "true" ]]; then
STATE=success; REASON="Full build with tests passed."
elif [[ "$BUILD_RESULT" == "skipped" && "$SHOULD_RUN" == "false" ]]; then
STATE=success; REASON="No relevant changes — nothing to build."
elif [[ "$BUILD_RESULT" == "failure" && "$SKIP_TESTS" != "true" ]]; then
STATE=failure; REASON="Full build with tests failed."
fi
if [[ -n "$STATE" ]]; then
echo "Posting build-gate=$STATE on $GATE_SHA: $REASON"
gh api "repos/${GITHUB_REPOSITORY}/statuses/${GATE_SHA}" \
-f state="$STATE" \
-f context=build-gate \
-f description="$REASON" \
-f target_url="$RUN_URL"
else
echo "Not posting build-gate: build.result=$BUILD_RESULT skip_tests=$SKIP_TESTS should_run=$SHOULD_RUN"
echo "The build-gate context stays absent, so the PR remains un-mergeable"
echo "at 'Expected' without a red X — this covers a successful OR failed"
echo "compile-only run (e.g. a fork PR, where no tests ran so there is"
echo "nothing to fail on). A *-reviewed-for-build tag runs the full build"
echo "with tests and posts success (or failure) to the build-gate context."
fi
# compile-gate is an ADVISORY (non-required) check. It reflects only whether
# the build job compiled successfully, regardless of whether tests ran — a full
# build implies compilation succeeded. It gives *-reviewed-for-compile runs a
# distinct green/red on the PR without touching the required build-gate check.
compile-gate:
permissions: {}
needs: [changes, build]
if: always()
@@ -240,11 +398,11 @@ jobs:
steps:
- run: |
if [[ "${{ needs.build.result }}" == "success" ]]; then
echo "Build passed."
echo "Compilation succeeded."
elif [[ "${{ needs.build.result }}" == "skipped" && "${{ needs.changes.outputs.should_run }}" == "false" ]]; then
echo "No relevant changes — skipping is OK."
echo "No relevant changes — nothing to compile."
else
echo "Build failed or was cancelled."
echo "Compilation failed or was cancelled."
exit 1
fi
+4
View File
@@ -4,6 +4,10 @@ Your WebSearch tool is broken, don't use it. Instead fetch https://duckduckgo.co
# Claude Code Instructions for sailing-analytics
## Research before Answering
Make sure to research the topic/question well and thoroughly instead of just answering quickly and superficially.
## Java Coding Style Preferences
### Variable Declarations
+1 -2
View File
@@ -64,7 +64,6 @@ The project welcomes contributions in the form of pull requests, for example, en
The issue tracker at [https://bugzilla.sapsailing.com](https://bugzilla.sapsailing.com) is currently used for any sort of issue and enhancement request tracking. Help to migrate this smoothly to Github Issues would be much appreciated, ideally keeping issue numbers stable due to many references to those Bugzilla bug numbers, be it in the source code, the Wiki, or the build infrastructure.
## Code of Conduct
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](https://github.com/SAP/.github/blob/main/CODE_OF_CONDUCT.md) at all times.
@@ -271,7 +270,7 @@ to connect to the server's OSGi console.
Alternatively, use an "environment" definition that sets useful defaults, e.g., like this:
```
docker run -P -it --rm -e "SERVER_NAME=test77" -e "USE_ENVIRONMENT=live-master-server" -e "REPLICATE_MASTER_BEARER_TOKEN=BRxGpF0nr68Z4m/f13/MgiYhdRB3xoDCYd+rLc17rTs=" ghcr.io/eclipse-sailing-analytics/sailing-analytics:latest \
docker run -P -it --rm -e "SERVER_NAME=test77" -e "USE_ENVIRONMENT=live-master-server" -e "REPLICATE_MASTER_BEARER_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ghcr.io/eclipse-sailing-analytics/sailing-analytics:latest \
bash -c "rm env.sh; echo \"
SERVER_NAME=test77
USE_ENVIRONMENT=live-master-server
+5 -1
View File
@@ -22,7 +22,11 @@ echo "Trying to obtain bug summary/title from Github..."
BUG_SUMMARY="$( curl -s -H 'Accept: application/vnd.github+json' https://api.github.com/repos/eclipse-sailing-analytics/sailing-analytics/issues/${BUG_ID} | jq -r '.title' )"
echo "Found: ${BUG_SUMMARY}"
if [ -z "${USERNAME}" ]; then
read -p "Jenkins Username (e-mail): " USERNAME
if [[ "$OSTYPE" == *"$OS_FOR_GSED"* ]]; then
   read -e -p "Jenkins Username (e-mail): " USERNAME
  else
   read -p "Jenkins Username (e-mail): " USERNAME
fi
fi
if [ -z "${PASSWORD}" ]; then
read -s -p "Jenkins Password (API Token): " PASSWORD
@@ -0,0 +1,92 @@
# bash completion for merge-upstream-to-downstream.sh
#
# Provides Tab-completion for the script's named options and, intelligently,
# for their VALUES:
# * remote-name options (--fork-remote/--upstream-remote/--downstream-remote)
# complete against the git remotes of the repo containing the CWD;
# * remote-branch options (--upstream-branch/--downstream-branch/--fork-branch)
# complete against the branches OF THE MATCHING REMOTE — the script resolves
# each of those to a remote (via its paired --*-remote option already on the
# line, the MERGE_U2D_* env var, or the built-in default), so e.g.
# `--upstream-remote eclipse --upstream-branch <Tab>` lists eclipse's branches;
# * local-branch options (--local-*-branch) complete against local branches.
#
# Install (any one of):
# * source it from ~/.bashrc: source /path/to/merge-upstream-to-downstream.bash-completion
# * or drop/symlink into the user dir: ~/.local/share/bash-completion/completions/merge-upstream-to-downstream.sh
# * or system-wide: /usr/share/bash-completion/completions/merge-upstream-to-downstream.sh
# (The last two use on-demand loading; the file's basename must match the command
# being completed — hence the symlink name ending in .sh below.)
# Enumerate git remotes for the repo containing $PWD (empty & silent if not a repo).
__m2d_remotes() {
git remote 2>/dev/null
}
# Enumerate branch short-names present on a given remote (arg 1), via the
# remote-tracking refs. strip=3 turns refs/remotes/<remote>/<branch> into <branch>.
__m2d_remote_branches() {
local remote="$1"
[ -n "$remote" ] || return 0
git for-each-ref --format='%(refname:strip=3)' "refs/remotes/${remote}/" 2>/dev/null
}
# Enumerate local branch short-names. strip=2 turns refs/heads/<branch> into <branch>.
__m2d_local_branches() {
git for-each-ref --format='%(refname:strip=2)' refs/heads/ 2>/dev/null
}
# Resolve which remote a given "role" (fork|upstream|downstream) points at, using
# the SAME precedence the script itself uses: an explicit --<role>-remote already
# typed on the command line > the MERGE_U2D_<ROLE>_REMOTE env var > built-in default.
# $1 = role; the current COMP_WORDS are scanned for the explicit flag.
__m2d_resolve_remote() {
local role="$1" flag="--${1}-remote" i
# 1) explicit flag already on the line (supports both "--flag val" and "--flag=val").
for (( i = 1; i < COMP_CWORD; i++ )); do
case "${COMP_WORDS[i]}" in
"$flag") [ $((i + 1)) -lt $COMP_CWORD ] && { printf '%s' "${COMP_WORDS[i+1]}"; return 0; } ;;
"$flag"=*) printf '%s' "${COMP_WORDS[i]#*=}"; return 0 ;;
esac
done
# 2) env var, then 3) built-in default.
case "$role" in
fork) printf '%s' "${MERGE_U2D_FORK_REMOTE:-}" ;;
upstream) printf '%s' "${MERGE_U2D_UPSTREAM_REMOTE:-eclipse}" ;;
downstream) printf '%s' "${MERGE_U2D_DOWNSTREAM_REMOTE:-sap}" ;;
esac
}
_merge_upstream_to_downstream() {
local cur prev opts
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="--fork-remote --upstream-remote --downstream-remote \
--upstream-branch --downstream-branch --fork-branch \
--local-upstream-branch --local-downstream-branch --local-fork-branch \
-h --help"
# Complete the VALUE when the previous word is an option that takes one.
case "$prev" in
--fork-remote|--upstream-remote|--downstream-remote)
COMPREPLY=( $(compgen -W "$(__m2d_remotes)" -- "$cur") ); return 0 ;;
--upstream-branch)
COMPREPLY=( $(compgen -W "$(__m2d_remote_branches "$(__m2d_resolve_remote upstream)")" -- "$cur") ); return 0 ;;
--downstream-branch)
COMPREPLY=( $(compgen -W "$(__m2d_remote_branches "$(__m2d_resolve_remote downstream)")" -- "$cur") ); return 0 ;;
--fork-branch)
COMPREPLY=( $(compgen -W "$(__m2d_remote_branches "$(__m2d_resolve_remote fork)")" -- "$cur") ); return 0 ;;
--local-upstream-branch|--local-downstream-branch|--local-fork-branch)
COMPREPLY=( $(compgen -W "$(__m2d_local_branches)" -- "$cur") ); return 0 ;;
esac
# Otherwise complete an option NAME. compgen -W filters by "$cur", so an
# empty cur (bare Tab right after the command) lists every option, while a
# partial like "--fork" narrows it — and a stray non-option word yields the
# options too rather than nothing.
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
return 0
}
# Register for the plain name, the .sh form, and any path invocation (./x, /abs/x).
complete -F _merge_upstream_to_downstream \
merge-upstream-to-downstream.sh \
./merge-upstream-to-downstream.sh
+311
View File
@@ -0,0 +1,311 @@
#!/usr/bin/env bash
#
# merge-upstream-to-downstream.sh
#
# Merge an upstream repo into a downstream repo by way of a PR opened from a
# secondary account's fork, so the account you review with stays free to approve
# the PR (branch protection commonly forbids self-approval / requires last-push
# approval by someone other than the pusher).
#
# The fork remote's URL is expected to embed a PAT (https://<token>@host/owner/repo);
# that PAT is the single source of truth for credentials and is read at runtime,
# never printed. Keep this script free of any local repo/remote/branch details:
# everything is passed as named options so the script itself is safe to commit.
#
# Flow (names below are the resolved values, see options):
# 1. Check out <local-fork-branch> (should track <fork-remote>/<fork-branch>).
# 2. Fetch <downstream-remote>/<downstream-branch> -> ff <local-downstream-branch>.
# 3. Fetch <upstream-remote>/<upstream-branch> -> ff <local-upstream-branch>.
# 4. Merge <local-downstream-branch> into <local-fork-branch>.
# 5. Merge <local-upstream-branch> into <local-fork-branch>.
# 6. Push <local-fork-branch> -> <fork-remote>/<fork-branch>.
# 7. Open PR <fork-owner>:<fork-branch> -> <downstream-repo>:<downstream-branch>,
# body = incoming commit titles, maintainer-edit enabled (gh default),
# authored via the fork PAT.
#
# Options (all named; only --fork-remote is required unless MERGE_U2D_FORK_REMOTE
# is set). Precedence: command-line option > MERGE_U2D_* env var > built-in default.
# --fork-remote NAME (required) remote for the fork; its URL holds the PAT
# env: MERGE_U2D_FORK_REMOTE
# --upstream-remote NAME default: eclipse env: MERGE_U2D_UPSTREAM_REMOTE
# --downstream-remote NAME default: sap env: MERGE_U2D_DOWNSTREAM_REMOTE
# --upstream-branch NAME default: main env: MERGE_U2D_UPSTREAM_BRANCH
# --downstream-branch NAME default: main env: MERGE_U2D_DOWNSTREAM_BRANCH
# --fork-branch NAME default: main env: MERGE_U2D_FORK_BRANCH
# --local-upstream-branch NAME default: <upstream-remote>-<upstream-branch>
# env: MERGE_U2D_LOCAL_UPSTREAM_BRANCH
# --local-downstream-branch NAME default: <downstream-remote>-<downstream-branch>
# env: MERGE_U2D_LOCAL_DOWNSTREAM_BRANCH
# --local-fork-branch NAME default: <fork-remote>-<downstream-remote>-<downstream-branch>
# env: MERGE_U2D_LOCAL_FORK_BRANCH
# -h | --help show this help and exit
#
# So, e.g., `export MERGE_U2D_FORK_REMOTE=aksajhfduwafe` in your .bashrc lets you
# run the script with no arguments at all.
#
# Example (author's layout, downstream remote actually named "github"):
# ./merge-upstream-to-downstream.sh --fork-remote aksajhfduwafe --downstream-remote github \
# --local-downstream-branch sap-main --local-fork-branch aksajhfduwafe-sap-main
#
# (Defaults assume the downstream remote is named "sap"; the author's is "github",
# so the local-branch names are given explicitly to match sap-main / aksajhfduwafe-sap-main.
# Alternatively rename the remote to "sap" and the local-branch defaults line up.)
set -euo pipefail
# --- defaults -----------------------------------------------------------------
# Precedence for every setting: command-line option > environment variable >
# built-in default. Each var is seeded here from its MERGE_U2D_* environment
# variable (empty if unset) or the built-in default; the option parser below
# then overwrites a var ONLY when its flag is actually given. So you can export
# e.g. MERGE_U2D_FORK_REMOTE=aksajhfduwafe in your .bashrc and run with no args,
# while still overriding any single value on the command line.
FORK_REMOTE="${MERGE_U2D_FORK_REMOTE:-}"
UPSTREAM_REMOTE="${MERGE_U2D_UPSTREAM_REMOTE:-eclipse}"
DOWNSTREAM_REMOTE="${MERGE_U2D_DOWNSTREAM_REMOTE:-sap}"
UPSTREAM_BRANCH="${MERGE_U2D_UPSTREAM_BRANCH:-main}"
DOWNSTREAM_BRANCH="${MERGE_U2D_DOWNSTREAM_BRANCH:-main}"
FORK_BRANCH="${MERGE_U2D_FORK_BRANCH:-main}"
LOCAL_UPSTREAM_BRANCH="${MERGE_U2D_LOCAL_UPSTREAM_BRANCH:-}" # resolved after parsing if left empty
LOCAL_DOWNSTREAM_BRANCH="${MERGE_U2D_LOCAL_DOWNSTREAM_BRANCH:-}"
LOCAL_FORK_BRANCH="${MERGE_U2D_LOCAL_FORK_BRANCH:-}"
usage() {
cat <<EOF
Usage: $(basename "$0") --fork-remote NAME [options]
Merge an upstream repo into a downstream repo via a PR opened from a secondary
account's fork, so the reviewing account stays free to approve. The fork remote's
URL must embed a PAT (https://<token>@host/owner/repo); it is read at runtime and
never printed.
Precedence for every setting: command-line option > MERGE_U2D_* env var > default.
Defaults shown below already reflect any MERGE_U2D_* env var currently exported,
so e.g. \`export MERGE_U2D_FORK_REMOTE=aksajhfduwafe\` lets you run with no args.
Required (unless MERGE_U2D_FORK_REMOTE is set):
--fork-remote NAME remote for the fork; its URL holds the PAT
[env MERGE_U2D_FORK_REMOTE] (current: ${FORK_REMOTE:-<unset>})
Remotes:
--upstream-remote NAME upstream remote (default: ${UPSTREAM_REMOTE})
[env MERGE_U2D_UPSTREAM_REMOTE]
--downstream-remote NAME downstream/base remote (default: ${DOWNSTREAM_REMOTE})
[env MERGE_U2D_DOWNSTREAM_REMOTE]
Remote branches:
--upstream-branch NAME branch on upstream remote (default: ${UPSTREAM_BRANCH})
[env MERGE_U2D_UPSTREAM_BRANCH]
--downstream-branch NAME branch on downstream remote(default: ${DOWNSTREAM_BRANCH})
[env MERGE_U2D_DOWNSTREAM_BRANCH]
--fork-branch NAME branch on fork remote (default: ${FORK_BRANCH})
[env MERGE_U2D_FORK_BRANCH]
Local branches (defaults derive from the names above):
--local-upstream-branch NAME default: <upstream-remote>-<upstream-branch>
[env MERGE_U2D_LOCAL_UPSTREAM_BRANCH]
--local-downstream-branch NAME default: <downstream-remote>-<downstream-branch>
[env MERGE_U2D_LOCAL_DOWNSTREAM_BRANCH]
--local-fork-branch NAME default: <fork-remote>-<downstream-remote>-<downstream-branch>
[env MERGE_U2D_LOCAL_FORK_BRANCH]
Other:
-h, --help show this help and exit
Example (downstream remote named "github" rather than the default "sap"):
$(basename "$0") --fork-remote aksajhfduwafe --downstream-remote github \\
--local-downstream-branch sap-main --local-fork-branch aksajhfduwafe-sap-main
EOF
}
# --- named-option parsing -----------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
--fork-remote) FORK_REMOTE="$2"; shift 2 ;;
--upstream-remote) UPSTREAM_REMOTE="$2"; shift 2 ;;
--downstream-remote) DOWNSTREAM_REMOTE="$2"; shift 2 ;;
--upstream-branch) UPSTREAM_BRANCH="$2"; shift 2 ;;
--downstream-branch) DOWNSTREAM_BRANCH="$2"; shift 2 ;;
--fork-branch) FORK_BRANCH="$2"; shift 2 ;;
--local-upstream-branch) LOCAL_UPSTREAM_BRANCH="$2"; shift 2 ;;
--local-downstream-branch) LOCAL_DOWNSTREAM_BRANCH="$2"; shift 2 ;;
--local-fork-branch) LOCAL_FORK_BRANCH="$2"; shift 2 ;;
--*=*) # support --opt=value form
set -- "${1%%=*}" "${1#*=}" "${@:2}" ;;
-h|--help) usage; exit 0 ;;
*) printf 'Unknown option: %s\n\n' "$1" >&2; usage >&2; exit 2 ;;
esac
done
# --- resolve derived defaults -------------------------------------------------
[ -n "$FORK_REMOTE" ] || { printf 'ERROR: --fork-remote is required (or set MERGE_U2D_FORK_REMOTE).\n\n' >&2; usage >&2; exit 2; }
: "${LOCAL_UPSTREAM_BRANCH:=${UPSTREAM_REMOTE}-${UPSTREAM_BRANCH}}"
: "${LOCAL_DOWNSTREAM_BRANCH:=${DOWNSTREAM_REMOTE}-${DOWNSTREAM_BRANCH}}"
: "${LOCAL_FORK_BRANCH:=${FORK_REMOTE}-${DOWNSTREAM_REMOTE}-${DOWNSTREAM_BRANCH}}"
PR_TITLE="Merged latest ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH} updates to downstream"
# ------------------------------------------------------------------------------
log() { printf '\n\033[1;34m==>\033[0m %s\n' "$*"; }
die() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; }
# Restore the branch the user started on, even on failure; clean temp file.
ORIGINAL_REF="$(git rev-parse --abbrev-ref HEAD)"
PR_BODY_FILE=""
cleanup() {
[ -n "$PR_BODY_FILE" ] && rm -f "$PR_BODY_FILE"
git checkout --quiet "$ORIGINAL_REF" 2>/dev/null || true
}
trap cleanup EXIT
# --- preflight ----------------------------------------------------------------
command -v gh >/dev/null 2>&1 || die "gh CLI not found on PATH."
for r in "$FORK_REMOTE" "$UPSTREAM_REMOTE" "$DOWNSTREAM_REMOTE"; do
git remote get-url "$r" >/dev/null 2>&1 || die "remote '$r' not defined."
done
git show-ref --verify --quiet "refs/heads/$LOCAL_FORK_BRANCH" \
|| die "local branch '$LOCAL_FORK_BRANCH' does not exist (create it tracking $FORK_REMOTE/$FORK_BRANCH)."
git show-ref --verify --quiet "refs/heads/$LOCAL_DOWNSTREAM_BRANCH" \
|| die "local branch '$LOCAL_DOWNSTREAM_BRANCH' does not exist (create it tracking $DOWNSTREAM_REMOTE/$DOWNSTREAM_BRANCH)."
git show-ref --verify --quiet "refs/heads/$LOCAL_UPSTREAM_BRANCH" \
|| die "local branch '$LOCAL_UPSTREAM_BRANCH' does not exist (create it tracking $UPSTREAM_REMOTE/$UPSTREAM_BRANCH)."
# Block only on modified/staged TRACKED files — those are what branch switching
# and merging would clobber. Untracked files (build artifacts, .bak, etc.) are
# left alone by checkout/merge, so they must not block the run.
[ -z "$(git status --porcelain --untracked-files=no)" ] || die "working tree has uncommitted tracked changes; commit/stash first."
# Extract the bare PAT embedded in the fork remote URL (https://<token>@host/...).
# Never echoed; only exported into gh's environment for the PR call.
FORK_URL="$(git remote get-url "$FORK_REMOTE")"
FORK_PAT="$(printf '%s' "$FORK_URL" | sed -nE 's#^https://([^@/]+)@.*#\1#p')"
[ -n "$FORK_PAT" ] || die "could not extract a PAT from the '$FORK_REMOTE' remote URL (expected https://<token>@host/owner/repo)."
FORK_OWNER="$(printf '%s' "$FORK_URL" | sed -nE 's#^https://[^@/]+@[^/]+/([^/]+)/.*#\1#p')"
FORK_HOST="$(printf '%s' "$FORK_URL" | sed -nE 's#^https://[^@/]+@([^/]+)/.*#\1#p')"
[ -n "$FORK_OWNER" ] || die "could not parse the fork owner from the '$FORK_REMOTE' URL."
: "${FORK_HOST:=github.com}"
# Derive the downstream (base) repo "owner/name" from the downstream remote URL,
# supporting both git@host:owner/repo(.git) and https://host/owner/repo(.git).
# Strip any trailing ".git" explicitly — a non-greedy name group won't drop it.
DOWNSTREAM_URL="$(git remote get-url "$DOWNSTREAM_REMOTE")"
BASE_REPO="$(printf '%s' "$DOWNSTREAM_URL" \
| sed -nE 's#^(git@[^:]+:|https?://[^/]+/)([^/]+/[^/]+)$#\2#p')"
BASE_REPO="${BASE_REPO%.git}"
[ -n "$BASE_REPO" ] || die "could not derive owner/name from '$DOWNSTREAM_REMOTE' URL: $DOWNSTREAM_URL"
# --- 1. check out the fork branch ---------------------------------------------
log "Checking out $LOCAL_FORK_BRANCH"
git checkout "$LOCAL_FORK_BRANCH"
# --- 2. fetch downstream into its local branch --------------------------------
log "Fetching $DOWNSTREAM_REMOTE/$DOWNSTREAM_BRANCH into $LOCAL_DOWNSTREAM_BRANCH"
git fetch "$DOWNSTREAM_REMOTE" "$DOWNSTREAM_BRANCH"
git checkout "$LOCAL_DOWNSTREAM_BRANCH"
git merge --ff-only "$DOWNSTREAM_REMOTE/$DOWNSTREAM_BRANCH" \
|| die "$LOCAL_DOWNSTREAM_BRANCH cannot fast-forward to $DOWNSTREAM_REMOTE/$DOWNSTREAM_BRANCH (local diverged?)."
# --- 3. fetch upstream into its local branch ----------------------------------
log "Fetching $UPSTREAM_REMOTE/$UPSTREAM_BRANCH into $LOCAL_UPSTREAM_BRANCH"
git fetch "$UPSTREAM_REMOTE" "$UPSTREAM_BRANCH"
git checkout "$LOCAL_UPSTREAM_BRANCH"
git merge --ff-only "$UPSTREAM_REMOTE/$UPSTREAM_BRANCH" \
|| die "$LOCAL_UPSTREAM_BRANCH cannot fast-forward to $UPSTREAM_REMOTE/$UPSTREAM_BRANCH (local diverged?)."
# --- upstream-contribution guard ----------------------------------------------
# The whole point is to carry UPSTREAM changes downstream. If upstream has no
# commit that isn't already on downstream main, there is nothing to contribute:
# stop before merging/pushing/PRing so we never open a ridiculous empty (or
# downstream-into-itself) PR. (A downstream-only advance with no upstream content
# is excluded here too, precisely because it contributes nothing from upstream.)
UPSTREAM_CONTRIB_COUNT="$(git rev-list --count "$DOWNSTREAM_REMOTE/$DOWNSTREAM_BRANCH..$LOCAL_UPSTREAM_BRANCH")"
if [ "$UPSTREAM_CONTRIB_COUNT" -eq 0 ]; then
log "$UPSTREAM_REMOTE/$UPSTREAM_BRANCH has no commits beyond $DOWNSTREAM_REMOTE/$DOWNSTREAM_BRANCH — nothing to contribute. No merge, push, or PR. Done."
exit 0
fi
log "$UPSTREAM_CONTRIB_COUNT upstream commit(s) not yet on downstream; proceeding."
# --- 4 & 5. merge downstream then upstream into the fork branch ---------------
git checkout "$LOCAL_FORK_BRANCH"
log "Merging $LOCAL_DOWNSTREAM_BRANCH into $LOCAL_FORK_BRANCH"
git merge --no-edit "$LOCAL_DOWNSTREAM_BRANCH" \
|| die "merge of $LOCAL_DOWNSTREAM_BRANCH hit conflicts; resolve, commit, then re-run (fetches are idempotent)."
log "Merging $LOCAL_UPSTREAM_BRANCH into $LOCAL_FORK_BRANCH"
git merge --no-edit "$LOCAL_UPSTREAM_BRANCH" \
|| die "merge of $LOCAL_UPSTREAM_BRANCH hit conflicts; resolve, commit, then re-run."
# --- compute the commits this PR would bring into the downstream base ---------
# Broader than the upstream-contribution guard above: this is everything the PR
# delivers to downstream (upstream commits + any downstream re-sync), used for
# the PR body. It stays a defensive belt-and-braces check — if it somehow ends
# up empty (e.g. everything was already merged concurrently), don't push/PR.
INCOMING_RANGE="$DOWNSTREAM_REMOTE/$DOWNSTREAM_BRANCH..$LOCAL_FORK_BRANCH"
INCOMING_COUNT="$(git rev-list --count "$INCOMING_RANGE")"
if [ "$INCOMING_COUNT" -eq 0 ]; then
log "No new commits over $DOWNSTREAM_REMOTE/$DOWNSTREAM_BRANCH — nothing to push or PR. Done."
exit 0
fi
log "$INCOMING_COUNT commit(s) will be proposed to $BASE_REPO."
# --- already-open-PR short-circuit (idempotency) ------------------------------
# If an open PR already exists from this fork head into the downstream base AND
# its head commit already equals what we're about to push, there is nothing to
# do: don't push, don't open a duplicate PR — just report the existing one. If a
# PR exists but its head is OLDER (new upstream commits arrived since), we fall
# through and push, which updates that same PR; step 7 then reports it.
HEAD_SPEC="${FORK_OWNER}:${FORK_BRANCH}"
LOCAL_FORK_TIP="$(git rev-parse "$LOCAL_FORK_BRANCH")"
# Match on head branch AND head-fork owner (—head filters by branch name only,
# which could collide with a same-named branch from a different fork). Emit
# "<sha> <url>" for the first match, or nothing.
EXISTING_PR="$(GH_TOKEN="$FORK_PAT" GH_HOST="$FORK_HOST" \
gh pr list --repo "$BASE_REPO" --head "$FORK_BRANCH" --base "$DOWNSTREAM_BRANCH" --state open \
--json url,headRefOid,headRepositoryOwner \
--jq "[.[] | select(.headRepositoryOwner.login == \"$FORK_OWNER\")][0] | select(.) | \"\(.headRefOid) \(.url)\"" 2>/dev/null || true)"
if [ -n "$EXISTING_PR" ]; then
EXISTING_PR_SHA="${EXISTING_PR%% *}"
EXISTING_PR_URL="${EXISTING_PR#* }"
if [ "$EXISTING_PR_SHA" = "$LOCAL_FORK_TIP" ]; then
log "An open PR already targets $BASE_REPO:$DOWNSTREAM_BRANCH from $HEAD_SPEC at this exact commit — nothing to do: $EXISTING_PR_URL"
exit 0
fi
log "An open PR exists ($EXISTING_PR_URL) but at an older head; pushing will update it."
fi
# --- 6. push the fork branch --------------------------------------------------
log "Pushing $LOCAL_FORK_BRANCH -> $FORK_REMOTE/$FORK_BRANCH"
git push "$FORK_REMOTE" "$LOCAL_FORK_BRANCH:$FORK_BRANCH"
# --- 7. create (or report) the PR ---------------------------------------------
PR_BODY_FILE="$(mktemp)"
{
echo "Automated merge of upstream \`$UPSTREAM_REMOTE/$UPSTREAM_BRANCH\` (and re-sync of downstream) into \`$BASE_REPO:$DOWNSTREAM_BRANCH\`."
echo
echo "### Incoming commits ($INCOMING_COUNT)"
echo
git log --no-merges --pretty=format:'- %s (%h)' "$INCOMING_RANGE"
echo
} > "$PR_BODY_FILE"
log "Opening PR $HEAD_SPEC -> $BASE_REPO:$DOWNSTREAM_BRANCH (authored by the fork account)"
# GH_TOKEN makes gh act as the fork account so the maintainer-edit grant sticks
# (gh enables it by default; we deliberately omit --no-maintainer-edit) and the
# review account stays free to approve. GH_HOST pins gh to the fork's host.
# NOTE: `gh pr create --head` takes owner:branch (cross-fork form), but
# `gh pr list --head` (the fallback below) takes the BRANCH NAME ONLY — hence we
# filter the list by head-fork owner separately, matching the early check above.
if ! GH_TOKEN="$FORK_PAT" GH_HOST="$FORK_HOST" \
gh pr create \
--repo "$BASE_REPO" \
--base "$DOWNSTREAM_BRANCH" \
--head "$HEAD_SPEC" \
--title "$PR_TITLE" \
--body-file "$PR_BODY_FILE" ; then
EXISTING="$(GH_TOKEN="$FORK_PAT" GH_HOST="$FORK_HOST" \
gh pr list --repo "$BASE_REPO" --head "$FORK_BRANCH" --base "$DOWNSTREAM_BRANCH" --state open \
--json url,headRepositoryOwner \
--jq "[.[] | select(.headRepositoryOwner.login == \"$FORK_OWNER\")][0] | select(.) | .url" 2>/dev/null || true)"
[ -n "$EXISTING" ] && log "A PR already exists for $HEAD_SPEC (updated by the push): $EXISTING" \
|| die "gh pr create failed and no existing open PR was found."
fi
log "Done."
@@ -7,7 +7,6 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -18,6 +17,7 @@ import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.Course;
@@ -27,6 +27,7 @@ import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.markpassingcalculation.Candidate;
import com.sap.sailing.domain.markpassingcalculation.CandidateChooser;
import com.sap.sailing.domain.markpassingcalculation.MarkPassingCalculator;
import com.sap.sailing.domain.markpassinghash.MarkPassingRaceFingerprintRegistry;
import com.sap.sailing.domain.shared.tracking.impl.TimedComparator;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
import com.sap.sailing.domain.tracking.GPSFixTrack;
@@ -132,6 +133,13 @@ public class CandidateChooserImpl implements CandidateChooser {
* with the dynamic (re-)calculations triggered by fixes and other data popping in.
*/
private Map<Competitor, Map<Candidate, Set<Edge>>> allEdges = new HashMap<>();
/**
* Reverse index of {@link #allEdges}: for each candidate, the set of edges whose {@link Edge#getEnd()} is that
* candidate. Kept in sync with {@link #allEdges} so that {@link #removeEdgesForCandidate} can remove all edges
* touching a candidate in O(in-degree + out-degree) instead of O(total graph size).
*/
private Map<Competitor, Map<Candidate, Set<Edge>>> incomingEdges = new HashMap<>();
/**
* The candidates found, keyed by the {@link Competitor} to whose track they belong.
@@ -399,6 +407,7 @@ public class CandidateChooserImpl implements CandidateChooser {
});
fixedPassings.put(c, fixedPasses);
allEdges.put(c, new HashMap<Candidate, Set<Edge>>());
incomingEdges.put(c, new HashMap<Candidate, Set<Edge>>());
fixedPasses.addAll(startAndEnd);
addCandidates(c, startAndEnd);
}
@@ -444,9 +453,27 @@ public class CandidateChooserImpl implements CandidateChooser {
}
}
}
final TimePoint updateStationarySequencesStartedAt = TimePoint.now();
logger.fine(()->"DIAG START updateStationarySequences for "+c);
updateStationarySequences(c, newFixes, fixesReplacingExistingOnes);
logger.fine(()->"DIAG END updateStationarySequences for "+c+": "+ updateStationarySequencesStartedAt.until(TimePoint.now()));
final TimePoint removeCandidatesStartedAt = TimePoint.now();
logger.fine(()->"DIAG START removeCandidates for "+c);
removeCandidates(c, oldCans);
logger.fine(()->"DIAG END removeCandidates for "+c+": "+
removeCandidatesStartedAt.until(TimePoint.now()));
final TimePoint addCandidatesStartedAt = TimePoint.now();
logger.fine(()->"DIAG START addCandidates for "+c);
addCandidates(c, newCans);
logger.fine(()->"DIAG END addCandidates for "+c+": "+
addCandidatesStartedAt.until(TimePoint.now()));
final TimePoint findShortestPathStartedAt = TimePoint.now();
logger.fine(()->"DIAG START findShortestPath for "+c);
findShortestPath(c);
logger.fine(()->"DIAG END findShortestPath for "+c+": "+
findShortestPathStartedAt.until(TimePoint.now()));
}
/**
@@ -554,7 +581,8 @@ public class CandidateChooserImpl implements CandidateChooser {
private void createNewEdges(Competitor c, Iterable<Candidate> newCandidates) {
assert perCompetitorLocks.get(c).isWriteLocked();
final Boolean isGateStart = race.isGateStart();
Map<Candidate, Set<Edge>> edgesForCompetitor = allEdges.get(c);
final Map<Candidate, Set<Edge>> edgesForCompetitor = allEdges.get(c);
final Map<Candidate, Set<Edge>> incomingEdgesForCompetitor = incomingEdges.get(c);
final Iterable<Candidate> competitorCandidates = getFilteredCandidates(c);
for (Candidate newCan : newCandidates) {
synchronized (competitorCandidates) {
@@ -646,7 +674,7 @@ public class CandidateChooserImpl implements CandidateChooser {
edge = new Edge(early, late,
startTimingProbability * estimatedDistanceProbability, race.getRace().getCourse().getNumberOfWaypoints());
}
addEdge(edgesForCompetitor, edge);
addEdge(edgesForCompetitor, incomingEdgesForCompetitor, edge);
}
}
}
@@ -665,7 +693,7 @@ public class CandidateChooserImpl implements CandidateChooser {
return early.getTimePoint() == null || late.getTimePoint() == null || early.getTimePoint().before(late.getTimePoint());
}
private void addEdge(Map<Candidate, Set<Edge>> edgesForCompetitor, Edge e) {
private void addEdge(Map<Candidate, Set<Edge>> edgesForCompetitor, Map<Candidate, Set<Edge>> incomingEdgesForCompetitor, Edge e) {
logger.finest(()->"Adding "+ e.toString());
Set<Edge> edgeSet = edgesForCompetitor.get(e.getStart());
if (edgeSet == null) {
@@ -673,6 +701,12 @@ public class CandidateChooserImpl implements CandidateChooser {
edgesForCompetitor.put(e.getStart(), edgeSet);
}
edgeSet.add(e); // FIXME what about edges that should replace an edge between the same two candidates? Will those edges somehow be removed?
Set<Edge> incomingEdgeSet = incomingEdgesForCompetitor.get(e.getEnd());
if (incomingEdgeSet == null) {
incomingEdgeSet = new HashSet<>();
incomingEdgesForCompetitor.put(e.getEnd(), incomingEdgeSet);
}
incomingEdgeSet.add(e);
}
/**
@@ -995,22 +1029,47 @@ public class CandidateChooserImpl implements CandidateChooser {
* path analysis is triggered yet.
*/
private void updateFilteredCandidatesAndAdjustGraph(Competitor c, Iterable<Candidate> newCandidates, Iterable<Candidate> removedCandidates) {
Pair<Iterable<Candidate>, Iterable<Candidate>> filteredCandidatesAddedAndRemoved = updateFilteredCandidates(c, newCandidates, removedCandidates);
final TimePoint filteringStartedAt = TimePoint.now();
logger.fine(()->"DIAG START updateFilteredCandidates for "+c);
Pair<Iterable<Candidate>, Iterable<Candidate>> filteredCandidatesAddedAndRemoved =
updateFilteredCandidates(c, newCandidates, removedCandidates);
logger.fine(()->"DIAG END updateFilteredCandidates for "+c+": "+
filteringStartedAt.until(TimePoint.now()));
final TimePoint adjustGraphStartedAt = TimePoint.now();
logger.fine(()->"DIAG START adjustGraph for "+c);
adjustGraph(c, filteredCandidatesAddedAndRemoved);
logger.fine(()->"DIAG END adjustGraph for "+c+": "+
adjustGraphStartedAt.until(TimePoint.now()));
}
/**
* Adjusts the {@link #allEdges graph} based on the nodes added and removed. No {@link #findShortestPath(Competitor)
* path analysis} is performed yet.
*/
private void adjustGraph(Competitor c,
private void adjustGraph(
Competitor c,
Pair<Iterable<Candidate>, Iterable<Candidate>> filteredCandidatesAddedAndRemoved) {
final Map<Candidate, Set<Edge>> competitorEdges = allEdges.get(c);
final Map<Candidate, Set<Edge>> competitorIncomingEdges = incomingEdges.get(c);
logger.fine(() -> "DIAG REMOVE WORKLOAD for " + c.getName() + ": removedCandidates="
+ Util.size(filteredCandidatesAddedAndRemoved.getB()) + ", edgeSets=" + competitorEdges.size()
+ ", edgesBeforeRemoval="
+ Long.toString(competitorEdges.values().stream().collect(Collectors.summingLong(Set::size))));
final TimePoint removeEdgesStartedAt = TimePoint.now();
logger.fine(()->"DIAG START removeEdgesForCandidates for " + c);
for (final Candidate candidateRemoved : filteredCandidatesAddedAndRemoved.getB()) {
logger.finest(()->"Removing all edges containing " + candidateRemoved + "of "+ c);
removeEdgesForCandidate(candidateRemoved, competitorEdges);
logger.finest(() -> "Removing all edges containing " + candidateRemoved + "of " + c);
removeEdgesForCandidate(candidateRemoved, competitorEdges, competitorIncomingEdges);
}
logger.fine(()->"DIAG END removeEdgesForCandidates for " + c + ": "
+ removeEdgesStartedAt.until(TimePoint.now()));
final TimePoint createNewEdgesStartedAt = TimePoint.now();
logger.fine(()->"DIAG START createNewEdges for " + c);
createNewEdges(c, filteredCandidatesAddedAndRemoved.getA());
logger.fine(()->"DIAG END createNewEdges for " + c + ": "
+ createNewEdgesStartedAt.until(TimePoint.now()));
}
/**
@@ -1079,13 +1138,22 @@ public class CandidateChooserImpl implements CandidateChooser {
}
}
private void removeEdgesForCandidate(Candidate can, Map<Candidate, Set<Edge>> edges) {
edges.remove(can);
for (Set<Edge> set : edges.values()) {
for (Iterator<Edge> i = set.iterator(); i.hasNext();) {
final Edge e = i.next();
if (e.getStart() == can || e.getEnd() == can) {
i.remove();
private void removeEdgesForCandidate(Candidate can, Map<Candidate, Set<Edge>> edges, Map<Candidate, Set<Edge>> incoming) {
final Set<Edge> outgoing = edges.remove(can);
if (outgoing != null) {
for (final Edge e : outgoing) {
final Set<Edge> inSet = incoming.get(e.getEnd());
if (inSet != null) {
inSet.remove(e);
}
}
}
final Set<Edge> incomingForCan = incoming.remove(can);
if (incomingForCan != null) {
for (final Edge e : incomingForCan) {
final Set<Edge> outSet = edges.get(e.getStart());
if (outSet != null) {
outSet.remove(e);
}
}
}
@@ -173,6 +173,13 @@ public class JumpyTrackSmootheningTest {
@Test
public void testMarkPassingCalculatorForAdjusted() throws Exception {
// final java.util.logging.FileHandler fileHandler =
// new java.util.logging.FileHandler(
// System.getProperty("user.home") + "/Desktop/jumpy-markpassing.log",
// false);
// fileHandler.setFormatter(new java.util.logging.SimpleFormatter());
// java.util.logging.Logger.getLogger("").addHandler(fileHandler);
try {
final DynamicGPSFixTrack<Competitor, GPSFixMoving> track = readTrack("GallagherZelenka.gpx.gz");
final Duration durationForAdjustedTrack;
final Duration durationForOriginalTrack;
@@ -201,6 +208,11 @@ public class JumpyTrackSmootheningTest {
assertTrue(durationForAdjustedTrack.times(2).compareTo(durationForOriginalTrack) < 0,
"Expected duration for mark passing analysis on adjusted track to be at least two times less than for original track: "+
durationForAdjustedTrack+" vs. "+durationForOriginalTrack);
} finally {
// java.util.logging.Logger.getLogger("").removeHandler(fileHandler);
// fileHandler.close();
}
}
private DynamicGPSFixTrack<Competitor, GPSFixMoving> readTrack(String filename) throws Exception {
@@ -2,7 +2,6 @@ package com.sap.sse.branding.impl;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import com.sap.sse.branding.BrandingConfigurationService;
public class Activator implements BundleActivator {
@@ -287,6 +287,7 @@
"error_title": "Ein Fehler ist aufgetreten",
"error_unknown" : "Ups, da ist was schief gegangen.\nBitte versuche es nochmal!",
"error_event_settings_not_saved" : "Das Event wurde angelegt, aber die Einstellungen konnten nicht gespeichert werden. Bitte erstelle es nicht noch einmal öffne es stattdessen aus der Liste.",
"error_with_code" : "Ups, da ist was schief gegangen.\nBitte versuche es nochmal! ({{code}})",
"error_field_required": "Feld ist erforderlich.",
"error_field_already_exists": "Existiert bereits.",
@@ -293,6 +293,7 @@
"error_title": "An error occurred",
"error_unknown" : "Oops, something went wrong here.\nPlease try again!",
"error_event_settings_not_saved" : "The event was created, but its settings could not be saved. Please do not create it again open it from the list instead.",
"error_with_code" : "Oops, something went wrong here.\nPlease try again! ({{code}})",
"error_field_required": "Field is required.",
"error_field_already_exists": "Already exists.",