Skip to content

Repository files navigation

Diffyt - a YouTrack Diffing App

Diffyt icon

A YouTrack app that diffs issues and knowledge base articles: the versions of one item over time, or two items against each other.

It adds two items to the issue options menu ("…" in the issue toolbar) and two to the article options menu:

  • Compare versions - - lists every summary, description, and custom field change from the issue's activity stream and shows a line-by-line diff, inline or side by side, with optional word-level highlighting.
  • Compare with another issue - - diffs the current issue against another one picked through a search field. The search matches issue IDs and summary text. Two modes: Content (summary, description, and multi-line text fields, opened first) and Fields (all other custom fields as one YAML-style document). Only the latest state of both issues is compared.
  • Compare versions and Compare with another article in the article menu do the same for articles, diffing the title and content only (articles have no custom fields, so there is no Fields mode). Article history comes from the ArticleSummaryCategory and ArticleDescriptionCategory activities.

The diffing uses react-diff-viewer-continued.

Compare versions

  • Every version is the complete issue state after one save; v1 is the state at creation.
  • Choose a view: All (opened first) lists every change in time order; Content (summary, description, and multi-line text fields) and Fields (all other custom fields as one YAML-style document) list only the versions that changed that part, plus v1.
  • Select one version to see what changed in it, or tick two to diff them directly. In the All view a Content text is never diffed against a Fields document: once a Content row is ticked, the checkboxes of Fields rows are disabled, and vice versa. v1 pairs with either kind.
  • Collapse the version list to give the diff the full width.
  • Dates follow the date format and time zone from your YouTrack profile (Profile → General), read once per widget from GET /api/users/me. If that request fails, YouTrack's default format (d MMM yyyy HH:mm) is used.

All widgets are frontend only: they call the YouTrack REST API (GET /api/issues/{id}/activities for the DescriptionCategory, SummaryCategory, and CustomFieldCategory categories, plus GET /api/users/me for the date format) through the Host API. There is no app backend, no workflows, and no settings.

Quick Start

  1. Install dependencies:
npm install
  1. Create .env in the project root (see .env.example):
YOUTRACK_HOST=https://your-youtrack.url
YOUTRACK_TOKEN=perm:your-permanent-token

Get a permanent token: YouTrack profile → Account Security → New token. See token management.

  1. Build and upload:
npm run update
  1. In YouTrack, open Administration → Apps → Diffyt, attach the app to a project, then open an issue in that project and pick Compare versions from the "…" menu.

Project Structure

manifest.json                     # App manifest: one ISSUE_OPTIONS_MENU_ITEM widget
public/icon.svg                   # App icon
src/
├── common/
│   ├── utils/logger.ts           # Frontend logger
│   └── compare/                  # Everything the four widgets share
│       ├── entity.ts             # Issue/article adapter: REST base, categories, labels
│       ├── api.ts                # REST types + fetches (activities, snapshot, search)
│       ├── versions.ts           # Timeline of full entity states (v1 + one per save)
│       ├── rows.ts               # List rows per view (All | Content | Fields), kind compatibility
│       ├── selection.ts          # Selection rules, diff derivation, version titles
│       ├── date-format.ts        # Profile date format/time zone + Java-pattern formatter
│       ├── field-values.ts       # Custom field value presentation and multi-value set arithmetic
│       ├── issue-state.ts        # Field catalogue and Content/Fields text rendering
│       ├── search-queries.ts     # Search plan for the "compare to other" field
│       ├── versions-app.tsx/.css # "Compare versions" UI (version list + diff)
│       ├── compare-app.tsx/.css  # "Compare with another …" UI (search + diff)
│       ├── version-list.tsx, entity-search.tsx, diff-pane.tsx/.css, use-dark-theme.ts, types.ts, base.css
└── widgets/
    ├── issue-versions/          # Issue: VersionsApp with the ISSUE adapter
    ├── issue-compare/            # Issue: CompareApp with the ISSUE adapter
    ├── article-versions/         # Article: VersionsApp with the ARTICLE adapter
    └── article-compare/          # Article: CompareApp with the ARTICLE adapter
        └── index.html / index.tsx / app.tsx / widget-icon.svg

Scripts

Script What it does
npm run build Clean, lint, typecheck, build dist/, validate the manifest
npm run build:nolint Same without lint/typecheck
npm run lint / npm run lint:fix ESLint
npm run typecheck tsc --noEmit for the widget code
npm run upload-local Upload dist/ using .env credentials
npm run update build + upload-local
npm run watch Rebuild on change and upload after every build (refresh the YouTrack page)
npm run dev Upload a dev bundle that loads from localhost:9000, then start Vite with HMR
npm run pack Create diffyt.zip for manual upload

Compare with another issue / article

The search field runs a YouTrack search query. Text that looks like an issue ID (ABC-12 or a bare number) is searched with issue id:; other text is searched in summaries with summary:, falling back to a free-text search when that finds nothing. The current issue is excluded from the results. After a pick, both issues' current state is loaded and diffed, current issue on the left. The Fields document covers the union of both issues' custom fields, in the current issue's project order first.

How the versions diff is derived

All Summary, Description, and custom field activity items are sorted by time and traversed to build a timeline of complete issue states. Items with the same timestamp were saved together and form one version, labelled with everything it changed (e.g. "Summary, Priority"). v1 is the state at creation, dated with the issue's creation time and reporter.

Each version holds two parts. The Content and Fields tabs pick which part is diffed and list only the versions that changed that part (plus v1); version numbers are global, so a tab may show v1, v3, v7. The All tab lists every change in one timeline with a small Content / Fields tag per row; a save that changed both parts (e.g. "Summary, Priority") is split into a Content row ("Summary") and a Fields row ("Priority") with the same version number. Because each version is a full state, any two picks of the same kind diff correctly; the checkboxes of the other kind are disabled while one kind is selected. v1 (Initial) is a full state of both kinds, so it pairs with any row; on its own it shows the Content state at creation.

The Content part is a Summary: heading, the summary, a blank line, a Description: heading, and the description, followed by one section per multi-line Text custom field (field type text), each headed by the field's name. The heading lines are rendered as section titles in the diff:

Summary:
Start button stays grey after restart

Description:
Intro paragraph about the feature.
…

Steps to reproduce:
1. Restart the device.
…
  • Summary and Description activity items hold the text after the change in added; the text at creation is the oldest item's removed, or the current text when the field never changed.
  • Custom field items carry the values added and removed. For multi-value fields these are only the changed values, so states are reconstructed by starting from the issue's current field values and walking backwards (before = after − added + removed). Without current values, states are built forwards from the oldest item's removed.
  • The Fields part lists every custom field of the issue in project order, including unchanged ones (they fold away with "Only changes"), plus fields that appear in history but no longer exist. Text fields are left out here because they are sections of the Content part. The document is compared with the diff viewer's YAML method, which is line-based, so word-level highlighting is not available in that mode:
Priority: Critical
Subsystems:
- UI
- API
Due Date: 1 Oct 2026
Type: Bug
  • One selected version: diff against the previous version listed in the tab. v1 (labelled Initial) has no predecessor, so it is shown in the same view with nothing highlighted.
  • Two selected versions: diff between them, oldest on the left. The first pick is the baseline; a third pick replaces the second.

Requests, all in parallel: the Summary/Description list with added only (pages of 42), the custom field list with added and removed, the oldest Summary and Description items with removed, and the issue's created, reporter, summary, description, and current customFields.

About

Youtrack app for comparing tickets and versions

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages