gitea.watch.changes
changes
The snapshot a watch run takes of an issue, and the comparison of two of them.
An issue payload is far larger than the handful of things worth waking someone up for, and most of it changes for reasons nobody wants a line of output about. A snapshot keeps the four that a watch reports on - who it is assigned to, what it is labelled, which comments it carries, and whether it is there at all - plus the fields needed to name the issue in the report.
Comments are compared by hash rather than by count, so that a comment added and
another deleted between two runs is two changes and not none, and so that the
comparison needs nothing from the previous run except the hashes it recorded.
comment_hash is stable across re-fetches: the same comment always hashes to the
same value, and an edited one does not, which is what makes an edit show up as a
change rather than disappear.
A comment is reported whether or not the issue carrying it was already known. An issue seen for the first time is compared against an empty snapshot rather than passed over, so a comment that was already on it is an addition and not a baseline - what it costs to pass over is a comment nobody is ever told about, since the run that passed over it also recorded it.
updated_at is recorded but is not itself compared. Gitea bumps it for every
edit, including ones a watch has nothing to say about, so comparing it would
report a body reworded as indistinguishable from a comment added. The
consequence, stated plainly: an issue whose title or body alone was edited is
not reported as changed.
Functions:
gitea.watch.changes.usable_identifier
Read a value that has to be a whole number to identify anything.
Every identifier a watch reads - an issue's global ID, its number, a column's ID - is read through here, so that a payload with a nonsense one in it is refused the same way whichever field it was in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Any
|
The value the payload carries for the identifier. |
required |
Returns:
| Type | Description |
|---|---|
int | None
|
The identifier, or None when the value is not one. |
int | None
|
it is an |
int | None
|
whose ID came back as a boolean under the same entry. |
Source code in src/gitea/watch/changes.py
gitea.watch.changes.comment_hash
Hash a comment's identity, stably across re-fetches.
The digest is taken over the comment's ID, author, body and timestamps, serialized as JSON so that no value can be confused with the boundary between two of them - a body ending in the separator would otherwise hash as a different comment's body beginning with it.
Including the timestamps means an edited comment hashes differently from the comment it replaced, so an edit is reported as a change rather than passing for the comment already recorded.
The author is taken by ID and never by login, because a login is renameable and the digest has to survive a rename: hashing the login would turn every comment a renamed user ever wrote into a removal and an addition, on every issue being watched, although nothing about any of them changed. A comment's author cannot change, so the ID is only there to tell two comments apart when the payload carries no comment ID of its own - and a payload carrying no user ID contributes no author at all rather than falling back to the login, which would put the rename back.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
comment
|
dict[str, Any]
|
The comment data returned by the API. A payload missing any of these fields hashes as if it carried them empty, rather than raising. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The first |
Source code in src/gitea/watch/changes.py
gitea.watch.changes.issue_key
Build the key an issue is recorded under.
The global ID is used rather than the number shown in the web UI, because a project scope holds issues from several repositories and their numbers collide.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
issue
|
dict[str, Any]
|
The issue data returned by the API. |
required |
Returns:
| Type | Description |
|---|---|
str | None
|
The key, or None when the payload carries no usable global ID. |
Source code in src/gitea/watch/changes.py
gitea.watch.changes.issue_snapshot
issue_snapshot(
issue: dict[str, Any],
comments: list[dict[str, Any]],
repository: str | None = None,
) -> dict[str, Any]
Reduce an issue and its comments to what a watch compares and reports.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
issue
|
dict[str, Any]
|
The issue data returned by the API. |
required |
comments
|
list[dict[str, Any]]
|
The issue's comments, as returned by the API. Pass an empty list for an issue whose comments were not fetched; its comment hashes are then empty and no comment change is ever reported for it. |
required |
repository
|
str | None
|
Full name of the repository holding the issue, used to name it in the report, or None when it could not be determined. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The snapshot of the issue. |
Source code in src/gitea/watch/changes.py
gitea.watch.changes.detect_changes
detect_changes(
current: dict[str, dict[str, Any]],
previous: dict[str, dict[str, Any]] | None,
) -> list[dict[str, Any]]
Compare the snapshots of one scope against the ones recorded for it.
An issue that changed in more than one way contributes one record per way, as a digest reads better naming each change than one line naming three.
An issue the recorded scope has not seen is one of those: it contributes the
new record naming it, and then the same records everything else
contributes, taken against the empty snapshot it is being compared to - so
the assignees, labels and comments it already carries are reported as added.
Reporting only new would make them baseline, which loses a comment written
between an issue being opened and the run that first saw it, and loses it
permanently: the run that swallowed it records it and the next one has
nothing left to compare against. It matters most to a consumer that reacts
to a kind rather than to an issue - one acting on comments and not on
new would never learn the comment was there.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current
|
dict[str, dict[str, Any]]
|
The snapshot of each issue in the scope now, keyed by
|
required |
previous
|
dict[str, dict[str, Any]] | None
|
The snapshots the last run recorded for the scope, or None when the scope has never been recorded. None baselines the scope: no change is reported, whatever is in it. |
required |
Returns:
| Type | Description |
|---|---|
list[dict[str, Any]]
|
The changes since the recorded snapshots, ordered by issue and then by |
list[dict[str, Any]]
|
what changed. Empty when nothing changed, which is the whole point of |
list[dict[str, Any]]
|
the command. |
Source code in src/gitea/watch/changes.py
gitea.watch.changes.format_change
Render one change as the line the human digest prints for it.
The issue is named the way it is written in a browser's address bar and in
prose - owner/repo#15 - so a line can be read, pasted and grepped without
consulting the scope it came from.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
change
|
dict[str, Any]
|
The change record. |
required |
Returns:
| Type | Description |
|---|---|
str
|
One line naming the issue, what changed and what it changed to. |