Files
calendula/CONTRIBUTING.md
Jean-Luc Makiola a3d136f55d
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 27s
Release — F-Droid repo + Gitea/Codeberg release / release (push) Has been skipped
Write a contributing guide, and fix what the forge move left stale (#96)
(docs) Update all the docs to be up to date with current status quo
2026-07-30 10:40:07 +02:00

8.1 KiB

Contributing to Calendula

Calendula is a Material 3 Expressive calendar app that lives strictly on top of Android's CalendarContract — no app database, no sync stack, no network access. That constraint shapes most review comments, so docs/ARCHITECTURE.md is worth skimming before you write code. This file is the practical how.

Codeberg is the canonical home — issues, pull requests, releases. The self-hosted Gitea instance referenced in the release docs is build infrastructure only; there is nothing to contribute there. Be decent to the people you meet in the tracker.

Start with an issue

You want to Do this
Add a feature Open an issue first and wait for a go-ahead
Fix a bug Open an issue, then a pull request
Fix a typo, a comment, or docs Just open the pull request
Add or fix a translation Don'tuse Weblate

Features get an opinion before they get code: whether Calendula should do a thing at all is the one decision a patch can't make. A feature PR that arrives without a discussed issue may be closed unmerged even when the code is good — please don't spend a weekend on one first.

Bugs are more straightforward, but still start with an issue: it's what carries the milestone and gives the changelog something to link.

Issue templates cover bug, crash, feature and question. For a crash, let the app do the work — Settings → Report a problem, or the prompt shown after a crash, captures the stack trace and prefills the form. The report contains app, Android and device versions plus the trace; no calendar content, no personal data.

Which branch to target

Calendula releases by merging a version bump into main, so main is a release trigger rather than a staging area. Work is assembled on release branches first.

Once your issue has a milestone, that milestone names your branch:

Milestone Target branch
2.18.0 release/v2.18.0

Every milestone has a matching branch. If it's somehow missing, target main and mention it in the PR — it will be retargeted. Don't pick an older release branch: they're kept after shipping, so the newest one isn't necessarily yours.

Translations

Never edit a values-*/strings.xml file in a pull request — German included. Translations are owned by a self-hosted Weblate that writes to this repository directly, and a hand-edit is overwritten on the next sync.

Translate Calendula on Weblate

Adding a new English string to values/strings.xml is normal PR work; Weblate picks it up and offers it to translators. Partial translations are expected and fine — missing keys are informational. Stale and orphaned keys are not, so run

python3 scripts/check_translations.py

before pushing. It reports those more clearly than lint's MissingTranslation does.

Build & test

git clone --recurse-submodules https://codeberg.org/jlmakiola/calendula.git

The floret-kit submodule is a composite build compiled from source. An existing clone needs git submodule update --init --recursive, or nothing resolves.

  • JDK 17 — not newer; the Android Gradle Plugin requires exactly 17. Set JAVA_HOME if your default differs.
  • Android SDK — platform 37 (compileSdk) and build-tools 36.0.0, located via ANDROID_HOME or a gitignored local.properties with sdk.dir. If you go the local.properties route the included build needs its own copy at floret-kit/local.properties; ANDROID_HOME covers both at once and is the easier path.

The Gradle wrapper is checked in, so no system Gradle is needed.

./gradlew lint test assembleDebug     # roughly what CI runs

A single test class, or a pattern:

./gradlew testDebugUnitTest --tests "de.jeanlucmakiola.calendula.domain.SimpleRecurrenceTest"
./gradlew testDebugUnitTest --tests "*SimpleRecurrence*"

CI reports one CI check per pull request: lintDebug, testDebugUnitTest, assembleDebug, and a Trivy scan. Pull requests touching only docs, F-Droid metadata or the licence skip the Android build and go green quickly. More detail in docs/BUILDING.md.

The rules

These are the ones that turn into review comments.

  1. No network. Calendula holds no INTERNET permission, and that's a feature rather than an oversight. Anything that would need one is a product decision before it's a patch — the crash reporter deliberately opens a prefilled web issue instead of posting anything itself.
  2. The provider is the only database. No Room, no cache, no local mirror of events. CalendarContract is the single source of truth, which is also why externally synced changes work for free.
  3. Don't patch UI state after a write. A ContentObserver re-queries and views recompose from fresh provider state. Hand-patching a list after saving appears to work, then quietly diverges from what the provider actually stored.
  4. domain/ has no Android imports. Models, validation, recurrence rendering, conflict snapshots and the .ics codec stay pure Kotlin so they remain JVM-testable.
  5. Tests run on the JVM. JUnit 5 + Truth + Turbine. The seams exist for you: fake the data source (FakeCalendarDataSource), and feed mappers plain maps through ColumnReader instead of cursors. Instrumented tests are a last resort, not a default.
  6. Read before touching the subtle pipelines. Recurring writes (UNTIL vs DURATION, exception URIs, series splits), save-conflict detection and reminder delivery (post-before-mark) follow provider-driven rules that are documented in docs/ARCHITECTURE.md and are not guessable from the code alone.
  7. Don't break reproducible builds. vcsInfo, dependenciesInfo and the AGP metadata block are disabled on purpose so the official F-Droid repo can verify our binary against a from-source rebuild. scripts/check_reproducible_release.sh runs on every pull request, including docs-only ones.

UI conventions

Material 3 Expressive throughout, built from the system's own tokens and components — colour-scheme tokens rather than hardcoded colours, ListItem for settings rows.

Selection pickers are full-screen. Every browse-style "choose one" surface uses floret-kit's FullScreenPicker / OptionPicker; one that needs a commit or extra action passes it through the picker's actions slot. The exception is the recurring-scope chooser (this / this and following / all), which stays a compact dialog — a two- or three-option decision reads better as a popup than as a nearly empty screen. AlertDialog is for plain confirmations only, and radio- or text-list dialogs aren't used at all.

Shared UI machinery lives in the floret-kit submodule and has its own contributing guide; changing it means a pull request against that repository plus a submodule bump here.

Commits & pull requests

Conventional commits, scoped to the area you touched:

fix(calendars): keep an event's own calendar when it is switched off
feat(month): pull-to-expand the split view (#38)
docs(architecture): record what the second review pass changed

Types in use: feat fix docs refactor style chore ci build revert. Reference the issue in the subject or the body. Keep commits small — small commits revert cleanly, which matters more here than a tidy history.

If your change is user-visible, add an entry under ## [Unreleased] in CHANGELOG.md. Match the surrounding voice: entries describe what changed for the person using the app, and why, not what changed in the code. Link the issue and add its reference at the bottom of the file. It may get reworded when the release is cut, so don't agonise over it.

Please don't commit planning or design documents. Code, tests, architecture notes and the changelog land; the reasoning belongs in the commit message and the issue.

Licence

Calendula is MIT. By contributing you agree your changes ship under the same licence.