Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@

## [Unreleased]

## [1.17.0] - 2026-04-07

### Added

- File-level VCS change tracking — additions and removals are now recorded per file (with extension) instead of only as project-level aggregates, enabling richer activity reports in Hub

### Fixed

- Binary files are now skipped during commit diff calculations to avoid corrupted line counts

## [1.16.0] - 2026-03-31

### Changed
Expand Down Expand Up @@ -304,7 +314,8 @@

- Support IntelliJ Platform 2024.3.5

[Unreleased]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.16.0...HEAD
[Unreleased]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.17.0...HEAD
[1.17.0]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.16.0...v1.17.0
[1.16.0]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.15.3...v1.16.0
[1.15.3]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.15.2...v1.15.3
[1.15.2]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.15.1...v1.15.2
Expand Down
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,34 +27,38 @@ Managers approve with one click, finance exports to CSV or PDF.

## Auto-Generated Timesheets & Team Activity Tracking for JetBrains IDEs

CodeClocker is a JetBrains IDE plugin that automatically tracks coding time and generates weekly timesheets from real IDE activity. Every line item is backed by commits and branches — no guesswork, no manual entry. Use it locally for personal productivity, or connect to [CodeClocker Hub](https://hub.codeclocker.com/) to unlock team timesheets, approval workflows, and billing exports.
CodeClocker automatically tracks coding time and generates weekly timesheets from real IDE activity.
It gives developers a pre-filled draft they can review and adjust instead of reconstructing the whole week from scratch.
Use it locally for personal productivity, or connect to [CodeClocker Hub](https://hub.codeclocker.com/) to unlock team timesheets, approval workflows, team awareness updates, and billing exports.

### For Teams & Managers

- **Auto-generated weekly timesheets** — pre-filled from actual IDE activity with per-project and per-branch breakdowns.
- **Evidence-linked worklogs** — connect timesheet entries to branches and commits for easier review and client reporting.
- **One-click approvals** — managers review and approve team timesheets from a single dashboard.
- **CSV & PDF exports** — export approved timesheets for payroll, accounting, or client billing.
- **Automated email reminders** — nudge developers to submit and managers to approve on time.
- **Automated reminders** — nudge developers to submit and managers to approve on time.
- **Team activity dashboard** — see weekly hours, submission status, and project activity across the team.
- **Team awareness updates** — daily pulse emails and Slack summaries help teammates stay aware of what the team is working on, what moved forward, and what may need coordination.
- **Slack integrations** — post team activity summaries to Slack channels on a schedule.
- **Role-based access** — owners, managers, and members with appropriate permissions.
- **Anonymous mode** — teams can enable anonymous activity tracking so managers see aggregated team stats without individual breakdowns. Non-anonymous mode shows per-member contributions for teams that prefer full transparency. **To prevent spying**, once a team is created with anonymous mode enabled, **the setting cannot be changed**.
- **Anonymous mode** — teams can enable anonymous activity tracking so managers see aggregated team stats without individual breakdowns. Non-anonymous mode shows per-member contributions for teams that prefer full transparency. **To prevent spying, once a team is created with anonymous mode enabled, the setting cannot be changed.**

Built for software teams that need weekly timesheets, manager approvals, and invoice-ready exports without asking developers to run timers.
Built for software teams that need weekly timesheets, manager approvals, team awareness, and invoice-ready exports without asking developers to run timers.
Start a free team trial at [hub.codeclocker.com](https://hub.codeclocker.com/).

### For Developers

- **Automatic time tracking** — records active coding time per project silently in the background.
- **Daily & weekly goals** — set targets globally or per project, get notified when you hit them.
- **Automatic time tracking** — records active coding time per project automatically in the background.
- **Daily & weekly goals** — set targets globally or per project, and get notified when you hit them.
- **Pomodoro Timer** — built-in timer with configurable work/break intervals and status bar countdown.
- **What I Was Doing** — generate standup-ready summaries with time breakdowns, branch details, and commits.
- **Activity Report** — tree-view of daily activity with project breakdown, commit history, and CSV export.
- **VCS / Git insights** — tracks added & removed lines from version control activity.
- **Auto-pause** — pauses tracking when IDE loses focus or on inactivity timeout.
- **Teammate awareness** — daily pulse updates help you stay aware of what your teammates are working on and what moved forward across the team.
- **VCS / Git insights** — tracks added and removed lines from version control activity.
- **Auto-pause** — pauses tracking when the IDE loses focus or on inactivity timeout.
- **Privacy first** — all data stays on your machine in Local Mode. Hub sync is opt-in.
- **Team privacy controls** — your team can run in anonymous mode where managers only see team-level totals, not individual activity. You stay in control of what's visible.
- **Team privacy controls** — your team can run in anonymous mode where managers only see team-level totals, not individual activity.

### Supported IDEs

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pluginGroup = com.codeclocker
pluginName = CodeClocker
pluginRepositoryUrl = https://github.com/codeclocker/codeclocker-intellij-plugin
# SemVer format -> https://semver.org
pluginVersion = 1.16.0
pluginVersion = 1.17.0

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 242
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,15 @@ public void checkinSuccessful() {
continue;
}

String beforeContent = beforeRevision == null ? null : beforeRevision.getContent();
String afterContent = afterRevision == null ? null : afterRevision.getContent();

if (isBinaryContent(beforeContent) || isBinaryContent(afterContent)) {
continue;
}

LineDifferenceResult diff =
LineDifferenceCalculator.calculateLineDifferences(
beforeRevision == null ? null : beforeRevision.getContent(),
afterRevision == null ? null : afterRevision.getContent());
LineDifferenceCalculator.calculateLineDifferences(beforeContent, afterContent);

String extension = getExtension(relativePath);

Expand Down Expand Up @@ -153,6 +158,10 @@ private String getGitAuthor(Project project, GitRepository repo) {
return null;
}

private static boolean isBinaryContent(String content) {
return content != null && content.indexOf('\0') >= 0;
}

private String getExtension(String relativePath) {
int lastDotIndex = relativePath.lastIndexOf('.');

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.codeclocker.plugin.intellij.local;

/** Record of per-file VCS changes (additions/removals) within an hour for a project. */
public class FileChangeRecord {

private String fileName;
private long additions;
private long removals;
private String extension;

public FileChangeRecord() {
// Required for XML serialization
}

public FileChangeRecord(String fileName, long additions, long removals, String extension) {
this.fileName = fileName;
this.additions = additions;
this.removals = removals;
this.extension = extension;
}

public String getFileName() {
return fileName;
}

public void setFileName(String fileName) {
this.fileName = fileName;
}

public long getAdditions() {
return additions;
}

public void setAdditions(long additions) {
this.additions = additions;
}

public long getRemovals() {
return removals;
}

public void setRemovals(long removals) {
this.removals = removals;
}

public String getExtension() {
return extension;
}

public void setExtension(String extension) {
this.extension = extension;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,32 @@ public void mergeProject(
}
merged.setCommits(mergedCommits);

// Merge file changes (sum additions/removals per file)
Map<String, FileChangeRecord> fileMap = new HashMap<>();
for (FileChangeRecord fc : existing.getFileChanges()) {
fileMap.merge(
fc.getFileName(),
fc,
(a, b) ->
new FileChangeRecord(
a.getFileName(),
a.getAdditions() + b.getAdditions(),
a.getRemovals() + b.getRemovals(),
a.getExtension()));
}
for (FileChangeRecord fc : incoming.getFileChanges()) {
fileMap.merge(
fc.getFileName(),
fc,
(a, b) ->
new FileChangeRecord(
a.getFileName(),
a.getAdditions() + b.getAdditions(),
a.getRemovals() + b.getRemovals(),
a.getExtension()));
}
merged.setFileChanges(new ArrayList<>(fileMap.values()));

return merged;
});
return projects;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class ProjectActivitySnapshot {
private boolean reported;
private List<BranchActivityRecord> branchActivity = new ArrayList<>();
private List<CommitRecord> commits = new ArrayList<>();
private List<FileChangeRecord> fileChanges = new ArrayList<>();

public ProjectActivitySnapshot() {
// Required for XML serialization
Expand Down Expand Up @@ -78,6 +79,14 @@ public void setCommits(List<CommitRecord> commits) {
this.commits = commits != null ? commits : new ArrayList<>();
}

public List<FileChangeRecord> getFileChanges() {
return fileChanges;
}

public void setFileChanges(List<FileChangeRecord> fileChanges) {
this.fileChanges = fileChanges != null ? fileChanges : new ArrayList<>();
}

public String getRecordId() {
return recordId;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.codeclocker.plugin.intellij.config.ConfigProvider;
import com.codeclocker.plugin.intellij.local.BranchActivityRecord;
import com.codeclocker.plugin.intellij.local.CommitRecord;
import com.codeclocker.plugin.intellij.local.FileChangeRecord;
import com.codeclocker.plugin.intellij.local.LocalStateRepository;
import com.codeclocker.plugin.intellij.local.ProjectActivitySnapshot;
import com.codeclocker.plugin.intellij.reporting.TimeSpentSampleDto.BranchActivityDto;
Expand Down Expand Up @@ -130,19 +131,28 @@ public void saveToLocalStorage(

Map<String, Long> projectAdditions = new HashMap<>();
Map<String, Long> projectRemovals = new HashMap<>();
Map<String, List<FileChangeRecord>> projectFileChanges = new HashMap<>();

for (Entry<String, Map<String, ChangesSample>> projectEntry : changesSamples.entrySet()) {
String projectName = projectEntry.getKey();
long totalAdditions = 0;
long totalRemovals = 0;
List<FileChangeRecord> fileRecords = new ArrayList<>();

for (ChangesSample fileSample : projectEntry.getValue().values()) {
totalAdditions += fileSample.additions().get();
totalRemovals += fileSample.removals().get();
for (Entry<String, ChangesSample> fileEntry : projectEntry.getValue().entrySet()) {
ChangesSample sample = fileEntry.getValue();
long add = sample.additions().get();
long rem = sample.removals().get();
totalAdditions += add;
totalRemovals += rem;

String ext = sample.metadata().getOrDefault("extension", "");
fileRecords.add(new FileChangeRecord(fileEntry.getKey(), add, rem, ext));
}

projectAdditions.put(projectName, totalAdditions);
projectRemovals.put(projectName, totalRemovals);
projectFileChanges.put(projectName, fileRecords);
}

String currentHourKey =
Expand All @@ -156,6 +166,7 @@ public void saveToLocalStorage(

ProjectActivitySnapshot snapshot =
new ProjectActivitySnapshot(deltaSeconds, additions, removals, false);
snapshot.setFileChanges(projectFileChanges.getOrDefault(projectName, List.of()));

BranchActivityTracker branchTracker = getBranchActivityTracker();
if (branchTracker != null) {
Expand Down Expand Up @@ -184,6 +195,7 @@ public void saveToLocalStorage(

ProjectActivitySnapshot snapshot =
new ProjectActivitySnapshot(0, additions, removals, false);
snapshot.setFileChanges(projectFileChanges.getOrDefault(projectName, List.of()));
getLocalStateRepository().mergeProjectCurrentHour(projectName, snapshot);
}
}
Expand Down Expand Up @@ -259,15 +271,30 @@ public void syncLocalDataToServer(String apiKey) {
commitDtos));
}

if (snapshot.getAdditions() > 0 || snapshot.getRemovals() > 0) {
if (snapshot.getFileChanges() != null && !snapshot.getFileChanges().isEmpty()) {
for (FileChangeRecord fc : snapshot.getFileChanges()) {
if (fc.getAdditions() > 0 || fc.getRemovals() > 0) {
Map<String, String> meta =
(fc.getExtension() != null && !fc.getExtension().isEmpty())
? Map.of("extension", fc.getExtension())
: Collections.emptyMap();
ChangesSampleDto changesDto =
new ChangesSampleDto(
samplingStartedAt, fc.getAdditions(), fc.getRemovals(), meta);
changesByProject
.computeIfAbsent(projectName, k -> new HashMap<>())
.put(fc.getFileName(), changesDto);
}
}
} else if (snapshot.getAdditions() > 0 || snapshot.getRemovals() > 0) {
// Backward compat: old data without file-level detail
String syntheticFileName = "local-sync-" + hourKey;
ChangesSampleDto changesDto =
new ChangesSampleDto(
samplingStartedAt,
snapshot.getAdditions(),
snapshot.getRemovals(),
Collections.emptyMap());

changesByProject
.computeIfAbsent(projectName, k -> new HashMap<>())
.put(syntheticFileName, changesDto);
Expand Down
Loading