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't — use 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_HOMEif your default differs. - Android SDK — platform 37 (
compileSdk) and build-tools 36.0.0, located viaANDROID_HOMEor a gitignoredlocal.propertieswithsdk.dir. If you go thelocal.propertiesroute the included build needs its own copy atfloret-kit/local.properties;ANDROID_HOMEcovers 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.
- No network. Calendula holds no
INTERNETpermission, 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. - The provider is the only database. No Room, no cache, no local mirror of
events.
CalendarContractis the single source of truth, which is also why externally synced changes work for free. - Don't patch UI state after a write. A
ContentObserverre-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. domain/has no Android imports. Models, validation, recurrence rendering, conflict snapshots and the.icscodec stay pure Kotlin so they remain JVM-testable.- Tests run on the JVM. JUnit 5 + Truth + Turbine. The seams exist for you:
fake the data source (
FakeCalendarDataSource), and feed mappers plain maps throughColumnReaderinstead of cursors. Instrumented tests are a last resort, not a default. - 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.mdand are not guessable from the code alone. - Don't break reproducible builds.
vcsInfo,dependenciesInfoand 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.shruns 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.