mirror of
https://github.com/shumadrid/obsidian-git-changelog.git
synced 2026-07-22 12:20:31 +00:00
Compare commits
94 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28fbb87a58 | ||
|
|
f8c4ba9cce | ||
|
|
da39f3c03f | ||
|
|
c0b5af4c33 | ||
|
|
17dd8af17a | ||
|
|
758e0894f1 | ||
|
|
bf0a1c423f | ||
|
|
ce31de08d9 | ||
|
|
b0ee224df1 | ||
|
|
003839ecd5 | ||
|
|
18b9e3fde0 | ||
|
|
f73534721b | ||
|
|
1e51e7133b | ||
|
|
bff0523bdd | ||
|
|
4c2b879790 | ||
|
|
65b4d25ec7 | ||
|
|
d462290099 | ||
|
|
6d856a18c9 | ||
|
|
e47e967cd3 | ||
|
|
c17b5fa397 | ||
|
|
002412e7a1 | ||
|
|
de8ec18fb7 | ||
|
|
cefed096f2 | ||
|
|
4d9f831662 | ||
|
|
541bfc217e | ||
|
|
c10368ab75 | ||
|
|
717dc0db22 | ||
|
|
e661c75182 | ||
|
|
54d7b1db4d | ||
|
|
b1f407a7b4 | ||
|
|
b37d52b7ea | ||
|
|
8f39e82434 | ||
|
|
e58a981253 | ||
|
|
4f55251839 | ||
|
|
f267afbea3 | ||
|
|
53a7b0335f | ||
|
|
a8ef4e2097 | ||
|
|
f73934999e | ||
|
|
7d4d0ecdb3 | ||
|
|
d907fc8984 | ||
|
|
29f984736b | ||
|
|
b37f46e571 | ||
|
|
2628a4ffca | ||
|
|
790fc02936 | ||
|
|
7302642bcb | ||
|
|
559f0d3645 | ||
|
|
fd92aea22a | ||
|
|
b7ea0b94be | ||
|
|
fc878be95c | ||
|
|
2dc5eff0bc | ||
|
|
0520fde1da | ||
|
|
36db45148b | ||
|
|
87b64f9645 | ||
|
|
6501eb49fd | ||
|
|
1a3c9b4fd3 | ||
|
|
9f6f05073f | ||
|
|
bd74646c5c | ||
|
|
a391872096 | ||
|
|
2b4a8950b9 | ||
|
|
6127851cdb | ||
|
|
2f6f8d1027 | ||
|
|
436de553fe | ||
|
|
342fe8305d | ||
|
|
110803edb7 | ||
|
|
2818559485 | ||
|
|
11fcb2317c | ||
|
|
6d6d1e524c | ||
|
|
7289a9b353 | ||
|
|
804e172fdf | ||
|
|
37cea59fb8 | ||
|
|
393e4c5a4a | ||
|
|
5b409fa426 | ||
|
|
874d80fbd1 | ||
|
|
de5a0da2fb | ||
|
|
37d6e1a442 | ||
|
|
a4fcae79d9 | ||
|
|
16f59e0fc3 | ||
|
|
b2e9e3d7c7 | ||
|
|
a0a7e921f3 | ||
|
|
4ab65e7d76 | ||
|
|
a729bbf5fd | ||
|
|
b527020f4e | ||
|
|
bcf4aa7d6d | ||
|
|
aacbfc1ab0 | ||
|
|
c872ed88c8 | ||
|
|
1b2ff8f794 | ||
|
|
378ca4a429 | ||
|
|
bec4d31f45 | ||
|
|
2a38b5e9e2 | ||
|
|
cdc2c7f8e2 | ||
|
|
376e3635c3 | ||
|
|
a90ab8e6c0 | ||
|
|
9184626023 | ||
|
|
bc921d370e |
96 changed files with 7128 additions and 3011 deletions
1
.env.example
Normal file
1
.env.example
Normal file
|
|
@ -0,0 +1 @@
|
|||
OBSIDIAN_CONFIG_FOLDER=
|
||||
58
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
58
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -1,6 +1,6 @@
|
|||
name: Bug report
|
||||
description: Report a bug and help improve the plugin
|
||||
title: '[BUG] Short description of the bug'
|
||||
title: '[BUG] '
|
||||
labels: bug
|
||||
assignees: shumadrid
|
||||
body:
|
||||
|
|
@ -11,60 +11,52 @@ body:
|
|||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of the bug. Include any relevant details.
|
||||
description: A clear and concise description of the bug. Include any relevant details and steps to reproduce.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Provide a step-by-step description.
|
||||
value: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Notice that '...'
|
||||
...
|
||||
label: Operating system
|
||||
description: Which OS are you using?
|
||||
options:
|
||||
- Windows
|
||||
- Linux
|
||||
- macOS
|
||||
- Android
|
||||
- iOS
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
- type: dropdown
|
||||
id: installation-method
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: What did you expect to happen?
|
||||
label: Installation Method (Linux)
|
||||
description: Only necessary on Linux
|
||||
options:
|
||||
- Flatpak
|
||||
- AppImage
|
||||
- Snap
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: What actually happened? Include error messages if available.
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment Information
|
||||
description: Environment Information
|
||||
value: |
|
||||
- **Plugin Version**: [e.g., 1.0.0]
|
||||
- **Obsidian Version**: [e.g., v1.3.2]
|
||||
- **Operating System**: [e.g., Windows 10]
|
||||
- **Plugin Version**:
|
||||
- **Obsidian Version**:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Attachments
|
||||
description: Required for bug reproduction
|
||||
value: |
|
||||
- Please attach a video showing the bug. It is not mandatory, but might be very helpful to speed up the bug fix
|
||||
- Please attach a sample vault where the bug can be reproduced. It is not mandatory, but might be very helpful to speed up the bug fix
|
||||
validations:
|
||||
required: true
|
||||
description: Attach a video or the vault where the bug occurs. Can be helpful to speed up the bug fix.
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Confirmations
|
||||
description: Ensure the following conditions are met
|
||||
options:
|
||||
- label: I attached a video showing the bug, or it is not necessary
|
||||
required: true
|
||||
- label: I attached a sample vault where the bug can be reproduced, or it is not necessary
|
||||
required: true
|
||||
- label: I have tested the bug with the latest version of the plugin
|
||||
required: true
|
||||
- label: I have checked GitHub for existing bugs
|
||||
|
|
|
|||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
1
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1 +0,0 @@
|
|||
blank_issues_enabled: false
|
||||
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
|
@ -1,6 +1,6 @@
|
|||
name: Feature request
|
||||
description: Request a feature and help improve the plugin
|
||||
title: '[FR] Short description of the feature'
|
||||
title: '[FR] '
|
||||
labels: enhancement
|
||||
assignees: shumadrid
|
||||
body:
|
||||
|
|
@ -14,47 +14,11 @@ body:
|
|||
description: A clear and concise description of the feature request. Include any relevant details.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Details
|
||||
description: Provide a step-by-step description.
|
||||
value: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Notice that '...'
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Desired Behavior
|
||||
description: What do you want to happen?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: What actually happens?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Attachments
|
||||
description: Required for feature investigation
|
||||
value: |
|
||||
- Please attach a video showing the current behavior. It is not mandatory, but might be very helpful to speed up the feature implementation
|
||||
- Please attach a sample vault where the desired Feature Request could be applied. It is not mandatory, but might be very helpful to speed up the feature implementation
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Confirmations
|
||||
description: Ensure the following conditions are met
|
||||
options:
|
||||
- label: I attached a video showing the current behavior, or it is not necessary
|
||||
required: true
|
||||
- label: I attached a sample vault where the desired Feature Request could be applied, or it is not necessary
|
||||
required: true
|
||||
- label: I have tested the absence of the requested feature with the latest version of the plugin
|
||||
required: true
|
||||
- label: I have checked GitHub for existing Feature Requests
|
||||
|
|
|
|||
11
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
name: Question
|
||||
description: Ask a question about the plugin
|
||||
title: '[Q] '
|
||||
labels: question
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Question
|
||||
description: Write your question here.
|
||||
validations:
|
||||
required: true
|
||||
BIN
.github/quick-add-macro.png
vendored
Normal file
BIN
.github/quick-add-macro.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
BIN
.github/showcase.gif
vendored
Normal file
BIN
.github/showcase.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 MiB |
BIN
.github/vault-git-history-comparison-modal.png
vendored
Normal file
BIN
.github/vault-git-history-comparison-modal.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
37
.github/workflows/dependency-review.yml
vendored
Normal file
37
.github/workflows/dependency-review.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Dependency Review Action
|
||||
#
|
||||
# This Action will scan dependency manifest files that change as part of a Pull Request,
|
||||
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
|
||||
# Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable
|
||||
# packages will be blocked from merging.
|
||||
#
|
||||
# Source repository: https://github.com/actions/dependency-review-action
|
||||
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
|
||||
name: 'Dependency review'
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
# If using a dependency submission action in this workflow this permission will need to be set to:
|
||||
#
|
||||
# permissions:
|
||||
# contents: write
|
||||
#
|
||||
# https://docs.github.com/en/enterprise-cloud@latest/code-security/supply-chain-security/understanding-your-software-supply-chain/using-the-dependency-submission-api
|
||||
permissions:
|
||||
contents: read
|
||||
# Write permissions for pull-requests are required for using the `comment-summary-in-pr` option, comment out if you aren't using this option
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
# Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options.
|
||||
with:
|
||||
comment-summary-in-pr: always
|
||||
deny-licenses: GPL-1.0-or-later, LGPL-2.0-or-later
|
||||
retry-on-snapshot-warnings: true
|
||||
53
.github/workflows/main.yaml
vendored
Normal file
53
.github/workflows/main.yaml
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
name: 'Main Workflow'
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
svelte-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: Install modules
|
||||
run: npm install
|
||||
- name: Run Svelte-Check
|
||||
run: npm run build:compile:svelte
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install modules
|
||||
run: npm install
|
||||
- name: Run build
|
||||
run: npm run build
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install modules
|
||||
run: npm install
|
||||
- name: Run Prettier
|
||||
run: npm run format:check
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install modules
|
||||
run: npm install
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
|
||||
spellcheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install modules
|
||||
run: npm install
|
||||
- name: Run CSpell
|
||||
run: npm run spellcheck
|
||||
73
CHANGELOG.md
73
CHANGELOG.md
|
|
@ -1,5 +1,78 @@
|
|||
# CHANGELOG
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Features
|
||||
|
||||
- "Review changes reminder" feature. Receive a notification to review changes in your vault at a recurring interval (disabled by default).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- More accurate descriptions for the whitespace ignore settings.
|
||||
|
||||
## 0.5.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Stop auto initializing the first checkpoint (#4 Thanks to @texastoland for the suggestion).
|
||||
- Shorten "Approve & create new checkpoint" button text.
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### What's Changed
|
||||
|
||||
#### Features
|
||||
|
||||
- New "Compare two vault states in history" and "Show vault changes since the last checkpoint" commands. The results of these commands are shown in a temporary side view.
|
||||
- Add validation to settings tab UI and polish the layout.
|
||||
- Display a message in the file changelog view when the opened file is ignored by Git.
|
||||
- Mention unusual scenarios that can lead to the "File has no git history" state in the file changelog view. (#3 Thanks to @AMC-Albert for the suggestion!)
|
||||
- Option to convert ExcludeFilesAndFolders to an include list.
|
||||
- Option to temporarily disable the ExcludeFilesAndFolders list.
|
||||
- Reorganized the settings internally. Users that are updating from a previous version will need to reconfigure all settings that have been affected.
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Clear the file changelog view if no DiffViews are visible.
|
||||
- Improved conditional changelog recalculation logic on settings changes.
|
||||
- Fix an issue where the date labels of the changelog versions were sometimes staying outdated.
|
||||
- Prevent excluding the git root folder from the vault changelog.
|
||||
- Prevent adding paths above git root folder to the exclude list to avoid git errors.
|
||||
- Apply the "Day start time" setting only when the day interval for a changelog is active (to avoid confusion).
|
||||
- Update the minimum required Obsidian version to 1.8.9.
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Features
|
||||
|
||||
- Add options for ignoring whitespace changes.
|
||||
- Expand "Copy commit hash" context menu option to also copy the file path when possible.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Make the plugin load faster by reducing file size.
|
||||
- Disable "Change interval" button for the file changelog when no file is opened.
|
||||
- Don't forget the collapsed state of the first version when chaining recomputes.
|
||||
- Make the status bar stats recognize renamed files once they're committed instead of treating them as new.
|
||||
- Ensure version labels are displayed in the user's specified timezone.
|
||||
- Remove redundant Git plugin initialization check on startup.
|
||||
- Add more detail to incompatibility notices about the installed Git plugin version.
|
||||
- Prevent expanding large number of versions in the vault changelog view as a temporary workaround for performance degradations.
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Features
|
||||
|
||||
- Add "Copy commit hash" context menu item.
|
||||
- Add "Git changelog: Exclude/Reinclude" context menu item.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Show loading state when status bar stats are computing.
|
||||
- Improve ExcludeFilesAndFolders parsing logic.
|
||||
- Fix faulty status bar's queue logic that was sabotaging other tasks.
|
||||
- Revert faulty "is binary file" check.
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Bug Fixes
|
||||
|
|
|
|||
244
README.md
244
README.md
|
|
@ -1,66 +1,104 @@
|
|||
# Git Changelog
|
||||
|
||||
A new [Obsidian](https://obsidian.md) plugin that utilizes Git commit history to display dynamic changelogs in your sidebar. Also usable as a practical data loss monitoring tool.
|
||||
An [Obsidian](https://obsidian.md) plugin that utilizes Git commit history to display dynamic changelogs in your sidebar. Also usable as a practical data loss monitoring tool.
|
||||
|
||||
> [!NOTE]
|
||||
> January 2026 update: I didn't forget about this plugin, I'm just busy with other stuff currently. I plan to continue working on it when I get the time.
|
||||
> If in the meantime a change happens in Obsidian or the Git plugin that breaks this plugin, I'll fix it.
|
||||
|
||||

|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Features](#features)
|
||||
- [Vault Changelog View](#vault-changelog-view)
|
||||
- [File Changelog View](#file-changelog-view)
|
||||
- [Changelog Customizability](#changelog-customizability)
|
||||
- [Live Stats in the Status Bar for the Current Note](#live-stats-in-the-status-bar-for-the-current-note)
|
||||
- [Integration with Git Plugin's Diff View](#integration-with-git-plugins-diff-view)
|
||||
- [Compare Two Vault States in History](#compare-two-vault-states-in-history)
|
||||
- [Show Vault Changes since the Last Checkpoint](#show-vault-changes-since-the-last-checkpoint)
|
||||
- [Data Loss Monitoring](#data-loss-monitoring)
|
||||
- [Common Causes of Data Loss](#common-causes-of-data-loss)
|
||||
- [How to Use](#how-to-use)
|
||||
- [Limitations](#limitations)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Alternatives](#alternatives)
|
||||
- [FAQ](#faq)
|
||||
- [Debugging](#debugging)
|
||||
- [Contributing](#contributing)
|
||||
- [Credits](#credits)
|
||||
|
||||
## Installation
|
||||
|
||||
To install the plugin, follow these steps:
|
||||
|
||||
1. Make sure to have the [BRAT plugin](https://obsidian.md/plugins?id=obsidian42-brat) installed and enabled.
|
||||
2. Paste this link in your browser and press enter:
|
||||
|
||||
`obsidian://brat?plugin=https://github.com/shumadrid/obsidian-git-changelog`
|
||||
|
||||
3. An Obsidian pop-up window should appear. In the window, click the 'Add plugin' button once and wait a few seconds for the plugin to install.
|
||||
|
||||
The plugin is NOT yet available in [the official Community Plugins repository](https://obsidian.md/plugins).
|
||||
[Download](https://obsidian.md/plugins?id=git-changelog) the plugin from the official Community Plugins repository.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Requires the [Git plugin](https://github.com/Vinzent03/obsidian-git) to be installed. Installing it and maintaining a Git repository can be beneficial, even if you rely on other syncing services.
|
||||
|
||||
> [!NOTE]
|
||||
> This plugin is currently in beta and still under development, so your existing settings may break after each update.
|
||||
> This plugin is currently in beta and still under development, so your existing settings may break after each update. If some saved settings were broken, they will be mentioned in the changelog.
|
||||
|
||||
## Features
|
||||
|
||||
To easily see the what's new in future updates, it's recommended to use the [Plugin Update Tracker](https://obsidian.md/plugins?id=obsidian-plugin-update-tracker).
|
||||
|
||||
### Vault Changelog View
|
||||
|
||||
- Detects renamed and moved files separately.
|
||||
- Shows the total count of lines added and deleted.
|
||||
- Displays counts of files added, modified, deleted, and moved/renamed (total or text and non-text separately). If a file is both renamed/moved and modified, it will contribute towards the renamed files stats.
|
||||
- Lists per file lines added and deleted, along with additional file statuses (**A**dded, **R**enamed, **M**oved, or **D**eleted).
|
||||
- **Exclude Files and Folders:**
|
||||
|
||||
Usually you specify files and folders you want to exclude from your Git repository in the `.gitignore` file.
|
||||
|
||||
But this setting is useful if you want to keep some files in your repository but exclude them from appearing in the vault changelog.
|
||||
|
||||
Common examples for this: `.trash` and `.obsidian` folders, `.canvas` files, or syncing service conflict files that can clutter the changelog stats.
|
||||
|
||||

|
||||
|
||||
### File Changelog View
|
||||
- Detects renamed and moved files separately.
|
||||
- Shows the total count of lines added and deleted.
|
||||
- Displays counts of files added, modified, moved/renamed and deleted (total or text and non-text separately). **NOTE:** If a file is both renamed/moved and modified, it will contribute towards the renamed/moved files stats.
|
||||
- Lists per file lines added and deleted, along with additional file statuses (**A**dded, **R**enamed, **M**oved, or **D**eleted).
|
||||
- The files are sorted from most number of lines changed to least.
|
||||
- **Exclude Files and Folders:**
|
||||
|
||||
- Shows the count of added and deleted lines for all previous versions of the active note.
|
||||
- Only Markdown files are currently supported.
|
||||
- Usually you specify files and folders you want to exclude from your Git repository in the `.gitignore` file.
|
||||
|
||||
Example of suggested items to put in `.gitignore` include OS specific files like `.DS_Store`, `.obsidian/workspace.json` and other plugins' cache files that constantly change and trigger commits even when no changes were made to the vault. These files usually don't hold any important user data and are regenerated easily (in case of needing to restore the vault from a backup).
|
||||
|
||||
But this setting is useful if you want to keep some files backed up by your repository (e.g. because they're valuable), but exclude them from appearing in the vault changelog.
|
||||
|
||||
Common examples for this: `/.trash`, `/attachments` and `/.obsidian` folders, debug/log files, or conflict files generated by a syncing service that can clutter the changelog stats.
|
||||
|
||||
Also use this setting to filter files and folders that already exist in previous commits, since specifying files in .gitignore doesn't remove them from previous commits.
|
||||
|
||||
- **Context menu integration:** To exclude a file or folder, just right-click on it inside the File explorer and select "Git changelog: Exclude" from the context menu, no need to manually define paths.
|
||||
|
||||
- For advanced users: If you configured your Git repository to be below the vault root directory, the paths should be relative to the Git repo, not the vault.
|
||||
|
||||
- **Include items instead:**
|
||||
|
||||
- Convert the `Exclude Files and Folders` list to an include list, while excluding everything else.
|
||||
|
||||
- An example case where this can come in handy is when you notice your vault misbehaving, you can trigger this setting and put `.obsidian` in the list to exclusively see changes made inside your configuration folder.
|
||||
|
||||
Then you can easily investigate what settings recently changed and if some of the changes are the cause of your issue.
|
||||
|
||||
### File Changelog View
|
||||
|
||||

|
||||
|
||||
- Shows the count of added and deleted lines for all previous versions of the active note.
|
||||
|
||||
> [!WARNING]
|
||||
> Using the "Expand all" toggle will reduce view performance. This will be fixed soon.
|
||||
> Showing a lot of versions and files will reduce view performance. This will be fixed soon.
|
||||
|
||||
### Changelog Customizability
|
||||
|
||||
- Set a custom day start time (if you're a night owl and want to track your actual days).
|
||||
- Set a custom timezone to base the intervals on or detect it automatically.
|
||||
- Choose to view hourly, daily, weekly, or monthly intervals.
|
||||
- Easily switch between hourly, daily, weekly, and monthly intervals.
|
||||
- Choose from 5 options to ignore whitespace changes with varying intensity.
|
||||
- **Adjust file rename detection strictness:**
|
||||
|
||||
Git can't actually track which files you renamed. Instead, it tries to detect renames by comparing the file contents of the old and new files.
|
||||
- Git can't actually track which files you renamed. Instead, it tries to detect renames by comparing the file contents of the old and new files.
|
||||
|
||||
It can falsely mark new files as some old file getting renamed, and it can miss actual renames and mark them as separate files getting deleted and created.
|
||||
It can falsely mark new files as some old file getting renamed, and it can miss actual renames and mark them as separate files getting deleted and created.
|
||||
|
||||
Higher strictness results in detecting less renames.
|
||||
- Higher strictness results in detecting less renames.
|
||||
|
||||
### Live Stats in the Status Bar for the Current Note
|
||||
|
||||
|
|
@ -70,6 +108,37 @@ The plugin is NOT yet available in [the official Community Plugins repository](h
|
|||
|
||||
- Click on any file version in the changelog views to open the corresponding diff view showing the changes.
|
||||
|
||||
> [!WARNING]
|
||||
> Currently, only the "Split" diff view works. The "Unified" view shows inaccurate changes.
|
||||
|
||||
### Compare Two Vault States in History
|
||||
|
||||
- Use the `Compare two vault states in history` command to compare any two points in the vault's Git history.
|
||||
|
||||
- All settings that apply to the changelog views also apply to this command.
|
||||
|
||||

|
||||
|
||||
### Show Vault Changes since the Last Checkpoint
|
||||
|
||||
- The easiest way to track all changes made to your vault over time is to use the `Show vault changes since the last checkpoint` command to open a temporary view that shows all changes that happened since the last checkpoint (meaning: the last time you ran this command and approved the shown changes by creating a new checkpoint).
|
||||
|
||||
- If you're someone who's always concerned about data integrity, you don't need to have the vault changelog view open all the time in order to track changes. 😁 Just run this command every now and then to inspect all changes made since the last time you checked. (Be it a few minutes ago or a few months.)
|
||||
|
||||
- Optionally receive a notification at a recurring set interval (in minutes) that reminds you to review the changes made since the last checkpoint.
|
||||
|
||||
- If you don't like that this command is using the latest commit as the comparison point instead of the live vault state:
|
||||
|
||||
- You can create an action (macro) using the [QuickAdd](https://obsidian.md/plugins?id=quickadd) plugin that will:
|
||||
1. Run the "Commit-and-sync" or "Commit" command, depending on if you use Github to sync or not (these commands are available through the Git plugin).
|
||||
2. Wait for some time for the commit to complete (e.g. 200ms).
|
||||
3. Run the "Show Vault Changes since the Last Checkpoint" command.
|
||||
- That way you will always compare the current state of your vault to the state of the last checkpoint.
|
||||
- You can assign a hotkey to this macro by exposing it as an Obsidian command ([see how](https://youtu.be/gYK3VDQsZJo?t=179)).
|
||||
- 
|
||||
|
||||
- All settings that apply to the changelog views also apply to this command.
|
||||
|
||||
## Data Loss Monitoring
|
||||
|
||||
This plugin can also serve as a tool for early detection of data loss: if the displayed changelog stats seem incorrect, **unintended** vault changes might have occurred.
|
||||
|
|
@ -80,34 +149,63 @@ This plugin can also serve as a tool for early detection of data loss: if the di
|
|||
- ❌ Faulty plugins
|
||||
- ⛓️💥 Device malfunction
|
||||
- 🤷♂️ Accidental text overwrites and other user errors
|
||||
- 📜 Running faulty or outdated scripts
|
||||
- 🤖 **AI Tools**: They have a tendency to overwrite or delete random content.
|
||||
|
||||
### How to Use
|
||||
The result of all of this can be you asking yourself:
|
||||
|
||||
This plugin was not designed to accumulate all changes made inside an interval.
|
||||
> Wait, didn’t this note have more lines yesterday? 🤔
|
||||
|
||||
For example, if you add a block of text, delete it, re-add it, then delete it again in the same day, the changelog will show zero modifications instead of a bunch of additions and deletions.
|
||||
This plugin tries it's best to help you answer that question. 😎
|
||||
|
||||
This is because this plugin only compares neighboring versions (intervals) directly, and if a block of text didn't exist in the previous interval and is also absent in the latest interval, zero changes have occurred.
|
||||
- The recommended way to monitor for data loss is to simply frequently run the [Show Vault Changes since the Last Checkpoint](#show-vault-changes-since-the-last-checkpoint) command and check if the shown changes seem correct.
|
||||
|
||||
Consequently, if you add text that gets lost within the same interval, you can detect this data loss **not** by seeing a high 'lines deleted' count, but by noticing a lack of expected additions.
|
||||
### How it works
|
||||
|
||||
If some lost data was never committed, it won't be recoverable by Git. So it's recommended to configure a frequent (< 5 minutes) auto-commit interval (no need to manually stage and commit files).
|
||||
- This plugin was not designed to accumulate all changes made inside an interval.
|
||||
|
||||
But if some version of a file wasn't committed, you can still rely on the [File Recovery](https://help.obsidian.md/plugins/file-recovery) plugin, or the [Version history](https://help.obsidian.md/Obsidian+Sync/Version+history) feature to recover it.
|
||||
For example, if you add a block of text, delete it, re-add it, then delete it again in the same day, the changelog will show zero modifications instead of a bunch of additions and deletions.
|
||||
|
||||
This is because this plugin only compares neighboring versions (intervals) directly, and if a block of text didn't exist in the previous interval and is also absent in the latest interval, zero changes have occurred.
|
||||
|
||||
Consequently, if you add text that gets lost within the same interval, you can detect this data loss **not** by seeing a high 'lines deleted' count, but by noticing a lack of expected additions.
|
||||
|
||||
- A "Copy commit hash" context menu option is provided so that you can further investigate the commit tied to some suspicious version.
|
||||
|
||||
- If some lost data was never committed, it won't be recoverable by Git. So it's recommended to configure a frequent (< 5 minutes) auto-commit interval (no need to manually stage and commit files).
|
||||
|
||||
But if some version of a file wasn't committed, you can still rely on the [File Recovery](https://help.obsidian.md/plugins/file-recovery) plugin, or the [Version history](https://help.obsidian.md/Obsidian+Sync/Version+history) feature to recover it.
|
||||
|
||||
Use the [Version History Diff](https://github.com/kometenstaub/obsidian-version-history-diff) plugin for easier navigation through those versions.
|
||||
|
||||
- It's also recommended to occasionally check the integrity of your repo. To do this, open a terminal at your vault's location and just run:
|
||||
|
||||
```bash
|
||||
git fsck -full
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
⚠️ There are quite a few limitations, and some of them are simply the result of choosing this specific approach to changelog management.
|
||||
|
||||
- No meaningful way of tracking changes to canvas files.
|
||||
|
||||
- No mobile support yet.
|
||||
|
||||
- File changelog doesn't clear if opening a diff view but keeps showing stats for the previous note. This is intentional for quickly switching between versions but may result in showing stats for a closed note. This will be fixed in the future.
|
||||
|
||||
- Only status bar counts update live; other views refresh on each commit because of performance reasons.
|
||||
- Assumes linear Git commit history. Unexpected branching or merging can produce unexpected results.
|
||||
|
||||
- Assumes your vault has a linear commit & date history (which it should have). This plugin is not tested or made for repos with non-linear commit history.
|
||||
|
||||
- Git does not track changes inside other nested Git repositories—you should use proper Git submodules instead if you want to track changes for multiple repositories inside your vault.
|
||||
|
||||
- By Git design, files/folders specified in `.gitignore` aren't watched for changes.
|
||||
|
||||
- Git decides if a file is binary (non-text) or a text file by analyzing the file contents rather than looking at the file extension. If you rename a text file to have a `.png` extension, Git will still count its lines and treat it as a text file.
|
||||
|
||||
- If some data loss is only a few words/lines in a heavily edited file, you probably won't notice it. Even though those few lines could have been important.
|
||||
- When using this plugin, the Git plugin status bar branch indicator may get duplicated. (weird)
|
||||
|
||||
- Interaction with submodules isn't tested yet!
|
||||
|
||||
## Roadmap
|
||||
|
|
@ -123,47 +221,79 @@ But if some version of a file wasn't committed, you can still rely on the [File
|
|||
- [ ] Code cleanup and refactoring.
|
||||
- [ ] Improve README.
|
||||
- [ ] Folder stats in the Vault changelog view.
|
||||
- [ ] Command to compare the current state of the vault (repo) to any point in history.
|
||||
- [x] Command to compare any two points in the vault (repo) history.
|
||||
- [ ] Extensive per-file type stats.
|
||||
- [ ] Optimize computing stats performance.
|
||||
- [ ] Integrate the status bar and the file changelog view with [Git plugin's](https://github.com/Vinzent03/obsidian-git) diff views.
|
||||
- [ ] Notify if the amount of changes between neighboring commits exceeds a threshold.
|
||||
- [ ] File & folder stats inside the File explorer view.
|
||||
- [ ] Search & filtering features.
|
||||
|
||||
## Alternatives
|
||||
|
||||
If you don't want to depend on Git, check out the [Vault Changelog](https://github.com/philoserf/obsidian-vault-changelog) plugin.
|
||||
If you don't want to depend on Git, check out the these alternative plugins:
|
||||
|
||||
- [Vault Changelog](https://github.com/philoserf/obsidian-vault-changelog) - tested and reliable, writes a changelog note to your vault
|
||||
|
||||
- [List Modified](https://github.com/franciskafieh/obsidian-list-modified) - popular, links all modified files meeting certain criteria to a note.
|
||||
|
||||
- [Edit History](https://github.com/antoniotejada/obsidian-edit-history) - similar to this plugin, but generates it's own history files instead of relying on other tools.
|
||||
|
||||
- [Time Machine](https://github.com/dsebastien/obsidian-time-machine) - experimental, relies on snapshots from the Obsidian's core "File recovery" plugin.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Will this plugin alter my Git repository/config?
|
||||
- Why is the File changelog view showing inaccurate history for some files?
|
||||
|
||||
- No. This plugin is intended to be read-only and does not alter any Git configurations.
|
||||
However, it's in beta, so bugs are possible.
|
||||
- Most common cases for this are: **The file had its file path changed (renamed/moved or one of its parent folders was renamed/moved)**...
|
||||
|
||||
### I have been using Obsidian for some time but don't have a Git repo set up. Can I still use this plugin to view the history of my vault?
|
||||
1. and the change isn't committed yet.
|
||||
|
||||
- No. 😔
|
||||
2. and was also significantly modified, so that the similarity of the old and new version went below the `File move/rename detection strictness` threshold, resulting in Git treating the file as a new file instead of a renamed/moved one.
|
||||
|
||||
3. but the only changes were letter capitalization changes, and now Git is saying that the file has no history. [Why?](https://github.com/shumadrid/obsidian-git-changelog/issues/3) (TLDR: Git is case sensitive, your file system might not be). To solve this:
|
||||
|
||||
1. Add an emoji (or anything) as a prefix in the name to all the affected files/folders in your vault (an easier way is to only prefix all the topmost folders).
|
||||
|
||||
2. Commit all the changes.
|
||||
|
||||
3. After committing, the files will be tracked with the newly capitalized file paths, and you can remove the emoji prefixes.
|
||||
|
||||
- Will this plugin alter my Git repository/config?
|
||||
|
||||
- No. This plugin is intended to be read-only and does not alter any Git configurations. However, it's in beta, so bugs are possible.
|
||||
|
||||
The only exception is if you explicitly click the `Apply` button for the `Difference detection algorithm` setting, which applies the selected algorithm to the `.git/config` file. But this setting is easily reversible.
|
||||
|
||||
- I have been using Obsidian for some time but don't have a Git repo set up. Can I still use this plugin to view the history of my vault?
|
||||
|
||||
- No. 😔
|
||||
|
||||
- After making substantial changes, the plugin is showing unusual stats.
|
||||
|
||||
- Try disabling all exclusion rules and see if it was some exclusion rule that was influencing the stats.
|
||||
|
||||
Example scenario: A folder that was marked as excluded is now moved to a different location, the existing rule no longer affects that folder, so the plugin is showing the files inside that folder as added instead of moved.
|
||||
|
||||
## Debugging
|
||||
|
||||
By default, debug messages for this plugin are hidden. They aren't intended for the user.
|
||||
- By default, debug messages for this plugin are hidden. They aren't intended for the user.
|
||||
|
||||
To show them, enable `Verbose` mode in the console and run the following command:
|
||||
To show them, enable `Verbose` mode in the console and run the following command:
|
||||
|
||||
```js
|
||||
window.DEBUG.enable('git-changelog');
|
||||
```
|
||||
```bash
|
||||
window.DEBUG.enable('git-changelog');
|
||||
```
|
||||
|
||||
For more details, refer to [this guide](https://github.com/mnaoumov/obsidian-dev-utils?tab=readme-ov-file#debugging).
|
||||
- For more details, refer to [this guide](https://github.com/mnaoumov/obsidian-dev-utils?tab=readme-ov-file#debugging).
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to submit a PR or a feature request. I'd especially appreciate help with styling and restructuring the code.
|
||||
- Feel free to submit a PR or a feature request. I'd especially appreciate help with styling and restructuring the code.
|
||||
|
||||
This plugin is in beta, so please do report all bugs that you find.
|
||||
- This plugin is in beta, so please do report all bugs that you find.
|
||||
|
||||
For easier development, [define](https://github.com/mnaoumov/generator-obsidian-plugin?tab=readme-ov-file#build-development-version) a `OBSIDIAN_CONFIG_DIR` variable and run `npm run dev`.
|
||||
- For easier development, [define](https://github.com/mnaoumov/obsidian-dev-utils?tab=readme-ov-file#build-development-version) a `OBSIDIAN_CONFIG_FOLDER` variable and run `npm run dev`.
|
||||
|
||||
## Credits
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"dictionaryDefinitions": [],
|
||||
"dictionaries": [],
|
||||
"words": [
|
||||
"allownonascii",
|
||||
"binaryop",
|
||||
"callouts",
|
||||
"currencyformat",
|
||||
|
|
@ -78,16 +79,19 @@
|
|||
"Reinitiate",
|
||||
"Repr",
|
||||
"shumadrid",
|
||||
"sonarqube",
|
||||
"sourcemaps",
|
||||
"striptime",
|
||||
"subsources",
|
||||
"subtags",
|
||||
"succ",
|
||||
"Syncthing",
|
||||
"texastoland",
|
||||
"tsbuildinfo",
|
||||
"tsdoc",
|
||||
"tsdocs",
|
||||
"tseslint",
|
||||
"Uncollapsible",
|
||||
"unregisters",
|
||||
"vararg",
|
||||
"varargs",
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ export default typescriptEslint.config(
|
|||
// overrides
|
||||
'default-case': 'off',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||
'no-unused-labels': 'off',
|
||||
'no-labels': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': [
|
||||
'error',
|
||||
{ allowNumber: true }
|
||||
|
|
@ -87,7 +89,28 @@ export default typescriptEslint.config(
|
|||
'error',
|
||||
{
|
||||
ignore: [-1, 0, 1, 2],
|
||||
ignoreDefaultValues: true
|
||||
ignoreDefaultValues: true,
|
||||
ignoreClassFieldInitialValues: true
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'error',
|
||||
{
|
||||
allowExpressions: true,
|
||||
allowTypedFunctionExpressions: true,
|
||||
allowHigherOrderFunctions: true
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'simple-git',
|
||||
message: 'Please copy paste the needed enum instead.',
|
||||
allowTypeImports: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@
|
|||
"id": "git-changelog",
|
||||
"isDesktopOnly": true,
|
||||
"name": "Git Changelog",
|
||||
"version": "0.2.1",
|
||||
"minAppVersion": "1.7.2"
|
||||
"version": "0.6.0",
|
||||
"minAppVersion": "1.8.10"
|
||||
}
|
||||
|
|
|
|||
3953
package-lock.json
generated
3953
package-lock.json
generated
File diff suppressed because it is too large
Load diff
18
package.json
18
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "git-changelog",
|
||||
"version": "0.2.1",
|
||||
"version": "0.6.0",
|
||||
"description": "Uses Git to display dynamic vault and file changelogs in the sidebar, useful for spotting data loss.",
|
||||
"keywords": [],
|
||||
"license": "MIT",
|
||||
|
|
@ -16,19 +16,18 @@
|
|||
"format": "obsidian-dev-utils format",
|
||||
"format:check": "obsidian-dev-utils format:check",
|
||||
"lint": "obsidian-dev-utils lint",
|
||||
"lint:fix": "obsidian-dev-utils lint:fix",
|
||||
"spellcheck": "obsidian-dev-utils spellcheck",
|
||||
"version": "obsidian-dev-utils version"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/deep-equal": "^1.0.4",
|
||||
"@types/node": "^22.13.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.23.0",
|
||||
"@typescript-eslint/parser": "^8.23.0",
|
||||
"builtin-modules": "^5.0.0",
|
||||
"color2k": "^2.0.3",
|
||||
"compare-versions": "^6.1.1",
|
||||
"deep-equal": "^2.2.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild-svelte": "^0.9.2",
|
||||
"eslint": "^9.22.0",
|
||||
|
|
@ -38,11 +37,9 @@
|
|||
"eslint-plugin-svelte": "^3.2.2",
|
||||
"eslint-plugin-unicorn": "^57.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"jiti": "^2.4.2",
|
||||
"lodash": "^4.17.21",
|
||||
"obsidian": "^1.8.7",
|
||||
"obsidian-dev-utils": "^19.8.2",
|
||||
"obsidian-typings": "^2.35.0",
|
||||
"obsidian": "latest",
|
||||
"obsidian-dev-utils": "26.4.0",
|
||||
"obsidian-typings": "latest",
|
||||
"prettier": "3.5.3",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"sass": "^1.83.4",
|
||||
|
|
@ -50,10 +47,11 @@
|
|||
"spacetime": "^7.8.0",
|
||||
"svelte": "^5.23.1",
|
||||
"svelte-eslint-parser": "^0.43.0",
|
||||
"svelte-infinite": "^0.5.0",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"type-fest": "^4.39.1",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.25.0",
|
||||
"svelte-infinite": "^0.5.0"
|
||||
"typescript-eslint": "^8.25.0"
|
||||
},
|
||||
"overrides": {
|
||||
"boolean": "npm:dry-uninstall"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { BuildOptions } from 'esbuild';
|
||||
import type { CliTaskResult } from 'obsidian-dev-utils/ScriptUtils/CliUtils';
|
||||
|
||||
import {
|
||||
|
|
@ -9,6 +10,10 @@ export async function buildWithSvelte(
|
|||
developmentMode: boolean
|
||||
): Promise<CliTaskResult> {
|
||||
return await buildObsidianPlugin({
|
||||
customizeEsbuildOptions: (options: BuildOptions) => {
|
||||
options.dropLabels = developmentMode ? undefined : ['DEV'];
|
||||
},
|
||||
|
||||
customEsbuildPlugins: [
|
||||
{
|
||||
name: 'add-condition',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { getDirname } from 'obsidian-dev-utils/Path';
|
||||
import { getFolderName } from 'obsidian-dev-utils/Path';
|
||||
import { existsSync } from 'obsidian-dev-utils/ScriptUtils/NodeModules';
|
||||
import {
|
||||
execFromRoot,
|
||||
getRootDir,
|
||||
getRootFolder,
|
||||
resolvePathFromRootSafe
|
||||
} from 'obsidian-dev-utils/ScriptUtils/Root';
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ export async function formatWithPrettier(rewrite: boolean): Promise<void> {
|
|||
|
||||
const prettierJsonPath = resolvePathFromRootSafe('.prettierrc.json');
|
||||
if (!existsSync(prettierJsonPath)) {
|
||||
const packageDirectory = getRootDir(getDirname(import.meta.url));
|
||||
const packageDirectory = getRootFolder(getFolderName(import.meta.url));
|
||||
if (!packageDirectory) {
|
||||
throw new Error('Could not find package directory.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,41 @@
|
|||
import type { GitChangelogPluginTypes } from 'constants.ts';
|
||||
import type { ObsidianGitPlugin } from 'gitPluginTypes.ts';
|
||||
import type { Debouncer, PluginSettingTab } from 'obsidian';
|
||||
import type { ChangelogGenerationSettings } from 'settings/settings.ts';
|
||||
import type { Debouncer } from 'obsidian';
|
||||
import type { ExtractPluginSettingsWrapper } from 'obsidian-dev-utils/obsidian/Plugin/PluginTypesBase';
|
||||
import type { GitChangelogSettings } from 'settings/settings.ts';
|
||||
import type { SimpleGit } from 'simple-git';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
import {
|
||||
COMPARE_REPO_COMMITS_VIEW_CONFIG,
|
||||
COMPARE_TO_CHECKPOINT_VIEW_CONFIG
|
||||
} from 'constants.ts';
|
||||
import { FileChangelogManager } from 'core/FileChangelogManager.ts';
|
||||
import { runHashObjectEmptyTree } from 'core/gitOperations/runHashObjectEmptyTree.ts';
|
||||
import { changelogGenerationSettingsChanged } from 'core/helper.ts';
|
||||
import { VaultChangelogManager } from 'core/VaultChangelogManager.ts';
|
||||
import { debounce, ItemView, Notice } from 'obsidian';
|
||||
import { addContextMenuItems } from 'menu.ts';
|
||||
import { debounce, Notice } from 'obsidian';
|
||||
import { invokeAsyncSafely } from 'obsidian-dev-utils/Async';
|
||||
import { PluginBase } from 'obsidian-dev-utils/obsidian/Plugin/PluginBase';
|
||||
import { changelogGenerationSettingsChanged } from 'settings/helper.ts';
|
||||
import { GitChangelogPluginSettings } from 'settings/settings.ts';
|
||||
import { getTimeZone } from 'settings/ui/CustomTimeZone.ts';
|
||||
import { GitChangelogSettingsManager } from 'settings/settingsManager.ts';
|
||||
import { getLocaleToAssign } from 'settings/ui/CustomLocale.ts';
|
||||
import {
|
||||
gitPluginCompatibleVersion,
|
||||
gitPluginTestedVersion
|
||||
} from 'settings/ui/GitPluginWarning.ts';
|
||||
import { handleReviewChangesReminderInterval } from 'settings/ui/ReviewChangesReminderInterval.ts';
|
||||
import spacetime from 'spacetime';
|
||||
import { TaskManager } from 'TaskManager.svelte.ts';
|
||||
import { formatDateHour } from 'timeUtils.ts';
|
||||
import { assertNotNull, removeCompareVersionsView } from 'utils.ts';
|
||||
import { CompareRepoCommitsView } from 'Views/CompareRepoCommits/CompareRepoCommits.ts';
|
||||
import { CompareToCheckpointView } from 'Views/CompareToCheckpoint/CompareToCheckpoint.ts';
|
||||
import {
|
||||
FILE_CHANGELOG_VIEW_CONFIG,
|
||||
FileChangelogView
|
||||
} from 'Views/FileChangelog/FileChangelog.ts';
|
||||
import { getActiveGitRelativeFile, isDiffView } from 'Views/helper.ts';
|
||||
import { getActiveGitRelativeFile, isDiffViewVisible } from 'Views/helper.ts';
|
||||
import {
|
||||
VAULT_CHANGELOG_VIEW_CONFIG,
|
||||
VaultChangelogView
|
||||
|
|
@ -37,23 +51,26 @@ import {
|
|||
GitRepoMissingError
|
||||
} from './types.ts';
|
||||
|
||||
export class GitChangelogPlugin extends PluginBase<GitChangelogPluginSettings> {
|
||||
export class GitChangelogPlugin extends PluginBase<GitChangelogPluginTypes> {
|
||||
public debouncedChangelogSettingsChangedCheck:
|
||||
| Debouncer<
|
||||
[
|
||||
oldSettings: ReadonlyDeep<GitChangelogPluginSettings>,
|
||||
newSettings: ReadonlyDeep<GitChangelogPluginSettings>
|
||||
oldSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>,
|
||||
newSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>
|
||||
],
|
||||
void
|
||||
>
|
||||
| undefined;
|
||||
|
||||
public gitPluginState = $state<GitPluginState>();
|
||||
public gitRepoReady = $state<boolean>();
|
||||
// Used for keeping relative interval labels like "today" and "yesterday" up to date
|
||||
public currentDay = $state<string>();
|
||||
public emptyTreeHash: string | undefined;
|
||||
|
||||
public gitRepoReady = $state<boolean>();
|
||||
|
||||
// Used for keeping relative interval labels like "today" and "yesterday" up to date
|
||||
public dependenciesReady = $derived(
|
||||
(this.gitPluginState === GitPluginState.Enabled ||
|
||||
this.gitPluginState === GitPluginState.UntestedVersion) &&
|
||||
|
|
@ -61,14 +78,31 @@ export class GitChangelogPlugin extends PluginBase<GitChangelogPluginSettings> {
|
|||
);
|
||||
|
||||
public detectedTimeZone: string | undefined;
|
||||
public detectedLocale: string | undefined;
|
||||
public utcCurrentDateHour = $state<string>(
|
||||
formatDateHour(spacetime.now('utc'))
|
||||
); // In UTC so it doesn't depend on the timezone setting
|
||||
|
||||
public vaultChangelogManager = $state<VaultChangelogManager>();
|
||||
|
||||
public fileChangelogManager = $state<FileChangelogManager>();
|
||||
public settingsOfComputedCache?: ChangelogGenerationSettings;
|
||||
public statusBarStats?: StatusBarStats;
|
||||
|
||||
public cachedActiveGitFile: string | undefined;
|
||||
|
||||
public compareVersionsUtcNewerDate: string | undefined;
|
||||
public compareVersionsUtcOlderDate: string | undefined;
|
||||
public localeSafe = $state<string>('en-US'); // Properly loaded in onLayoutReady
|
||||
|
||||
public checkpointReminderNotice: Notice | undefined;
|
||||
|
||||
public get emptyTreeHashUnsafe(): string {
|
||||
return assertNotNull(this.emptyTreeHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* If we were to persist this, it would be less flexible if the user changes his repo or the hashing algorithm and we would need to either manually have a database of all possible empty tree hashes or validate it in runtime
|
||||
*/
|
||||
private emptyTreeHash: string | undefined;
|
||||
public async addFileChangelogView(): Promise<void> {
|
||||
await this.app.workspace.ensureSideLeaf(
|
||||
FILE_CHANGELOG_VIEW_CONFIG.type,
|
||||
|
|
@ -102,9 +136,17 @@ export class GitChangelogPlugin extends PluginBase<GitChangelogPluginSettings> {
|
|||
this.consoleDebug(`Error:`, error.stack);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
public displayNotice(message: string, timeout: number = 3 * 1000): void {
|
||||
new Notice(`${this.manifest.name}\n${message}`, timeout);
|
||||
public displayNotice(
|
||||
message: DocumentFragment | string,
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
timeout: number = 3 * 1000
|
||||
): void {
|
||||
if (message instanceof DocumentFragment) {
|
||||
new Notice(message, timeout);
|
||||
return;
|
||||
}
|
||||
|
||||
new Notice(`${this.manifest.name}:\n${message}`, timeout);
|
||||
}
|
||||
|
||||
public async getGit(): Promise<SimpleGit> {
|
||||
|
|
@ -138,40 +180,60 @@ export class GitChangelogPlugin extends PluginBase<GitChangelogPluginSettings> {
|
|||
}
|
||||
}
|
||||
|
||||
public override async saveSettings(
|
||||
newSettings: GitChangelogPluginSettings,
|
||||
debounceOnSettingsSave = true
|
||||
): Promise<void> {
|
||||
const oldSettings = this.settings;
|
||||
await super.saveSettings(newSettings);
|
||||
if (debounceOnSettingsSave) {
|
||||
this.debouncedChangelogSettingsChangedCheck?.(oldSettings, newSettings);
|
||||
} else {
|
||||
this.onSettingsSave(oldSettings, newSettings);
|
||||
public async getEmptyTreeHash(): Promise<string> {
|
||||
if (this.emptyTreeHash) {
|
||||
return this.emptyTreeHash;
|
||||
}
|
||||
const git = await this.getGit();
|
||||
const emptyTreeHash = await runHashObjectEmptyTree({ git });
|
||||
this.emptyTreeHash ??= emptyTreeHash;
|
||||
return this.emptyTreeHash;
|
||||
}
|
||||
|
||||
protected override createPluginSettings(
|
||||
data: unknown
|
||||
): GitChangelogPluginSettings {
|
||||
return new GitChangelogPluginSettings(data);
|
||||
// We only pass the relevant settings for checking because if some invalid value was changed to some other invalid value, that means the default value is used in both cases and we discard that change
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
public override async onSaveSettings(
|
||||
_newSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>,
|
||||
_oldSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>,
|
||||
skipCheck?: boolean
|
||||
): Promise<void> {
|
||||
// If the local setting was modified externally, we need to update the localeSafe property.
|
||||
this.localeSafe = getLocaleToAssign(this);
|
||||
|
||||
if (skipCheck === true) {
|
||||
return;
|
||||
}
|
||||
this.debouncedChangelogSettingsChangedCheck?.(_oldSettings, _newSettings);
|
||||
}
|
||||
|
||||
protected override createPluginSettingsTab(): null | PluginSettingTab {
|
||||
protected override createSettingsTab(): GitChangelogSettingsTab {
|
||||
return new GitChangelogSettingsTab(this);
|
||||
}
|
||||
|
||||
// Functions below are from obsidian-dev-utils (https://github.com/mnaoumov/obsidian-dev-utils/blob/main/src/obsidian/Plugin/PluginBase.ts)
|
||||
protected override createSettingsManager(): GitChangelogSettingsManager {
|
||||
return new GitChangelogSettingsManager(this);
|
||||
}
|
||||
|
||||
protected override onloadComplete(): void {
|
||||
// Runs when the plugin is unloaded.
|
||||
this.register(() => {
|
||||
this.debouncedChangelogSettingsChangedCheck?.cancel();
|
||||
this.fileChangelogManager?.taskManager.abort();
|
||||
this.vaultChangelogManager?.taskManager.abort();
|
||||
this.statusBarStats?.destroy();
|
||||
});
|
||||
// Runs when the plugin is unloaded.
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
protected override async onunloadImpl(): Promise<void> {
|
||||
this.debouncedChangelogSettingsChangedCheck?.cancel();
|
||||
this.fileChangelogManager?.taskManager.abort();
|
||||
this.vaultChangelogManager?.taskManager.abort();
|
||||
this.statusBarStats?.destroy();
|
||||
|
||||
if (this.checkpointReminderNotice) {
|
||||
this.checkpointReminderNotice.hide();
|
||||
this.checkpointReminderNotice = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async onloadImpl(): Promise<void> {
|
||||
await super.onloadImpl();
|
||||
this.registerView(VAULT_CHANGELOG_VIEW_CONFIG.type, (leaf) => {
|
||||
return new VaultChangelogView(leaf, this);
|
||||
});
|
||||
|
|
@ -180,14 +242,33 @@ export class GitChangelogPlugin extends PluginBase<GitChangelogPluginSettings> {
|
|||
return new FileChangelogView(leaf, this);
|
||||
});
|
||||
|
||||
this.registerView(COMPARE_TO_CHECKPOINT_VIEW_CONFIG.type, (leaf) => {
|
||||
return new CompareToCheckpointView(leaf, this);
|
||||
});
|
||||
|
||||
this.registerView(COMPARE_REPO_COMMITS_VIEW_CONFIG.type, (leaf) => {
|
||||
return new CompareRepoCommitsView({
|
||||
leaf,
|
||||
plugin: this,
|
||||
utcOlderDate: assertNotNull(this.compareVersionsUtcOlderDate),
|
||||
utcNewerDate: assertNotNull(this.compareVersionsUtcNewerDate)
|
||||
});
|
||||
});
|
||||
|
||||
addCommands(this);
|
||||
|
||||
addContextMenuItems(this);
|
||||
|
||||
this.debouncedChangelogSettingsChangedCheck = debounce(
|
||||
(
|
||||
oldSettings: GitChangelogPluginSettings,
|
||||
newSettings: GitChangelogPluginSettings
|
||||
oldSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>,
|
||||
newSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>
|
||||
) => {
|
||||
this.onSettingsSave(oldSettings, newSettings);
|
||||
this.onSaveSettingsCheck(oldSettings, newSettings);
|
||||
},
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
700,
|
||||
|
|
@ -196,6 +277,11 @@ export class GitChangelogPlugin extends PluginBase<GitChangelogPluginSettings> {
|
|||
}
|
||||
|
||||
protected override async onLayoutReady(): Promise<void> {
|
||||
// This is a temporary view, and shouldn't persist between sessions
|
||||
removeCompareVersionsView(this);
|
||||
|
||||
this.localeSafe = getLocaleToAssign(this);
|
||||
|
||||
// These have to be initiated first.
|
||||
this.vaultChangelogManager = new VaultChangelogManager({
|
||||
plugin: this,
|
||||
|
|
@ -206,12 +292,12 @@ export class GitChangelogPlugin extends PluginBase<GitChangelogPluginSettings> {
|
|||
taskManager: new TaskManager(this)
|
||||
});
|
||||
|
||||
if (this.settings.statusBarStats) {
|
||||
if (this.settings.showStatusBarStats) {
|
||||
this.initStatusBar();
|
||||
}
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on('file-open', () => {
|
||||
this.app.workspace.on('active-leaf-change', () => {
|
||||
this.updateActiveGitFile();
|
||||
})
|
||||
);
|
||||
|
|
@ -223,18 +309,19 @@ export class GitChangelogPlugin extends PluginBase<GitChangelogPluginSettings> {
|
|||
})
|
||||
);
|
||||
|
||||
// Every 5 minutes it checks if it's a new day in order to update the potentially outdated interval labels.
|
||||
this.registerInterval(
|
||||
window.setInterval(
|
||||
() => {
|
||||
const timezone = getTimeZone(
|
||||
this.settings.changelogGenerationSettings,
|
||||
this
|
||||
);
|
||||
this.currentDay = spacetime.now(timezone).format('day');
|
||||
// Every minute it checks if it's a new hour (most common interval) in order to update the potentially outdated interval labels.
|
||||
const utcCurrentDate = spacetime.now('utc');
|
||||
// Svelte triggers updates only if the strings are different.
|
||||
this.utcCurrentDateHour = formatDateHour(utcCurrentDate);
|
||||
|
||||
// Register minute interval for checkpoint reminders
|
||||
invokeAsyncSafely(() => handleReviewChangesReminderInterval(this));
|
||||
},
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
5 * 60 * 1000
|
||||
60 * 1000
|
||||
)
|
||||
);
|
||||
|
||||
|
|
@ -248,67 +335,19 @@ export class GitChangelogPlugin extends PluginBase<GitChangelogPluginSettings> {
|
|||
await this.addVaultChangelogView();
|
||||
await this.addFileChangelogView();
|
||||
|
||||
const newSettings = this.settingsClone;
|
||||
newSettings.firstStartup = false;
|
||||
await this.saveSettings(newSettings, false);
|
||||
await this.settingsManager.editAndSave(
|
||||
(settings: GitChangelogSettings): void => {
|
||||
settings.firstStartup = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Also checks the status of the Git plugin
|
||||
this.updateActiveGitFile();
|
||||
|
||||
// Check status of the Git plugin
|
||||
this.getGitPlugin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers events for the views to listen to instead of updating the changelogs directly from the settings so that if the views aren't active we don't do unnecessary computation.
|
||||
* This allows us to check if relevant settings have changed and only then recompute changelogs, instead of recomputing after every single settings change.
|
||||
*/
|
||||
private onSettingsSave(
|
||||
oldSettings: ReadonlyDeep<GitChangelogPluginSettings>,
|
||||
newSettings: GitChangelogPluginSettings
|
||||
): void {
|
||||
if (
|
||||
changelogGenerationSettingsChanged({
|
||||
newChangelogSettings: newSettings.changelogGenerationSettings,
|
||||
oldChangelogSettings: oldSettings.changelogGenerationSettings,
|
||||
plugin: this
|
||||
})
|
||||
) {
|
||||
// The main settings, that trigger recalculation for all stats when they change.
|
||||
this.app.workspace.trigger('git-changelog:generation-settings-changed');
|
||||
} else {
|
||||
if (
|
||||
this.fileChangelogManager?.generationSettingsChanged(
|
||||
oldSettings,
|
||||
newSettings
|
||||
) &&
|
||||
this.cachedActiveGitFile
|
||||
) {
|
||||
this.app.workspace.trigger(
|
||||
'git-changelog:file-changelog-generation-settings-changed'
|
||||
);
|
||||
}
|
||||
if (
|
||||
this.statusBarStats &&
|
||||
StatusBarStats.generationSettingsChanged(oldSettings, newSettings) &&
|
||||
this.cachedActiveGitFile
|
||||
) {
|
||||
this.app.workspace.trigger('git-changelog:status-bar-settings-changed');
|
||||
}
|
||||
if (
|
||||
this.vaultChangelogManager?.generationSettingsChanged(
|
||||
oldSettings,
|
||||
newSettings
|
||||
)
|
||||
) {
|
||||
this.app.workspace.trigger(
|
||||
'git-changelog:vault-changelog-generation-settings-changed'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setNewActiveGitFile(activeGitFile: string | undefined): void {
|
||||
this.fileChangelogManager?.resetAndGetSignal();
|
||||
this.fileChangelogManager?.resetAndGetSignal(); // Does this belong inside the file changelog class?
|
||||
this.cachedActiveGitFile = activeGitFile;
|
||||
this.app.workspace.trigger('git-changelog:active-git-file-changed');
|
||||
}
|
||||
|
|
@ -325,17 +364,84 @@ export class GitChangelogPlugin extends PluginBase<GitChangelogPluginSettings> {
|
|||
|
||||
if (currentActiveGitFile) {
|
||||
this.setNewActiveGitFile(currentActiveGitFile);
|
||||
} else {
|
||||
const currentActiveView =
|
||||
this.app.workspace.getActiveViewOfType(ItemView);
|
||||
|
||||
// If a DiffView is active, don't clear the active git file, but keep it, so that the file changelog still shows stats for that previously active file. Only works if the diff view is also focused.
|
||||
if (isDiffView(currentActiveView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (
|
||||
!isDiffViewVisible({
|
||||
app: this.app
|
||||
})
|
||||
) {
|
||||
// If a DiffView is visible, don't clear the active git file, but keep it, so that the file changelog still shows stats for that previously active file.
|
||||
this.setNewActiveGitFile(undefined);
|
||||
}
|
||||
} catch {
|
||||
/* Just do nothing */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers events for the views to listen to instead of updating the changelogs directly from the settings so that if the views aren't active we don't do unnecessary computation.
|
||||
* This allows us to check if relevant settings have changed and only then recompute changelogs, instead of recomputing after every single settings change.
|
||||
*/
|
||||
private onSaveSettingsCheck(
|
||||
oldSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>,
|
||||
newSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>
|
||||
): void {
|
||||
try {
|
||||
if (
|
||||
changelogGenerationSettingsChanged({
|
||||
newSettings,
|
||||
oldSettings
|
||||
})
|
||||
) {
|
||||
// The main settings, that trigger recalculation for all stats when they change.
|
||||
this.app.workspace.trigger('git-changelog:generation-settings-changed');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.fileChangelogManager?.specificSettingsChanged(
|
||||
oldSettings,
|
||||
newSettings
|
||||
) &&
|
||||
this.cachedActiveGitFile
|
||||
) {
|
||||
this.app.workspace.trigger(
|
||||
'git-changelog:file-changelog-generation-settings-changed'
|
||||
);
|
||||
}
|
||||
|
||||
// Handle destroying and initializing the status bar stats
|
||||
if (this.settings.showStatusBarStats !== !!this.statusBarStats) {
|
||||
const shouldEnableStatusBarStats = this.settings.showStatusBarStats;
|
||||
if (shouldEnableStatusBarStats) {
|
||||
this.initStatusBar();
|
||||
} else {
|
||||
this.statusBarStats?.destroy?.();
|
||||
this.statusBarStats = undefined;
|
||||
}
|
||||
}
|
||||
// Handle status bar settings changes
|
||||
else if (
|
||||
this.statusBarStats &&
|
||||
StatusBarStats.generationSettingsChanged(oldSettings, newSettings) &&
|
||||
this.cachedActiveGitFile
|
||||
) {
|
||||
this.app.workspace.trigger('git-changelog:status-bar-settings-changed');
|
||||
}
|
||||
|
||||
if (
|
||||
this.vaultChangelogManager?.specificSettingsChanged(
|
||||
oldSettings,
|
||||
newSettings
|
||||
)
|
||||
) {
|
||||
this.app.workspace.trigger(
|
||||
'git-changelog:vault-changelog-generation-settings-changed'
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
/* Empty */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import {
|
|||
addToQueueAndWait
|
||||
} from 'obsidian-dev-utils/obsidian/Queue';
|
||||
|
||||
export const GIT_OPERATION_TIMEOUT_MILLISECONDS = 80_000;
|
||||
// Obsidian default behavior is to timeout long running tasks after 60 seconds
|
||||
export const GIT_OPERATION_TIMEOUT_MILLISECONDS = 59_000;
|
||||
|
||||
export class TaskManager {
|
||||
public queueSize = $state(0);
|
||||
|
|
|
|||
154
src/Views/CompareRepoCommits/CompareModal.ts
Normal file
154
src/Views/CompareRepoCommits/CompareModal.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import type { GitChangelogPlugin } from 'GitChangelogPlugin.svelte.ts';
|
||||
import type { PromiseResolve } from 'obsidian-dev-utils/Async';
|
||||
import type { ModalOptionsBase } from 'obsidian-dev-utils/obsidian/Modals/ModalBase';
|
||||
import type { CompareRepoCommitsViewState } from 'types.ts';
|
||||
|
||||
import { ButtonComponent, moment } from 'obsidian';
|
||||
import { CssClass } from 'obsidian-dev-utils/CssClass';
|
||||
import { ModalBase } from 'obsidian-dev-utils/obsidian/Modals/ModalBase';
|
||||
import { SettingEx } from 'obsidian-dev-utils/obsidian/SettingEx';
|
||||
import { assertNotNull } from 'utils.ts';
|
||||
|
||||
export class CompareVersionsModal extends ModalBase<
|
||||
CompareRepoCommitsViewState | undefined,
|
||||
ModalOptionsBase
|
||||
> {
|
||||
public plugin: GitChangelogPlugin;
|
||||
|
||||
// Date-time component automatically converts dates to the system timezone in the UI and stores them as UTC
|
||||
private utcOlderDate: Date | undefined;
|
||||
private utcNewerDate: Date;
|
||||
private compareRepoCommitsViewEphemeralState:
|
||||
| CompareRepoCommitsViewState
|
||||
| undefined;
|
||||
|
||||
public constructor({
|
||||
options,
|
||||
resolve,
|
||||
modalCssClass,
|
||||
plugin,
|
||||
utcOlderDateString,
|
||||
utcNewerDateString
|
||||
}: {
|
||||
options: ModalOptionsBase;
|
||||
resolve: PromiseResolve<CompareRepoCommitsViewState | undefined>;
|
||||
modalCssClass: string;
|
||||
plugin: GitChangelogPlugin;
|
||||
utcOlderDateString: string | undefined;
|
||||
utcNewerDateString: string | undefined;
|
||||
}) {
|
||||
super(options, resolve, modalCssClass);
|
||||
this.plugin = plugin;
|
||||
|
||||
// Load the newer date from the previous modal or if this is the first time the user is comparing in this Obsidian session then assign the current date
|
||||
this.utcNewerDate = utcNewerDateString
|
||||
? new Date(utcNewerDateString)
|
||||
: new Date();
|
||||
if (utcOlderDateString) {
|
||||
this.utcOlderDate = new Date(utcOlderDateString);
|
||||
}
|
||||
}
|
||||
|
||||
public override onClose(): void {
|
||||
super.onClose();
|
||||
|
||||
this.resolve(this.compareRepoCommitsViewEphemeralState);
|
||||
}
|
||||
|
||||
public override onOpen(): void {
|
||||
super.onOpen();
|
||||
this.contentEl.addClass('git-changelog-checkpoint-modal');
|
||||
|
||||
const titleElement = new SettingEx(this.contentEl);
|
||||
titleElement.setName('Compare two vault states in git history');
|
||||
titleElement.setDesc(
|
||||
createFragment((fragment) => {
|
||||
fragment.appendText(
|
||||
`The nearest commit that came before the each specified date will be used.`
|
||||
);
|
||||
fragment.createEl('br');
|
||||
fragment.appendText(
|
||||
`Specify the dates in your system timezone (not the one configured in the plugin settings).`
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
this.createTimeInput({
|
||||
text: 'Newer date',
|
||||
onChange: (value) => {
|
||||
this.utcNewerDate = value;
|
||||
},
|
||||
startValue: this.utcNewerDate
|
||||
});
|
||||
|
||||
this.createTimeInput({
|
||||
text: 'Older date',
|
||||
onChange: (value) => {
|
||||
this.utcOlderDate = value;
|
||||
},
|
||||
startValue: this.utcOlderDate
|
||||
});
|
||||
|
||||
this.createCompareButton();
|
||||
}
|
||||
|
||||
private createTimeInput({
|
||||
onChange,
|
||||
text,
|
||||
startValue
|
||||
}: {
|
||||
onChange: (value: Date) => void;
|
||||
text: string;
|
||||
startValue: Date | undefined;
|
||||
}): void {
|
||||
const setting = new SettingEx(this.contentEl);
|
||||
|
||||
setting.setName(text).addDateTime((dateTime) => {
|
||||
if (startValue) {
|
||||
dateTime.setValue(startValue);
|
||||
}
|
||||
dateTime.onChange(onChange);
|
||||
});
|
||||
}
|
||||
|
||||
private validateDates(): string | undefined {
|
||||
if (!this.utcOlderDate || !this.utcNewerDate) {
|
||||
return 'Both dates must be selected.';
|
||||
}
|
||||
|
||||
// Try parsing both dates
|
||||
if (!moment.isDate(this.utcOlderDate)) {
|
||||
return 'Invalid start date.';
|
||||
}
|
||||
if (!moment.isDate(this.utcNewerDate)) {
|
||||
return 'Invalid end date.';
|
||||
}
|
||||
|
||||
// Check if the older date is before the newer date
|
||||
return this.utcOlderDate < this.utcNewerDate
|
||||
? undefined
|
||||
: 'Older date must be before the newer date.';
|
||||
}
|
||||
|
||||
private createCompareButton(): void {
|
||||
const approveButton = new ButtonComponent(this.contentEl);
|
||||
approveButton.setButtonText('Compare');
|
||||
approveButton.setCta();
|
||||
approveButton.onClick(() => {
|
||||
const validationMessage = this.validateDates();
|
||||
|
||||
if (validationMessage) {
|
||||
this.plugin.displayNotice(validationMessage);
|
||||
} else {
|
||||
// Input dates are in UTC, so that git can process them accurately.
|
||||
this.compareRepoCommitsViewEphemeralState = {
|
||||
utcOlderDate: assertNotNull(this.utcOlderDate).toISOString(),
|
||||
utcNewerDate: this.utcNewerDate.toISOString()
|
||||
};
|
||||
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
approveButton.setClass(CssClass.OkButton);
|
||||
}
|
||||
}
|
||||
212
src/Views/CompareRepoCommits/CompareRepoCommits.svelte
Normal file
212
src/Views/CompareRepoCommits/CompareRepoCommits.svelte
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
<script lang="ts">
|
||||
import type { VaultChangelogEntry } from 'core/ChangelogEntry.svelte.ts';
|
||||
import type GitChangelogPlugin from 'main.ts';
|
||||
|
||||
import {
|
||||
COMPARED_VERSIONS_DATE_SEPARATOR_ICON,
|
||||
COPY_COMMIT_HASH_ICON,
|
||||
TOGGLE_FILES_SUMMARY_OPTION_ICON
|
||||
} from 'constants.ts';
|
||||
import { findFirstCommitBefore } from 'core/gitOperations/findFirstCommitBefore.ts';
|
||||
import { runRepoDiff } from 'core/gitOperations/runRepoDiff.ts';
|
||||
import { setIcon } from 'obsidian';
|
||||
import { CssClass } from 'obsidian-dev-utils/CssClass';
|
||||
import { getTimeZone } from 'settings/ui/CustomTimeZone.ts';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { FileSummariesDisplayMode } from 'types.ts';
|
||||
import { assertNotNull } from 'utils.ts';
|
||||
import DependenciesStatusCheck from 'Views/components/DependenciesStatusCheck.svelte';
|
||||
import { formatFullDate } from 'Views/formatters.ts';
|
||||
import VersionComponent from 'Views/VaultChangelog/components/Version.svelte';
|
||||
|
||||
// eslint-disable-next-line capitalized-comments
|
||||
// svelte-ignore non_reactive_update
|
||||
enum CompareVersionsState {
|
||||
Loading = 'Loading...',
|
||||
OnCheckpointCommit = 'Specified dates fall inside the same commit.',
|
||||
BeforeInitialCommit = 'Both dates are before the initial commit.',
|
||||
NoChanges = 'The versions are the same.',
|
||||
Loaded = 'Loaded',
|
||||
UnknownError = 'Unknown error occurred.'
|
||||
}
|
||||
|
||||
interface Properties {
|
||||
plugin: GitChangelogPlugin;
|
||||
closeView: () => void;
|
||||
utcOlderDate: string;
|
||||
utcNewerDate: string;
|
||||
}
|
||||
|
||||
const { plugin, closeView, utcOlderDate, utcNewerDate }: Properties =
|
||||
$props();
|
||||
|
||||
let filesSummaryDisplayModeButton = $state<HTMLElement>();
|
||||
let version = $state<VaultChangelogEntry>();
|
||||
let timezoneAdjustedOlderDate = $state<string>();
|
||||
let timezoneAdjustedNewerDate = $state<string>();
|
||||
|
||||
let compareVersionsState = $state<CompareVersionsState>(
|
||||
CompareVersionsState.Loading
|
||||
);
|
||||
|
||||
const showFilesCountSummariesMode = FileSummariesDisplayMode.TextAndBinary;
|
||||
|
||||
let arrowIconElement = $state<HTMLElement>();
|
||||
let sameCommitIconElement = $state<HTMLElement>();
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const git = await plugin.getGit();
|
||||
const timeZone = getTimeZone(plugin);
|
||||
|
||||
const upperCommit = await findFirstCommitBefore({
|
||||
abortSignal: new AbortController().signal,
|
||||
isoString: utcNewerDate,
|
||||
git,
|
||||
timeZone
|
||||
});
|
||||
if (!upperCommit) {
|
||||
// Since the newer date must be newer than the older date and the newer date is already older than the first commit, that means both dates belong to the time before the repository was created and therefore when comparing the state of the repository between these two dates, the state is the same: non-existent.
|
||||
compareVersionsState = CompareVersionsState.BeforeInitialCommit;
|
||||
return;
|
||||
}
|
||||
|
||||
let lowerCommitIsEmptyTree = false;
|
||||
let lowerCommit = await findFirstCommitBefore({
|
||||
abortSignal: new AbortController().signal,
|
||||
isoString: utcOlderDate,
|
||||
git,
|
||||
timeZone
|
||||
});
|
||||
if (!lowerCommit) {
|
||||
lowerCommitIsEmptyTree = true;
|
||||
|
||||
lowerCommit = {
|
||||
hash: await plugin.getEmptyTreeHash(),
|
||||
// This date won't be used in this scenario, so we can set it to anything.
|
||||
timeZoneAdjustedDate: upperCommit.timeZoneAdjustedDate
|
||||
};
|
||||
}
|
||||
|
||||
timezoneAdjustedNewerDate = formatFullDate(
|
||||
upperCommit.timeZoneAdjustedDate.toNativeDate(),
|
||||
plugin.localeSafe,
|
||||
getTimeZone(plugin)
|
||||
);
|
||||
|
||||
if (lowerCommit.hash === upperCommit.hash) {
|
||||
compareVersionsState = CompareVersionsState.OnCheckpointCommit;
|
||||
// Show the single date of the commit if this is the case
|
||||
return;
|
||||
}
|
||||
|
||||
// Potentially indicate that we're comparing to empty state because the provided older date is older than the repository
|
||||
timezoneAdjustedOlderDate = lowerCommitIsEmptyTree
|
||||
? 'Beginning'
|
||||
: formatFullDate(
|
||||
lowerCommit.timeZoneAdjustedDate.toNativeDate(),
|
||||
plugin.localeSafe,
|
||||
getTimeZone(plugin)
|
||||
);
|
||||
|
||||
version = await runRepoDiff({
|
||||
git,
|
||||
oldCommit: lowerCommit,
|
||||
newCommit: upperCommit,
|
||||
abortSignal: new AbortController().signal,
|
||||
convertToIncludeList: plugin.settings.convertToIncludeList,
|
||||
diffAlgorithm: plugin.settings.diffAlgorithm,
|
||||
excludeFilesAndFoldersLines:
|
||||
plugin.settings.excludeFilesAndFoldersLines,
|
||||
enableExclusionList: plugin.settings.enableExclusionList,
|
||||
ignoreBlankLines: plugin.settings.ignoreBlankLines,
|
||||
renameDetectionStrictness: plugin.settings.renameDetectionStrictness,
|
||||
renameLimit: plugin.settings.renameLimit,
|
||||
whitespaceIgnoreMode: plugin.settings.whitespaceIgnoreMode,
|
||||
emptyTreeHash: await plugin.getEmptyTreeHash(),
|
||||
plugin
|
||||
});
|
||||
|
||||
// If you pass identical commit hashes to git diff, or if there are no changes, it will just output nothing.
|
||||
compareVersionsState = version
|
||||
? CompareVersionsState.Loaded
|
||||
: CompareVersionsState.NoChanges;
|
||||
} catch {
|
||||
compareVersionsState = CompareVersionsState.UnknownError;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (arrowIconElement) {
|
||||
setIcon(arrowIconElement, COMPARED_VERSIONS_DATE_SEPARATOR_ICON);
|
||||
}
|
||||
|
||||
if (sameCommitIconElement) {
|
||||
setIcon(sameCommitIconElement, COPY_COMMIT_HASH_ICON);
|
||||
}
|
||||
|
||||
if (filesSummaryDisplayModeButton) {
|
||||
setIcon(filesSummaryDisplayModeButton, TOGGLE_FILES_SUMMARY_OPTION_ICON);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
filesSummaryDisplayModeButton = undefined;
|
||||
arrowIconElement = undefined;
|
||||
sameCommitIconElement = undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="git-changelog-view">
|
||||
<div class="nav-header git-changelog-force-center">
|
||||
<div class="nav-buttons-container git-changelog-top-margin">
|
||||
<button type="button" class={CssClass.OkButton} onclick={closeView}>
|
||||
Exit comparison
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DependenciesStatusCheck {plugin}>
|
||||
<div class="git-changelog-compared-versions-container">
|
||||
{#if timezoneAdjustedOlderDate && (compareVersionsState === CompareVersionsState.Loaded || compareVersionsState === CompareVersionsState.NoChanges)}
|
||||
<div class="git-changelog-compared-version-title">
|
||||
{timezoneAdjustedOlderDate}
|
||||
</div>
|
||||
<span
|
||||
class="git-changelog-compared-version-title-arrow icon git-changelog-stat-color"
|
||||
bind:this={arrowIconElement}
|
||||
></span>
|
||||
{/if}
|
||||
<div class="git-changelog-compared-version-title">
|
||||
<!-- ' ' because it serves as a space placeholder -->
|
||||
{timezoneAdjustedNewerDate &&
|
||||
compareVersionsState !== CompareVersionsState.Loading &&
|
||||
compareVersionsState !== CompareVersionsState.UnknownError
|
||||
? timezoneAdjustedNewerDate
|
||||
: ' '}
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-files-container">
|
||||
{#if compareVersionsState === CompareVersionsState.Loaded}
|
||||
<VersionComponent
|
||||
version={assertNotNull(version)}
|
||||
{plugin}
|
||||
hideTitleAndMakeUncollapsible={true}
|
||||
showFilesCountSummaries={showFilesCountSummariesMode}
|
||||
/>
|
||||
{:else}
|
||||
<div class="pane-empty git-changelog-git-issue">
|
||||
{#if compareVersionsState === CompareVersionsState.OnCheckpointCommit}
|
||||
<span
|
||||
class="icon git-changelog-on-checkpoint-commit-icon"
|
||||
bind:this={sameCommitIconElement}
|
||||
></span>
|
||||
{/if}
|
||||
{compareVersionsState}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</DependenciesStatusCheck>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
87
src/Views/CompareRepoCommits/CompareRepoCommits.ts
Normal file
87
src/Views/CompareRepoCommits/CompareRepoCommits.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { WorkspaceLeaf } from 'obsidian';
|
||||
import type { CompareRepoCommitsViewState } from 'types.ts';
|
||||
|
||||
import { COMPARE_REPO_COMMITS_VIEW_CONFIG } from 'constants.ts';
|
||||
import { ItemView } from 'obsidian';
|
||||
import { mount, unmount } from 'svelte';
|
||||
import { removeCompareVersionsView } from 'utils.ts';
|
||||
import CompareRepoCommitsComponent from 'Views/CompareRepoCommits/CompareRepoCommits.svelte';
|
||||
import { registerCloseViewIfDeferred } from 'Views/helper.ts';
|
||||
|
||||
export class CompareRepoCommitsView
|
||||
extends ItemView
|
||||
implements CompareRepoCommitsViewState
|
||||
{
|
||||
public plugin: GitChangelogPlugin;
|
||||
public utcOlderDate: string;
|
||||
public utcNewerDate: string;
|
||||
private _view: CompareRepoCommitsComponent | undefined = undefined;
|
||||
|
||||
public constructor({
|
||||
leaf,
|
||||
plugin,
|
||||
utcOlderDate,
|
||||
utcNewerDate
|
||||
}: {
|
||||
leaf: WorkspaceLeaf;
|
||||
plugin: GitChangelogPlugin;
|
||||
utcOlderDate: string;
|
||||
utcNewerDate: string;
|
||||
}) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
this.utcOlderDate = utcOlderDate;
|
||||
this.utcNewerDate = utcNewerDate;
|
||||
|
||||
this.navigation = false;
|
||||
}
|
||||
|
||||
public async destroy(): Promise<void> {
|
||||
if (this._view) {
|
||||
await unmount(this._view);
|
||||
this._view = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public override getDisplayText(): string {
|
||||
return COMPARE_REPO_COMMITS_VIEW_CONFIG.name;
|
||||
}
|
||||
|
||||
public override getIcon(): string {
|
||||
return COMPARE_REPO_COMMITS_VIEW_CONFIG.icon;
|
||||
}
|
||||
|
||||
public override getViewType(): string {
|
||||
return COMPARE_REPO_COMMITS_VIEW_CONFIG.type;
|
||||
}
|
||||
|
||||
public override async onClose(): Promise<void> {
|
||||
await this.destroy();
|
||||
return super.onClose();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
public override async onOpen(): Promise<void> {
|
||||
this._view = mount(CompareRepoCommitsComponent, {
|
||||
props: {
|
||||
plugin: this.plugin,
|
||||
closeView: () => {
|
||||
// Await this.close();
|
||||
removeCompareVersionsView(this.plugin);
|
||||
},
|
||||
utcOlderDate: this.utcOlderDate,
|
||||
utcNewerDate: this.utcNewerDate
|
||||
},
|
||||
target: this.contentEl
|
||||
}) as CompareRepoCommitsComponent;
|
||||
|
||||
registerCloseViewIfDeferred(this, this.plugin);
|
||||
}
|
||||
|
||||
public override onunload(): void {
|
||||
super.onunload();
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
276
src/Views/CompareToCheckpoint/CompareToCheckpoint.svelte
Normal file
276
src/Views/CompareToCheckpoint/CompareToCheckpoint.svelte
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
<script lang="ts">
|
||||
import type { VaultChangelogEntry } from 'core/ChangelogEntry.svelte.ts';
|
||||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { GitChangelogSettings } from 'settings/settings.ts';
|
||||
|
||||
import {
|
||||
COMPARED_VERSIONS_DATE_SEPARATOR_ICON,
|
||||
COPY_COMMIT_HASH_ICON,
|
||||
TOGGLE_FILES_SUMMARY_OPTION_ICON
|
||||
} from 'constants.ts';
|
||||
import { getCommitTimestampOrUndefined } from 'core/gitOperations/getCommitTimestamp.ts';
|
||||
import { runRepoDiff } from 'core/gitOperations/runRepoDiff.ts';
|
||||
import { setIcon } from 'obsidian';
|
||||
import { CssClass } from 'obsidian-dev-utils/CssClass';
|
||||
import { getTimeZone } from 'settings/ui/CustomTimeZone.ts';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { FileSummariesDisplayMode } from 'types.ts';
|
||||
import { assertNotNull } from 'utils.ts';
|
||||
import DependenciesStatusCheck from 'Views/components/DependenciesStatusCheck.svelte';
|
||||
import { formatFullDate } from 'Views/formatters.ts';
|
||||
import VersionComponent from 'Views/VaultChangelog/components/Version.svelte';
|
||||
|
||||
// eslint-disable-next-line capitalized-comments
|
||||
// svelte-ignore non_reactive_update
|
||||
enum ModalCheckpointState {
|
||||
Loading = 'Loading...',
|
||||
OnCheckpointCommit = 'No commits occurred since the last checkpoint.',
|
||||
NoCommits = 'No commits in the repository.',
|
||||
NoCheckpoints = 'No checkpoints yet.',
|
||||
CheckpointCommitNoLongerExists = 'The commit tied to the last checkpoint no longer exists or the checkpoint value is corrupted.',
|
||||
NoChanges = 'No committed included changes since the last checkpoint.',
|
||||
Loaded = 'Loaded',
|
||||
UnknownError = 'Unknown error occurred.'
|
||||
}
|
||||
|
||||
interface Properties {
|
||||
plugin: GitChangelogPlugin;
|
||||
closeView: () => void;
|
||||
}
|
||||
|
||||
const { plugin, closeView }: Properties = $props();
|
||||
|
||||
let filesSummaryDisplayModeButton = $state<HTMLElement>();
|
||||
let version = $state<VaultChangelogEntry>();
|
||||
let startingCheckpointDate = $state<string>();
|
||||
let latestStateDate = $state<string>();
|
||||
|
||||
let upperCommitHash: string | undefined;
|
||||
let modalCheckpointState = $state<ModalCheckpointState>(
|
||||
ModalCheckpointState.Loading
|
||||
);
|
||||
let approveButtonText = $state<string>('Loading...');
|
||||
let setCta = $state<boolean>(false);
|
||||
let showFilesCountSummariesMode = $state(
|
||||
FileSummariesDisplayMode.TextAndBinary
|
||||
);
|
||||
|
||||
let arrowIconElement = $state<HTMLElement>();
|
||||
let onCheckpointCommitIconElement = $state<HTMLElement>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function toggleFilesSummaryOption(): void {
|
||||
showFilesCountSummariesMode =
|
||||
showFilesCountSummariesMode === FileSummariesDisplayMode.Total
|
||||
? FileSummariesDisplayMode.TextAndBinary
|
||||
: FileSummariesDisplayMode.Total;
|
||||
}
|
||||
|
||||
async function onApproveButtonClick(event: MouseEvent): Promise<void> {
|
||||
event.stopPropagation();
|
||||
|
||||
// UpperCommitHash could be cleaned up
|
||||
const checkpointToSave = upperCommitHash;
|
||||
closeView();
|
||||
|
||||
if (checkpointToSave) {
|
||||
await plugin.settingsManager.editAndSave(
|
||||
(settings: GitChangelogSettings): void => {
|
||||
// We only save the latest commit as a new checkpoint if the user closes the temporary view by clicking "approve".
|
||||
// Otherwise we don't change anything.
|
||||
settings.checkpointCommits.push(checkpointToSave);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const git = await plugin.getGit();
|
||||
|
||||
const upperCommit = await getCommitTimestampOrUndefined({
|
||||
abortSignal: new AbortController().signal,
|
||||
git,
|
||||
timeZone: getTimeZone(plugin)
|
||||
});
|
||||
if (!upperCommit) {
|
||||
// If the repo has no commits, git will throw this error:
|
||||
// "Fatal: your current branch 'main' does not have any commits yet"
|
||||
// And the upperCommit will be undefined.
|
||||
modalCheckpointState = ModalCheckpointState.NoCommits;
|
||||
approveButtonText = 'Close';
|
||||
|
||||
// Don't show approve button, because if upperCommit doesn't exist, then that means there are no commits in the repo, so we don't have anything to save as a checkpoint anyway.
|
||||
return;
|
||||
}
|
||||
|
||||
// Save this because if the user clicks "approve" we need to register the latest commit as the latest checkpoint.
|
||||
// We can't depend on the version.commitHash because version might not get computed in certain states (most common scenario is when there are no previous checkpoints).
|
||||
upperCommitHash = upperCommit.hash;
|
||||
|
||||
const lowerCommitHash = plugin.settings.checkpointCommits.at(-1);
|
||||
if (!lowerCommitHash) {
|
||||
// There are no checkpoints to compare to.
|
||||
modalCheckpointState = ModalCheckpointState.NoCheckpoints;
|
||||
|
||||
setCta = true;
|
||||
approveButtonText = 'Create first checkpoint';
|
||||
return;
|
||||
}
|
||||
|
||||
if (lowerCommitHash === upperCommit.hash) {
|
||||
modalCheckpointState = ModalCheckpointState.OnCheckpointCommit;
|
||||
|
||||
// Do not show the "approve" button because there is no point in registering the same commit as the last checkpoint.
|
||||
approveButtonText = 'Close';
|
||||
|
||||
// Show the date of the latest commit if this is the case
|
||||
latestStateDate = formatFullDate(
|
||||
upperCommit.timeZoneAdjustedDate.toNativeDate(),
|
||||
plugin.localeSafe,
|
||||
getTimeZone(plugin)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const lowerCommit = await getCommitTimestampOrUndefined({
|
||||
abortSignal: new AbortController().signal,
|
||||
commitHash: lowerCommitHash,
|
||||
git,
|
||||
timeZone: getTimeZone(plugin)
|
||||
});
|
||||
|
||||
if (!lowerCommit) {
|
||||
// The passed commit hash was corrupted (that's possible because it's read from data.json) or the commit no longer exists (is deleted from repo).
|
||||
// If it's some other error like GitPluginMissingError, then that will be handled and displayed over this in DependenciesStatusCheck anyway, so it doesn't need to be accurate here
|
||||
modalCheckpointState =
|
||||
ModalCheckpointState.CheckpointCommitNoLongerExists;
|
||||
|
||||
setCta = true;
|
||||
approveButtonText = 'Create new checkpoint';
|
||||
return;
|
||||
}
|
||||
|
||||
startingCheckpointDate = formatFullDate(
|
||||
lowerCommit.timeZoneAdjustedDate.toNativeDate(),
|
||||
plugin.localeSafe,
|
||||
getTimeZone(plugin)
|
||||
);
|
||||
|
||||
version = await runRepoDiff({
|
||||
git,
|
||||
oldCommit: lowerCommit,
|
||||
newCommit: upperCommit,
|
||||
abortSignal: new AbortController().signal,
|
||||
convertToIncludeList: plugin.settings.convertToIncludeList,
|
||||
diffAlgorithm: plugin.settings.diffAlgorithm,
|
||||
excludeFilesAndFoldersLines:
|
||||
plugin.settings.excludeFilesAndFoldersLines,
|
||||
ignoreBlankLines: plugin.settings.ignoreBlankLines,
|
||||
enableExclusionList: plugin.settings.enableExclusionList,
|
||||
renameDetectionStrictness: plugin.settings.renameDetectionStrictness,
|
||||
renameLimit: plugin.settings.renameLimit,
|
||||
whitespaceIgnoreMode: plugin.settings.whitespaceIgnoreMode,
|
||||
emptyTreeHash: await plugin.getEmptyTreeHash(),
|
||||
plugin
|
||||
});
|
||||
|
||||
latestStateDate = formatFullDate(
|
||||
upperCommit.timeZoneAdjustedDate.toNativeDate(),
|
||||
plugin.localeSafe,
|
||||
getTimeZone(plugin)
|
||||
);
|
||||
|
||||
// If you pass identical commit hashes to git diff, or if there are no changes, it will just output nothing.
|
||||
modalCheckpointState = version
|
||||
? ModalCheckpointState.Loaded
|
||||
: ModalCheckpointState.NoChanges;
|
||||
|
||||
setCta = true;
|
||||
approveButtonText = 'Create new checkpoint';
|
||||
} catch {
|
||||
modalCheckpointState = ModalCheckpointState.UnknownError;
|
||||
approveButtonText = 'Close';
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (arrowIconElement) {
|
||||
setIcon(arrowIconElement, COMPARED_VERSIONS_DATE_SEPARATOR_ICON);
|
||||
}
|
||||
|
||||
if (onCheckpointCommitIconElement) {
|
||||
setIcon(onCheckpointCommitIconElement, COPY_COMMIT_HASH_ICON);
|
||||
}
|
||||
|
||||
if (filesSummaryDisplayModeButton) {
|
||||
setIcon(filesSummaryDisplayModeButton, TOGGLE_FILES_SUMMARY_OPTION_ICON);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
filesSummaryDisplayModeButton = undefined;
|
||||
arrowIconElement = undefined;
|
||||
onCheckpointCommitIconElement = undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="git-changelog-view">
|
||||
<!-- {#if !compactMode} -->
|
||||
<div class="nav-header git-changelog-force-center">
|
||||
<div class="nav-buttons-container git-changelog-top-margin">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={modalCheckpointState === ModalCheckpointState.Loading}
|
||||
class={`${setCta ? 'mod-cta ' : ''}${CssClass.OkButton}`}
|
||||
onclick={onApproveButtonClick}
|
||||
>
|
||||
{approveButtonText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DependenciesStatusCheck {plugin}>
|
||||
<div class="git-changelog-compared-versions-container">
|
||||
{#if startingCheckpointDate && (modalCheckpointState === ModalCheckpointState.Loaded || modalCheckpointState === ModalCheckpointState.NoChanges)}
|
||||
<div class="git-changelog-compared-version-title">
|
||||
{startingCheckpointDate}
|
||||
</div>
|
||||
<span
|
||||
class="git-changelog-compared-version-title-arrow icon git-changelog-stat-color"
|
||||
bind:this={arrowIconElement}
|
||||
></span>
|
||||
{/if}
|
||||
<div class="git-changelog-compared-version-title">
|
||||
<!-- ' ' because it serves as a space placeholder -->
|
||||
{latestStateDate &&
|
||||
modalCheckpointState !== ModalCheckpointState.Loading &&
|
||||
modalCheckpointState !== ModalCheckpointState.UnknownError
|
||||
? latestStateDate
|
||||
: ' '}
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-files-container">
|
||||
{#if modalCheckpointState === ModalCheckpointState.Loaded}
|
||||
<VersionComponent
|
||||
version={assertNotNull(version)}
|
||||
{plugin}
|
||||
hideTitleAndMakeUncollapsible={true}
|
||||
showFilesCountSummaries={showFilesCountSummariesMode}
|
||||
/>
|
||||
{:else}
|
||||
<div class="pane-empty git-changelog-git-issue">
|
||||
{#if modalCheckpointState === ModalCheckpointState.OnCheckpointCommit}
|
||||
<span
|
||||
class="icon git-changelog-on-checkpoint-commit-icon"
|
||||
bind:this={onCheckpointCommitIconElement}
|
||||
></span>
|
||||
{/if}
|
||||
{modalCheckpointState}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</DependenciesStatusCheck>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
70
src/Views/CompareToCheckpoint/CompareToCheckpoint.ts
Normal file
70
src/Views/CompareToCheckpoint/CompareToCheckpoint.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { WorkspaceLeaf } from 'obsidian';
|
||||
|
||||
import { COMPARE_TO_CHECKPOINT_VIEW_CONFIG } from 'constants.ts';
|
||||
import { ItemView } from 'obsidian';
|
||||
import { mount, unmount } from 'svelte';
|
||||
import { removeCompareVersionsView } from 'utils.ts';
|
||||
import CompareToCheckpointComponent from 'Views/CompareToCheckpoint/CompareToCheckpoint.svelte';
|
||||
import { registerCloseViewIfDeferred } from 'Views/helper.ts';
|
||||
|
||||
/**
|
||||
* This was originally a modal, but user should also be able to click on the changes to inspect them, so it was migrated to a temporary view.
|
||||
*/
|
||||
export class CompareToCheckpointView extends ItemView {
|
||||
public plugin: GitChangelogPlugin;
|
||||
private _view: CompareToCheckpointComponent | undefined = undefined;
|
||||
|
||||
public constructor(leaf: WorkspaceLeaf, plugin: GitChangelogPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
this.navigation = false;
|
||||
// This.closeable = true;
|
||||
}
|
||||
|
||||
public async destroy(): Promise<void> {
|
||||
if (this._view) {
|
||||
await unmount(this._view);
|
||||
this._view = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public override getDisplayText(): string {
|
||||
return COMPARE_TO_CHECKPOINT_VIEW_CONFIG.name;
|
||||
}
|
||||
|
||||
public override getIcon(): string {
|
||||
return COMPARE_TO_CHECKPOINT_VIEW_CONFIG.icon;
|
||||
}
|
||||
|
||||
public override getViewType(): string {
|
||||
return COMPARE_TO_CHECKPOINT_VIEW_CONFIG.type;
|
||||
}
|
||||
|
||||
public override async onClose(): Promise<void> {
|
||||
await this.destroy();
|
||||
return super.onClose();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
public override async onOpen(): Promise<void> {
|
||||
this._view = mount(CompareToCheckpointComponent, {
|
||||
props: {
|
||||
plugin: this.plugin,
|
||||
closeView: () => {
|
||||
// Await this.close();
|
||||
removeCompareVersionsView(this.plugin);
|
||||
}
|
||||
},
|
||||
target: this.contentEl
|
||||
}) as CompareToCheckpointComponent;
|
||||
|
||||
registerCloseViewIfDeferred(this, this.plugin);
|
||||
}
|
||||
|
||||
public override onunload(): void {
|
||||
super.onunload();
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,12 @@
|
|||
import type { GitChangelogPlugin } from 'GitChangelogPlugin.svelte.ts';
|
||||
import type { EventRef } from 'obsidian';
|
||||
|
||||
import { INFO_TOOLTIP_ICON } from 'constants.ts';
|
||||
import { runCheckIgnore } from 'core/gitOperations/runCheckIgnore.ts';
|
||||
import { setIcon } from 'obsidian';
|
||||
import { onDestroy, untrack } from 'svelte';
|
||||
import { LoaderState } from 'svelte-infinite';
|
||||
import { assertNotNull } from 'utils.ts';
|
||||
import ChangeIntervalButton from 'Views/components/ChangeIntervalButton.svelte';
|
||||
import DependenciesStatusCheck from 'Views/components/DependenciesStatusCheck.svelte';
|
||||
import InfiniteScroller from 'Views/components/InfiniteScroller.svelte';
|
||||
|
|
@ -30,12 +34,14 @@
|
|||
let settingsChangedReference: EventRef;
|
||||
let activeFileChangedReference: EventRef;
|
||||
|
||||
let infoTooltipIcon = $state<HTMLElement>();
|
||||
|
||||
const changelogManager = $derived(plugin.fileChangelogManager);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
||||
const loaderState = new LoaderState();
|
||||
let intervalAdjective = $state<string>();
|
||||
|
||||
const changelogState = $derived.by(() => {
|
||||
let changelogState = $derived.by(() => {
|
||||
if (!changelogManager) {
|
||||
return FileChangelogState.Initializing;
|
||||
}
|
||||
|
|
@ -56,10 +62,43 @@
|
|||
if (!plugin.cachedActiveGitFile) {
|
||||
return FileChangelogState.NoMarkdownFileOpen;
|
||||
}
|
||||
|
||||
return FileChangelogState.EmptyHistory;
|
||||
//
|
||||
untrack(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
checkFileInGitIgnore();
|
||||
});
|
||||
// Set a loading state while checkFileInGitIgnore is in progress
|
||||
return FileChangelogState.Recomputing;
|
||||
});
|
||||
|
||||
// Const fileInGitIgnore = $state<boolean>();
|
||||
|
||||
// If the file git log yielded no entries, check if it's because the file is ignored by git, or if it's just a new file with no history. Should be instant so no visible UI mismatch.
|
||||
async function checkFileInGitIgnore(): Promise<void> {
|
||||
if (!plugin.cachedActiveGitFile || !changelogManager) {
|
||||
return;
|
||||
}
|
||||
const abortSignal = changelogManager.taskManager.getAbortSignal();
|
||||
const cachedActiveGitFile = plugin.cachedActiveGitFile;
|
||||
|
||||
await changelogManager.taskManager.enqueueAndWait(async () => {
|
||||
try {
|
||||
const git = await plugin.getGit();
|
||||
const isIgnored = await runCheckIgnore({
|
||||
abortSignal,
|
||||
activeGitFile: cachedActiveGitFile,
|
||||
git
|
||||
});
|
||||
|
||||
changelogState = isIgnored
|
||||
? FileChangelogState.GitIgnoredFileOpen
|
||||
: FileChangelogState.EmptyHistory;
|
||||
} catch {
|
||||
changelogState = FileChangelogState.EmptyHistory;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let fileChangelogSettingsChangedReference: EventRef;
|
||||
|
||||
headChangeReference = plugin.app.workspace.on(
|
||||
|
|
@ -99,6 +138,15 @@
|
|||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (infoTooltipIcon) {
|
||||
setIcon(
|
||||
infoTooltipIcon,
|
||||
assertNotNull(infoTooltipIcon.getAttr('data-icon'))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$effect.pre(() => {
|
||||
setIntervalAdjective();
|
||||
});
|
||||
|
|
@ -127,7 +175,8 @@
|
|||
<ChangeIntervalButton
|
||||
{changelogManager}
|
||||
enabled={changelogState !== FileChangelogState.Initializing &&
|
||||
plugin.dependenciesReady === true}
|
||||
plugin.dependenciesReady === true &&
|
||||
!!plugin.cachedActiveGitFile}
|
||||
></ChangeIntervalButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -137,17 +186,15 @@
|
|||
<DependenciesStatusCheck {plugin}>
|
||||
<div class="nav-files-container">
|
||||
{#if changelogState === FileChangelogState.HasEntries}
|
||||
<!-- eslint-disable-next-line @typescript-eslint/no-non-null-assertion -->
|
||||
<InfiniteScroller
|
||||
{loaderState}
|
||||
triggerLoad={// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
changelogManager!.handleScroll}
|
||||
triggerLoad={assertNotNull(changelogManager).handleScroll}
|
||||
>
|
||||
<!-- eslint-disable-next-line @typescript-eslint/no-non-null-assertion -->
|
||||
{#each changelogManager!.visibleEntries! as entry, index (entry.commitHash)}
|
||||
{#each assertNotNull(assertNotNull(changelogManager).visibleEntries) as entry, index (entry.commitHash)}
|
||||
<Version
|
||||
previousEntry={// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
changelogManager!.visibleEntries!.at(index + 1)}
|
||||
previousEntry={assertNotNull(
|
||||
assertNotNull(changelogManager).visibleEntries
|
||||
).at(index + 1)}
|
||||
{entry}
|
||||
{plugin}
|
||||
{index}
|
||||
|
|
@ -160,9 +207,27 @@
|
|||
Loading {intervalAdjective} versions...
|
||||
</div>
|
||||
{:else if changelogState === FileChangelogState.NoMarkdownFileOpen}
|
||||
<div class="pane-empty">No markdown file opened.</div>
|
||||
<div class="pane-empty">No file opened.</div>
|
||||
{:else if changelogState === FileChangelogState.EmptyHistory}
|
||||
<div class="pane-empty">File has no Git history.</div>
|
||||
<div class="pane-empty">
|
||||
<div>File has no Git history.</div>
|
||||
<div class="git-changelog-row">
|
||||
<div
|
||||
bind:this={infoTooltipIcon}
|
||||
data-icon={INFO_TOOLTIP_ICON}
|
||||
aria-label="Due to Git limitations, it doesn't show the history of files that had their:
|
||||
1. file path changed without the changes being committed.
|
||||
2. file path changes be capitalization changes only."
|
||||
class="icon git-changelog-tooltip-icon"
|
||||
></div>
|
||||
<a
|
||||
href="https://github.com/shumadrid/obsidian-git-changelog?tab=readme-ov-file#faq"
|
||||
class="external-link git-changelog-read-more">Read more</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else if changelogState === FileChangelogState.GitIgnoredFileOpen}
|
||||
<div class="pane-empty">File is ignored by Git.</div>
|
||||
{/if}
|
||||
<!-- if changelogState === FileChangelogState.Initializing, show nothing -->
|
||||
</div>
|
||||
|
|
@ -170,4 +235,18 @@
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.git-changelog-tooltip-icon {
|
||||
margin-top: var(--size-4-1);
|
||||
}
|
||||
|
||||
.git-changelog-read-more {
|
||||
font-size: var(--font-small);
|
||||
}
|
||||
|
||||
.git-changelog-row {
|
||||
display: flex;
|
||||
gap: var(--size-4-1);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export class FileChangelogView extends ItemView {
|
|||
}
|
||||
|
||||
public override onunload(): void {
|
||||
super.onunload();
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.destroy();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@
|
|||
import type { GitChangelogPlugin } from 'GitChangelogPlugin.svelte.ts';
|
||||
|
||||
import { OPEN_FILE_ICON } from 'constants.ts';
|
||||
import { mayTriggerChangelogMenu } from 'menu.ts';
|
||||
import { setIcon } from 'obsidian';
|
||||
import { getTimeZone } from 'settings/ui/CustomTimeZone.ts';
|
||||
import { DiffFileStatus } from 'types.ts';
|
||||
import { assertNotNull } from 'utils.ts';
|
||||
import DiffStatsComponent from 'Views/components/DiffStats.svelte';
|
||||
import { FileChangelogView } from 'Views/FileChangelog/FileChangelog.ts';
|
||||
import { composeAriaLabel, composeVersionTitle } from 'Views/formatters.ts';
|
||||
import {
|
||||
canOpenInDiffView,
|
||||
|
|
@ -23,14 +27,27 @@
|
|||
|
||||
let openFileButton = $state<HTMLElement>();
|
||||
|
||||
const formattedVersionDateLabel = $derived.by(() => {
|
||||
const currentDate = plugin.utcCurrentDateHour;
|
||||
const locale = plugin.localeSafe;
|
||||
return composeVersionTitle({
|
||||
interval: plugin.settings.fileChangelogInterval,
|
||||
dayStartHour: plugin.settings.dayStartHour,
|
||||
locale,
|
||||
timeZone: getTimeZone(plugin),
|
||||
utcCurrentDateHour: currentDate,
|
||||
timeZoneAdjustedEntryDate: entry.timeZoneAdjustedDate
|
||||
});
|
||||
});
|
||||
|
||||
function primaryClick(event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
|
||||
if (isVersionClickable()) {
|
||||
changelogFileClick({
|
||||
// Plugin.emptyTreeHash is guaranteed to be initialized. Git plugin throws an error when you pass Plugin.emptyTreeHash but I haven't been able to work around it and it doesn't affect usability.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
aReference: previousEntry?.commitHash ?? plugin.emptyTreeHash!,
|
||||
|
||||
aReference: previousEntry?.commitHash ?? plugin.emptyTreeHashUnsafe,
|
||||
bReference: entry.commitHash,
|
||||
event,
|
||||
file: entry,
|
||||
|
|
@ -54,8 +71,10 @@
|
|||
|
||||
$effect(() => {
|
||||
if (openFileButton) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
setIcon(openFileButton, openFileButton.getAttr('data-icon')!);
|
||||
setIcon(
|
||||
openFileButton,
|
||||
assertNotNull(openFileButton.getAttr('data-icon'))
|
||||
);
|
||||
}
|
||||
});
|
||||
function isVersionClickable(): boolean {
|
||||
|
|
@ -67,17 +86,6 @@
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
const formattedVersionDateLabel = $derived.by(() => {
|
||||
// CurrentDay just used to trigger updates
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const currentDay = plugin.currentDay;
|
||||
return composeVersionTitle({
|
||||
interval: plugin.settings.fileChangelogInterval,
|
||||
plugin,
|
||||
timezoneAdjustedEntryDate: entry.timezoneAdjustedDate
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
|
|
@ -87,6 +95,23 @@
|
|||
aria-label={composeAriaLabel(entry)}
|
||||
data-tooltip-position="bottom"
|
||||
onclick={primaryClick}
|
||||
onauxclick={(event) => {
|
||||
event.stopPropagation();
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (event.button == 2) {
|
||||
const view = plugin.app.workspace.getActiveViewOfType(FileChangelogView);
|
||||
if (view) {
|
||||
mayTriggerChangelogMenu({
|
||||
event,
|
||||
gitRelativePath: entry.pathGitRelative,
|
||||
commitHash: entry.commitHash,
|
||||
// Source: VAULT_CHANGELOG_VIEW_CONFIG.type,
|
||||
view: view.leaf,
|
||||
plugin
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="git-changelog-file-name-container">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
// Import { SimpleGit } from "src/gitManager/simpleGit";
|
||||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { EventRef } from 'obsidian';
|
||||
import type { GitChangelogSettings } from 'settings/settings.ts';
|
||||
|
||||
import { TOGGLE_FILES_SUMMARY_OPTION_ICON } from 'constants.ts';
|
||||
import { setIcon } from 'obsidian';
|
||||
import { onDestroy, untrack } from 'svelte';
|
||||
import { LoaderState } from 'svelte-infinite';
|
||||
import { FilesSummariesDisplayMode } from 'types.ts';
|
||||
import { FileSummariesDisplayMode } from 'types.ts';
|
||||
import { assertNotNull } from 'utils.ts';
|
||||
import { getIntervalAdjectiveString } from 'Views/formatters.ts';
|
||||
|
||||
import ChangeIntervalButton from '../components/ChangeIntervalButton.svelte';
|
||||
|
|
@ -140,24 +142,39 @@
|
|||
collapseButton = undefined;
|
||||
});
|
||||
|
||||
function toggleFilesSummaryOption(): void {
|
||||
async function toggleFilesSummaryOption(): Promise<void> {
|
||||
showFilesCountSummariesMode =
|
||||
showFilesCountSummariesMode === FilesSummariesDisplayMode.Total
|
||||
? FilesSummariesDisplayMode.TextAndBinary
|
||||
: FilesSummariesDisplayMode.Total;
|
||||
showFilesCountSummariesMode === FileSummariesDisplayMode.Total
|
||||
? FileSummariesDisplayMode.TextAndBinary
|
||||
: FileSummariesDisplayMode.Total;
|
||||
|
||||
const newSettings = plugin.settingsClone;
|
||||
newSettings.fileSummariesDisplayMode = showFilesCountSummariesMode;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
plugin.saveSettings(newSettings);
|
||||
await plugin.settingsManager.editAndSave(
|
||||
(settings: GitChangelogSettings): void => {
|
||||
settings.fileSummariesDisplayMode = showFilesCountSummariesMode;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function toggleCollapsedState(): void {
|
||||
// AllEntriesCollapsed needs to be cached because it is a reactive derived value that won't have a consistent state across the whole loop.
|
||||
const everythingCollapsed = allEntriesCollapsed;
|
||||
if (changelogManager?.visibleEntries)
|
||||
for (const entry of changelogManager.visibleEntries)
|
||||
entry.isCollapsed = !everythingCollapsed;
|
||||
if (changelogManager?.visibleEntries) {
|
||||
// Temporary workaround to stop freezing when trying to expand a large number of versions.
|
||||
if (
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
changelogManager?.visibleEntries?.length < 80 ||
|
||||
everythingCollapsed !== true
|
||||
) {
|
||||
for (const entry of changelogManager.visibleEntries)
|
||||
entry.isCollapsed = !everythingCollapsed;
|
||||
} else {
|
||||
plugin.displayNotice(
|
||||
`Too many versions. Unable to expand them all because of the impact on performance.`,
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
1500
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -191,7 +208,7 @@
|
|||
data-icon={TOGGLE_FILES_SUMMARY_OPTION_ICON}
|
||||
aria-disabled={changelogState !== VaultChangelogState.HasEntries}
|
||||
aria-label={showFilesCountSummariesMode ===
|
||||
FilesSummariesDisplayMode.Total
|
||||
FileSummariesDisplayMode.Total
|
||||
? 'Text/media summary stats'
|
||||
: 'Total files summary stats'}
|
||||
bind:this={filesSummaryDisplayModeButton}
|
||||
|
|
@ -207,11 +224,9 @@
|
|||
{#if changelogState === VaultChangelogState.HasEntries}
|
||||
<InfiniteScroller
|
||||
{loaderState}
|
||||
triggerLoad={// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
changelogManager!.handleScroll}
|
||||
triggerLoad={assertNotNull(changelogManager).handleScroll}
|
||||
>
|
||||
<!-- eslint-disable-next-line @typescript-eslint/no-non-null-assertion -->
|
||||
{#each changelogManager!.visibleEntries! as version (version.commitHash)}
|
||||
{#each assertNotNull(assertNotNull(changelogManager).visibleEntries) as version (version.commitHash)}
|
||||
<VersionComponent
|
||||
{version}
|
||||
{plugin}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export class VaultChangelogView extends ItemView {
|
|||
}
|
||||
|
||||
public override onunload(): void {
|
||||
super.onunload();
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.destroy();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
|
||||
import { OPEN_FILE_ICON } from 'constants.ts';
|
||||
import { isFileRenamedOrMoved } from 'core/gitOperations/helper.ts';
|
||||
import { mayTriggerChangelogMenu } from 'menu.ts';
|
||||
import { setIcon } from 'obsidian';
|
||||
import { mayTriggerFileMenu } from 'utils.ts';
|
||||
import { assertNotNull } from 'utils.ts';
|
||||
import {
|
||||
canOpenInDiffView,
|
||||
changelogFileClick,
|
||||
|
|
@ -39,8 +40,7 @@
|
|||
$effect(() => {
|
||||
for (const b of buttons) {
|
||||
if (b) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
setIcon(b, b.getAttr('data-icon')!);
|
||||
setIcon(b, assertNotNull(b.getAttr('data-icon')));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -54,8 +54,7 @@
|
|||
|
||||
if (!isClickable) return;
|
||||
changelogFileClick({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
aReference: previousVersionCommitHash ?? plugin.emptyTreeHash!,
|
||||
aReference: previousVersionCommitHash ?? plugin.emptyTreeHashUnsafe,
|
||||
bReference: currentVersionCommitHash,
|
||||
event,
|
||||
file,
|
||||
|
|
@ -69,7 +68,10 @@
|
|||
.gitManager.getRelativeVaultPath(file.pathGitRelative);
|
||||
}
|
||||
|
||||
const fileOpenable = fileOpenableInObsidian(getRelativeVaultPath(), plugin);
|
||||
const fileOpenable = fileOpenableInObsidian({
|
||||
relativeVaultPath: getRelativeVaultPath(),
|
||||
plugin
|
||||
});
|
||||
|
||||
function openVaultFile(event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
|
|
@ -87,19 +89,19 @@
|
|||
data-tooltip-position="bottom"
|
||||
aria-label={ariaLabel}
|
||||
onclick={primaryClick}
|
||||
onauxclick={// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
(event) => {
|
||||
onauxclick={(event) => {
|
||||
event.stopPropagation();
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (event.button == 2) {
|
||||
const view = plugin.app.workspace.getActiveViewOfType(VaultChangelogView);
|
||||
if (view) {
|
||||
mayTriggerFileMenu({
|
||||
app: plugin.app,
|
||||
mayTriggerChangelogMenu({
|
||||
event,
|
||||
filePath: getRelativeVaultPath(),
|
||||
source: 'git-source-control',
|
||||
view: view.leaf
|
||||
gitRelativePath: file.pathGitRelative,
|
||||
commitHash: currentVersionCommitHash,
|
||||
// Source: VAULT_CHANGELOG_VIEW_CONFIG.type,
|
||||
view: view.leaf,
|
||||
plugin
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
type: DiffFileStatus.Modified
|
||||
},
|
||||
{
|
||||
count: filesSummary.renamedFiles,
|
||||
count: filesSummary.renamedAndMovedFiles,
|
||||
icon: FILE_RENAMED_ICON,
|
||||
type: DiffFileStatus.Renamed
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
import type { VaultChangelogEntry } from 'core/ChangelogEntry.svelte.ts';
|
||||
import type GitChangelogPlugin from 'main.ts';
|
||||
|
||||
import { mayTriggerChangelogMenu } from 'menu.ts';
|
||||
import { getTimeZone } from 'settings/ui/CustomTimeZone.ts';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { FilesSummariesDisplayMode } from 'types.ts';
|
||||
import { FileSummariesDisplayMode } from 'types.ts';
|
||||
import { composeVersionTitle } from 'Views/formatters.ts';
|
||||
import { VaultChangelogView } from 'Views/VaultChangelog/VaultChangelog.ts';
|
||||
|
||||
import DiffStatsComponent from '../../components/DiffStats.svelte';
|
||||
import FileComponent from './File.svelte';
|
||||
|
|
@ -12,65 +15,98 @@
|
|||
|
||||
interface Properties {
|
||||
plugin: GitChangelogPlugin;
|
||||
showFilesCountSummaries: FilesSummariesDisplayMode;
|
||||
showFilesCountSummaries: FileSummariesDisplayMode;
|
||||
version: VaultChangelogEntry;
|
||||
hideTitleAndMakeUncollapsible?: boolean;
|
||||
}
|
||||
|
||||
const { plugin, showFilesCountSummaries, version }: Properties = $props();
|
||||
const {
|
||||
plugin,
|
||||
showFilesCountSummaries,
|
||||
version,
|
||||
hideTitleAndMakeUncollapsible: hideTitleAndCollapseIcon
|
||||
}: Properties = $props();
|
||||
|
||||
const formattedVersionDateLabel = $derived.by(() => {
|
||||
// CurrentDay just used to trigger updates
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const currentDay = plugin.currentDay;
|
||||
// Doing it this way to ensure they're properly reacted to if changed.
|
||||
const currentDate = plugin.utcCurrentDateHour;
|
||||
const locale = plugin.localeSafe;
|
||||
return composeVersionTitle({
|
||||
interval: plugin.settings.vaultChangelogGenerationSettings.interval,
|
||||
plugin,
|
||||
timezoneAdjustedEntryDate: version.timezoneAdjustedDate
|
||||
interval: plugin.settings.vaultChangelogInterval,
|
||||
dayStartHour: plugin.settings.dayStartHour,
|
||||
locale,
|
||||
timeZone: getTimeZone(plugin),
|
||||
utcCurrentDateHour: currentDate,
|
||||
timeZoneAdjustedEntryDate: version.timeZoneAdjustedDate
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class:is-collapsed={version.isCollapsed}>
|
||||
<div
|
||||
class:is-collapsed={hideTitleAndCollapseIcon ? false : version.isCollapsed}
|
||||
class="git-changelog-bottom-padding"
|
||||
>
|
||||
<div
|
||||
class="tree-item-self is-clickable git-changelog-bottom-padding"
|
||||
class={`tree-item-self${hideTitleAndCollapseIcon ? '' : ' is-clickable'}`}
|
||||
data-tooltip-position="bottom"
|
||||
onclick={/* eslint-disable-next-line @typescript-eslint/explicit-function-return-type */
|
||||
() => {
|
||||
version.isCollapsed = !version.isCollapsed;
|
||||
onclick={hideTitleAndCollapseIcon
|
||||
? undefined
|
||||
: () => {
|
||||
version.isCollapsed = !version.isCollapsed;
|
||||
}}
|
||||
onauxclick={(event) => {
|
||||
event.stopPropagation();
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (event.button == 2) {
|
||||
const view =
|
||||
plugin.app.workspace.getActiveViewOfType(VaultChangelogView);
|
||||
if (view) {
|
||||
mayTriggerChangelogMenu({
|
||||
event,
|
||||
commitHash: version.commitHash,
|
||||
view: view.leaf,
|
||||
plugin
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="tree-item-icon nav-folder-collapse-indicator collapse-icon"
|
||||
class:is-collapsed={version.isCollapsed}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="svg-icon right-triangle"><path d="M3 8L12 17L21 8" /></svg
|
||||
{#if !hideTitleAndCollapseIcon}
|
||||
<div
|
||||
class="tree-item-icon nav-folder-collapse-indicator collapse-icon"
|
||||
class:is-collapsed={version.isCollapsed}
|
||||
>
|
||||
</div>
|
||||
<div class="file-stats git-changelog-files-summaries-stats">
|
||||
<div class="git-changelog-entry-title">
|
||||
<div>{formattedVersionDateLabel}</div>
|
||||
{#if !version.previousVersionCommitHash}
|
||||
<div>
|
||||
<span class="nav-file-tag git-changelog-initial-version-tag"
|
||||
>Initial</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="svg-icon right-triangle"><path d="M3 8L12 17L21 8" /></svg
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="file-stats git-changelog-files-summaries-stats">
|
||||
{#if !hideTitleAndCollapseIcon}
|
||||
<div class="git-changelog-entry-title">
|
||||
<div>{formattedVersionDateLabel}</div>
|
||||
{#if !version.previousVersionCommitHash}
|
||||
<div>
|
||||
<span class="nav-file-tag git-changelog-initial-version-tag"
|
||||
>Initial</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- if more than one option is selected then show labels -->
|
||||
{#if showFilesCountSummaries === FilesSummariesDisplayMode.Total}
|
||||
{#if showFilesCountSummaries === FileSummariesDisplayMode.Total}
|
||||
<div class="git-changelog-stat">
|
||||
<DayFilesStatusComponent
|
||||
filesSummary={version.getChangelogFilesSummary()}
|
||||
|
|
@ -78,7 +114,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showFilesCountSummaries === FilesSummariesDisplayMode.TextAndBinary}
|
||||
{#if showFilesCountSummaries === FileSummariesDisplayMode.TextAndBinary}
|
||||
{#if version.binaryFiles.length > 0}
|
||||
<div class="git-changelog-stat">
|
||||
<DayFilesStatusComponent
|
||||
|
|
@ -112,7 +148,7 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if !version.isCollapsed}
|
||||
{#if hideTitleAndCollapseIcon ? true : !version.isCollapsed}
|
||||
<div class="tree-item-children" transition:slide|local={{ duration: 150 }}>
|
||||
{#each version.textFiles as file (file.pathGitRelative)}
|
||||
{#if file !== undefined && file !== undefined}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,30 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { Spacetime } from 'spacetime';
|
||||
import type { DiffFile } from 'types.ts';
|
||||
|
||||
import { normalizePath } from 'obsidian';
|
||||
import { getUserLocale } from 'settings/ui/CustomLocale.ts';
|
||||
import { getTimeZone } from 'settings/ui/CustomTimeZone.ts';
|
||||
import { getDayStartTime } from 'settings/ui/DayStartTime.ts';
|
||||
import spacetime from 'spacetime';
|
||||
import { applyDayStartTimeSetting } from 'timeUtils.ts';
|
||||
import { applyDayStartHourSetting } from 'timeUtils.ts';
|
||||
import { ChangelogInterval } from 'types.ts';
|
||||
import { getFileNameFromPath, isMoved, isRenamed } from 'utils.ts';
|
||||
import {
|
||||
assertNotNull,
|
||||
getFileNameFromPath,
|
||||
isMoved,
|
||||
isRenamed
|
||||
} from 'utils.ts';
|
||||
|
||||
export function composeAriaLabel(file: DiffFile): string {
|
||||
try {
|
||||
let ariaString = '';
|
||||
if (isMoved(file)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
ariaString += file.fromPathGitRelative!;
|
||||
ariaString += assertNotNull(file.fromPathGitRelative);
|
||||
ariaString += ' →';
|
||||
ariaString += '\n';
|
||||
ariaString += file.pathGitRelative;
|
||||
} else if (isRenamed(file)) {
|
||||
ariaString += getFileNameFromPath({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
normalizedFilePath: normalizePath(file.fromPathGitRelative!)
|
||||
normalizedFilePath: normalizePath(
|
||||
assertNotNull(file.fromPathGitRelative)
|
||||
)
|
||||
});
|
||||
ariaString += ' → ';
|
||||
ariaString += getFileNameFromPath({
|
||||
|
|
@ -55,14 +56,27 @@ export function getIntervalAdjectiveString(
|
|||
}
|
||||
|
||||
export function composeDailyVersionDisplayText({
|
||||
fullyAdjustedCurrentDate,
|
||||
fullyAdjustedEntryDate,
|
||||
plugin
|
||||
timeZoneAdjustedCurrentDate,
|
||||
locale,
|
||||
timeZone,
|
||||
timeZoneAdjustedEntryDate,
|
||||
dayStartHour
|
||||
}: {
|
||||
fullyAdjustedCurrentDate: Spacetime;
|
||||
fullyAdjustedEntryDate: Spacetime;
|
||||
plugin: GitChangelogPlugin;
|
||||
timeZoneAdjustedCurrentDate: Spacetime;
|
||||
timeZoneAdjustedEntryDate: Spacetime;
|
||||
locale: string;
|
||||
dayStartHour: number;
|
||||
timeZone: string;
|
||||
}): string {
|
||||
const fullyAdjustedEntryDate = applyDayStartHourSetting({
|
||||
dayStartHour,
|
||||
timeZoneAdjustedDate: timeZoneAdjustedEntryDate
|
||||
});
|
||||
const fullyAdjustedCurrentDate = applyDayStartHourSetting({
|
||||
dayStartHour,
|
||||
timeZoneAdjustedDate: timeZoneAdjustedCurrentDate
|
||||
});
|
||||
|
||||
const isToday = fullyAdjustedEntryDate.isSame(
|
||||
fullyAdjustedCurrentDate,
|
||||
'day'
|
||||
|
|
@ -79,154 +93,152 @@ export function composeDailyVersionDisplayText({
|
|||
return 'Yesterday';
|
||||
}
|
||||
|
||||
return formatDate(
|
||||
fullyAdjustedEntryDate.toNativeDate(),
|
||||
getUserLocale(plugin)
|
||||
);
|
||||
return formatDate(fullyAdjustedEntryDate.toNativeDate(), locale, timeZone);
|
||||
}
|
||||
|
||||
// Only for composing the UI string
|
||||
export function applyDayDisplayOffset({
|
||||
dayStartTime,
|
||||
timezoneAdjustedDate
|
||||
dayStartHour,
|
||||
timeZoneAdjustedDate
|
||||
}: {
|
||||
dayStartTime: number;
|
||||
timezoneAdjustedDate: Spacetime;
|
||||
dayStartHour: number;
|
||||
timeZoneAdjustedDate: Spacetime;
|
||||
}): Spacetime {
|
||||
const dayOffset = timezoneAdjustedDate.hour() < dayStartTime ? 1 : 0;
|
||||
return timezoneAdjustedDate.clone().subtract(dayOffset, 'day');
|
||||
const dayOffset = timeZoneAdjustedDate.hour() < dayStartHour ? 1 : 0;
|
||||
return timeZoneAdjustedDate.clone().subtract(dayOffset, 'day');
|
||||
}
|
||||
|
||||
export function composeHourlyVersionDisplayText({
|
||||
fullyAdjustedCurrentDate,
|
||||
fullyAdjustedEntryDate,
|
||||
timezoneAdjustedEntryDate,
|
||||
plugin
|
||||
timeZoneAdjustedEntryDate,
|
||||
locale,
|
||||
timeZone,
|
||||
timeZoneAdjustedCurrentDate
|
||||
}: {
|
||||
fullyAdjustedCurrentDate: Spacetime;
|
||||
fullyAdjustedEntryDate: Spacetime;
|
||||
timezoneAdjustedEntryDate: Spacetime;
|
||||
plugin: GitChangelogPlugin;
|
||||
timeZoneAdjustedEntryDate: Spacetime;
|
||||
locale: string;
|
||||
timeZoneAdjustedCurrentDate: Spacetime;
|
||||
timeZone: string;
|
||||
}): string {
|
||||
// Use the fullyAdjusted dates only to check if the dates belong to the same day after the dayStartTime setting is applied.
|
||||
const isToday = fullyAdjustedEntryDate.isSame(
|
||||
fullyAdjustedCurrentDate,
|
||||
const isToday = timeZoneAdjustedEntryDate.isSame(
|
||||
timeZoneAdjustedCurrentDate,
|
||||
'day'
|
||||
);
|
||||
const isYesterday = fullyAdjustedEntryDate.isSame(
|
||||
fullyAdjustedCurrentDate.clone().subtract(1, 'days'),
|
||||
const isYesterday = timeZoneAdjustedEntryDate.isSame(
|
||||
timeZoneAdjustedCurrentDate.clone().subtract(1, 'days'),
|
||||
'day'
|
||||
);
|
||||
|
||||
const userLocale = getUserLocale(plugin);
|
||||
|
||||
// Replaces the day part of the date time string with today or yesterday labels.
|
||||
if (isToday || isYesterday) {
|
||||
const timeFormatter = new Intl.DateTimeFormat(userLocale, {
|
||||
timeStyle: 'short'
|
||||
const timeFormatter = new Intl.DateTimeFormat(locale, {
|
||||
timeStyle: 'short',
|
||||
timeZone
|
||||
});
|
||||
const timeString = timeFormatter.format(
|
||||
timezoneAdjustedEntryDate.startOf('hour').toNativeDate()
|
||||
timeZoneAdjustedEntryDate.startOf('hour').toNativeDate()
|
||||
);
|
||||
return `${isToday ? 'Today' : 'Yesterday'}, ${timeString}`;
|
||||
}
|
||||
|
||||
const formatter = new Intl.DateTimeFormat(userLocale, {
|
||||
const formatter = new Intl.DateTimeFormat(locale, {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short'
|
||||
});
|
||||
|
||||
// Use the timezoneAdjustedEntryDate in the UI and just potentially subtract a day if it crosses the dayStartTime boundary. Don't show the fake fullyAdjustedEntryDate that has the dayStartTime offset applied to hours.
|
||||
const timezoneAdjustedEntryDateWithDayOffset = applyDayDisplayOffset({
|
||||
dayStartTime: getDayStartTime(plugin.settings.changelogGenerationSettings),
|
||||
timezoneAdjustedDate: timezoneAdjustedEntryDate
|
||||
timeStyle: 'short',
|
||||
timeZone
|
||||
});
|
||||
|
||||
return formatter.format(
|
||||
timezoneAdjustedEntryDateWithDayOffset.startOf('hour').toNativeDate()
|
||||
timeZoneAdjustedEntryDate.startOf('hour').toNativeDate()
|
||||
);
|
||||
}
|
||||
|
||||
export function composeMonthlyVersionDisplayText({
|
||||
fullyAdjustedEntryDate,
|
||||
plugin
|
||||
timeZoneAdjustedEntryDate,
|
||||
locale,
|
||||
timeZone
|
||||
}: {
|
||||
fullyAdjustedEntryDate: Spacetime;
|
||||
plugin: GitChangelogPlugin;
|
||||
timeZoneAdjustedEntryDate: Spacetime;
|
||||
locale: string;
|
||||
timeZone: string;
|
||||
}): string {
|
||||
return formatMonthYear(
|
||||
fullyAdjustedEntryDate.toNativeDate(),
|
||||
getUserLocale(plugin)
|
||||
timeZoneAdjustedEntryDate.toNativeDate(),
|
||||
locale,
|
||||
timeZone
|
||||
);
|
||||
}
|
||||
|
||||
export function composeVersionTitle({
|
||||
interval,
|
||||
plugin,
|
||||
timezoneAdjustedEntryDate
|
||||
dayStartHour,
|
||||
locale,
|
||||
timeZone,
|
||||
timeZoneAdjustedEntryDate,
|
||||
utcCurrentDateHour
|
||||
}: {
|
||||
interval: ChangelogInterval;
|
||||
plugin: GitChangelogPlugin;
|
||||
timezoneAdjustedEntryDate: Spacetime;
|
||||
dayStartHour: number;
|
||||
locale: string;
|
||||
utcCurrentDateHour: string;
|
||||
timeZone: string;
|
||||
timeZoneAdjustedEntryDate: Spacetime;
|
||||
}): string {
|
||||
const timezoneAdjustedCurrentDate = spacetime.now(
|
||||
getTimeZone(plugin.settings.changelogGenerationSettings, plugin)
|
||||
);
|
||||
const fullyAdjustedCurrentDate = applyDayStartTimeSetting({
|
||||
dayStartTime: getDayStartTime(plugin.settings.changelogGenerationSettings),
|
||||
timezoneAdjustedDate: timezoneAdjustedCurrentDate
|
||||
});
|
||||
const fullyAdjustedEntryDate = applyDayStartTimeSetting({
|
||||
dayStartTime: getDayStartTime(plugin.settings.changelogGenerationSettings),
|
||||
timezoneAdjustedDate: timezoneAdjustedEntryDate
|
||||
});
|
||||
// Clipped to the hour
|
||||
const timeZoneAdjustedCurrentDate =
|
||||
spacetime(utcCurrentDateHour).goto(timeZone);
|
||||
|
||||
switch (interval) {
|
||||
case ChangelogInterval.Hourly: {
|
||||
return composeHourlyVersionDisplayText({
|
||||
fullyAdjustedCurrentDate,
|
||||
fullyAdjustedEntryDate,
|
||||
timezoneAdjustedEntryDate,
|
||||
plugin
|
||||
timeZoneAdjustedEntryDate,
|
||||
timeZoneAdjustedCurrentDate,
|
||||
locale,
|
||||
timeZone
|
||||
});
|
||||
}
|
||||
case ChangelogInterval.Daily: {
|
||||
return composeDailyVersionDisplayText({
|
||||
fullyAdjustedCurrentDate,
|
||||
fullyAdjustedEntryDate,
|
||||
plugin
|
||||
timeZoneAdjustedEntryDate,
|
||||
timeZoneAdjustedCurrentDate,
|
||||
dayStartHour,
|
||||
locale,
|
||||
timeZone
|
||||
});
|
||||
}
|
||||
case ChangelogInterval.Weekly: {
|
||||
return composeWeeklyVersionDisplayText({
|
||||
fullyAdjustedCurrentDate,
|
||||
fullyAdjustedEntryDate,
|
||||
plugin
|
||||
timeZoneAdjustedEntryDate,
|
||||
timeZoneAdjustedCurrentDate,
|
||||
locale,
|
||||
timeZone
|
||||
});
|
||||
}
|
||||
case ChangelogInterval.Monthly: {
|
||||
return composeMonthlyVersionDisplayText({
|
||||
fullyAdjustedEntryDate,
|
||||
plugin
|
||||
timeZoneAdjustedEntryDate,
|
||||
locale,
|
||||
timeZone
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function composeWeeklyVersionDisplayText({
|
||||
fullyAdjustedCurrentDate,
|
||||
fullyAdjustedEntryDate,
|
||||
plugin
|
||||
timeZoneAdjustedEntryDate,
|
||||
timeZoneAdjustedCurrentDate,
|
||||
locale,
|
||||
timeZone
|
||||
}: {
|
||||
fullyAdjustedCurrentDate: Spacetime;
|
||||
fullyAdjustedEntryDate: Spacetime;
|
||||
plugin: GitChangelogPlugin;
|
||||
timeZoneAdjustedCurrentDate: Spacetime;
|
||||
timeZoneAdjustedEntryDate: Spacetime;
|
||||
locale: string;
|
||||
timeZone: string;
|
||||
}): string {
|
||||
const fullyAdjustedEntryWeek = fullyAdjustedEntryDate.startOf('week');
|
||||
const timeZoneAdjustedEntryWeek = timeZoneAdjustedEntryDate.startOf('week');
|
||||
|
||||
// In order for this to be accurate we need to normalize the dates to the start of the interval, which is a week here. If comparing would be based on what the actual commit date is of the current version is, instead of that interval that version belongs to, the diffs would be inconsistent because e.g. when comparing the latest version with the latest commit on thursday with the previous version that had it's last commit on wednesday, the diff would count 2 weeks difference instead of 1.
|
||||
const weeksDifference = fullyAdjustedEntryWeek.diff(
|
||||
fullyAdjustedCurrentDate.startOf('week'),
|
||||
// In order for this to be accurate we need to normalize the dates to the start of the interval, which is a week here.
|
||||
// If comparing would be based on what the actual commit date is of the current version is, instead of that interval that version belongs to, the diffs would be inconsistent because e.g. when comparing the latest version with the latest commit on thursday with the previous version that had it's last commit on wednesday, the diff would count 2 weeks difference instead of 1.
|
||||
const weeksDifference = timeZoneAdjustedEntryWeek.diff(
|
||||
timeZoneAdjustedCurrentDate.startOf('week'),
|
||||
'weeks'
|
||||
);
|
||||
|
||||
|
|
@ -237,34 +249,41 @@ export function composeWeeklyVersionDisplayText({
|
|||
return 'Last week';
|
||||
}
|
||||
|
||||
const userLocale = getUserLocale(plugin);
|
||||
const weekNumber = timeZoneAdjustedEntryDate.week();
|
||||
|
||||
const weekNumber = fullyAdjustedEntryDate.week();
|
||||
|
||||
const isCurrentYear = fullyAdjustedEntryDate.isSame(
|
||||
fullyAdjustedCurrentDate,
|
||||
const isCurrentYear = timeZoneAdjustedEntryDate.isSame(
|
||||
timeZoneAdjustedCurrentDate,
|
||||
'year'
|
||||
);
|
||||
|
||||
const nativeFullyAdjustedEntryWeek = fullyAdjustedEntryWeek.toNativeDate();
|
||||
const nativeTimeZoneAdjustedEntryWeek =
|
||||
timeZoneAdjustedEntryWeek.toNativeDate();
|
||||
|
||||
if (isCurrentYear) {
|
||||
const monthString = new Intl.DateTimeFormat(userLocale, {
|
||||
month: 'short'
|
||||
}).format(nativeFullyAdjustedEntryWeek);
|
||||
const monthString = new Intl.DateTimeFormat(locale, {
|
||||
month: 'short',
|
||||
timeZone
|
||||
}).format(nativeTimeZoneAdjustedEntryWeek);
|
||||
return `Week ${weekNumber}, ${monthString}`;
|
||||
}
|
||||
|
||||
const monthAndYearString = new Intl.DateTimeFormat(userLocale, {
|
||||
const monthAndYearString = new Intl.DateTimeFormat(locale, {
|
||||
month: 'short',
|
||||
year: '2-digit'
|
||||
}).format(nativeFullyAdjustedEntryWeek);
|
||||
year: '2-digit',
|
||||
timeZone
|
||||
}).format(nativeTimeZoneAdjustedEntryWeek);
|
||||
|
||||
return `Week ${weekNumber}, ${monthAndYearString}`;
|
||||
}
|
||||
|
||||
export function formatDate(date: Date, locale: string): string {
|
||||
const formatter = new Intl.DateTimeFormat(locale);
|
||||
export function formatDate(
|
||||
date: Date,
|
||||
locale: string,
|
||||
timeZone: string
|
||||
): string {
|
||||
const formatter = new Intl.DateTimeFormat(locale, {
|
||||
timeZone
|
||||
});
|
||||
return formatter.format(date);
|
||||
}
|
||||
|
||||
|
|
@ -278,10 +297,31 @@ export function formatDiffFileType(file: DiffFile): string {
|
|||
return fileExtension.toLocaleUpperCase();
|
||||
}
|
||||
|
||||
export function formatMonthYear(date: Date, locale: string): string {
|
||||
export function formatMonthYear(
|
||||
date: Date,
|
||||
locale: string,
|
||||
timeZone: string
|
||||
): string {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
year: 'numeric',
|
||||
timeZone
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function formatFullDate(
|
||||
date: Date,
|
||||
locale: string,
|
||||
timeZone: string
|
||||
): string {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
year: 'numeric',
|
||||
timeZone
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import type { ItemView } from 'obsidian';
|
||||
import type { App, ItemView } from 'obsidian';
|
||||
|
||||
import { MarkdownView, TFile } from 'obsidian';
|
||||
import { getNewLeaf } from 'utils.ts';
|
||||
|
||||
import type GitChangelogPlugin from '../main.ts';
|
||||
import type { DiffFile } from '../types.ts';
|
||||
|
||||
import { getNewLeaf } from '../utils.ts';
|
||||
|
||||
export function changelogFileClick({
|
||||
aReference,
|
||||
bReference,
|
||||
|
|
@ -27,10 +26,16 @@ export function changelogFileClick({
|
|||
}
|
||||
}
|
||||
|
||||
export function fileOpenableInObsidian(
|
||||
relativeVaultPath: string,
|
||||
plugin: GitChangelogPlugin
|
||||
): boolean {
|
||||
export function fileOpenableInObsidian({
|
||||
relativeVaultPath,
|
||||
plugin
|
||||
}: {
|
||||
relativeVaultPath?: string;
|
||||
plugin: GitChangelogPlugin;
|
||||
}): boolean {
|
||||
if (!relativeVaultPath) {
|
||||
return false;
|
||||
}
|
||||
// This isn't perfect because some old file path could match an unrelated file's current path in the current state of the vault.
|
||||
const existingFile =
|
||||
plugin.app.vault.getAbstractFileByPath(relativeVaultPath);
|
||||
|
|
@ -82,11 +87,18 @@ export function getDisplayPath(path: string): string {
|
|||
return path.split('/').last()?.replace(/\.md$/, '') ?? '';
|
||||
}
|
||||
|
||||
export function isDiffView(view: ItemView | null): boolean {
|
||||
return (
|
||||
view?.getViewType() === 'diff-view' ||
|
||||
view?.getViewType() === 'split-diff-view'
|
||||
);
|
||||
/**
|
||||
* Assumes that no file view is currently active
|
||||
*/
|
||||
export function isDiffViewVisible({ app }: { app: App }): boolean {
|
||||
const diffViews = [
|
||||
...app.workspace.getLeavesOfType('diff-view'),
|
||||
...app.workspace.getLeavesOfType('split-diff-view')
|
||||
];
|
||||
|
||||
return diffViews.some((leaf) => {
|
||||
return leaf.containerEl.isShown();
|
||||
});
|
||||
}
|
||||
|
||||
export function openFile({
|
||||
|
|
@ -131,3 +143,18 @@ export function showDiff(
|
|||
event
|
||||
});
|
||||
}
|
||||
|
||||
export function registerCloseViewIfDeferred(
|
||||
compareView: ItemView,
|
||||
plugin: GitChangelogPlugin
|
||||
): void {
|
||||
// On each workspace layout change, check if this view is still visible.
|
||||
compareView.registerEvent(
|
||||
plugin.app.workspace.on('active-leaf-change', () => {
|
||||
// If the view exists but isn't visible, close it
|
||||
if (!compareView.contentEl.isShown()) {
|
||||
compareView.leaf.detach();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
import type { GitChangelogPlugin } from 'GitChangelogPlugin.svelte.ts';
|
||||
import type { GitChangelogSettings } from 'settings/settings.ts';
|
||||
import type { CompareRepoCommitsViewState } from 'types.ts';
|
||||
|
||||
import {
|
||||
COMPARE_REPO_COMMITS_VIEW_CONFIG,
|
||||
COMPARE_TO_CHECKPOINT_VIEW_CONFIG
|
||||
} from 'constants.ts';
|
||||
import { CssClass } from 'obsidian-dev-utils/CssClass';
|
||||
import { showModal } from 'obsidian-dev-utils/obsidian/Modals/ModalBase';
|
||||
import { openCompareToCheckpointView } from 'settings/ui/ReviewChangesReminderInterval.ts';
|
||||
import { removeCompareVersionsView } from 'utils.ts';
|
||||
import { CompareVersionsModal } from 'Views/CompareRepoCommits/CompareModal.ts';
|
||||
import { FILE_CHANGELOG_VIEW_CONFIG } from 'Views/FileChangelog/FileChangelog.ts';
|
||||
import { VAULT_CHANGELOG_VIEW_CONFIG } from 'Views/VaultChangelog/VaultChangelog.ts';
|
||||
|
||||
|
|
@ -29,4 +40,77 @@ export function addCommands(plugin: GitChangelogPlugin): void {
|
|||
id: `open-${FILE_CHANGELOG_VIEW_CONFIG.type}`,
|
||||
name: `Open ${FILE_CHANGELOG_VIEW_CONFIG.name.toLocaleLowerCase()} view`
|
||||
});
|
||||
|
||||
plugin.addCommand({
|
||||
callback: async () => {
|
||||
await openCompareToCheckpointView(plugin);
|
||||
},
|
||||
id: `show-${COMPARE_TO_CHECKPOINT_VIEW_CONFIG.type}`,
|
||||
name: `Show ${COMPARE_TO_CHECKPOINT_VIEW_CONFIG.name.toLocaleLowerCase()}`
|
||||
});
|
||||
|
||||
plugin.addCommand({
|
||||
callback: async () => {
|
||||
await plugin.settingsManager.editAndSave(
|
||||
(settings: GitChangelogSettings): void => {
|
||||
settings.enableExclusionList = !settings.enableExclusionList;
|
||||
}
|
||||
);
|
||||
|
||||
// Hotkeys or the command palette don't work in the settings tab under normal circumstances so we don't have to handle refreshing the settingsTab display
|
||||
plugin.displayNotice(
|
||||
plugin.settings.enableExclusionList
|
||||
? 'Exclusion list enabled.'
|
||||
: 'Exclusion list disabled.',
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
1500
|
||||
);
|
||||
},
|
||||
id: `toggle-exclusion-list`,
|
||||
name: `Toggle exclusion list`
|
||||
});
|
||||
|
||||
addCompareRepoVersionsCommand(plugin);
|
||||
}
|
||||
|
||||
function addCompareRepoVersionsCommand(plugin: GitChangelogPlugin): void {
|
||||
plugin.addCommand({
|
||||
id: 'compare-two-versions',
|
||||
name: 'Compare two vault states in history',
|
||||
callback: async () => {
|
||||
// Always returns undefined if the modal was exited without clicking "approve" button.
|
||||
const compareRepoCommitsViewState = await showModal<
|
||||
CompareRepoCommitsViewState | undefined
|
||||
>(
|
||||
(resolve) =>
|
||||
new CompareVersionsModal({
|
||||
plugin,
|
||||
resolve,
|
||||
modalCssClass: CssClass.ConfirmModal,
|
||||
options: {
|
||||
app: plugin.app,
|
||||
cssClass: CssClass.ConfirmModal
|
||||
},
|
||||
utcOlderDateString: plugin.compareVersionsUtcOlderDate,
|
||||
utcNewerDateString: plugin.compareVersionsUtcNewerDate
|
||||
})
|
||||
);
|
||||
|
||||
if (compareRepoCommitsViewState) {
|
||||
// Close any existing COMPARE_REPO_STATES_VIEW views
|
||||
removeCompareVersionsView(plugin);
|
||||
|
||||
plugin.compareVersionsUtcNewerDate =
|
||||
compareRepoCommitsViewState.utcNewerDate;
|
||||
plugin.compareVersionsUtcOlderDate =
|
||||
compareRepoCommitsViewState.utcOlderDate;
|
||||
|
||||
await plugin.app.workspace.ensureSideLeaf(
|
||||
COMPARE_REPO_COMMITS_VIEW_CONFIG.type,
|
||||
'left',
|
||||
{ reveal: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,27 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { PluginTypesBase } from 'obsidian-dev-utils/obsidian/Plugin/PluginTypesBase';
|
||||
import type { GitChangelogSettings } from 'settings/settings.ts';
|
||||
import type { GitChangelogSettingsManager } from 'settings/settingsManager.ts';
|
||||
import type { GitChangelogSettingsTab } from 'settings/settingsTab.ts';
|
||||
|
||||
export const PLUGIN_NAME = 'Git Changelog';
|
||||
export const PLUGIN_NAME_SENTENCE_CASE = 'Git changelog';
|
||||
export const MIN_COMPATIBLE_GIT_PLUGIN_VERSION = '2.31.1';
|
||||
export const MAX_TESTED_GIT_PLUGIN_VERSION = '2.32.1';
|
||||
export const MAX_TESTED_GIT_PLUGIN_VERSION = '2.34.0';
|
||||
|
||||
export interface GitChangelogPluginTypes extends PluginTypesBase {
|
||||
plugin: GitChangelogPlugin;
|
||||
pluginSettings: GitChangelogSettings;
|
||||
pluginSettingsManager: GitChangelogSettingsManager;
|
||||
pluginSettingsTab: GitChangelogSettingsTab;
|
||||
}
|
||||
|
||||
// Strings
|
||||
export const FEEDBACK_URL =
|
||||
'https://github.com/shumadrid/obsidian-git-changelog/issues';
|
||||
export const EXCLUDE_FILES_AND_FOLDERS = 'Exclude files and folders';
|
||||
export const CHANGES_SINCE_LAST_CHECKPOINT =
|
||||
'changes since the last checkpoint';
|
||||
|
||||
// Icons
|
||||
export const ADDITIONS_ICON = 'list-plus';
|
||||
|
|
@ -17,8 +38,26 @@ export const TOGGLE_FILES_SUMMARY_OPTION_ICON = 'list-tree';
|
|||
export const OPEN_FILE_ICON = 'file-input';
|
||||
export const PLUS_ICON = 'plus';
|
||||
export const MINUS_ICON = 'minus';
|
||||
export const COPY_COMMIT_HASH_ICON = 'git-commit-horizontal';
|
||||
export const COMPARED_VERSIONS_DATE_SEPARATOR_ICON = 'clock-10';
|
||||
export const INFO_TOOLTIP_ICON = 'info';
|
||||
|
||||
export const CHANGELOG_LOAD_AMOUNT_BASE_MULTIPLIER = 35;
|
||||
export const CHANGELOG_LOAD_AMOUNT_VERSIONS = 10;
|
||||
export const FILE_VIEW_VERSIONS_MULTIPLIER = 2.4;
|
||||
export const VAULT_MAX_COUNT_MULTIPLIER = 6;
|
||||
|
||||
// Settings
|
||||
export const MIN_RENAME_DETECTION_STRICTNESS = 1;
|
||||
export const MAX_RENAME_DETECTION_STRICTNESS = 100;
|
||||
|
||||
export const COMPARE_TO_CHECKPOINT_VIEW_CONFIG = {
|
||||
icon: 'spline',
|
||||
name: 'Vault changes since the last checkpoint',
|
||||
type: 'compare-to-checkpoint-view'
|
||||
};
|
||||
export const COMPARE_REPO_COMMITS_VIEW_CONFIG = {
|
||||
icon: 'spline',
|
||||
name: "Vault's git history comparison",
|
||||
type: 'vault-git-history-comparison-view'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type {
|
|||
|
||||
export abstract class ChangelogEntry {
|
||||
public constructor(
|
||||
public timezoneAdjustedDate: Spacetime,
|
||||
public timeZoneAdjustedDate: Spacetime,
|
||||
|
||||
public commitHash: string // Represents a single commit that's just the latest commit of a certain interval (day,week...).
|
||||
) {}
|
||||
|
|
@ -33,16 +33,16 @@ export class FileChangelogEntry extends ChangelogEntry implements DiffFile {
|
|||
pathGitRelative,
|
||||
status,
|
||||
textDiffStats,
|
||||
timezoneAdjustedDate
|
||||
timeZoneAdjustedDate
|
||||
}: {
|
||||
commitHash: string;
|
||||
fromPathGitRelative?: string;
|
||||
pathGitRelative: string;
|
||||
status: DiffFileStatus;
|
||||
textDiffStats?: TextDiffStats;
|
||||
timezoneAdjustedDate: Spacetime;
|
||||
timeZoneAdjustedDate: Spacetime;
|
||||
}) {
|
||||
super(timezoneAdjustedDate, commitHash);
|
||||
super(timeZoneAdjustedDate, commitHash);
|
||||
this.pathGitRelative = pathGitRelative;
|
||||
this.status = status;
|
||||
this.fromPathGitRelative = fromPathGitRelative;
|
||||
|
|
@ -75,25 +75,25 @@ export class VaultChangelogEntry extends ChangelogEntry {
|
|||
binaryFiles,
|
||||
binaryFilesSummaryCached,
|
||||
commitHash,
|
||||
previousDayLastCommitHash,
|
||||
previousVersionLastCommitHash,
|
||||
textFiles,
|
||||
textFilesSummaryCached,
|
||||
timezoneAdjustedDate
|
||||
timeZoneAdjustedDate
|
||||
}: {
|
||||
binaryFiles: DiffFile[];
|
||||
binaryFilesSummaryCached: FilesSummary;
|
||||
commitHash: string;
|
||||
previousDayLastCommitHash?: string;
|
||||
previousVersionLastCommitHash?: string;
|
||||
textFiles: DiffFile[];
|
||||
textFilesSummaryCached: FilesSummary;
|
||||
timezoneAdjustedDate: Spacetime;
|
||||
timeZoneAdjustedDate: Spacetime;
|
||||
}) {
|
||||
super(timezoneAdjustedDate, commitHash);
|
||||
super(timeZoneAdjustedDate, commitHash);
|
||||
this.textFiles = textFiles;
|
||||
this.binaryFiles = binaryFiles;
|
||||
this.textFilesSummaryCached = textFilesSummaryCached;
|
||||
this.binaryFilesSummaryCached = binaryFilesSummaryCached;
|
||||
this.previousVersionCommitHash = previousDayLastCommitHash;
|
||||
this.previousVersionCommitHash = previousVersionLastCommitHash;
|
||||
}
|
||||
|
||||
public override getPotentialGitFilePath(): undefined {
|
||||
|
|
@ -143,9 +143,9 @@ export class VaultChangelogEntry extends ChangelogEntry {
|
|||
modifiedFiles:
|
||||
this.textFilesSummaryCached.modifiedFiles +
|
||||
this.binaryFilesSummaryCached.modifiedFiles,
|
||||
renamedFiles:
|
||||
this.textFilesSummaryCached.renamedFiles +
|
||||
this.binaryFilesSummaryCached.renamedFiles
|
||||
renamedAndMovedFiles:
|
||||
this.textFilesSummaryCached.renamedAndMovedFiles +
|
||||
this.binaryFilesSummaryCached.renamedAndMovedFiles
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { GitChangelogPluginTypes } from 'constants.ts';
|
||||
import type { ChangelogEntry } from 'core/ChangelogEntry.svelte.ts';
|
||||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { GitChangelogPluginSettings } from 'settings/settings.ts';
|
||||
import type { ExtractPluginSettingsWrapper } from 'obsidian-dev-utils/obsidian/Plugin/PluginTypesBase';
|
||||
import type { GitChangelogSettings } from 'settings/settings.ts';
|
||||
import type { Spacetime } from 'spacetime';
|
||||
import type { TaskManager } from 'TaskManager.svelte.ts';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
|
@ -12,7 +14,9 @@ import {
|
|||
extractLastCommitsForInterval,
|
||||
GIT_MAX_CONCURRENT_PROCESSES
|
||||
} from 'core/helper.ts';
|
||||
import { getTimeZone } from 'settings/ui/CustomTimeZone.ts';
|
||||
import { AbortError, ChangelogInterval } from 'types.ts';
|
||||
import { assertNotNull } from 'utils.ts';
|
||||
|
||||
export abstract class ChangelogManager<T extends ChangelogEntry> {
|
||||
public visibleEntries = $state<T[] | undefined>();
|
||||
|
|
@ -20,7 +24,7 @@ export abstract class ChangelogManager<T extends ChangelogEntry> {
|
|||
|
||||
public hasEntries = $derived((this.visibleEntries?.length ?? 0) > 0);
|
||||
|
||||
protected reservedEntries = $state<T[]>([]);
|
||||
protected reservedEntries: T[] = [];
|
||||
|
||||
protected plugin: GitChangelogPlugin;
|
||||
|
||||
|
|
@ -96,6 +100,8 @@ export abstract class ChangelogManager<T extends ChangelogEntry> {
|
|||
// We want to immediately cancel all current operations for the changelog and schedule the operation in a queue.
|
||||
const abortSignal = this.resetAndGetSignal();
|
||||
|
||||
// Abort controllers don't just serve to optimize performance by stopping the execution of cancelled functions, but they also serve to prevent those cancelled functions from mutating current state and introducing race conditions.
|
||||
// All abort signals need to be created on call time, and also the plugin settings (it's not strictly necessary, because operations with outdated settings will always get cancelled anyway).
|
||||
this.taskManager.enqueueSafely(async () => {
|
||||
await this.computeChangelog(abortSignal);
|
||||
});
|
||||
|
|
@ -118,12 +124,14 @@ export abstract class ChangelogManager<T extends ChangelogEntry> {
|
|||
});
|
||||
}
|
||||
|
||||
public generationSettingsChanged(
|
||||
oldSettings: ReadonlyDeep<GitChangelogPluginSettings>,
|
||||
newSettings: GitChangelogPluginSettings
|
||||
): boolean {
|
||||
return this.getInterval(oldSettings) !== this.getInterval(newSettings);
|
||||
}
|
||||
public abstract specificSettingsChanged(
|
||||
oldSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>,
|
||||
newSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>
|
||||
): boolean;
|
||||
|
||||
public resetAndGetSignal(): AbortSignal {
|
||||
// Reset `visibleEntries` to undefined each time when you schedule a recompute so that the UI correctly updates to "loading" state while it waits for the stats to compute.
|
||||
|
|
@ -140,7 +148,7 @@ export abstract class ChangelogManager<T extends ChangelogEntry> {
|
|||
public abstract setNextInterval(): Promise<void>;
|
||||
|
||||
public abstract getInterval(
|
||||
settings?: ReadonlyDeep<GitChangelogPluginSettings>
|
||||
settings?: ReadonlyDeep<GitChangelogSettings>
|
||||
): ChangelogInterval;
|
||||
|
||||
protected abstract updateEntries({
|
||||
|
|
@ -161,21 +169,27 @@ export abstract class ChangelogManager<T extends ChangelogEntry> {
|
|||
abortSignal: AbortSignal;
|
||||
activeGitFile?: string;
|
||||
}): Promise<T[]> {
|
||||
// Terminology:
|
||||
// ActiveGitFile: the current file path of some live file version.
|
||||
// FilePath: the path of some file in history.
|
||||
|
||||
const git = await this.plugin.getGit();
|
||||
// Gets all commits newer (>=) than the commit of the latest cached version.
|
||||
const timezoneAdjustedLogs = await runLog({
|
||||
const timeZoneAdjustedLogs = await runLog({
|
||||
abortSignal,
|
||||
filePath: activeGitFile,
|
||||
lowerBoundaryCommit: this.latestCachedVersion?.commitHash,
|
||||
maxCount: undefined,
|
||||
plugin: this.plugin,
|
||||
upperBoundaryCommit: undefined
|
||||
upperBoundaryCommit: undefined,
|
||||
git,
|
||||
renameDetectionStrictness: this.plugin.settings.renameDetectionStrictness,
|
||||
timeZone: getTimeZone(this.plugin)
|
||||
});
|
||||
|
||||
const extractedVersions = await extractLastCommitsForInterval({
|
||||
changelogGenerationSettings:
|
||||
this.plugin.settings.changelogGenerationSettings,
|
||||
dayStartHour: this.plugin.settings.dayStartHour,
|
||||
interval: this.getInterval(),
|
||||
timezoneAdjustedLogs
|
||||
timeZoneAdjustedLogs
|
||||
});
|
||||
|
||||
// Always recalculate the latest version in cached changelog (because it likely has outdated stats), but only if there are any versions to recalculate.
|
||||
|
|
@ -187,7 +201,7 @@ export abstract class ChangelogManager<T extends ChangelogEntry> {
|
|||
extractedVersions.push({
|
||||
filePath: versionBeforeLatestCached.getPotentialGitFilePath(),
|
||||
hash: versionBeforeLatestCached.commitHash,
|
||||
timezoneAdjustedDate: versionBeforeLatestCached.timezoneAdjustedDate
|
||||
timeZoneAdjustedDate: versionBeforeLatestCached.timeZoneAdjustedDate
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -202,8 +216,7 @@ export abstract class ChangelogManager<T extends ChangelogEntry> {
|
|||
|
||||
// If initial version was reached, diff it against an empty state.
|
||||
if (extractedVersions.length > 0 && this.cacheHasNoCompleteVersion()) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const lastCommit = extractedVersions.at(-1)!;
|
||||
const lastCommit = assertNotNull(extractedVersions.at(-1));
|
||||
await this.appendToEntries({
|
||||
abortSignal,
|
||||
currentCommit: lastCommit,
|
||||
|
|
@ -245,6 +258,10 @@ export abstract class ChangelogManager<T extends ChangelogEntry> {
|
|||
abortSignal: AbortSignal;
|
||||
filePath: string | undefined;
|
||||
}): Promise<void> {
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
|
||||
// If we are re-computing or initially loading we can't rely on the reserved entries because there aren't any (that are valid).
|
||||
if (this.visibleEntries === undefined) {
|
||||
await this.retrieveMoreEntries({
|
||||
|
|
@ -254,6 +271,7 @@ export abstract class ChangelogManager<T extends ChangelogEntry> {
|
|||
});
|
||||
} else {
|
||||
// If necessary, retrieve additional entries (if there are any left) and then append.
|
||||
// Check for sufficient versions when the task gets it's turn, not when it is added to the queue.
|
||||
if (this.shouldRetrieveMoreReserveEntries()) {
|
||||
await this.maybeRetrieveReserveEntries({ abortSignal });
|
||||
}
|
||||
|
|
@ -381,31 +399,34 @@ export abstract class ChangelogManager<T extends ChangelogEntry> {
|
|||
) {
|
||||
logCycles++;
|
||||
|
||||
const timezoneAdjustedLogs = await runLog({
|
||||
const git = await this.plugin.getGit();
|
||||
const timeZoneAdjustedLogs = await runLog({
|
||||
abortSignal,
|
||||
filePath: startingFilePath,
|
||||
lowerBoundaryCommit: undefined,
|
||||
maxCount: logMaxCount,
|
||||
plugin: this.plugin,
|
||||
git,
|
||||
renameDetectionStrictness:
|
||||
this.plugin.settings.renameDetectionStrictness,
|
||||
timeZone: getTimeZone(this.plugin),
|
||||
upperBoundaryCommit: startingCommit
|
||||
});
|
||||
|
||||
// All we need from a version is its latest commit, not all commits included in that interval
|
||||
const extractedVersions = await extractLastCommitsForInterval({
|
||||
changelogGenerationSettings:
|
||||
this.plugin.settings.changelogGenerationSettings,
|
||||
dayStartHour: this.plugin.settings.dayStartHour,
|
||||
interval,
|
||||
previouslySeenFullyAdjustedDates: fullyAdjustedSeenDates,
|
||||
timezoneAdjustedLogs
|
||||
timeZoneAdjustedLogs
|
||||
});
|
||||
|
||||
if (timezoneAdjustedLogs.length < logMaxCount) {
|
||||
if (timeZoneAdjustedLogs.length < logMaxCount) {
|
||||
reachedInitialCommit = true;
|
||||
}
|
||||
// If getting file changelog versions and need to loop many times, we need to track the file path across renames so that we can follow the target file across its whole history.
|
||||
startingFilePath = timezoneAdjustedLogs.at(-1)?.filePath;
|
||||
startingFilePath = timeZoneAdjustedLogs.at(-1)?.filePath;
|
||||
|
||||
startingCommit = timezoneAdjustedLogs.at(-1)?.hash;
|
||||
startingCommit = timeZoneAdjustedLogs.at(-1)?.hash;
|
||||
|
||||
lastCommitsInEachVersion.push(...extractedVersions);
|
||||
|
||||
|
|
@ -488,10 +509,12 @@ export abstract class ChangelogManager<T extends ChangelogEntry> {
|
|||
if (this.shouldRetrieveMoreReserveEntries()) {
|
||||
await this.retrieveMoreEntries({
|
||||
abortSignal,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
filePath: this.oldestCachedVersion!.getPotentialGitFilePath(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
upperBoundaryCommit: this.oldestCachedVersion!.commitHash
|
||||
|
||||
filePath: assertNotNull(
|
||||
this.oldestCachedVersion
|
||||
).getPotentialGitFilePath(),
|
||||
|
||||
upperBoundaryCommit: assertNotNull(this.oldestCachedVersion).commitHash
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -519,8 +542,7 @@ export abstract class ChangelogManager<T extends ChangelogEntry> {
|
|||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const oldestNewEntry = newEntries.at(-1)!;
|
||||
const oldestNewEntry = assertNotNull(newEntries.at(-1));
|
||||
// If updating the latest incomplete version, keep the isCollapsed state from current view. (File changelog entries aren't collapsible)
|
||||
if (
|
||||
oldestNewEntry instanceof VaultChangelogEntry &&
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { GitChangelogPluginTypes } from 'constants.ts';
|
||||
import type { FileChangelogEntry } from 'core/ChangelogEntry.svelte.ts';
|
||||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { GitChangelogPluginSettings } from 'settings/settings.ts';
|
||||
import type { ExtractPluginSettingsWrapper } from 'obsidian-dev-utils/obsidian/Plugin/PluginTypesBase';
|
||||
import type { GitChangelogSettings } from 'settings/settings.ts';
|
||||
import type { TaskManager } from 'TaskManager.svelte.ts';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { ChangelogInterval, FileLogEntry } from 'types.ts';
|
||||
|
|
@ -12,21 +14,18 @@ import {
|
|||
} from 'constants.ts';
|
||||
import { ChangelogManager } from 'core/ChangelogManager.svelte.ts';
|
||||
import { runFileDiff } from 'core/gitOperations/runFileDiff.ts';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { validateChangelogInterval } from 'settings/validation/changelogInterval.ts';
|
||||
import { deepEqual } from 'obsidian-dev-utils/Object';
|
||||
import { pickFileChangelogSettings } from 'settings/settings.ts';
|
||||
|
||||
export class FileChangelogManager extends ChangelogManager<FileChangelogEntry> {
|
||||
public constructor({
|
||||
// ActiveGitFile,
|
||||
plugin,
|
||||
taskManager
|
||||
}: {
|
||||
// ActiveGitFile?: string;
|
||||
plugin: GitChangelogPlugin;
|
||||
taskManager: TaskManager;
|
||||
}) {
|
||||
super({ plugin, taskManager });
|
||||
// This.cachedActiveGitFile = activeGitFile;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -47,23 +46,35 @@ export class FileChangelogManager extends ChangelogManager<FileChangelogEntry> {
|
|||
}
|
||||
|
||||
public override async setNextInterval(): Promise<void> {
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
await this.plugin.settingsManager.editAndSave(
|
||||
(settings: GitChangelogSettings): void => {
|
||||
settings.fileChangelogInterval = this.getNextInterval();
|
||||
},
|
||||
true
|
||||
);
|
||||
this.plugin.app.workspace.trigger(
|
||||
'git-changelog:file-changelog-generation-settings-changed'
|
||||
);
|
||||
}
|
||||
|
||||
newSettings.fileChangelogInterval = this.getNextInterval();
|
||||
public override specificSettingsChanged(
|
||||
oldSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>,
|
||||
newSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>
|
||||
): boolean {
|
||||
const oldVaultGenerationSettings = pickFileChangelogSettings(oldSettings);
|
||||
const newVaultGenerationSettings = pickFileChangelogSettings(newSettings);
|
||||
|
||||
await this.plugin.saveSettings(newSettings, false);
|
||||
return !deepEqual(oldVaultGenerationSettings, newVaultGenerationSettings);
|
||||
}
|
||||
|
||||
public override getInterval(
|
||||
settings: ReadonlyDeep<GitChangelogPluginSettings> = this.plugin.settings
|
||||
settings: ReadonlyDeep<GitChangelogSettings> = this.plugin.settings
|
||||
): ChangelogInterval {
|
||||
const interval = settings.fileChangelogInterval;
|
||||
|
||||
if (!validateChangelogInterval(interval)) {
|
||||
return DEFAULT_SETTINGS.fileChangelogInterval;
|
||||
}
|
||||
|
||||
return interval;
|
||||
return settings.fileChangelogInterval;
|
||||
}
|
||||
|
||||
protected override async loadEntries({
|
||||
|
|
@ -124,11 +135,17 @@ export class FileChangelogManager extends ChangelogManager<FileChangelogEntry> {
|
|||
newCommit: FileLogEntry;
|
||||
oldCommit?: FileLogEntry;
|
||||
}): Promise<FileChangelogEntry | undefined> {
|
||||
const git = await this.plugin.getGit();
|
||||
return await runFileDiff({
|
||||
abortSignal,
|
||||
newCommit,
|
||||
oldCommit,
|
||||
plugin: this.plugin
|
||||
plugin: this.plugin,
|
||||
diffAlgorithm: this.plugin.settings.diffAlgorithm,
|
||||
whitespaceIgnoreMode: this.plugin.settings.whitespaceIgnoreMode,
|
||||
ignoreBlankLines: this.plugin.settings.ignoreBlankLines,
|
||||
emptyTreeHash: await this.plugin.getEmptyTreeHash(),
|
||||
git
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { GitChangelogPluginTypes } from 'constants.ts';
|
||||
import type { VaultChangelogEntry } from 'core/ChangelogEntry.svelte.ts';
|
||||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { GitChangelogPluginSettings } from 'settings/settings.ts';
|
||||
import type { ExtractPluginSettingsWrapper } from 'obsidian-dev-utils/obsidian/Plugin/PluginTypesBase';
|
||||
import type { GitChangelogSettings } from 'settings/settings.ts';
|
||||
import type { TaskManager } from 'TaskManager.svelte.ts';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { ChangelogInterval, LogEntry } from 'types.ts';
|
||||
|
|
@ -12,8 +14,8 @@ import {
|
|||
} from 'constants.ts';
|
||||
import { ChangelogManager } from 'core/ChangelogManager.svelte.ts';
|
||||
import { runRepoDiff } from 'core/gitOperations/runRepoDiff.ts';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { validateChangelogInterval } from 'settings/validation/changelogInterval.ts';
|
||||
import { deepEqual } from 'obsidian-dev-utils/Object';
|
||||
import { pickVaultChangelogSettings } from 'settings/settings.ts';
|
||||
|
||||
export class VaultChangelogManager extends ChangelogManager<VaultChangelogEntry> {
|
||||
private collapseFirstVersion: boolean | undefined;
|
||||
|
|
@ -38,42 +40,44 @@ export class VaultChangelogManager extends ChangelogManager<VaultChangelogEntry>
|
|||
}
|
||||
|
||||
public override resetAndGetSignal(): AbortSignal {
|
||||
this.collapseFirstVersion = this.visibleEntries?.at(0)?.isCollapsed;
|
||||
const firstEntryExists = this.visibleEntries?.at(0);
|
||||
// Preserves the first entry collapsed state when users shuffles intervals and triggers multiple recomputes before any of them have the chance to finish.
|
||||
if (firstEntryExists) {
|
||||
this.collapseFirstVersion = firstEntryExists.isCollapsed;
|
||||
}
|
||||
return super.resetAndGetSignal();
|
||||
}
|
||||
|
||||
public override async setNextInterval(): Promise<void> {
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
|
||||
newSettings.vaultChangelogGenerationSettings.interval =
|
||||
this.getNextInterval();
|
||||
|
||||
await this.plugin.saveSettings(newSettings, false);
|
||||
await this.plugin.settingsManager.editAndSave(
|
||||
(settings: GitChangelogSettings): void => {
|
||||
settings.vaultChangelogInterval = this.getNextInterval();
|
||||
},
|
||||
true
|
||||
);
|
||||
this.plugin.app.workspace.trigger(
|
||||
'git-changelog:vault-changelog-generation-settings-changed'
|
||||
);
|
||||
}
|
||||
|
||||
public override generationSettingsChanged(
|
||||
oldSettings: ReadonlyDeep<GitChangelogPluginSettings>,
|
||||
newSettings: GitChangelogPluginSettings
|
||||
public override specificSettingsChanged(
|
||||
oldSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>,
|
||||
newSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>
|
||||
): boolean {
|
||||
if (
|
||||
oldSettings.vaultChangelogGenerationSettings.gitDiffIgnore !==
|
||||
newSettings.vaultChangelogGenerationSettings.gitDiffIgnore
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return super.generationSettingsChanged(oldSettings, newSettings);
|
||||
const oldVaultGenerationSettings = pickVaultChangelogSettings(oldSettings);
|
||||
const newVaultGenerationSettings = pickVaultChangelogSettings(newSettings);
|
||||
|
||||
return !deepEqual(oldVaultGenerationSettings, newVaultGenerationSettings);
|
||||
}
|
||||
|
||||
public override getInterval(
|
||||
settings: ReadonlyDeep<GitChangelogPluginSettings> = this.plugin.settings
|
||||
settings: ReadonlyDeep<GitChangelogSettings> = this.plugin.settings
|
||||
): ChangelogInterval {
|
||||
const interval = settings.vaultChangelogGenerationSettings.interval;
|
||||
|
||||
if (!validateChangelogInterval(interval)) {
|
||||
return DEFAULT_SETTINGS.vaultChangelogGenerationSettings.interval;
|
||||
}
|
||||
|
||||
return interval;
|
||||
return settings.vaultChangelogInterval;
|
||||
}
|
||||
|
||||
protected override calculateVersionsToAppend(resetCache: boolean): number {
|
||||
|
|
@ -91,11 +95,23 @@ export class VaultChangelogManager extends ChangelogManager<VaultChangelogEntry>
|
|||
newCommit: LogEntry;
|
||||
oldCommit?: LogEntry;
|
||||
}): Promise<undefined | VaultChangelogEntry> {
|
||||
const git = await this.plugin.getGit();
|
||||
return await runRepoDiff({
|
||||
abortSignal,
|
||||
newCommit,
|
||||
oldCommit,
|
||||
plugin: this.plugin
|
||||
plugin: this.plugin,
|
||||
git,
|
||||
diffAlgorithm: this.plugin.settings.diffAlgorithm,
|
||||
renameLimit: this.plugin.settings.renameLimit,
|
||||
renameDetectionStrictness: this.plugin.settings.renameDetectionStrictness,
|
||||
emptyTreeHash: await this.plugin.getEmptyTreeHash(),
|
||||
excludeFilesAndFoldersLines:
|
||||
this.plugin.settings.excludeFilesAndFoldersLines,
|
||||
enableExclusionList: this.plugin.settings.enableExclusionList,
|
||||
convertToIncludeList: this.plugin.settings.convertToIncludeList,
|
||||
whitespaceIgnoreMode: this.plugin.settings.whitespaceIgnoreMode,
|
||||
ignoreBlankLines: this.plugin.settings.ignoreBlankLines
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
86
src/core/findFirstFileCommitBefore.ts
Normal file
86
src/core/findFirstFileCommitBefore.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import type { SimpleGit } from 'simple-git';
|
||||
import type { LogEntry } from 'types.ts';
|
||||
|
||||
import { runLog } from 'core/gitOperations/runLog.ts';
|
||||
import spacetime from 'spacetime';
|
||||
import { AbortError } from 'types.ts';
|
||||
import { assertNotNull } from 'utils.ts';
|
||||
|
||||
// Assumes the average neighboring commits are 5 minutes apart
|
||||
const AVERAGE_COMMIT_FREQUENCY_MINUTES = 5;
|
||||
|
||||
export async function findFirstFileCommitBefore({
|
||||
abortSignal,
|
||||
filePath,
|
||||
minutes,
|
||||
timeZone,
|
||||
git,
|
||||
renameDetectionStrictness
|
||||
}: {
|
||||
abortSignal: AbortSignal;
|
||||
filePath: string;
|
||||
minutes: number;
|
||||
timeZone: string;
|
||||
git: SimpleGit;
|
||||
renameDetectionStrictness: number;
|
||||
}): Promise<LogEntry | undefined> {
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
const maxCount = Math.ceil(minutes / AVERAGE_COMMIT_FREQUENCY_MINUTES) + 20;
|
||||
|
||||
let firstEntriesOutsideInterval: LogEntry[] = [];
|
||||
let startingFilePath = filePath;
|
||||
const currentTime = spacetime.now(timeZone);
|
||||
let startingCommit: string | undefined;
|
||||
|
||||
while (
|
||||
// If it's the first run
|
||||
!firstEntriesOutsideInterval.at(-1) ||
|
||||
// Only continue if commit(s) that happened before the specified interval aren't reached yet.
|
||||
|
||||
assertNotNull(firstEntriesOutsideInterval.at(-1)).timeZoneAdjustedDate.diff(
|
||||
currentTime,
|
||||
|
||||
'minutes'
|
||||
) <
|
||||
// Not <=
|
||||
minutes
|
||||
) {
|
||||
firstEntriesOutsideInterval = await runLog({
|
||||
abortSignal,
|
||||
filePath: startingFilePath,
|
||||
lowerBoundaryCommit: undefined,
|
||||
maxCount,
|
||||
upperBoundaryCommit: startingCommit,
|
||||
git,
|
||||
renameDetectionStrictness,
|
||||
timeZone
|
||||
});
|
||||
|
||||
if (firstEntriesOutsideInterval.length < maxCount) {
|
||||
// Reached initial commit, can't go back any further, exit the loop.
|
||||
break;
|
||||
}
|
||||
|
||||
// If getting file changelog versions and need to loop many times, we need to track the file path across renames so that we can follow the target file across its whole history.
|
||||
|
||||
startingFilePath = assertNotNull(
|
||||
assertNotNull(firstEntriesOutsideInterval.at(-1)).filePath
|
||||
);
|
||||
|
||||
startingCommit = assertNotNull(firstEntriesOutsideInterval.at(-1)).hash;
|
||||
}
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
|
||||
// We just need to get the most recent commit that's still outside the interval.
|
||||
for (const entry of firstEntriesOutsideInterval) {
|
||||
if (entry.timeZoneAdjustedDate.diff(currentTime, 'minutes') >= minutes) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
// If all commits fall inside the interval, or file isn't in the repo
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -1,27 +1,44 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { LogResult, SimpleGit } from 'simple-git';
|
||||
import type { LogEntry } from 'types.ts';
|
||||
|
||||
import { getTimeZone } from 'settings/ui/CustomTimeZone.ts';
|
||||
import { getRenameDetectionStrictness } from 'settings/ui/RenameDetectionStrictnessSlider.ts';
|
||||
import spacetime from 'spacetime';
|
||||
import { AbortError } from 'types.ts';
|
||||
import { assertNotNull } from 'utils.ts';
|
||||
|
||||
/**
|
||||
* Returns the first commit before the specified date or the specified number of minutes.
|
||||
* If no commits are found, returns undefined.
|
||||
*
|
||||
* Either `minutes` or `isoString` must be provided.
|
||||
*
|
||||
* @param abortSignal - The abort signal to cancel the operation.
|
||||
* @param minutes - The number of minutes to look back from now.
|
||||
* @param isoString - The ISO string to look back from.
|
||||
* @param git - The SimpleGit instance.
|
||||
* @param timeZone - The time zone to adjust the commit date to.
|
||||
*/
|
||||
export async function findFirstCommitBefore({
|
||||
abortSignal,
|
||||
filePath,
|
||||
minutes,
|
||||
plugin
|
||||
isoString,
|
||||
git,
|
||||
timeZone
|
||||
}: {
|
||||
abortSignal: AbortSignal;
|
||||
filePath: string;
|
||||
minutes: number;
|
||||
plugin: GitChangelogPlugin;
|
||||
minutes?: number;
|
||||
isoString?: string;
|
||||
git: SimpleGit;
|
||||
|
||||
timeZone: string;
|
||||
}): Promise<LogEntry | undefined> {
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
|
||||
const options: Record<string, unknown> = {
|
||||
...(filePath ? { file: filePath } : {}),
|
||||
// "--no-patch": null,
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
'--before': `${minutes * 60 - 3} seconds ago`,
|
||||
'--before': isoString ?? `${assertNotNull(minutes) * 60 - 3} seconds ago`,
|
||||
'--diff-merges': 'first-parent',
|
||||
format: {
|
||||
date: '%cI',
|
||||
|
|
@ -32,27 +49,14 @@ export async function findFirstCommitBefore({
|
|||
strictDate: true
|
||||
};
|
||||
|
||||
if (filePath) {
|
||||
options['--name-status'] = null;
|
||||
// --name-only
|
||||
options['--follow'] = null;
|
||||
|
||||
const renameDetectionStrictness = getRenameDetectionStrictness(
|
||||
plugin.settings.changelogGenerationSettings
|
||||
);
|
||||
options['--find-renames'] = `${renameDetectionStrictness.toString()}%`;
|
||||
let result: LogResult;
|
||||
try {
|
||||
result = await git.log(options);
|
||||
} catch {
|
||||
// E.g. error when there are no commits in the repo
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
const git = await plugin.getGit();
|
||||
const result = await git.log(options);
|
||||
const timezone = getTimeZone(
|
||||
plugin.settings.changelogGenerationSettings,
|
||||
plugin
|
||||
);
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
|
|
@ -62,10 +66,11 @@ export async function findFirstCommitBefore({
|
|||
return undefined;
|
||||
}
|
||||
|
||||
return result.all.map<LogEntry>((entry) => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
filePath: entry.diff!.files.first()!.file,
|
||||
hash: entry.hash,
|
||||
timezoneAdjustedDate: spacetime(entry.date).goto(timezone)
|
||||
}))[0];
|
||||
return result.all
|
||||
.map<LogEntry>((entry) => ({
|
||||
filePath: undefined,
|
||||
hash: entry.hash,
|
||||
timeZoneAdjustedDate: spacetime(entry.date).goto(timeZone)
|
||||
}))
|
||||
.at(0);
|
||||
}
|
||||
|
|
|
|||
63
src/core/gitOperations/getCommitTimestamp.ts
Normal file
63
src/core/gitOperations/getCommitTimestamp.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import type { LogResult, SimpleGit } from 'simple-git';
|
||||
import type { LogEntry } from 'types.ts';
|
||||
|
||||
import spacetime from 'spacetime';
|
||||
import { AbortError } from 'types.ts';
|
||||
|
||||
/**
|
||||
* Returns undefined if an error happens during a git log. For example, passed a non-existent commit hash or if there are no commits in the repo.
|
||||
*/
|
||||
export async function getCommitTimestampOrUndefined({
|
||||
abortSignal,
|
||||
commitHash,
|
||||
git,
|
||||
timeZone
|
||||
}: {
|
||||
abortSignal: AbortSignal;
|
||||
commitHash?: string;
|
||||
git: SimpleGit;
|
||||
timeZone: string;
|
||||
}): Promise<LogEntry | undefined> {
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
|
||||
const options: Record<string, unknown> = {
|
||||
// "--no-patch": null,
|
||||
|
||||
'--diff-merges': 'first-parent',
|
||||
format: {
|
||||
date: '%cI',
|
||||
hash: '%H'
|
||||
},
|
||||
maxCount: 1,
|
||||
// Splitter: '\0',
|
||||
strictDate: true
|
||||
};
|
||||
if (commitHash) {
|
||||
options[commitHash] = null;
|
||||
}
|
||||
|
||||
let result: LogResult;
|
||||
try {
|
||||
result = await git.log(options);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (result?.latest == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
|
||||
return result.all
|
||||
.map<LogEntry>((entry) => ({
|
||||
filePath: undefined,
|
||||
hash: entry.hash,
|
||||
timeZoneAdjustedDate: spacetime(entry.date).goto(timeZone)
|
||||
}))
|
||||
.at(0);
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
|
||||
async function runHashObjectEmptyTree({
|
||||
plugin
|
||||
}: {
|
||||
plugin: GitChangelogPlugin;
|
||||
}): Promise<string> {
|
||||
const git = await plugin.getGit();
|
||||
|
||||
const emptyTreeHash = await git.raw([
|
||||
'hash-object',
|
||||
'-t',
|
||||
'tree',
|
||||
'/dev/null'
|
||||
]);
|
||||
|
||||
return emptyTreeHash.trim();
|
||||
}
|
||||
|
||||
export async function getEmptyTreeHash({
|
||||
plugin
|
||||
}: {
|
||||
plugin: GitChangelogPlugin;
|
||||
}): Promise<string> {
|
||||
if (plugin.emptyTreeHash) {
|
||||
return plugin.emptyTreeHash;
|
||||
}
|
||||
|
||||
const emptyTreeHash = await runHashObjectEmptyTree({ plugin });
|
||||
if (!plugin.emptyTreeHash) {
|
||||
plugin.emptyTreeHash = emptyTreeHash;
|
||||
}
|
||||
return plugin.emptyTreeHash;
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { DiffFile, FilesSummary } from 'types.ts';
|
||||
|
||||
import { normalizePath } from 'obsidian';
|
||||
import { getDiffAlgorithm } from 'settings/ui/DiffAlgorithmOptions.ts';
|
||||
import { DiffAlgorithm, DiffFileStatus } from 'types.ts';
|
||||
import { DiffAlgorithm, DiffFileStatus, WhitespaceIgnoreMode } from 'types.ts';
|
||||
import { getFileNameFromPath } from 'utils.ts';
|
||||
import { getDisplayExtensionFromPath } from 'Views/formatters.ts';
|
||||
|
||||
|
|
@ -16,17 +14,20 @@ export function addFileStatusToSummary(
|
|||
} else if (status === DiffFileStatus.Deleted) {
|
||||
file.deletedFiles++;
|
||||
} else if (isFileRenamedOrMoved(status)) {
|
||||
file.renamedFiles++;
|
||||
file.renamedAndMovedFiles++;
|
||||
} else {
|
||||
file.modifiedFiles++;
|
||||
}
|
||||
}
|
||||
|
||||
export function assignDiffAlgorithm(
|
||||
arguments_: string[],
|
||||
plugin: GitChangelogPlugin
|
||||
): void {
|
||||
switch (getDiffAlgorithm(plugin.settings.changelogGenerationSettings)) {
|
||||
export function assignDiffAlgorithm({
|
||||
arguments_,
|
||||
diffAlgorithm
|
||||
}: {
|
||||
arguments_: string[];
|
||||
diffAlgorithm: DiffAlgorithm;
|
||||
}): void {
|
||||
switch (diffAlgorithm) {
|
||||
case DiffAlgorithm.Default: {
|
||||
arguments_.push('--diff-algorithm=default');
|
||||
break;
|
||||
|
|
@ -43,6 +44,38 @@ export function assignDiffAlgorithm(
|
|||
}
|
||||
}
|
||||
|
||||
export function assignWhitespaceIgnoreSettings({
|
||||
arguments_,
|
||||
whitespaceIgnoreMode,
|
||||
ignoreBlankLines
|
||||
}: {
|
||||
arguments_: string[];
|
||||
whitespaceIgnoreMode: WhitespaceIgnoreMode;
|
||||
ignoreBlankLines: boolean;
|
||||
}): void {
|
||||
switch (whitespaceIgnoreMode) {
|
||||
case WhitespaceIgnoreMode.None: {
|
||||
break;
|
||||
}
|
||||
case WhitespaceIgnoreMode.SpaceAtEol: {
|
||||
arguments_.push('--ignore-space-at-eol');
|
||||
break;
|
||||
}
|
||||
case WhitespaceIgnoreMode.SpaceChange: {
|
||||
arguments_.push('--ignore-space-change');
|
||||
break;
|
||||
}
|
||||
case WhitespaceIgnoreMode.AllSpace: {
|
||||
arguments_.push('--ignore-all-space');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ignoreBlankLines) {
|
||||
arguments_.push('--ignore-blank-lines');
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateFileStatusRenamedOrMoved(
|
||||
oldPath: string,
|
||||
newPath: string
|
||||
|
|
@ -81,7 +114,7 @@ export function calculatePerFileTypeSummaries({
|
|||
addedFiles: 0,
|
||||
deletedFiles: 0,
|
||||
modifiedFiles: 0,
|
||||
renamedFiles: 0
|
||||
renamedAndMovedFiles: 0
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { SimpleGit } from 'simple-git';
|
||||
|
||||
/**
|
||||
* Doesn't work because of the way SimpleGit instance is set up in Git plugin.
|
||||
|
|
@ -6,16 +6,15 @@ import type GitChangelogPlugin from 'main.ts';
|
|||
export async function isAncestorOf({
|
||||
newCommit,
|
||||
oldCommit,
|
||||
plugin
|
||||
git
|
||||
}: {
|
||||
newCommit: string;
|
||||
oldCommit: string;
|
||||
plugin: GitChangelogPlugin;
|
||||
git: SimpleGit;
|
||||
}): Promise<boolean> {
|
||||
if (oldCommit === newCommit) {
|
||||
return true;
|
||||
}
|
||||
const git = await plugin.getGit();
|
||||
|
||||
const result = await git.raw([
|
||||
'merge-base',
|
||||
|
|
@ -23,7 +22,6 @@ export async function isAncestorOf({
|
|||
oldCommit,
|
||||
newCommit
|
||||
]);
|
||||
// Plugin.consoleDebug(result);
|
||||
|
||||
if (result === '1') {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,20 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { SimpleGit } from 'simple-git';
|
||||
|
||||
import { AbortError } from 'types.ts';
|
||||
|
||||
export async function runCheckIgnore({
|
||||
abortSignal,
|
||||
activeGitFile,
|
||||
plugin
|
||||
git
|
||||
}: {
|
||||
abortSignal: AbortSignal;
|
||||
activeGitFile: string;
|
||||
plugin: GitChangelogPlugin;
|
||||
git: SimpleGit;
|
||||
}): Promise<boolean> {
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
const gitCheckIgnoreResult = await plugin
|
||||
.getGitPlugin()
|
||||
.gitManager.git.checkIgnore(activeGitFile);
|
||||
const gitCheckIgnoreResult = await git.checkIgnore(activeGitFile);
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { FileLogEntry } from 'types.ts';
|
||||
import type { SimpleGit } from 'simple-git';
|
||||
import type {
|
||||
DiffAlgorithm,
|
||||
FileLogEntry,
|
||||
WhitespaceIgnoreMode
|
||||
} from 'types.ts';
|
||||
|
||||
import { FileChangelogEntry } from 'core/ChangelogEntry.svelte.ts';
|
||||
import { getEmptyTreeHash } from 'core/gitOperations/getEmptyTreeHash.ts';
|
||||
import {
|
||||
assignDiffAlgorithm,
|
||||
assignWhitespaceIgnoreSettings,
|
||||
calculateFileStatusRenamedOrMoved
|
||||
} from 'core/gitOperations/helper.ts';
|
||||
import { AbortError, DiffFileStatus } from 'types.ts';
|
||||
|
|
@ -13,19 +18,33 @@ export async function runFileDiff({
|
|||
abortSignal,
|
||||
newCommit,
|
||||
oldCommit,
|
||||
plugin
|
||||
plugin,
|
||||
git,
|
||||
diffAlgorithm,
|
||||
whitespaceIgnoreMode,
|
||||
ignoreBlankLines,
|
||||
emptyTreeHash
|
||||
}: {
|
||||
abortSignal: AbortSignal;
|
||||
newCommit: FileLogEntry;
|
||||
oldCommit?: FileLogEntry;
|
||||
plugin: GitChangelogPlugin;
|
||||
plugin?: GitChangelogPlugin;
|
||||
git: SimpleGit;
|
||||
diffAlgorithm: DiffAlgorithm;
|
||||
whitespaceIgnoreMode: WhitespaceIgnoreMode;
|
||||
ignoreBlankLines: boolean;
|
||||
emptyTreeHash: string;
|
||||
}): Promise<FileChangelogEntry | undefined> {
|
||||
const isInitialVersion =
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
|
||||
const oldVersionIsEmpty =
|
||||
oldCommit === undefined || oldCommit.fileDeleted === true;
|
||||
|
||||
if (isInitialVersion && newCommit.fileDeleted) {
|
||||
if (oldVersionIsEmpty && newCommit.fileDeleted) {
|
||||
// I assumed that the other should always be defined if one is undefined, since newCommit.hash is only undefined for commits where the file was deleted, and it can't get deleted if it didn't exist before, but these are statuses calculated from comparing neighboring commits, but we are diffing selected commits only, so maybe it's possible that we get in a situation where we compare some initial version commit (that isn't the actual initial commit, so that commit could be a deletion of that file, if a file was newly added and then deleted in the same interval) with an empty state
|
||||
plugin.consoleDebug(
|
||||
plugin?.consoleDebug(
|
||||
'oldCommit and newCommit are both undefined, assumption is wrong'
|
||||
);
|
||||
|
||||
|
|
@ -38,20 +57,23 @@ export async function runFileDiff({
|
|||
'--no-renames'
|
||||
// `--exit-code`,
|
||||
];
|
||||
assignDiffAlgorithm(numstatArguments, plugin);
|
||||
assignDiffAlgorithm({ arguments_: numstatArguments, diffAlgorithm });
|
||||
assignWhitespaceIgnoreSettings({
|
||||
arguments_: numstatArguments,
|
||||
whitespaceIgnoreMode,
|
||||
ignoreBlankLines
|
||||
});
|
||||
|
||||
// Only one of these can be true at the same time since we are returning early if they are both true.
|
||||
if (isInitialVersion || newCommit.fileDeleted) {
|
||||
const emptyTreeHash = await getEmptyTreeHash({ plugin });
|
||||
|
||||
if (oldVersionIsEmpty || newCommit.fileDeleted) {
|
||||
numstatArguments.push(
|
||||
emptyTreeHash,
|
||||
isInitialVersion ? newCommit.hash : oldCommit.hash,
|
||||
oldVersionIsEmpty ? newCommit.hash : oldCommit.hash,
|
||||
// This part is important. It tells git where is the explicit separation between revisions and the file path. Without it, git will not always be able to parse the file path correctly.
|
||||
'--',
|
||||
// We can pass oldCommit.filePath if the new commit is just a deletion of that file, meaning the file names are the same. We just inverse the result later to count the showed additions as deletions.
|
||||
|
||||
isInitialVersion ? newCommit.filePath : oldCommit.filePath
|
||||
oldVersionIsEmpty ? newCommit.filePath : oldCommit.filePath
|
||||
);
|
||||
} else {
|
||||
numstatArguments.push(
|
||||
|
|
@ -62,7 +84,7 @@ export async function runFileDiff({
|
|||
|
||||
let fileStatus: DiffFileStatus;
|
||||
|
||||
if (isInitialVersion) {
|
||||
if (oldVersionIsEmpty) {
|
||||
fileStatus = DiffFileStatus.Added;
|
||||
} else if (newCommit.fileDeleted) {
|
||||
fileStatus = DiffFileStatus.Deleted;
|
||||
|
|
@ -75,11 +97,6 @@ export async function runFileDiff({
|
|||
);
|
||||
}
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
|
||||
const git = await plugin.getGit();
|
||||
const diffNumstatResult = await git.diffSummary(numstatArguments);
|
||||
|
||||
if (
|
||||
|
|
@ -92,8 +109,8 @@ export async function runFileDiff({
|
|||
}
|
||||
|
||||
const textDiffStats =
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
(newCommit.binary ?? diffNumstatResult.files.at(0)!.binary)
|
||||
// If diff result shows no changes, there will be no entries in files array, so in that case, this will assign empty stats to textDiffStats instead of undefined
|
||||
diffNumstatResult.files.at(0)?.binary === true
|
||||
? undefined
|
||||
: {
|
||||
baseStats: {
|
||||
|
|
@ -111,7 +128,7 @@ export async function runFileDiff({
|
|||
pathGitRelative: newCommit.filePath, // Passing oldCommit.filePath or undefined for fileDeleted case could be more logical, but not compatible with use in git commands.
|
||||
status: fileStatus,
|
||||
textDiffStats,
|
||||
timezoneAdjustedDate: newCommit.timezoneAdjustedDate
|
||||
timeZoneAdjustedDate: newCommit.timeZoneAdjustedDate
|
||||
});
|
||||
return fileEntry;
|
||||
}
|
||||
|
|
|
|||
17
src/core/gitOperations/runHashObjectEmptyTree.ts
Normal file
17
src/core/gitOperations/runHashObjectEmptyTree.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { SimpleGit } from 'simple-git';
|
||||
|
||||
export async function runHashObjectEmptyTree({
|
||||
git
|
||||
}: {
|
||||
git: SimpleGit;
|
||||
}): Promise<string> {
|
||||
const emptyTreeHash = await git.raw([
|
||||
'hash-object',
|
||||
'-t',
|
||||
'tree',
|
||||
'/dev/null'
|
||||
]);
|
||||
|
||||
// Trimming is required.
|
||||
return emptyTreeHash.trim();
|
||||
}
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { DiffResultNameStatusFile } from 'simple-git';
|
||||
import type { SimpleGit } from 'simple-git';
|
||||
import type { LogEntry } from 'types.ts';
|
||||
|
||||
import { unescapeGitFileOutput } from 'core/gitOperations/helper.ts';
|
||||
import { getTimeZone } from 'settings/ui/CustomTimeZone.ts';
|
||||
import { getRenameDetectionStrictness } from 'settings/ui/RenameDetectionStrictnessSlider.ts';
|
||||
import { DiffNameStatus } from 'simple-git';
|
||||
import spacetime from 'spacetime';
|
||||
import { AbortError } from 'types.ts';
|
||||
import { assertNotNull } from 'utils.ts';
|
||||
|
||||
import type { DiffResultNameStatusFile } from './simpleGitTypes.ts';
|
||||
|
||||
import { DiffNameStatus } from './simpleGitTypes.ts';
|
||||
|
||||
// Less efficient than running raw?
|
||||
export async function runLog({
|
||||
|
|
@ -15,16 +16,23 @@ export async function runLog({
|
|||
filePath,
|
||||
lowerBoundaryCommit,
|
||||
maxCount,
|
||||
plugin,
|
||||
upperBoundaryCommit
|
||||
upperBoundaryCommit,
|
||||
timeZone,
|
||||
git,
|
||||
renameDetectionStrictness
|
||||
}: {
|
||||
abortSignal: AbortSignal;
|
||||
filePath: string | undefined;
|
||||
lowerBoundaryCommit: string | undefined;
|
||||
maxCount?: number;
|
||||
plugin: GitChangelogPlugin;
|
||||
upperBoundaryCommit: string | undefined;
|
||||
git: SimpleGit;
|
||||
timeZone: string;
|
||||
renameDetectionStrictness: number;
|
||||
}): Promise<LogEntry[]> {
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
// This is confusing, and could be accidentally broken in the future
|
||||
const retrievingNewLogs = lowerBoundaryCommit !== undefined;
|
||||
const options: Record<string, unknown> = {
|
||||
|
|
@ -44,10 +52,10 @@ export async function runLog({
|
|||
options['--diff-merges'] = 'first-parent';
|
||||
options['--name-status'] = null;
|
||||
|
||||
// --name-only
|
||||
// `--follow` does not work well on non-linear history. It does not work for files that were just renamed in the working directory but haven't been committed yet. It needs commit information to track renames.
|
||||
// This problem can be solved by running a separate git diff name-status command before running git log, to detect potential renames and get the last committed filename of the current file, but the performance impact is not worth it.
|
||||
options['--follow'] = null;
|
||||
options['--find-renames'] =
|
||||
`${getRenameDetectionStrictness(plugin.settings.changelogGenerationSettings)}%`;
|
||||
options['--find-renames'] = `${renameDetectionStrictness}%`;
|
||||
}
|
||||
|
||||
if (upperBoundaryCommit) {
|
||||
|
|
@ -56,23 +64,17 @@ export async function runLog({
|
|||
options['--boundary'] = null;
|
||||
options[`${lowerBoundaryCommit}..HEAD`] = null;
|
||||
}
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
const git = await plugin.getGit();
|
||||
|
||||
const result = await git.log(options);
|
||||
const timezone = getTimeZone(
|
||||
plugin.settings.changelogGenerationSettings,
|
||||
plugin
|
||||
);
|
||||
|
||||
const logs: LogEntry[] = [];
|
||||
for (const entry of result.all) {
|
||||
let fileDeleted: boolean | undefined;
|
||||
|
||||
if (filePath) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const file = entry.diff!.files.at(0) as DiffResultNameStatusFile;
|
||||
const file = assertNotNull(entry.diff).files.at(
|
||||
0
|
||||
) as DiffResultNameStatusFile;
|
||||
|
||||
// Include X (Unknown) statuses and show error states instead of silently ignoring them. (Caused by repository corruption or other issues)
|
||||
//
|
||||
|
|
@ -98,14 +100,13 @@ export async function runLog({
|
|||
|
||||
logs.push({
|
||||
filePath: filePath
|
||||
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
unescapeGitFileOutput(entry.diff!.files.at(0)!.file)
|
||||
? unescapeGitFileOutput(
|
||||
assertNotNull(assertNotNull(entry.diff).files.at(0)).file
|
||||
)
|
||||
: undefined,
|
||||
hash: entry.hash,
|
||||
fileDeleted,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
binary: filePath ? entry.diff!.files.at(0)!.binary : undefined,
|
||||
timezoneAdjustedDate: spacetime(entry.date).goto(timezone)
|
||||
timeZoneAdjustedDate: spacetime(entry.date).goto(timeZone)
|
||||
});
|
||||
}
|
||||
return logs;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,25 @@
|
|||
/* eslint-disable no-magic-numbers */
|
||||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { DiffFile, FilesSummary, LogEntry, TextDiffFile } from 'types.ts';
|
||||
import type { SimpleGit } from 'simple-git';
|
||||
import type {
|
||||
DiffAlgorithm,
|
||||
DiffFile,
|
||||
FilesSummary,
|
||||
LogEntry,
|
||||
TextDiffFile,
|
||||
WhitespaceIgnoreMode
|
||||
} from 'types.ts';
|
||||
|
||||
import { VaultChangelogEntry } from 'core/ChangelogEntry.svelte.ts';
|
||||
import { getEmptyTreeHash } from 'core/gitOperations/getEmptyTreeHash.ts';
|
||||
import {
|
||||
addFileStatusToSummary,
|
||||
assignDiffAlgorithm,
|
||||
assignWhitespaceIgnoreSettings,
|
||||
calculateFileStatusRenamedOrMoved
|
||||
} from 'core/gitOperations/helper.ts';
|
||||
import { convertGitIgnoreToPathspec } from 'settings/ui/ExcludeFilesAndFolders.ts';
|
||||
import { getRenameLimit } from 'settings/ui/RenameDetectionFileLimit.ts';
|
||||
import { getRenameDetectionStrictness } from 'settings/ui/RenameDetectionStrictnessSlider.ts';
|
||||
import { AbortError, DiffFileStatus } from 'types.ts';
|
||||
import { insertSorted, parseContentChange } from 'utils.ts';
|
||||
import { assertNotNull, insertSorted, parseContentChange } from 'utils.ts';
|
||||
|
||||
import { runRepoDiffStatus } from './runRepoDiffStatus.ts';
|
||||
|
||||
|
|
@ -53,61 +59,94 @@ export function compareTextFiles(
|
|||
return leftFile.pathGitRelative.localeCompare(rightFile.pathGitRelative);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
export async function runRepoDiff({
|
||||
abortSignal,
|
||||
newCommit,
|
||||
oldCommit,
|
||||
plugin
|
||||
git,
|
||||
plugin,
|
||||
excludeFilesAndFoldersLines,
|
||||
convertToIncludeList,
|
||||
renameLimit,
|
||||
renameDetectionStrictness,
|
||||
diffAlgorithm,
|
||||
whitespaceIgnoreMode,
|
||||
ignoreBlankLines,
|
||||
emptyTreeHash,
|
||||
enableExclusionList
|
||||
}: {
|
||||
abortSignal: AbortSignal;
|
||||
newCommit: LogEntry;
|
||||
oldCommit?: LogEntry;
|
||||
plugin: GitChangelogPlugin;
|
||||
diffAlgorithm: DiffAlgorithm;
|
||||
git: SimpleGit;
|
||||
excludeFilesAndFoldersLines: readonly string[];
|
||||
enableExclusionList: boolean;
|
||||
plugin?: GitChangelogPlugin;
|
||||
convertToIncludeList: boolean;
|
||||
renameLimit: number;
|
||||
emptyTreeHash: string;
|
||||
renameDetectionStrictness: number;
|
||||
ignoreBlankLines: boolean;
|
||||
whitespaceIgnoreMode: WhitespaceIgnoreMode;
|
||||
}): Promise<undefined | VaultChangelogEntry> {
|
||||
if (newCommit === undefined) {
|
||||
plugin.consoleDebug('newCommit is undefined');
|
||||
plugin?.consoleDebug('newCommit is undefined');
|
||||
}
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
|
||||
const pathSpec = convertGitIgnoreToPathspec(plugin);
|
||||
const pathSpec = enableExclusionList
|
||||
? convertGitIgnoreToPathspec(
|
||||
excludeFilesAndFoldersLines,
|
||||
convertToIncludeList
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const numstatArguments = [
|
||||
'--numstat',
|
||||
`-l${getRenameLimit(plugin.settings.changelogGenerationSettings)}`,
|
||||
`--find-renames=${getRenameDetectionStrictness(plugin.settings.changelogGenerationSettings)}%`,
|
||||
`-l${renameLimit}`,
|
||||
`--find-renames=${renameDetectionStrictness}%`,
|
||||
'--color-moved=no',
|
||||
// Don't use empty files as rename candidates. If you delete any empty file and add a new empty file, that file will be considered a rename unless this flag is used.
|
||||
'--no-rename-empty',
|
||||
'-z'
|
||||
];
|
||||
assignDiffAlgorithm(numstatArguments, plugin);
|
||||
assignDiffAlgorithm({ arguments_: numstatArguments, diffAlgorithm });
|
||||
assignWhitespaceIgnoreSettings({
|
||||
arguments_: numstatArguments,
|
||||
whitespaceIgnoreMode,
|
||||
ignoreBlankLines
|
||||
});
|
||||
|
||||
let statusResult: Record<string, DiffFileStatus> | undefined;
|
||||
|
||||
if (oldCommit === undefined) {
|
||||
const emptyTreeHash = await getEmptyTreeHash({ plugin });
|
||||
numstatArguments.push(emptyTreeHash, newCommit.hash);
|
||||
} else {
|
||||
statusResult = await runRepoDiffStatus({
|
||||
newCommit: newCommit.hash,
|
||||
oldCommit: oldCommit.hash,
|
||||
pathSpec,
|
||||
plugin
|
||||
plugin,
|
||||
abortSignal,
|
||||
git
|
||||
});
|
||||
numstatArguments.push(oldCommit.hash, newCommit.hash);
|
||||
}
|
||||
|
||||
if (pathSpec.length > 0) {
|
||||
if (pathSpec && pathSpec.length > 0) {
|
||||
numstatArguments.push('--', ...pathSpec);
|
||||
}
|
||||
|
||||
const diffNumstatResult = await git.diff(numstatArguments);
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
const git = await plugin.getGit();
|
||||
const diffNumstatResult = await git.diff(numstatArguments);
|
||||
|
||||
const records = diffNumstatResult.split('\0').filter((token) => token !== '');
|
||||
|
||||
|
|
@ -119,13 +158,13 @@ export async function runRepoDiff({
|
|||
addedFiles: 0,
|
||||
deletedFiles: 0,
|
||||
modifiedFiles: 0,
|
||||
renamedFiles: 0
|
||||
renamedAndMovedFiles: 0
|
||||
};
|
||||
const binaryFilesSummary: FilesSummary = {
|
||||
addedFiles: 0,
|
||||
deletedFiles: 0,
|
||||
modifiedFiles: 0,
|
||||
renamedFiles: 0
|
||||
renamedAndMovedFiles: 0
|
||||
};
|
||||
|
||||
const textFiles: DiffFile[] = [];
|
||||
|
|
@ -164,13 +203,11 @@ export async function runRepoDiff({
|
|||
status = DiffFileStatus.Added;
|
||||
} else if (oldPath) {
|
||||
if (typeof oldPath !== 'string') {
|
||||
plugin.consoleDebug('oldPath is not a string!', oldPath);
|
||||
plugin?.consoleDebug('oldPath is not a string', oldPath);
|
||||
}
|
||||
status = calculateFileStatusRenamedOrMoved(oldPath, filePath);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
} else if (statusResult![filePath]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
status = statusResult![filePath];
|
||||
} else if (assertNotNull(statusResult)[filePath]) {
|
||||
status = assertNotNull(statusResult)[filePath];
|
||||
} else {
|
||||
status = DiffFileStatus.Modified;
|
||||
}
|
||||
|
|
@ -207,10 +244,10 @@ export async function runRepoDiff({
|
|||
binaryFiles,
|
||||
binaryFilesSummaryCached: binaryFilesSummary,
|
||||
commitHash: newCommit.hash,
|
||||
previousDayLastCommitHash: oldCommit?.hash,
|
||||
previousVersionLastCommitHash: oldCommit?.hash,
|
||||
textFiles,
|
||||
textFilesSummaryCached: textFilesSummary,
|
||||
timezoneAdjustedDate: newCommit.timezoneAdjustedDate
|
||||
timeZoneAdjustedDate: newCommit.timeZoneAdjustedDate
|
||||
});
|
||||
|
||||
return dayEntry;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { DiffResultNameStatusFile } from 'simple-git';
|
||||
import type { SimpleGit } from 'simple-git';
|
||||
|
||||
import { DiffNameStatus } from 'simple-git';
|
||||
import { DiffFileStatus } from 'types.ts';
|
||||
import { AbortError, DiffFileStatus } from 'types.ts';
|
||||
|
||||
import type { DiffResultNameStatusFile } from './simpleGitTypes.ts';
|
||||
|
||||
import { DiffNameStatus } from './simpleGitTypes.ts';
|
||||
|
||||
/**
|
||||
* Check the number of changed files and if they are added, modified or deleted. this function is needed because git diff --numstat doesn't say if a file is added or deleted
|
||||
|
|
@ -11,13 +14,21 @@ export async function runRepoDiffStatus({
|
|||
newCommit,
|
||||
oldCommit,
|
||||
pathSpec,
|
||||
plugin
|
||||
git,
|
||||
plugin,
|
||||
abortSignal
|
||||
}: {
|
||||
newCommit: string;
|
||||
oldCommit?: string;
|
||||
pathSpec: string[];
|
||||
plugin: GitChangelogPlugin;
|
||||
pathSpec: string[] | undefined;
|
||||
plugin?: GitChangelogPlugin;
|
||||
abortSignal: AbortSignal;
|
||||
git: SimpleGit;
|
||||
}): Promise<Record<string, DiffFileStatus> | undefined> {
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
|
||||
if (oldCommit === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -28,14 +39,17 @@ export async function runRepoDiffStatus({
|
|||
// Turns off rename detection, even when the configuration file gives the default to run rename detection.
|
||||
'--no-renames'
|
||||
];
|
||||
if (pathSpec.length > 0) {
|
||||
|
||||
// We don't have to assign the DiffAlgorithm and the WhitespaceIgnoreMode settings because the set that this function returns is just going to be used as a helper set in the runRepoDiff function for assigning an added or deleted file status to changed files that were detected in runRepoDiff using all the proper settings.
|
||||
// If after applying the diff settings, some changed files in runRepoDiff become identical, the runRepoDiff function will simply skip those, and those same files that were detected as changes in this function are never going to be accessed, so they can't return incorrect file statuses.
|
||||
// Also, crossing the rename threshold because of diff settings mismatch isn't a concern since this function isn't used to determine renamed file statuses.
|
||||
|
||||
if (pathSpec && pathSpec.length > 0) {
|
||||
diffStatusArguments.push('--', ...pathSpec);
|
||||
}
|
||||
const git = await plugin.getGit();
|
||||
const diffStatusResult = await git.diffSummary(diffStatusArguments);
|
||||
|
||||
const changedFilesMap: Record<string, DiffFileStatus> = {};
|
||||
|
||||
for (const file of diffStatusResult.files as DiffResultNameStatusFile[]) {
|
||||
switch (file.status) {
|
||||
case DiffNameStatus.ADDED: {
|
||||
|
|
@ -54,7 +68,7 @@ export async function runRepoDiffStatus({
|
|||
file.status ?? DiffNameStatus.MODIFIED
|
||||
)
|
||||
) {
|
||||
plugin.consoleDebug(
|
||||
plugin?.consoleDebug(
|
||||
`Unexpected file status of ${file.file}:`,
|
||||
file.status
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
/* eslint-disable unicorn/prevent-abbreviations */
|
||||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { LogEntry, TextDiffBaseStats } from 'types.ts';
|
||||
import type { SimpleGit } from 'simple-git';
|
||||
import type {
|
||||
DiffAlgorithm,
|
||||
LogEntry,
|
||||
TextDiffBaseStats,
|
||||
WhitespaceIgnoreMode
|
||||
} from 'types.ts';
|
||||
|
||||
import { assignDiffAlgorithm } from 'core/gitOperations/helper.ts';
|
||||
import {
|
||||
assignDiffAlgorithm,
|
||||
assignWhitespaceIgnoreSettings
|
||||
} from 'core/gitOperations/helper.ts';
|
||||
import { AbortError } from 'types.ts';
|
||||
import { parseContentChange } from 'utils.ts';
|
||||
|
||||
/**
|
||||
* Used for status bar stats.
|
||||
|
|
@ -12,45 +19,52 @@ import { parseContentChange } from 'utils.ts';
|
|||
export async function runWorkingDirFileDiff({
|
||||
abortSignal,
|
||||
oldCommit,
|
||||
plugin
|
||||
activeGitFile,
|
||||
git,
|
||||
diffAlgorithm,
|
||||
whitespaceIgnoreMode,
|
||||
ignoreBlankLines
|
||||
}: {
|
||||
diffAlgorithm: DiffAlgorithm;
|
||||
abortSignal: AbortSignal;
|
||||
oldCommit: LogEntry;
|
||||
plugin: GitChangelogPlugin;
|
||||
activeGitFile: string;
|
||||
git: SimpleGit;
|
||||
whitespaceIgnoreMode: WhitespaceIgnoreMode;
|
||||
ignoreBlankLines: boolean;
|
||||
}): Promise<TextDiffBaseStats | undefined> {
|
||||
const numstatArguments = [
|
||||
'--numstat',
|
||||
'--color-moved=no',
|
||||
'-z',
|
||||
'--no-renames',
|
||||
oldCommit.hash,
|
||||
`${oldCommit.filePath}`
|
||||
];
|
||||
|
||||
assignDiffAlgorithm(numstatArguments, plugin);
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
|
||||
const git = await plugin.getGit();
|
||||
const diffNumstatResult = await git.diff(numstatArguments);
|
||||
// Status bar calculations should handle cases of undefined oldCommit and oldCommit.fileDeleted === true before reaching this function
|
||||
|
||||
const parts = diffNumstatResult.split('\t');
|
||||
const addedString = parts[0];
|
||||
const deletedString = parts[1];
|
||||
const numstatArguments = ['--numstat', '--color-moved=no', '--no-renames'];
|
||||
|
||||
// Determine if this is a binary file or submodule.
|
||||
const isBinary = addedString === '-' && deletedString === '-';
|
||||
// Must come before the commit hashes and file paths
|
||||
assignDiffAlgorithm({ arguments_: numstatArguments, diffAlgorithm });
|
||||
assignWhitespaceIgnoreSettings({
|
||||
arguments_: numstatArguments,
|
||||
whitespaceIgnoreMode,
|
||||
ignoreBlankLines
|
||||
});
|
||||
|
||||
// Parse numeric values for text files.
|
||||
const textDiffStats = isBinary
|
||||
? undefined
|
||||
: parseContentChange({ addedStr: addedString, deletedStr: deletedString });
|
||||
numstatArguments.push(
|
||||
`${oldCommit.hash}:${oldCommit.filePath}`,
|
||||
activeGitFile
|
||||
);
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
const diffNumstatResult = await git.diffSummary(numstatArguments);
|
||||
|
||||
const textDiffStats =
|
||||
diffNumstatResult.files.at(0)?.binary === true
|
||||
? undefined
|
||||
: {
|
||||
baseStats: {
|
||||
additions: diffNumstatResult.insertions,
|
||||
deletions: diffNumstatResult.deletions
|
||||
}
|
||||
};
|
||||
|
||||
return textDiffStats?.baseStats;
|
||||
}
|
||||
|
|
|
|||
25
src/core/gitOperations/simpleGitTypes.ts
Normal file
25
src/core/gitOperations/simpleGitTypes.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export enum DiffNameStatus {
|
||||
ADDED = 'A',
|
||||
COPIED = 'C',
|
||||
DELETED = 'D',
|
||||
MODIFIED = 'M',
|
||||
RENAMED = 'R',
|
||||
CHANGED = 'T',
|
||||
UNMERGED = 'U',
|
||||
UNKNOWN = 'X',
|
||||
BROKEN = 'B'
|
||||
}
|
||||
|
||||
export interface DiffResultNameStatusFile extends DiffResultTextFile {
|
||||
status?: DiffNameStatus;
|
||||
from?: string;
|
||||
similarity: number;
|
||||
}
|
||||
|
||||
export interface DiffResultTextFile {
|
||||
file: string;
|
||||
changes: number;
|
||||
insertions: number;
|
||||
deletions: number;
|
||||
binary: false;
|
||||
}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
import type { ChangelogGenerationSettings } from 'settings/settings.ts';
|
||||
import type { GitChangelogPluginTypes } from 'constants.ts';
|
||||
import type { ExtractPluginSettingsWrapper } from 'obsidian-dev-utils/obsidian/Plugin/PluginTypesBase';
|
||||
import type { Spacetime, TimeUnit } from 'spacetime';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { ChangelogInterval, FilesSummary, LogEntry } from 'types.ts';
|
||||
|
||||
import { getDayStartTime } from 'settings/ui/DayStartTime.ts';
|
||||
import { applyDayStartTimeSetting } from 'timeUtils.ts';
|
||||
import { deepEqual } from 'obsidian-dev-utils/Object';
|
||||
import { pickGeneralChangelogSettings } from 'settings/settings.ts';
|
||||
import { applyDayStartHourSetting } from 'timeUtils.ts';
|
||||
|
||||
export const GIT_MAX_CONCURRENT_PROCESSES = 6;
|
||||
|
||||
|
|
@ -13,7 +16,7 @@ export function addStatsToSummary(
|
|||
): void {
|
||||
mainSummary.addedFiles += stats.addedFiles;
|
||||
mainSummary.deletedFiles += stats.deletedFiles;
|
||||
mainSummary.renamedFiles += stats.renamedFiles;
|
||||
mainSummary.renamedAndMovedFiles += stats.renamedAndMovedFiles;
|
||||
mainSummary.modifiedFiles += stats.modifiedFiles;
|
||||
}
|
||||
|
||||
|
|
@ -68,24 +71,24 @@ export async function isSameAsync({
|
|||
}
|
||||
|
||||
export async function extractLastCommitsForInterval({
|
||||
changelogGenerationSettings,
|
||||
interval,
|
||||
previouslySeenFullyAdjustedDates,
|
||||
timezoneAdjustedLogs
|
||||
timeZoneAdjustedLogs,
|
||||
dayStartHour
|
||||
}: {
|
||||
changelogGenerationSettings: ChangelogGenerationSettings;
|
||||
interval: ChangelogInterval;
|
||||
previouslySeenFullyAdjustedDates?: Set<Spacetime>;
|
||||
timezoneAdjustedLogs: LogEntry[];
|
||||
timeZoneAdjustedLogs: LogEntry[];
|
||||
dayStartHour: number;
|
||||
}): Promise<LogEntry[]> {
|
||||
const lastCommitsInEachInterval: LogEntry[] = [];
|
||||
const fullyAdjustedSeenDates =
|
||||
previouslySeenFullyAdjustedDates ?? new Set<Spacetime>();
|
||||
|
||||
for (const log of timezoneAdjustedLogs) {
|
||||
const fullyAdjustedLogDate = applyDayStartTimeSetting({
|
||||
dayStartTime: getDayStartTime(changelogGenerationSettings),
|
||||
timezoneAdjustedDate: log.timezoneAdjustedDate
|
||||
for (const log of timeZoneAdjustedLogs) {
|
||||
const fullyAdjustedLogDate = applyDayStartHourSetting({
|
||||
dayStartHour,
|
||||
timeZoneAdjustedDate: log.timeZoneAdjustedDate
|
||||
});
|
||||
|
||||
const logDateAlreadySeen = await dateAlreadySeen({
|
||||
|
|
@ -102,3 +105,24 @@ export async function extractLastCommitsForInterval({
|
|||
|
||||
return lastCommitsInEachInterval;
|
||||
}
|
||||
|
||||
export function changelogGenerationSettingsChanged({
|
||||
newSettings,
|
||||
oldSettings
|
||||
}: {
|
||||
newSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>;
|
||||
oldSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>;
|
||||
}): boolean {
|
||||
const oldVaultGenerationSettings = pickGeneralChangelogSettings(oldSettings);
|
||||
const newVaultGenerationSettings = pickGeneralChangelogSettings(newSettings);
|
||||
|
||||
// IsAncestor run
|
||||
|
||||
// Don't have to check if the detected system time zone or detected locale changed since they're only assigned once at startup.
|
||||
|
||||
return !deepEqual(oldVaultGenerationSettings, newVaultGenerationSettings);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,25 +62,5 @@ declare module 'obsidian' {
|
|||
callback: () => void,
|
||||
context?: unknown
|
||||
): EventRef;
|
||||
|
||||
on(
|
||||
name: 'obsidian-git:menu',
|
||||
callback: (
|
||||
menu: Menu,
|
||||
path: string,
|
||||
source: string,
|
||||
leaf?: WorkspaceLeaf
|
||||
) => unknown,
|
||||
context?: unknown
|
||||
): EventRef;
|
||||
trigger(name: string, ...data: unknown[]): void;
|
||||
|
||||
trigger(
|
||||
name: 'obsidian-git:menu',
|
||||
menu: Menu,
|
||||
path: string,
|
||||
source: string,
|
||||
leaf?: WorkspaceLeaf
|
||||
): void;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
286
src/menu.ts
Normal file
286
src/menu.ts
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { WorkspaceLeaf } from 'obsidian';
|
||||
import type { GitChangelogSettings } from 'settings/settings.ts';
|
||||
|
||||
import { COPY_COMMIT_HASH_ICON, PLUGIN_NAME_SENTENCE_CASE } from 'constants.ts';
|
||||
import { Menu, TFolder } from 'obsidian';
|
||||
import {
|
||||
convertPathToGitIgnoreRule,
|
||||
parseGitIgnoreLine
|
||||
} from 'settings/ui/ExcludeFilesAndFolders.ts';
|
||||
import { assertNotNull } from 'utils.ts';
|
||||
import { fileOpenableInObsidian } from 'Views/helper.ts';
|
||||
import { VAULT_CHANGELOG_VIEW_CONFIG } from 'Views/VaultChangelog/VaultChangelog.ts';
|
||||
|
||||
export function addContextMenuItems(plugin: GitChangelogPlugin): void {
|
||||
plugin.registerEvent(
|
||||
plugin.app.workspace.on('file-menu', (menu, file) => {
|
||||
const gitRelativePath = plugin
|
||||
.getGitPlugin()
|
||||
.gitManager.getRelativeRepoPath(file.path, true);
|
||||
|
||||
addExcludeMenuItem({
|
||||
menu,
|
||||
isFolder: file instanceof TFolder,
|
||||
gitRelativePath,
|
||||
plugin
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
plugin.registerEvent(
|
||||
plugin.app.workspace.on(
|
||||
'git-changelog:menu',
|
||||
(menu, inFileMenu, gitRelativePath, commitHash) => {
|
||||
handleChangelogViewContextMenu({
|
||||
menu,
|
||||
inFileMenu,
|
||||
gitRelativePath,
|
||||
commitHash,
|
||||
plugin
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function mayTriggerChangelogMenu({
|
||||
event,
|
||||
gitRelativePath,
|
||||
commitHash,
|
||||
view,
|
||||
plugin
|
||||
}: {
|
||||
event: MouseEvent;
|
||||
gitRelativePath?: string;
|
||||
commitHash?: string;
|
||||
view: WorkspaceLeaf;
|
||||
plugin: GitChangelogPlugin;
|
||||
}): void {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (event.button == 2) {
|
||||
const fileMenu = new Menu();
|
||||
let showFileMenu = false;
|
||||
// Check if it's a file right click.
|
||||
if (gitRelativePath !== undefined) {
|
||||
const vaultRelativePath = plugin
|
||||
.getGitPlugin()
|
||||
.gitManager.getRelativeVaultPath(gitRelativePath);
|
||||
const tFile = plugin.app.vault.getAbstractFileByPath(vaultRelativePath);
|
||||
|
||||
// If the target file is a file that currently exists in the vault, open the usual file menu.
|
||||
showFileMenu = fileOpenableInObsidian({
|
||||
relativeVaultPath: tFile?.path,
|
||||
plugin
|
||||
});
|
||||
if (showFileMenu) {
|
||||
plugin.app.workspace.trigger(
|
||||
'file-menu',
|
||||
fileMenu,
|
||||
assertNotNull(tFile),
|
||||
view.view.getViewType(),
|
||||
view
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Append the git-changelog menu items to the file menu.
|
||||
plugin.app.workspace.trigger(
|
||||
'git-changelog:menu',
|
||||
fileMenu,
|
||||
showFileMenu,
|
||||
gitRelativePath,
|
||||
commitHash
|
||||
);
|
||||
fileMenu.showAtPosition({ x: event.pageX, y: event.pageY });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the path is a relative path that goes above the root of the repository.
|
||||
*
|
||||
* If it starts with any number of /../, that means it's above the root.
|
||||
*
|
||||
* This is parsing the output of getRelativeRepoPath function.
|
||||
*
|
||||
* "../" is acceptable. (Non-absolute path)
|
||||
*/
|
||||
function isAbsoluteGitIgnoreRuleAboveRoot({
|
||||
absoluteGitIgnoreRule
|
||||
}: {
|
||||
absoluteGitIgnoreRule: string;
|
||||
}): boolean {
|
||||
return (
|
||||
absoluteGitIgnoreRule.startsWith('/../') ||
|
||||
// Case when path is the root folder itself
|
||||
absoluteGitIgnoreRule.startsWith('//')
|
||||
);
|
||||
}
|
||||
|
||||
export function addExcludeMenuItem({
|
||||
menu,
|
||||
isFolder,
|
||||
plugin,
|
||||
gitRelativePath
|
||||
}: {
|
||||
menu: Menu;
|
||||
isFolder: boolean;
|
||||
gitRelativePath: string;
|
||||
plugin: GitChangelogPlugin;
|
||||
}): void {
|
||||
const absoluteGitIgnoreRule = convertPathToAbsoluteGitIgnoreRule({
|
||||
isFolder,
|
||||
gitRelativePath
|
||||
});
|
||||
// Don't add the menu item if the path is above the root of the repository. Otherwise git would throw this error when trying to use this rule in pathspec:
|
||||
// Fatal: :(exclude,glob)../xx/**: '../xx/**' is outside repository at '/path/to/vault/repo'
|
||||
if (isAbsoluteGitIgnoreRuleAboveRoot({ absoluteGitIgnoreRule })) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.addItem((item) => {
|
||||
// The ExcludeFilesAndFolders rules are applied relative to the git repo.
|
||||
|
||||
// Only check absolute rules, we don't want to modify any relative rules that also affect other files
|
||||
const lineNumber = isAbsoluteGitIgnoreRuleInExcludeFilesAndFolders({
|
||||
absoluteGitIgnoreRule,
|
||||
plugin
|
||||
});
|
||||
const ruleAlreadyExists = lineNumber !== -1;
|
||||
const isIncludeList = plugin.settings.convertToIncludeList;
|
||||
let actionTitle: string;
|
||||
if (isIncludeList) {
|
||||
actionTitle = ruleAlreadyExists ? 'Re-exclude' : 'Include';
|
||||
} else {
|
||||
actionTitle = ruleAlreadyExists ? 'Reinclude' : 'Exclude';
|
||||
}
|
||||
|
||||
item
|
||||
.setSection('action')
|
||||
.setTitle(`${PLUGIN_NAME_SENTENCE_CASE}: ${actionTitle}`)
|
||||
.setIcon(VAULT_CHANGELOG_VIEW_CONFIG.icon)
|
||||
.onClick(async () => {
|
||||
await (ruleAlreadyExists
|
||||
? removeExcludeFilesAndFoldersItem(lineNumber, plugin)
|
||||
: addExcludeFilesAndFoldersItem({
|
||||
path: absoluteGitIgnoreRule,
|
||||
plugin
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function handleChangelogViewContextMenu({
|
||||
menu,
|
||||
inFileMenu,
|
||||
gitRelativePath,
|
||||
commitHash,
|
||||
plugin
|
||||
}: {
|
||||
menu: Menu;
|
||||
inFileMenu: boolean;
|
||||
gitRelativePath?: string;
|
||||
commitHash?: string;
|
||||
plugin: GitChangelogPlugin;
|
||||
}): void {
|
||||
// Skip adding the "Git changelog: Exclude" item again if the usual file menu is shown.
|
||||
if (!inFileMenu && gitRelativePath) {
|
||||
addExcludeMenuItem({
|
||||
menu,
|
||||
isFolder: false,
|
||||
gitRelativePath,
|
||||
plugin
|
||||
});
|
||||
}
|
||||
|
||||
if (commitHash) {
|
||||
let itemTitle = 'Copy commit hash';
|
||||
let contentToCopy = commitHash;
|
||||
|
||||
// If used on a file tile inside changelog versions.
|
||||
if (gitRelativePath) {
|
||||
itemTitle += `:path`;
|
||||
contentToCopy += `:${gitRelativePath}`;
|
||||
}
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle(itemTitle)
|
||||
.setSection('action')
|
||||
.setIcon(COPY_COMMIT_HASH_ICON)
|
||||
.onClick(async () => {
|
||||
await navigator.clipboard.writeText(contentToCopy);
|
||||
plugin.displayNotice(
|
||||
`Commit information ${contentToCopy} copied to clipboard.`,
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
1000
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function convertPathToAbsoluteGitIgnoreRule({
|
||||
isFolder,
|
||||
gitRelativePath
|
||||
}: {
|
||||
isFolder: boolean;
|
||||
gitRelativePath: string;
|
||||
}): string {
|
||||
// Add a leading slash to set the rule as absolute from root, so it only excludes that exact path
|
||||
let composedPath = '/';
|
||||
|
||||
composedPath += gitRelativePath;
|
||||
|
||||
// Add an explicit folder rule, so that the same path doesn't also apply for files with that same name
|
||||
if (isFolder) {
|
||||
composedPath += '/';
|
||||
}
|
||||
|
||||
return convertPathToGitIgnoreRule(composedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given absolute gitignore rule exists in the plugin's exclude files and folders settings
|
||||
* @param gitIgnoreAbsoluteRule - The absolute gitignore rule to check for
|
||||
* @param plugin - The GitChangelogPlugin instance
|
||||
* @returns The line number (index) where the rule was found, or -1 if not found
|
||||
*/
|
||||
export function isAbsoluteGitIgnoreRuleInExcludeFilesAndFolders({
|
||||
absoluteGitIgnoreRule,
|
||||
plugin
|
||||
}: {
|
||||
absoluteGitIgnoreRule: string;
|
||||
plugin: GitChangelogPlugin;
|
||||
}): number {
|
||||
const existingLines = plugin.settings.excludeFilesAndFoldersLines;
|
||||
|
||||
// Trim unescaped trailing white space from existing lines, since it gets trimmed by git anyways.
|
||||
// By doing this we won't miss already existing lines that only differ in trailing white space.
|
||||
const existingRules = existingLines.map((line) => parseGitIgnoreLine(line));
|
||||
return existingRules.indexOf(absoluteGitIgnoreRule);
|
||||
}
|
||||
|
||||
export async function removeExcludeFilesAndFoldersItem(
|
||||
lineNumber: number,
|
||||
plugin: GitChangelogPlugin
|
||||
): Promise<void> {
|
||||
await plugin.settingsManager.editAndSave(
|
||||
(settings: GitChangelogSettings): void => {
|
||||
settings.excludeFilesAndFoldersLines.splice(lineNumber, 1);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function addExcludeFilesAndFoldersItem({
|
||||
path,
|
||||
plugin
|
||||
}: {
|
||||
path: string;
|
||||
plugin: GitChangelogPlugin;
|
||||
}): Promise<void> {
|
||||
await plugin.settingsManager.editAndSave(
|
||||
(settings: GitChangelogSettings): void => {
|
||||
settings.excludeFilesAndFoldersLines.push(path);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -1,38 +1,25 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { TextComponent } from 'obsidian';
|
||||
import type {
|
||||
ChangelogGenerationSettings,
|
||||
IGitChangelogSettings
|
||||
} from 'settings/settings.ts';
|
||||
import type { GitChangelogSettingsTab } from 'settings/settingsTab.ts';
|
||||
|
||||
import { moment } from 'obsidian';
|
||||
import { NumberComponent } from 'obsidian-dev-utils/obsidian/Components/NumberComponent';
|
||||
import { TimeComponent } from 'obsidian-dev-utils/obsidian/Components/TimeComponent';
|
||||
import { SettingEx } from 'obsidian-dev-utils/obsidian/SettingEx';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
|
||||
export abstract class GitChangelogSetting {
|
||||
export abstract class SettingComponent {
|
||||
protected containerEl: HTMLElement;
|
||||
protected disabled: boolean;
|
||||
protected plugin: GitChangelogPlugin;
|
||||
protected settingTab?: GitChangelogSettingsTab;
|
||||
protected settingTab: GitChangelogSettingsTab;
|
||||
|
||||
public constructor({
|
||||
containerEl,
|
||||
disabled = false,
|
||||
plugin,
|
||||
settingTab
|
||||
plugin
|
||||
}: {
|
||||
containerEl: HTMLElement;
|
||||
disabled?: boolean;
|
||||
plugin: GitChangelogPlugin;
|
||||
settingTab?: GitChangelogSettingsTab;
|
||||
}) {
|
||||
this.plugin = plugin;
|
||||
this.containerEl = containerEl;
|
||||
this.disabled = disabled;
|
||||
this.settingTab = settingTab;
|
||||
this.settingTab = this.plugin.settingsTab;
|
||||
this.containerEl = this.settingTab.containerEl;
|
||||
}
|
||||
|
||||
public abstract display(): void;
|
||||
|
|
@ -47,46 +34,15 @@ export abstract class GitChangelogSetting {
|
|||
}
|
||||
|
||||
/**
|
||||
* It delays the update of the associated ui state of the conditional setting enabled or disabled state, so that the toggle animation can end its animation smoother.
|
||||
* Delays the update of the settings UI.
|
||||
* Used when the user toggles one of the settings that control enabled states of other settings. Delaying the update
|
||||
* allows most of the toggle animation to run, instead of abruptly jumping between enabled/disabled states.
|
||||
*/
|
||||
protected refreshDisplayDelayed(timeout = 80): void {
|
||||
protected refreshDisplayWithDelay(timeout = 80): void {
|
||||
if (this.settingTab) {
|
||||
setTimeout(() => this.settingTab?.display(), timeout);
|
||||
}
|
||||
}
|
||||
|
||||
protected setValueIfNonDefaultSetting({
|
||||
diffSettingsProperty,
|
||||
settingsProperty,
|
||||
text
|
||||
}: {
|
||||
diffSettingsProperty?: keyof ChangelogGenerationSettings;
|
||||
settingsProperty: keyof IGitChangelogSettings;
|
||||
text: NumberComponent | TextComponent | TimeComponent;
|
||||
}): void {
|
||||
const storedValue = diffSettingsProperty
|
||||
? (this.plugin.settings[settingsProperty] as ChangelogGenerationSettings)[
|
||||
diffSettingsProperty
|
||||
]
|
||||
: this.plugin.settings[settingsProperty];
|
||||
const defaultValue = diffSettingsProperty
|
||||
? (DEFAULT_SETTINGS[settingsProperty] as ChangelogGenerationSettings)[
|
||||
diffSettingsProperty
|
||||
]
|
||||
: DEFAULT_SETTINGS[settingsProperty];
|
||||
|
||||
if (defaultValue !== storedValue) {
|
||||
if (text instanceof NumberComponent) {
|
||||
text.setValue(Number(storedValue));
|
||||
} else if (text instanceof TimeComponent) {
|
||||
text.setValue(moment.duration({ hours: Number(storedValue) }));
|
||||
} else {
|
||||
text.setValue(
|
||||
typeof storedValue === 'object'
|
||||
? JSON.stringify(storedValue)
|
||||
: String(storedValue)
|
||||
);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.settingTab?.display();
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,23 +13,23 @@ export class TimeZoneSuggest extends AbstractInputSuggest<string> {
|
|||
|
||||
public getSuggestions(inputString: string): string[] {
|
||||
const lowerCaseInputString = inputString.toLowerCase();
|
||||
const timezones: string[] = [];
|
||||
const timeZones: string[] = [];
|
||||
|
||||
for (const timezone of TIME_ZONES_LIST) {
|
||||
if (timezone.toLowerCase().contains(lowerCaseInputString)) {
|
||||
timezones.push(timezone);
|
||||
for (const timeZone of TIME_ZONES_LIST) {
|
||||
if (timeZone.toLowerCase().contains(lowerCaseInputString)) {
|
||||
timeZones.push(timeZone);
|
||||
}
|
||||
}
|
||||
|
||||
return timezones;
|
||||
return timeZones;
|
||||
}
|
||||
|
||||
public renderSuggestion(timezone: string, element: HTMLElement): void {
|
||||
element.setText(timezone);
|
||||
public renderSuggestion(timeZone: string, element: HTMLElement): void {
|
||||
element.setText(timeZone);
|
||||
}
|
||||
|
||||
public override selectSuggestion(timezone: string): void {
|
||||
this.inputEl.value = timezone;
|
||||
public override selectSuggestion(timeZone: string): void {
|
||||
this.inputEl.value = timeZone;
|
||||
this.inputEl.trigger('input');
|
||||
this.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { ChangelogGenerationSettings } from 'settings/settings.ts';
|
||||
|
||||
import { deepEqual } from 'obsidian-dev-utils/Object';
|
||||
import {
|
||||
systemTimeZoneUnchanged,
|
||||
validateCustomTimeZone
|
||||
} from 'settings/ui/CustomTimeZone.ts';
|
||||
|
||||
export function changelogGenerationSettingsChanged({
|
||||
newChangelogSettings,
|
||||
oldChangelogSettings,
|
||||
plugin
|
||||
}: {
|
||||
newChangelogSettings: ChangelogGenerationSettings;
|
||||
oldChangelogSettings: ChangelogGenerationSettings;
|
||||
plugin: GitChangelogPlugin;
|
||||
}): boolean {
|
||||
if (!oldChangelogSettings) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IsAncestor run
|
||||
|
||||
if (
|
||||
!validateCustomTimeZone(newChangelogSettings.timezone) &&
|
||||
!systemTimeZoneUnchanged(plugin)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (deepEqual(oldChangelogSettings, newChangelogSettings)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1,148 +1,203 @@
|
|||
import { PluginSettingsBase } from 'obsidian-dev-utils/obsidian/Plugin/PluginSettingsBase';
|
||||
import type { GitChangelogPluginTypes } from 'constants.ts';
|
||||
import type { ExtractPluginSettingsWrapper } from 'obsidian-dev-utils/obsidian/Plugin/PluginTypesBase';
|
||||
import type { Except, ReadonlyDeep } from 'type-fest';
|
||||
|
||||
import spacetime from 'spacetime';
|
||||
import {
|
||||
ChangelogInterval,
|
||||
DiffAlgorithm,
|
||||
DiffMeasurementUnit,
|
||||
FileExplorerStats,
|
||||
FilesSummariesDisplayMode
|
||||
FileSummariesDisplayMode,
|
||||
WhitespaceIgnoreMode
|
||||
} from 'types.ts';
|
||||
|
||||
// Constants
|
||||
// Takes 0.3 ms
|
||||
export const TIME_ZONES_LIST = new Set(Object.keys(spacetime().timezones));
|
||||
export const MAX_SUPPORTED_INTERVAL = 99_999; // ~69 days
|
||||
export const AUTO_DETECT_PLACEHOLDER = 'Auto-detect';
|
||||
|
||||
/**
|
||||
* Each change in these settings triggers a recalculation of the changelogs statistics.
|
||||
*/
|
||||
export interface ChangelogGenerationSettings {
|
||||
// Time settings
|
||||
dayStartHour: number;
|
||||
// Diff settings
|
||||
detectMovedContent: boolean;
|
||||
diffAlgorithm: DiffAlgorithm;
|
||||
measurementUnit: DiffMeasurementUnit;
|
||||
renameDetectionStrictness: number;
|
||||
renameLimit: string;
|
||||
timezone: string;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export interface VaultChangelogGenerationSettings {
|
||||
gitDiffIgnore: string;
|
||||
interval: ChangelogInterval;
|
||||
}
|
||||
|
||||
export interface IGitChangelogSettings {
|
||||
autoCommitDisabledWarningDismissed: boolean;
|
||||
changelogGenerationSettings: ChangelogGenerationSettings;
|
||||
contentDeletionsAndMovesWarningThreshold: string;
|
||||
dedicatedFileTypeSummaries: string[];
|
||||
fileChangelogInterval: ChangelogInterval;
|
||||
fileExplorerInterval: string;
|
||||
fileExplorerStats: FileExplorerStats;
|
||||
filesChangesWarningThreshold: string;
|
||||
fileSummariesDisplayMode: FilesSummariesDisplayMode;
|
||||
firstStartup: boolean;
|
||||
notifyOnContentDeletionsAndMovesThresholdReached: boolean;
|
||||
notifyOnFilesChangesThresholdReached: boolean;
|
||||
statusBarInterval: string;
|
||||
statusBarStats: boolean;
|
||||
vaultChangelogGenerationSettings: VaultChangelogGenerationSettings;
|
||||
}
|
||||
|
||||
export class GitChangelogPluginSettings
|
||||
extends PluginSettingsBase
|
||||
implements IGitChangelogSettings
|
||||
{
|
||||
// State
|
||||
public autoCommitDisabledWarningDismissed: boolean =
|
||||
DEFAULT_SETTINGS.autoCommitDisabledWarningDismissed;
|
||||
|
||||
public firstStartup: boolean = DEFAULT_SETTINGS.firstStartup;
|
||||
|
||||
public changelogGenerationSettings: ChangelogGenerationSettings =
|
||||
DEFAULT_CHANGELOG_GENERATION_SETTINGS;
|
||||
|
||||
/**
|
||||
* The number refers to either words or lines depending on what the changelog is set up to count
|
||||
*/
|
||||
public contentDeletionsAndMovesWarningThreshold: string =
|
||||
DEFAULT_SETTINGS.contentDeletionsAndMovesWarningThreshold;
|
||||
export class GitChangelogSettings {
|
||||
// Plugin state
|
||||
public autoCommitDisabledWarningDismissed = false;
|
||||
public firstStartup = true;
|
||||
public activeMinutesPassedSinceLastCheckpoint = 0;
|
||||
|
||||
// ShowFilesSummaryCountOptions[]; //Set<ShowFilesSummaryCountOptions>;
|
||||
// VaultChangelogFilesVisibility: VaultChangelogFilesVisibility;
|
||||
// NotifyOnLargeCommitAdditions: boolean;
|
||||
// NotifyOnLargeCommitAdditionsWarningThreshold: string;
|
||||
public dedicatedFileTypeSummaries: string[] = [
|
||||
...DEFAULT_SETTINGS.dedicatedFileTypeSummaries
|
||||
];
|
||||
public dedicatedFileTypeSummaries: string[] = [];
|
||||
public fileSummariesDisplayMode: FileSummariesDisplayMode =
|
||||
FileSummariesDisplayMode.Total;
|
||||
|
||||
public fileChangelogInterval: ChangelogInterval =
|
||||
DEFAULT_SETTINGS.fileChangelogInterval;
|
||||
public fileExplorerInterval = '4320'; // In mins
|
||||
public fileExplorerStats: FileExplorerStats = FileExplorerStats.Disabled;
|
||||
|
||||
public fileExplorerInterval: string = DEFAULT_SETTINGS.fileExplorerInterval;
|
||||
public fileExplorerStats: FileExplorerStats =
|
||||
DEFAULT_SETTINGS.fileExplorerStats;
|
||||
public notifyOnHighContentDeletionsAndMoves = true;
|
||||
/**
|
||||
* The number refers to either words or lines depending on what the changelog is set up to count
|
||||
*/
|
||||
public contentDeletionsAndMovesWarningThreshold = '2000';
|
||||
|
||||
public filesChangesWarningThreshold: string =
|
||||
DEFAULT_SETTINGS.filesChangesWarningThreshold;
|
||||
public notifyOnHighFilesChanged = false;
|
||||
public filesChangedWarningThreshold = '50';
|
||||
|
||||
public fileSummariesDisplayMode: FilesSummariesDisplayMode =
|
||||
DEFAULT_SETTINGS.fileSummariesDisplayMode;
|
||||
public checkpointCommits: string[] = [];
|
||||
public reviewChangesReminderInterval = 0;
|
||||
|
||||
public notifyOnContentDeletionsAndMovesThresholdReached: boolean =
|
||||
DEFAULT_SETTINGS.notifyOnContentDeletionsAndMovesThresholdReached;
|
||||
public statusBarInterval = 30; // In mins
|
||||
public showStatusBarStats = false;
|
||||
|
||||
public notifyOnFilesChangesThresholdReached: boolean =
|
||||
DEFAULT_SETTINGS.notifyOnFilesChangesThresholdReached;
|
||||
// FileGenerationSettings
|
||||
public fileChangelogInterval: ChangelogInterval = ChangelogInterval.Daily;
|
||||
|
||||
public statusBarInterval: string = DEFAULT_SETTINGS.statusBarInterval;
|
||||
public statusBarStats: boolean = DEFAULT_SETTINGS.statusBarStats;
|
||||
// Specific Changelog Generation Settings
|
||||
public vaultChangelogGenerationSettings: VaultChangelogGenerationSettings =
|
||||
DEFAULT_VAULT_CHANGELOG_GENERATION_SETTINGS;
|
||||
// VaultGenerationSettings
|
||||
public excludeFilesAndFoldersLines: string[] = [];
|
||||
public enableExclusionList = true;
|
||||
public convertToIncludeList = false;
|
||||
public vaultChangelogInterval = ChangelogInterval.Daily;
|
||||
|
||||
public constructor(data: unknown) {
|
||||
super();
|
||||
// Object.assign(this, DEFAULT_SETTINGS);
|
||||
this.init(data);
|
||||
this._shouldSaveAfterLoad = true;
|
||||
}
|
||||
/**
|
||||
* Each change in these settings triggers a recalculation of all the changelogs statistics.
|
||||
*/
|
||||
// Time settings
|
||||
public dayStartHour = 0;
|
||||
// Diff settings
|
||||
public detectMovedContent = true;
|
||||
public diffAlgorithm = DiffAlgorithm.Inherit;
|
||||
public diffMeasurementUnit = DiffMeasurementUnit.Words;
|
||||
public renameDetectionStrictness = 50;
|
||||
public renameLimit = 1000;
|
||||
public timeZone = AUTO_DETECT_PLACEHOLDER;
|
||||
public locale = AUTO_DETECT_PLACEHOLDER;
|
||||
public whitespaceIgnoreMode = WhitespaceIgnoreMode.None;
|
||||
public ignoreBlankLines = false;
|
||||
}
|
||||
|
||||
export const DEFAULT_VAULT_CHANGELOG_GENERATION_SETTINGS: VaultChangelogGenerationSettings =
|
||||
{
|
||||
gitDiffIgnore: '',
|
||||
interval: ChangelogInterval.Daily
|
||||
} as const;
|
||||
// This is needed for checking if the specific generation settings have changed. And on each new added setting, the developer will have to explicitly include or exclude the added setting from all of these categories.
|
||||
|
||||
export const DEFAULT_CHANGELOG_GENERATION_SETTINGS: ChangelogGenerationSettings =
|
||||
{
|
||||
dayStartHour: 0,
|
||||
detectMovedContent: true,
|
||||
diffAlgorithm: DiffAlgorithm.Inherit,
|
||||
measurementUnit: DiffMeasurementUnit.Words,
|
||||
renameDetectionStrictness: 50,
|
||||
renameLimit: '1000',
|
||||
timezone: AUTO_DETECT_PLACEHOLDER,
|
||||
locale: AUTO_DETECT_PLACEHOLDER
|
||||
} as const;
|
||||
export const DEFAULT_SETTINGS: IGitChangelogSettings = {
|
||||
autoCommitDisabledWarningDismissed: false,
|
||||
changelogGenerationSettings: DEFAULT_CHANGELOG_GENERATION_SETTINGS,
|
||||
vaultChangelogGenerationSettings: DEFAULT_VAULT_CHANGELOG_GENERATION_SETTINGS,
|
||||
contentDeletionsAndMovesWarningThreshold: '2000',
|
||||
dedicatedFileTypeSummaries: [] as const,
|
||||
fileChangelogInterval: ChangelogInterval.Daily,
|
||||
fileExplorerInterval: '4320', // In mins
|
||||
fileExplorerStats: FileExplorerStats.Disabled,
|
||||
filesChangesWarningThreshold: '50',
|
||||
fileSummariesDisplayMode: FilesSummariesDisplayMode.Total,
|
||||
firstStartup: true,
|
||||
notifyOnContentDeletionsAndMovesThresholdReached: true,
|
||||
notifyOnFilesChangesThresholdReached: false,
|
||||
statusBarInterval: '30', // In mins
|
||||
statusBarStats: false
|
||||
} as const;
|
||||
export function pickVaultChangelogSettings(
|
||||
settings: ReadonlyDeep<ExtractPluginSettingsWrapper<GitChangelogPluginTypes>>
|
||||
): ReadonlyDeep<VaultGenerationSettings> {
|
||||
return {
|
||||
excludeFilesAndFoldersLines:
|
||||
settings.safeSettings.excludeFilesAndFoldersLines,
|
||||
convertToIncludeList: settings.safeSettings.convertToIncludeList,
|
||||
vaultChangelogInterval: settings.safeSettings.vaultChangelogInterval,
|
||||
enableExclusionList: settings.safeSettings.enableExclusionList
|
||||
};
|
||||
}
|
||||
|
||||
export function pickFileChangelogSettings(
|
||||
settings: ReadonlyDeep<ExtractPluginSettingsWrapper<GitChangelogPluginTypes>>
|
||||
): ReadonlyDeep<FileGenerationSettings> {
|
||||
return {
|
||||
fileChangelogInterval: settings.safeSettings.fileChangelogInterval
|
||||
};
|
||||
}
|
||||
|
||||
export function pickGeneralChangelogSettings(
|
||||
settings: ReadonlyDeep<ExtractPluginSettingsWrapper<GitChangelogPluginTypes>>
|
||||
): ReadonlyDeep<GenerationSettings> {
|
||||
return {
|
||||
dayStartHour: settings.safeSettings.dayStartHour,
|
||||
detectMovedContent: settings.safeSettings.detectMovedContent,
|
||||
diffAlgorithm: settings.safeSettings.diffAlgorithm,
|
||||
diffMeasurementUnit: settings.safeSettings.diffMeasurementUnit,
|
||||
renameDetectionStrictness: settings.safeSettings.renameDetectionStrictness,
|
||||
renameLimit: settings.safeSettings.renameLimit,
|
||||
timeZone: settings.safeSettings.timeZone,
|
||||
whitespaceIgnoreMode: settings.safeSettings.whitespaceIgnoreMode,
|
||||
ignoreBlankLines: settings.safeSettings.ignoreBlankLines
|
||||
};
|
||||
}
|
||||
|
||||
type VaultGenerationSettings = Except<
|
||||
GitChangelogSettings,
|
||||
| 'activeMinutesPassedSinceLastCheckpoint'
|
||||
| 'autoCommitDisabledWarningDismissed'
|
||||
| 'checkpointCommits'
|
||||
| 'contentDeletionsAndMovesWarningThreshold'
|
||||
| 'dayStartHour'
|
||||
| 'dedicatedFileTypeSummaries'
|
||||
| 'detectMovedContent'
|
||||
| 'diffAlgorithm'
|
||||
| 'diffMeasurementUnit'
|
||||
| 'fileChangelogInterval'
|
||||
| 'fileExplorerInterval'
|
||||
| 'fileExplorerStats'
|
||||
| 'filesChangedWarningThreshold'
|
||||
| 'fileSummariesDisplayMode'
|
||||
| 'firstStartup'
|
||||
| 'ignoreBlankLines'
|
||||
| 'locale'
|
||||
| 'notifyOnHighContentDeletionsAndMoves'
|
||||
| 'notifyOnHighFilesChanged'
|
||||
| 'renameDetectionStrictness'
|
||||
| 'renameLimit'
|
||||
| 'reviewChangesReminderInterval'
|
||||
| 'showStatusBarStats'
|
||||
| 'statusBarInterval'
|
||||
| 'timeZone'
|
||||
| 'whitespaceIgnoreMode'
|
||||
>;
|
||||
|
||||
type FileGenerationSettings = Except<
|
||||
GitChangelogSettings,
|
||||
| 'activeMinutesPassedSinceLastCheckpoint'
|
||||
| 'autoCommitDisabledWarningDismissed'
|
||||
| 'checkpointCommits'
|
||||
| 'contentDeletionsAndMovesWarningThreshold'
|
||||
| 'convertToIncludeList'
|
||||
| 'dayStartHour'
|
||||
| 'dedicatedFileTypeSummaries'
|
||||
| 'detectMovedContent'
|
||||
| 'diffAlgorithm'
|
||||
| 'diffMeasurementUnit'
|
||||
| 'enableExclusionList'
|
||||
| 'excludeFilesAndFoldersLines'
|
||||
| 'fileExplorerInterval'
|
||||
| 'fileExplorerStats'
|
||||
| 'filesChangedWarningThreshold'
|
||||
| 'fileSummariesDisplayMode'
|
||||
| 'firstStartup'
|
||||
| 'ignoreBlankLines'
|
||||
| 'locale'
|
||||
| 'notifyOnHighContentDeletionsAndMoves'
|
||||
| 'notifyOnHighFilesChanged'
|
||||
| 'renameDetectionStrictness'
|
||||
| 'renameLimit'
|
||||
| 'reviewChangesReminderInterval'
|
||||
| 'showStatusBarStats'
|
||||
| 'statusBarInterval'
|
||||
| 'timeZone'
|
||||
| 'vaultChangelogInterval'
|
||||
| 'whitespaceIgnoreMode'
|
||||
>;
|
||||
|
||||
type GenerationSettings = Except<
|
||||
GitChangelogSettings,
|
||||
| 'activeMinutesPassedSinceLastCheckpoint'
|
||||
| 'autoCommitDisabledWarningDismissed'
|
||||
| 'checkpointCommits'
|
||||
| 'contentDeletionsAndMovesWarningThreshold'
|
||||
| 'convertToIncludeList'
|
||||
| 'dedicatedFileTypeSummaries'
|
||||
| 'enableExclusionList'
|
||||
| 'excludeFilesAndFoldersLines'
|
||||
| 'fileChangelogInterval'
|
||||
| 'fileExplorerInterval'
|
||||
| 'fileExplorerStats'
|
||||
| 'filesChangedWarningThreshold'
|
||||
| 'fileSummariesDisplayMode'
|
||||
| 'firstStartup'
|
||||
| 'locale'
|
||||
| 'notifyOnHighContentDeletionsAndMoves'
|
||||
| 'notifyOnHighFilesChanged'
|
||||
| 'reviewChangesReminderInterval'
|
||||
| 'showStatusBarStats'
|
||||
| 'statusBarInterval'
|
||||
| 'vaultChangelogInterval'
|
||||
>;
|
||||
|
|
|
|||
277
src/settings/settingsManager.ts
Normal file
277
src/settings/settingsManager.ts
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
import type { GitChangelogPluginTypes } from 'constants.ts';
|
||||
import type { MaybeReturn } from 'obsidian-dev-utils/Type';
|
||||
|
||||
import {
|
||||
MAX_RENAME_DETECTION_STRICTNESS,
|
||||
MIN_RENAME_DETECTION_STRICTNESS
|
||||
} from 'constants.ts';
|
||||
import { Notice } from 'obsidian';
|
||||
import { PluginSettingsManagerBase } from 'obsidian-dev-utils/obsidian/Plugin/PluginSettingsManagerBase';
|
||||
import {
|
||||
GitChangelogSettings,
|
||||
MAX_SUPPORTED_INTERVAL
|
||||
} from 'settings/settings.ts';
|
||||
import { validateLocale } from 'settings/ui/CustomLocale.ts';
|
||||
import { validateCustomTimeZone } from 'settings/ui/CustomTimeZone.ts';
|
||||
import {
|
||||
ChangelogInterval,
|
||||
DiffAlgorithm,
|
||||
DiffMeasurementUnit,
|
||||
FileExplorerStats,
|
||||
WhitespaceIgnoreMode
|
||||
} from 'types.ts';
|
||||
|
||||
export class GitChangelogSettingsManager extends PluginSettingsManagerBase<GitChangelogPluginTypes> {
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
protected override async onLoadRecord(
|
||||
record: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
// Only migrate if this is legacy data - check for the existence of the old settings structure
|
||||
if ('vaultChangelogGenerationSettings' in record) {
|
||||
// Check if the legacy excludeFilesAndFoldersLines data exists
|
||||
const vaultSettings = record.vaultChangelogGenerationSettings as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const legacyExcludeLines = vaultSettings?.excludeFilesAndFoldersLines;
|
||||
|
||||
// Check if we need to migrate (legacy data exists)
|
||||
if (
|
||||
legacyExcludeLines &&
|
||||
Array.isArray(legacyExcludeLines) &&
|
||||
legacyExcludeLines.length > 0 &&
|
||||
(record.excludeFilesAndFoldersLines === undefined ||
|
||||
record.excludeFilesAndFoldersLines === null ||
|
||||
!Array.isArray(record.excludeFilesAndFoldersLines) ||
|
||||
record.excludeFilesAndFoldersLines.length === 0)
|
||||
) {
|
||||
// Migrate the data
|
||||
record.excludeFilesAndFoldersLines = legacyExcludeLines;
|
||||
// Remove the old settings structure to prevent future migrations
|
||||
delete record.vaultChangelogGenerationSettings;
|
||||
|
||||
// We are assuming that the user is updating their plugin version, so we show the "what's changed" notification and alert the user that some of their settings broke.
|
||||
const whatsNewFragment = createFragment((element) => {
|
||||
element.createEl('p', {
|
||||
text: 'Git changelog:\nA new version has been installed.\n\nSome of your settings for this plugin have been reset!'
|
||||
});
|
||||
|
||||
element.createEl('button', { text: 'Close' });
|
||||
|
||||
const seeButton = element.createEl('button', { text: "What's new?" });
|
||||
seeButton.onClickEvent(() => {
|
||||
window.open(
|
||||
'https://github.com/shumadrid/obsidian-git-changelog/releases'
|
||||
);
|
||||
});
|
||||
seeButton.addClass('git-changelog-left-padding');
|
||||
});
|
||||
new Notice(whatsNewFragment, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override createDefaultSettings(): GitChangelogSettings {
|
||||
return new GitChangelogSettings();
|
||||
}
|
||||
|
||||
protected override registerValidators(): void {
|
||||
super.registerValidators();
|
||||
|
||||
// If the active minutes cache is corrupted then just reset it.
|
||||
this.registerValidator(
|
||||
'activeMinutesPassedSinceLastCheckpoint',
|
||||
(activeMinutesPassedSinceLastCheckpoint): MaybeReturn<string> => {
|
||||
if (
|
||||
!Number.isInteger(activeMinutesPassedSinceLastCheckpoint) ||
|
||||
activeMinutesPassedSinceLastCheckpoint < 0
|
||||
) {
|
||||
// Reset to 0 if corrupted
|
||||
return 'Corrupted value detected. Resetting to 0.';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator(
|
||||
'diffMeasurementUnit',
|
||||
(measurementUnit): MaybeReturn<string> => {
|
||||
if (!Object.values(DiffMeasurementUnit).includes(measurementUnit)) {
|
||||
return 'Choose a valid option.';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator(
|
||||
'contentDeletionsAndMovesWarningThreshold',
|
||||
(contentDeletionsAndMovesWarningThreshold): MaybeReturn<string> => {
|
||||
if (
|
||||
!Number.isInteger(Number(contentDeletionsAndMovesWarningThreshold)) ||
|
||||
Number(contentDeletionsAndMovesWarningThreshold) < 1
|
||||
) {
|
||||
return 'Pick a positive whole number.';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator('locale', (locale): MaybeReturn<string> => {
|
||||
if (!validateLocale(locale) && locale !== this.defaultSettings.locale) {
|
||||
return 'Invalid locale code.';
|
||||
}
|
||||
});
|
||||
|
||||
this.registerValidator('timeZone', (timeZone): MaybeReturn<string> => {
|
||||
if (
|
||||
!validateCustomTimeZone(timeZone) &&
|
||||
timeZone !== this.defaultSettings.timeZone
|
||||
) {
|
||||
return 'Invalid timezone.';
|
||||
}
|
||||
});
|
||||
|
||||
const ONE_DAY_IN_HOURS = 24;
|
||||
|
||||
this.registerValidator(
|
||||
'dayStartHour',
|
||||
(dayStartHour): MaybeReturn<string> => {
|
||||
if (
|
||||
!Number.isInteger(dayStartHour) ||
|
||||
dayStartHour < 0 ||
|
||||
dayStartHour >= ONE_DAY_IN_HOURS
|
||||
) {
|
||||
return 'Invalid time.';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator(
|
||||
'diffAlgorithm',
|
||||
(diffAlgorithm): MaybeReturn<string> => {
|
||||
if (!Object.values(DiffAlgorithm).includes(diffAlgorithm)) {
|
||||
return 'Choose a valid option.';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator(
|
||||
'filesChangedWarningThreshold',
|
||||
(filesChangedWarningThreshold): MaybeReturn<string> => {
|
||||
if (
|
||||
!Number.isInteger(Number(filesChangedWarningThreshold)) ||
|
||||
Number(filesChangedWarningThreshold) < 1
|
||||
) {
|
||||
return 'Pick a positive whole number.';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator(
|
||||
'fileExplorerStats',
|
||||
(fileExplorerStats): MaybeReturn<string> => {
|
||||
if (!Object.values(FileExplorerStats).includes(fileExplorerStats)) {
|
||||
return 'Choose a valid option.';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator(
|
||||
'fileExplorerInterval',
|
||||
(fileExplorerInterval): MaybeReturn<string> => {
|
||||
if (
|
||||
!Number.isInteger(Number(fileExplorerInterval)) ||
|
||||
Number(fileExplorerInterval) < 1 ||
|
||||
Number(fileExplorerInterval) > MAX_SUPPORTED_INTERVAL
|
||||
) {
|
||||
return `Pick a positive whole number not greater than ${MAX_SUPPORTED_INTERVAL}.`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator(
|
||||
'renameLimit',
|
||||
(renameLimit): MaybeReturn<string> => {
|
||||
if (
|
||||
!(
|
||||
Number.isInteger(renameLimit) &&
|
||||
Number(renameLimit) >= 0 &&
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
Number(renameLimit) <= 99_999_999_999_999_999_999n
|
||||
)
|
||||
) {
|
||||
return 'Pick a non-negative whole number.';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator(
|
||||
'reviewChangesReminderInterval',
|
||||
(reviewChangesReminderInterval): MaybeReturn<string> => {
|
||||
if (
|
||||
!(
|
||||
Number.isInteger(reviewChangesReminderInterval) &&
|
||||
Number(reviewChangesReminderInterval) >= 0 &&
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
Number(reviewChangesReminderInterval) <= 999_999_999
|
||||
)
|
||||
) {
|
||||
return 'Pick a non-negative whole number.';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator(
|
||||
'renameDetectionStrictness',
|
||||
(renameDetectionStrictness): MaybeReturn<string> => {
|
||||
if (
|
||||
!Number.isInteger(renameDetectionStrictness) ||
|
||||
renameDetectionStrictness < MIN_RENAME_DETECTION_STRICTNESS ||
|
||||
renameDetectionStrictness > MAX_RENAME_DETECTION_STRICTNESS
|
||||
) {
|
||||
return `Pick a whole number between ${MIN_RENAME_DETECTION_STRICTNESS} and ${MAX_RENAME_DETECTION_STRICTNESS}.`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator(
|
||||
'statusBarInterval',
|
||||
(statusBarInterval): MaybeReturn<string> => {
|
||||
if (
|
||||
!Number.isInteger(Number(statusBarInterval)) ||
|
||||
Number(statusBarInterval) < 1 ||
|
||||
Number(statusBarInterval) > MAX_SUPPORTED_INTERVAL
|
||||
) {
|
||||
return `Pick a positive whole number not greater than ${MAX_SUPPORTED_INTERVAL}.`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator(
|
||||
'whitespaceIgnoreMode',
|
||||
(whitespaceIgnoreMode): MaybeReturn<string> => {
|
||||
if (
|
||||
!Object.values(WhitespaceIgnoreMode).includes(whitespaceIgnoreMode)
|
||||
) {
|
||||
return 'Choose a valid option.';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator(
|
||||
'fileChangelogInterval',
|
||||
(fileChangelogInterval): MaybeReturn<string> => {
|
||||
if (!Object.values(ChangelogInterval).includes(fileChangelogInterval)) {
|
||||
return 'Choose a valid option.';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.registerValidator(
|
||||
'vaultChangelogInterval',
|
||||
(vaultChangelogInterval): MaybeReturn<string> => {
|
||||
if (
|
||||
!Object.values(ChangelogInterval).includes(vaultChangelogInterval)
|
||||
) {
|
||||
return 'Choose a valid option.';
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,27 @@
|
|||
import type { GitChangelogPlugin } from 'GitChangelogPlugin.svelte.ts';
|
||||
import type { GitChangelogPluginTypes } from 'constants.ts';
|
||||
|
||||
import { PluginSettingsTabBase } from 'obsidian-dev-utils/obsidian/Plugin/PluginSettingsTabBase';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { CustomLocale } from 'settings/ui/CustomLocale.ts';
|
||||
import { DayStartHour } from 'settings/ui/DayStartTime.ts';
|
||||
import { DiffAlgorithmOptions } from 'settings/ui/DiffAlgorithmOptions.ts';
|
||||
import { IgnoreBlankLinesToggle } from 'settings/ui/IgnoreBlankLinesToggle.ts';
|
||||
import { IncludeItemsToggle } from 'settings/ui/IncludeItemsToggle.ts';
|
||||
import { MiscellaneousButtons } from 'settings/ui/MiscellaneousButtons.ts';
|
||||
import { WhitespaceIgnoreModeOptions } from 'settings/ui/WhitespaceIgnoreMode.ts';
|
||||
import { WhitespaceSettingsHeading } from 'settings/ui/WhitespaceSettingsHeading.ts';
|
||||
|
||||
import { AutoCommitDisabledWarning } from './ui/AutoCommitDisabledWarning.ts';
|
||||
import { CustomTimeZone } from './ui/CustomTimeZone.ts';
|
||||
import { DayStartTime } from './ui/DayStartTime.ts';
|
||||
import { DiffAlgorithmOptions } from './ui/DiffAlgorithmOptions.ts';
|
||||
import { ExcludeFilesAndFolders } from './ui/ExcludeFilesAndFolders.ts';
|
||||
import { GitPluginWarning } from './ui/GitPluginWarning.ts';
|
||||
import { RenameDetectionFileLimit } from './ui/RenameDetectionFileLimit.ts';
|
||||
import { RenameDetectionStrictnessSlider } from './ui/RenameDetectionStrictnessSlider.ts';
|
||||
import { ReviewChangesReminderInterval } from './ui/ReviewChangesReminderInterval.ts';
|
||||
import { StatusBarInterval } from './ui/StatusBarInterval.ts';
|
||||
import { StatusBarStatsToggle } from './ui/StatusBarStatsToggle.ts';
|
||||
|
||||
// Commented-out settings are for features that will be implemented later
|
||||
export class GitChangelogSettingsTab extends PluginSettingsTabBase<GitChangelogPlugin> {
|
||||
export class GitChangelogSettingsTab extends PluginSettingsTabBase<GitChangelogPluginTypes> {
|
||||
public override display(): void {
|
||||
const { containerEl, plugin } = this;
|
||||
|
||||
|
|
@ -26,73 +30,62 @@ export class GitChangelogSettingsTab extends PluginSettingsTabBase<GitChangelogP
|
|||
// Plugin.settings.notifyIfContentDeletionsAndMovesThresholdReached ??
|
||||
// DEFAULT_SETTINGS.notifyIfContentDeletionsAndMovesThresholdReached;
|
||||
|
||||
new GitPluginWarning({ containerEl, plugin }).display();
|
||||
new GitPluginWarning({ plugin }).display();
|
||||
|
||||
new AutoCommitDisabledWarning({
|
||||
containerEl,
|
||||
plugin
|
||||
}).display();
|
||||
new AutoCommitDisabledWarning({ plugin }).display();
|
||||
|
||||
new DayStartTime({ containerEl, plugin }).display();
|
||||
new DayStartHour({ plugin }).display();
|
||||
|
||||
new CustomTimeZone({ containerEl, plugin }).display();
|
||||
new CustomTimeZone({ plugin }).display();
|
||||
|
||||
new CustomLocale({ containerEl, plugin }).display();
|
||||
new CustomLocale({ plugin }).display();
|
||||
|
||||
new DiffAlgorithmOptions({
|
||||
containerEl,
|
||||
plugin
|
||||
}).display();
|
||||
// New DeletionsNotificationThreshold(plugin, containerEl, false, this).display();
|
||||
new DiffAlgorithmOptions({ plugin }).display();
|
||||
|
||||
// New DeletionsNotificationThreshold(plugin, false, this).display();
|
||||
// New configureDeletionsMovesAlert(
|
||||
// Plugin,
|
||||
// ContainerEl,
|
||||
//
|
||||
// !notifyOnLargeChanges
|
||||
// ).display();
|
||||
// New FileChangesNotificationThreshold(
|
||||
// Plugin,
|
||||
// ContainerEl,
|
||||
//
|
||||
// NotifyOnLargeChanges
|
||||
// ).display();
|
||||
// New ChangelogStatsInFileExplorerOptions(plugin, containerEl, false, this).display();
|
||||
// New ChangelogStatsInFileExplorerOptions(plugin, false, this).display();
|
||||
|
||||
// New FileExplorerStatsInterval(
|
||||
// Plugin,
|
||||
// ContainerEl,
|
||||
//
|
||||
// (plugin.settings?.fileExplorerChangelogStats ??
|
||||
// DEFAULT_SETTINGS.fileExplorerChangelogStats) ===
|
||||
// FileExplorerChangelogStats.Disabled
|
||||
// ).display();
|
||||
new ExcludeFilesAndFolders({ containerEl, plugin }).display();
|
||||
new ExcludeFilesAndFolders({ plugin }).display();
|
||||
|
||||
new RenameDetectionStrictnessSlider({
|
||||
containerEl,
|
||||
plugin
|
||||
}).display();
|
||||
new IncludeItemsToggle({ plugin }).display();
|
||||
|
||||
new RenameDetectionFileLimit({
|
||||
containerEl,
|
||||
plugin
|
||||
}).display();
|
||||
new RenameDetectionStrictnessSlider({ plugin }).display();
|
||||
|
||||
new StatusBarStatsToggle({
|
||||
containerEl,
|
||||
plugin,
|
||||
settingTab: this
|
||||
}).display();
|
||||
new RenameDetectionFileLimit({ plugin }).display();
|
||||
|
||||
new StatusBarStatsToggle({ plugin }).display();
|
||||
|
||||
new StatusBarInterval({
|
||||
containerEl,
|
||||
disabled: !(
|
||||
plugin.settings.statusBarStats ?? DEFAULT_SETTINGS.statusBarStats
|
||||
),
|
||||
disabled: !plugin.settings.showStatusBarStats,
|
||||
plugin
|
||||
}).display();
|
||||
|
||||
new MiscellaneousButtons({
|
||||
containerEl,
|
||||
plugin
|
||||
}).display();
|
||||
new ReviewChangesReminderInterval({ plugin }).display();
|
||||
|
||||
new WhitespaceSettingsHeading({ plugin }).display();
|
||||
|
||||
new WhitespaceIgnoreModeOptions({ plugin }).display();
|
||||
|
||||
new IgnoreBlankLinesToggle({ plugin }).display();
|
||||
|
||||
new MiscellaneousButtons({ plugin }).display();
|
||||
}
|
||||
// New DetectMovedContentToggle(plugin, containerEl).display();
|
||||
// New ChangelogMeasurementUnit(plugin, containerEl).display();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import type { GitChangelogSettings } from 'settings/settings.ts';
|
||||
|
||||
export class AutoCommitDisabledWarning extends GitChangelogSetting {
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class AutoCommitDisabledWarning extends SettingComponent {
|
||||
public display(): void {
|
||||
try {
|
||||
const gitPlugin = this.plugin.getGitPlugin();
|
||||
|
|
@ -19,10 +21,12 @@ export class AutoCommitDisabledWarning extends GitChangelogSetting {
|
|||
button.onClick(() => {
|
||||
warningSetting.settingEl.remove();
|
||||
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.autoCommitDisabledWarningDismissed = true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
this.plugin.settingsManager.editAndSave(
|
||||
(settings: GitChangelogSettings): void => {
|
||||
settings.autoCommitDisabledWarningDismissed = true;
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,14 @@
|
|||
import type { ChangelogGenerationSettings } from 'settings/settings.ts';
|
||||
|
||||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
import { DiffMeasurementUnit } from 'types.ts';
|
||||
|
||||
export class ChangelogMeasurementUnit extends GitChangelogSetting {
|
||||
export class ChangelogMeasurementUnit extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName('Changelog measurement unit')
|
||||
.addDropdown((dropdown) => {
|
||||
const options: Record<DiffMeasurementUnit, string> = {
|
||||
Lines: 'Lines',
|
||||
Words: 'Words'
|
||||
};
|
||||
dropdown.addOptions(options);
|
||||
dropdown.setValue(
|
||||
getMeasurementUnit(this.plugin.settings.changelogGenerationSettings)
|
||||
);
|
||||
|
||||
dropdown.onChange((value: string) => {
|
||||
const option =
|
||||
DiffMeasurementUnit[value as keyof typeof DiffMeasurementUnit];
|
||||
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.changelogGenerationSettings.measurementUnit = option;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
});
|
||||
.addTypedDropdown((dropdown) => {
|
||||
dropdown.addOption(DiffMeasurementUnit.Lines, 'Lines');
|
||||
dropdown.addOption(DiffMeasurementUnit.Words, 'Words');
|
||||
this.settingTab.bind(dropdown, 'diffMeasurementUnit');
|
||||
});
|
||||
}
|
||||
}
|
||||
export function getMeasurementUnit(
|
||||
changelogGenerationSettings: ChangelogGenerationSettings
|
||||
): DiffMeasurementUnit {
|
||||
return DiffMeasurementUnit.Lines;
|
||||
if (!validateMeasurementUnit(changelogGenerationSettings.measurementUnit)) {
|
||||
return DEFAULT_SETTINGS.changelogGenerationSettings.measurementUnit;
|
||||
}
|
||||
|
||||
return changelogGenerationSettings.measurementUnit;
|
||||
}
|
||||
|
||||
export function validateMeasurementUnit(
|
||||
measurementUnit: DiffMeasurementUnit
|
||||
): boolean {
|
||||
if (Object.values(DiffMeasurementUnit).includes(measurementUnit)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,23 @@
|
|||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
import { FileExplorerStats } from 'types.ts';
|
||||
|
||||
export class ChangelogStatsInFileExplorerOptions extends GitChangelogSetting {
|
||||
export class ChangelogStatsInFileExplorerOptions extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName('Show changelog stats in file explorer')
|
||||
.addDropdown((dropdown) => {
|
||||
const options: Record<FileExplorerStats, string> = {
|
||||
Disabled: 'Disabled',
|
||||
Folders: 'Folders',
|
||||
FoldersAndNotes: 'Notes'
|
||||
};
|
||||
dropdown.addOptions(options);
|
||||
dropdown.setValue(
|
||||
this.plugin.settings.fileExplorerStats ??
|
||||
DEFAULT_SETTINGS.fileExplorerStats
|
||||
.addTypedDropdown((dropdown) => {
|
||||
dropdown.addOption(FileExplorerStats.Disabled, 'Disabled');
|
||||
dropdown.addOption(FileExplorerStats.Folders, 'Folders');
|
||||
dropdown.addOption(
|
||||
FileExplorerStats.FoldersAndNotes,
|
||||
'Folders and notes'
|
||||
);
|
||||
dropdown.onChange((value: string) => {
|
||||
const option =
|
||||
FileExplorerStats[value as keyof typeof FileExplorerStats];
|
||||
|
||||
this.refreshDisplayDelayed(0);
|
||||
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.fileExplorerStats = option;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
this.settingTab.bind(dropdown, 'fileExplorerStats', {
|
||||
onChanged: () => {
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
this.refreshDisplayWithDelay(30);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class ContentDeletionsMovesThreshold extends GitChangelogSetting {
|
||||
export class ContentDeletionsMovesThreshold extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName('Custom threshold for deletions/moves alert')
|
||||
|
|
@ -9,33 +8,10 @@ export class ContentDeletionsMovesThreshold extends GitChangelogSetting {
|
|||
"Acceptable amount of deletions and moves between commits. Represents either words or lines, depending on your setup. Note that this doesn't mean between each interval but between the actual commits, meaning if your auto-commit interval is five minutes, this will trigger only if you manage to delete that many files inside those five minutes, which usually signals corruption or data loss."
|
||||
)
|
||||
.addText((text) => {
|
||||
text.setDisabled(this.disabled).onChange((value) => {
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.contentDeletionsAndMovesWarningThreshold =
|
||||
validateContentDeletionsMovesThreshold(value)
|
||||
? value
|
||||
: DEFAULT_SETTINGS.contentDeletionsAndMovesWarningThreshold;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
});
|
||||
text.setDisabled(this.disabled);
|
||||
// This.restrictToPositiveIntegerInput(text);
|
||||
|
||||
this.setValueIfNonDefaultSetting({
|
||||
settingsProperty: 'contentDeletionsAndMovesWarningThreshold',
|
||||
text
|
||||
});
|
||||
this.settingTab.bind(text, 'contentDeletionsAndMovesWarningThreshold');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function validateContentDeletionsMovesThreshold(
|
||||
contentDeletionsMovesThreshold: string
|
||||
): boolean {
|
||||
if (
|
||||
!Number.isInteger(Number(contentDeletionsMovesThreshold)) ||
|
||||
Number(contentDeletionsMovesThreshold) < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,50 +1,63 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
|
||||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class CustomLocale extends GitChangelogSetting {
|
||||
export class CustomLocale extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName('Date format locale')
|
||||
.setDesc(`Accepts a locale code (e.g. "en-US").`)
|
||||
.addText((text) => {
|
||||
text.inputEl.maxLength = 30;
|
||||
text
|
||||
.setPlaceholder(DEFAULT_SETTINGS.changelogGenerationSettings.locale)
|
||||
.onChange((value) => {
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.changelogGenerationSettings.locale = value;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
});
|
||||
|
||||
this.setValueIfNonDefaultSetting({
|
||||
settingsProperty: 'changelogGenerationSettings',
|
||||
diffSettingsProperty: 'locale',
|
||||
text
|
||||
this.settingTab.bind(text, 'locale', {
|
||||
onChanged: () => {
|
||||
// Without this check, plugin fails on startup
|
||||
if (this.plugin.settingsManager) {
|
||||
this.plugin.localeSafe = getLocaleToAssign(this.plugin);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getUserLocale(plugin: GitChangelogPlugin): string {
|
||||
const locale = plugin.settings.changelogGenerationSettings.locale;
|
||||
if (validateLocale(locale)) {
|
||||
return locale;
|
||||
function getSystemLocale(plugin: GitChangelogPlugin): string {
|
||||
if (plugin.detectedLocale === undefined) {
|
||||
// Detect the system locale using Intl API and save it for the current Obsidian session.
|
||||
const systemLocale = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
|
||||
if (validateLocale(systemLocale)) {
|
||||
plugin.detectedLocale = systemLocale;
|
||||
} else {
|
||||
plugin.displayNotice(
|
||||
"Couldn't detect a valid system locale: Obsidian installer version might be too old.\nFallback to en-US."
|
||||
);
|
||||
plugin.detectedLocale = 'en-US';
|
||||
}
|
||||
}
|
||||
// Decided against using the new Obsidian language API so that the plugin is compatible with older versions of Obsidian (for now, will update later).
|
||||
return Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
|
||||
return plugin.detectedLocale;
|
||||
}
|
||||
|
||||
export function validateLocale(locale?: string): boolean {
|
||||
export function getLocaleToAssign(plugin: GitChangelogPlugin): string {
|
||||
return plugin.settings.locale ===
|
||||
plugin.settingsManager.defaultSettings.locale
|
||||
? getSystemLocale(plugin)
|
||||
: plugin.settings.locale;
|
||||
}
|
||||
|
||||
export function validateLocale(locale: string): boolean {
|
||||
try {
|
||||
if (!locale || typeof locale !== 'string') {
|
||||
return false;
|
||||
}
|
||||
new Intl.Locale(locale);
|
||||
return Intl.DateTimeFormat.supportedLocalesOf([locale]).length > 0;
|
||||
if (Intl.DateTimeFormat.supportedLocalesOf([locale]).length === 0) {
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,77 +1,48 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
import type { ChangelogGenerationSettings } from 'settings/settings.ts';
|
||||
|
||||
import { Notice } from 'obsidian';
|
||||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
import { TimeZoneSuggest } from 'settings/components/suggest.ts';
|
||||
import { DEFAULT_SETTINGS, TIME_ZONES_LIST } from 'settings/settings.ts';
|
||||
import { TIME_ZONES_LIST } from 'settings/settings.ts';
|
||||
|
||||
export class CustomTimeZone extends GitChangelogSetting {
|
||||
export class CustomTimeZone extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName('Timezone')
|
||||
.addText((text) => {
|
||||
text
|
||||
.setPlaceholder(DEFAULT_SETTINGS.changelogGenerationSettings.timezone)
|
||||
.onChange((value) => {
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.changelogGenerationSettings.timezone = value;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
});
|
||||
|
||||
this.setValueIfNonDefaultSetting({
|
||||
diffSettingsProperty: 'timezone',
|
||||
settingsProperty: 'changelogGenerationSettings',
|
||||
text
|
||||
});
|
||||
this.settingTab.bind(text, 'timeZone');
|
||||
|
||||
new TimeZoneSuggest(this.plugin.app, text.inputEl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function detectSystemTimeZone(): string {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone.toLowerCase();
|
||||
}
|
||||
function getSystemTimeZone(plugin: GitChangelogPlugin): string {
|
||||
if (plugin.detectedTimeZone === undefined) {
|
||||
// Detect the system time zone using Intl API and save it for the current Obsidian session.
|
||||
const systemTimeZone = Intl.DateTimeFormat()
|
||||
.resolvedOptions()
|
||||
.timeZone.toLowerCase();
|
||||
|
||||
export function getSystemTimeZone(plugin: GitChangelogPlugin): string {
|
||||
if (plugin.detectedTimeZone !== undefined) {
|
||||
return plugin.detectedTimeZone;
|
||||
if (validateCustomTimeZone(systemTimeZone)) {
|
||||
plugin.detectedTimeZone = systemTimeZone;
|
||||
} else {
|
||||
plugin.displayNotice(
|
||||
"Couldn't detect a valid system time zone: Obsidian installer version might be too old.\nFallback to UTC."
|
||||
);
|
||||
plugin.detectedTimeZone = 'utc';
|
||||
}
|
||||
}
|
||||
|
||||
const systemTimeZone = detectSystemTimeZone();
|
||||
if (validateCustomTimeZone(systemTimeZone)) {
|
||||
plugin.detectedTimeZone = systemTimeZone;
|
||||
} else {
|
||||
new Notice(
|
||||
"Couldn't detect a valid system time zone: Obsidian installer version might be too old.\nFallback to UTC."
|
||||
);
|
||||
plugin.detectedTimeZone = 'utc';
|
||||
return 'utc';
|
||||
}
|
||||
return plugin.detectedTimeZone;
|
||||
}
|
||||
|
||||
export function getTimeZone(
|
||||
changelogGenerationSettings: ChangelogGenerationSettings,
|
||||
plugin: GitChangelogPlugin
|
||||
): string {
|
||||
return validateCustomTimeZone(changelogGenerationSettings.timezone)
|
||||
? changelogGenerationSettings.timezone
|
||||
: getSystemTimeZone(plugin);
|
||||
export function getTimeZone(plugin: GitChangelogPlugin): string {
|
||||
return plugin.settings.timeZone ===
|
||||
plugin.settingsManager.defaultSettings.timeZone
|
||||
? getSystemTimeZone(plugin)
|
||||
: plugin.settings.timeZone;
|
||||
}
|
||||
|
||||
export function systemTimeZoneUnchanged(plugin: GitChangelogPlugin): boolean {
|
||||
const systemTimeZone = detectSystemTimeZone();
|
||||
const cachedSystemTimeZone = plugin.detectedTimeZone;
|
||||
if (systemTimeZone !== cachedSystemTimeZone) {
|
||||
plugin.detectedTimeZone = systemTimeZone;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function validateCustomTimeZone(timezone: string): boolean {
|
||||
return TIME_ZONES_LIST.has(timezone.toLowerCase());
|
||||
export function validateCustomTimeZone(timeZone: string): boolean {
|
||||
return TIME_ZONES_LIST.has(timeZone.toLowerCase());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,60 +1,32 @@
|
|||
import type { ChangelogGenerationSettings } from 'settings/settings.ts';
|
||||
|
||||
import { moment } from 'obsidian';
|
||||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class DayStartTime extends GitChangelogSetting {
|
||||
export class DayStartHour extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName('Day start time')
|
||||
.setDesc('Adjust the day based on your schedule.')
|
||||
.setDesc(
|
||||
'Adjust the day based on your schedule. Applies exclusively to the day interval in the changelog views.'
|
||||
)
|
||||
.addTime((text) => {
|
||||
text
|
||||
.setValue(
|
||||
moment.duration({
|
||||
hours:
|
||||
this.plugin.settings.changelogGenerationSettings.dayStartHour
|
||||
})
|
||||
)
|
||||
.onChange((value) => {
|
||||
this.settingTab.bind(text, 'dayStartHour', {
|
||||
componentToPluginSettingsValueConverter: (
|
||||
uiValue: moment.Duration
|
||||
) => {
|
||||
// Clip any minutes. The smallest possible interval is an hour and this value should be clipped to that.
|
||||
// Allowing the day start time to be specified in minutes doesn't make sense because then you would need to handle the half an hour that belongs to the previous day and the other half an hour that belongs to the next day separately.
|
||||
text.setValue(
|
||||
moment.duration({
|
||||
hours: value.hours()
|
||||
})
|
||||
);
|
||||
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.changelogGenerationSettings.dayStartHour =
|
||||
value.hours();
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
});
|
||||
|
||||
text.inputEl.step = '3600';
|
||||
// Allowing the day start time to be specified in minutes doesn't make sense because then e.g. you would need to handle the half an hour that belongs to the previous day and the other half an hour that belongs to the next day separately.
|
||||
return uiValue.hours();
|
||||
},
|
||||
onChanged: (value) => {
|
||||
text.setValue(moment.duration(value, 'hours'));
|
||||
},
|
||||
shouldShowValidationMessage: false,
|
||||
pluginSettingsToComponentValueConverter: (
|
||||
pluginSettingsValue: number
|
||||
) => moment.duration(pluginSettingsValue, 'hours')
|
||||
});
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
text.setStep(3600);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getDayStartTime(
|
||||
changelogGenerationSettings: ChangelogGenerationSettings
|
||||
): number {
|
||||
return validateDayStartTime(changelogGenerationSettings.dayStartHour)
|
||||
? changelogGenerationSettings.dayStartHour
|
||||
: DEFAULT_SETTINGS.changelogGenerationSettings.dayStartHour;
|
||||
}
|
||||
|
||||
export const ONE_DAY_IN_HOURS = 24;
|
||||
|
||||
export function validateDayStartTime(dayStartTime: number): boolean {
|
||||
if (
|
||||
!Number.isInteger(dayStartTime) ||
|
||||
dayStartTime < 0 ||
|
||||
dayStartTime >= ONE_DAY_IN_HOURS
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,18 @@
|
|||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class DeletionsNotificationThreshold extends GitChangelogSetting {
|
||||
export class DeletionsNotificationThreshold extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName('Notify on large amount of changes.')
|
||||
.setDesc(
|
||||
'Notify if changes between neighboring commits exceed a threshold, which can be a sign of data loss or corruption.'
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(
|
||||
this.plugin.settings
|
||||
.notifyOnContentDeletionsAndMovesThresholdReached
|
||||
)
|
||||
.onChange((value) => {
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.notifyOnContentDeletionsAndMovesThresholdReached =
|
||||
value;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
|
||||
this.refreshDisplayDelayed();
|
||||
})
|
||||
);
|
||||
.addToggle((toggle) => {
|
||||
this.settingTab.bind(toggle, 'notifyOnHighContentDeletionsAndMoves', {
|
||||
onChanged: () => {
|
||||
this.refreshDisplayWithDelay();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class DetectMovedContentToggle extends GitChangelogSetting {
|
||||
export class DetectMovedContentToggle extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName('Detect moved lines/words')
|
||||
|
|
@ -8,16 +8,7 @@ export class DetectMovedContentToggle extends GitChangelogSetting {
|
|||
`If enabled, changelog will also track all moved words or lines between files or moved to another location in the same file. Adds significant computational overhead that increases with the number and size of changes.`
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(
|
||||
this.plugin.settings.changelogGenerationSettings.detectMovedContent
|
||||
)
|
||||
.onChange((value) => {
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.changelogGenerationSettings.detectMovedContent = value;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
})
|
||||
this.settingTab.bind(toggle, 'detectMovedContent')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,50 +1,64 @@
|
|||
import type { ChangelogGenerationSettings } from 'settings/settings.ts';
|
||||
import type GitChangelogPlugin from 'main.ts';
|
||||
|
||||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { appendCodeBlock } from 'obsidian-dev-utils/HTMLElement';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
import { DiffAlgorithm } from 'types.ts';
|
||||
|
||||
export class DiffAlgorithmOptions extends GitChangelogSetting {
|
||||
const GIT_CONFIG_PATH = '.git/config';
|
||||
|
||||
export class DiffAlgorithmOptions extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName('Difference detection algorithm')
|
||||
.setDesc(
|
||||
"It's recommended to set a custom diff algorithm in git config instead of here, for consistency with the Diff viewer and other plugins."
|
||||
createFragment((fragment) => {
|
||||
fragment.appendText(
|
||||
`For consistency with the unified Diff view and other git operations, it's recommended to also `
|
||||
);
|
||||
appendCodeBlock(fragment, 'Apply');
|
||||
fragment.appendText(` the custom algorithm to `);
|
||||
appendCodeBlock(fragment, GIT_CONFIG_PATH);
|
||||
fragment.appendText(`.`);
|
||||
})
|
||||
)
|
||||
.addDropdown((dropdown) => {
|
||||
const options: Record<DiffAlgorithm, string> = {
|
||||
Default: 'Default (faster)', // Myers
|
||||
Inherit: 'Use git config',
|
||||
Minimal: 'Minimal (more precise)'
|
||||
};
|
||||
dropdown.addOptions(options);
|
||||
|
||||
dropdown.setValue(
|
||||
getDiffAlgorithm(this.plugin.settings.changelogGenerationSettings)
|
||||
);
|
||||
|
||||
dropdown.onChange((value: string) => {
|
||||
const option = DiffAlgorithm[value as keyof typeof DiffAlgorithm];
|
||||
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.changelogGenerationSettings.diffAlgorithm = option;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
.addButton((button) => {
|
||||
button.setButtonText('Apply');
|
||||
button.setDisabled(shouldDisableApplyButton(this.plugin));
|
||||
button.onClick(async () => {
|
||||
const diffAlgorithm = this.plugin.settings.diffAlgorithm;
|
||||
if (diffAlgorithm !== DiffAlgorithm.Inherit) {
|
||||
try {
|
||||
const git = await this.plugin.getGit();
|
||||
await git.addConfig('diff.algorithm', diffAlgorithm);
|
||||
this.plugin.displayNotice(
|
||||
`Successfully applied "${diffAlgorithm}" diff algorithm to ${GIT_CONFIG_PATH}.`
|
||||
);
|
||||
} catch (error) {
|
||||
this.plugin.displayError(
|
||||
`Failed to apply diff algorithm to git config. ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.addTypedDropdown((dropdown) => {
|
||||
dropdown.addOption(DiffAlgorithm.Inherit, 'Inherit git config');
|
||||
dropdown.addOption(DiffAlgorithm.Default, 'Default (Faster)');
|
||||
dropdown.addOption(DiffAlgorithm.Minimal, 'Minimal (More precise)');
|
||||
this.settingTab.bind(dropdown, 'diffAlgorithm', {
|
||||
onChanged: () => {
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
this.refreshDisplayWithDelay(30);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getDiffAlgorithm(
|
||||
changelogGenerationSettings: ChangelogGenerationSettings
|
||||
): DiffAlgorithm {
|
||||
if (
|
||||
!Object.values(DiffAlgorithm).includes(
|
||||
changelogGenerationSettings.diffAlgorithm
|
||||
)
|
||||
) {
|
||||
return DEFAULT_SETTINGS.changelogGenerationSettings.diffAlgorithm;
|
||||
function shouldDisableApplyButton(plugin: GitChangelogPlugin): boolean {
|
||||
const diffAlgorithm = plugin.settings.diffAlgorithm;
|
||||
if (diffAlgorithm === DiffAlgorithm.Inherit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return changelogGenerationSettings.diffAlgorithm;
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,175 +1,161 @@
|
|||
// Import { compile } from "@gerhobbelt/gitignore-parser";
|
||||
|
||||
import type { GitChangelogPlugin } from 'GitChangelogPlugin.svelte.ts';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { EXCLUDE_FILES_AND_FOLDERS } from 'constants.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class ExcludeFilesAndFolders extends GitChangelogSetting {
|
||||
export class ExcludeFilesAndFolders extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName('Exclude files and folders (experimental)')
|
||||
.setName(
|
||||
createFragment((fragment) => {
|
||||
fragment.appendText(EXCLUDE_FILES_AND_FOLDERS);
|
||||
fragment
|
||||
.createEl('span', {
|
||||
cls: 'nav-file-tag git-changelog-experimental'
|
||||
})
|
||||
.setText('EXPERIMENTAL');
|
||||
})
|
||||
)
|
||||
.setDesc(
|
||||
createFragment((fragment) => {
|
||||
fragment.appendText(
|
||||
"Items listed here will still be committed to your repository but won't be included in the vault changelog. Use the same "
|
||||
"Items listed here will still be committed to your repository but won't be included in the vault changelog."
|
||||
);
|
||||
fragment.createEl('br');
|
||||
fragment.appendText('Use the same ');
|
||||
fragment.createEl('a', {
|
||||
href: 'https://gitcheatsheet.org/how-to/git-gitignore',
|
||||
text: 'syntax'
|
||||
});
|
||||
fragment.appendText(
|
||||
" as .gitignore, but note that negation patterns are not yet supported. The main .gitignore file still applies. Don't forget to put .md for markdown files."
|
||||
' as .gitignore, but note that negation patterns are not yet supported.'
|
||||
);
|
||||
fragment.createEl('br');
|
||||
fragment.appendText('The main .gitignore file still applies.');
|
||||
fragment.createEl('br');
|
||||
fragment.appendText("Don't forget to put .md for markdown files.");
|
||||
})
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
this.settingTab.bind(toggle, 'enableExclusionList', {
|
||||
onChanged: () => {
|
||||
this.refreshDisplayWithDelay();
|
||||
}
|
||||
});
|
||||
toggle.setTooltip(
|
||||
`List is ${toggle.getValue() ? 'enabled' : 'temporarily disabled'}`
|
||||
);
|
||||
})
|
||||
.addTextArea((text) => {
|
||||
text
|
||||
.setValue(
|
||||
this.plugin.settings.vaultChangelogGenerationSettings
|
||||
.gitDiffIgnore ??
|
||||
DEFAULT_SETTINGS.vaultChangelogGenerationSettings.gitDiffIgnore
|
||||
)
|
||||
.onChange((value) => {
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.vaultChangelogGenerationSettings.gitDiffIgnore = value;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
});
|
||||
text.setDisabled(!this.plugin.settings.enableExclusionList);
|
||||
if (!this.plugin.settings.enableExclusionList) {
|
||||
text.inputEl.addClass('git-changelog-disabled');
|
||||
}
|
||||
|
||||
this.settingTab.bind(text, 'excludeFilesAndFoldersLines', {
|
||||
componentToPluginSettingsValueConverter: (uiValue) =>
|
||||
splitExcludeItems(uiValue),
|
||||
|
||||
pluginSettingsToComponentValueConverter: (pluginSettingsValue) =>
|
||||
joinExcludeItems(pluginSettingsValue)
|
||||
});
|
||||
|
||||
text.inputEl.classList.add('git-changelog-text-area');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Doesn't work properly, using same gitignore for all submodules for now
|
||||
*/
|
||||
export function adjustGitignoreForSubmodule(
|
||||
gitignoreContent: string,
|
||||
submodulePath: string
|
||||
): string {
|
||||
// Ensure submodulePath ends with a slash for matching purposes.
|
||||
if (!submodulePath.endsWith('/')) {
|
||||
submodulePath += '/';
|
||||
}
|
||||
export function convertPathToGitIgnoreRule(path: string): string {
|
||||
// First escape special characters (except whitespace), so that git doesn't interpret them as such.
|
||||
const escaped = path.replaceAll(/(?<temp1>[\\!#*?[\]])/g, String.raw`\$1`);
|
||||
|
||||
// Regex for detecting glob characters.
|
||||
const globRegex = /[*?[\]]/;
|
||||
// Then escape the last trailing whitespace character, because git trims unescaped trailing whitespace from the end of the line.
|
||||
// Files normally end with a file extension, not whitespace, but a file with trailing whitespace can appear if Obsidian's "Detect all file extensions" setting is turned on.
|
||||
return escaped.replaceAll(/\s(?=\s*$)/g, String.raw`\ `);
|
||||
}
|
||||
|
||||
const lines = gitignoreContent.split('\n');
|
||||
const adjustedLines: string[] = [];
|
||||
export function joinExcludeItems(items: readonly string[]): string {
|
||||
return items.join('\n');
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
export function splitExcludeItems(items: string): string[] {
|
||||
return items.split('\n');
|
||||
}
|
||||
|
||||
// Keep blank lines and comments unchanged.
|
||||
if (trimmed === '' || trimmed.startsWith('#')) {
|
||||
adjustedLines.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
let isNegation = false;
|
||||
let rule = trimmed;
|
||||
|
||||
// Handle negation: remove the leading "!" for processing.
|
||||
if (rule.startsWith('!')) {
|
||||
isNegation = true;
|
||||
rule = rule.slice(1).trim();
|
||||
}
|
||||
|
||||
// If the rule explicitly starts with the submodule path,
|
||||
// Remove that prefix so it is relative to the submodule.
|
||||
if (rule.startsWith(submodulePath)) {
|
||||
const adjustedRule = rule.slice(submodulePath.length);
|
||||
adjustedLines.push(isNegation ? `!${adjustedRule}` : adjustedRule);
|
||||
} else if (globRegex.test(rule)) {
|
||||
// If the rule doesn't start with the submodule path...
|
||||
// - If it is hardcoded (no glob characters), drop it.
|
||||
// - If it contains globs, leave it intact.
|
||||
adjustedLines.push(line); // Leave the original rule unchanged.
|
||||
// Else: hardcoded rule outside the submodule is omitted.
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any empty lines which might have resulted from omitting rules.
|
||||
return adjustedLines.filter((l) => l.trim() !== '').join('\n');
|
||||
// Trim trailing spaces that aren't preceded by a backslash before you apply gitignore to pathspec transformations.
|
||||
export function parseGitIgnoreLine(gitIgnoreLine: string): string {
|
||||
// This regex will match trailing spaces that are not preceded by a backslash and stop trimming when a backslash is encountered
|
||||
return gitIgnoreLine
|
||||
.replace(/(?<!\\)\s+$/, '')
|
||||
.replace(/\\\s*$/, String.raw`\ `);
|
||||
}
|
||||
|
||||
export function convertGitIgnoreToPathspec(
|
||||
plugin: GitChangelogPlugin
|
||||
gitIgnoreRules: ReadonlyDeep<string[]>,
|
||||
include: boolean
|
||||
): string[] {
|
||||
const gitDiffIgnore =
|
||||
plugin.settings.vaultChangelogGenerationSettings.gitDiffIgnore;
|
||||
try {
|
||||
// Always exclude .git directory
|
||||
const excludes: string[] = [':(exclude,glob)**/.git/**'];
|
||||
const includes: string[] = [];
|
||||
const items: string[] = [];
|
||||
|
||||
for (const line of gitDiffIgnore
|
||||
.split('\n')
|
||||
.map((diffLine) => diffLine.trim())) {
|
||||
if (!(isValidGitIgnoreRule(line) && !line.startsWith('!'))) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
|
||||
const excludePrefix = include === true ? '' : 'exclude,';
|
||||
|
||||
for (const line of gitIgnoreRules.map((element) =>
|
||||
parseGitIgnoreLine(element)
|
||||
)) {
|
||||
if (!isValidGitIgnoreRule(line) || line.startsWith('!')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const formatted = formatGitIgnoreToPathspec(line);
|
||||
const formatted = convertGitIgnoreRuleToPathspec(line);
|
||||
|
||||
// Handle patterns differently based on their format:
|
||||
if (line.endsWith('/*')) {
|
||||
// Single-level match
|
||||
excludes.push(`:(exclude,glob)${formatted}`);
|
||||
items.push(`:(${excludePrefix}glob)${formatted}`);
|
||||
} else if (line.endsWith('/**') || line.endsWith('/')) {
|
||||
// Recursive match
|
||||
excludes.push(`:(exclude,glob)${formatted}`);
|
||||
items.push(`:(${excludePrefix}glob)${formatted}`);
|
||||
} else {
|
||||
// All other patterns - match both item and contents
|
||||
// Regardless of whether they contain special characters
|
||||
excludes.push(
|
||||
`:(exclude,glob)${formatted}`,
|
||||
`:(exclude,glob)${formatted}/**`
|
||||
items.push(
|
||||
`:(${excludePrefix}glob)${formatted}`,
|
||||
`:(${excludePrefix}glob)${formatted}/**`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return [...excludes, ...includes];
|
||||
} catch (error) {
|
||||
plugin.consoleDebug(`Error converting gitignore to pathspec: ${error}`);
|
||||
return items;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidGitIgnoreRule(rule: string): boolean {
|
||||
if (!rule || rule.trim().length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (rule.startsWith('#')) {
|
||||
export function isValidGitIgnoreRule(parsedGitIgnoreLine: string): boolean {
|
||||
// Empty lines are not valid rules
|
||||
if (!parsedGitIgnoreLine || parsedGitIgnoreLine.trim().length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Strip leading negation if present
|
||||
const patternToCheck = rule.startsWith('!') ? rule.slice(1) : rule;
|
||||
const parts = patternToCheck.split('\\');
|
||||
for (let index = 0; index < parts.length - 1; index++) {
|
||||
if (/\s/.test(parts[index])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (patternToCheck.includes('//') || patternToCheck.includes('**/**')) {
|
||||
// Comments are not valid rules
|
||||
if (parsedGitIgnoreLine.startsWith('#')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Quick check for square bracket balance
|
||||
const openBrackets = (patternToCheck.match(/\[/g) ?? []).length;
|
||||
const closeBrackets = (patternToCheck.match(/\]/g) ?? []).length;
|
||||
if (openBrackets !== closeBrackets) {
|
||||
if (
|
||||
parsedGitIgnoreLine.includes('//') ||
|
||||
parsedGitIgnoreLine.includes('**/**')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hardcoded checking for problematic patterns
|
||||
if (
|
||||
/^[./]*$/.test(rule) || // Matches '.', '..', '/', './', '../', etc.
|
||||
rule === '/' || // Root directory
|
||||
/^\.{2,}\/.*$/.test(rule) // Anything starting with multiple dots and slash
|
||||
/^[./]*$/.test(parsedGitIgnoreLine) || // Matches '.', '..', '/', './', '../', etc.
|
||||
parsedGitIgnoreLine === '/' || // Root directory
|
||||
/^\.{2,}\/.*$/.test(parsedGitIgnoreLine) // Anything starting with multiple dots and slash
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -177,29 +163,31 @@ export function isValidGitIgnoreRule(rule: string): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
function formatGitIgnoreToPathspec(pattern: string): string {
|
||||
pattern = pattern.trim();
|
||||
|
||||
// Works well for the usual cases. Doesn't try to handle all edge cases yet.
|
||||
function convertGitIgnoreRuleToPathspec(parsedGitIgnoreLine: string): string {
|
||||
// Handle root-relative paths
|
||||
let rootRelative = false;
|
||||
if (pattern.startsWith('/')) {
|
||||
if (parsedGitIgnoreLine.startsWith('/')) {
|
||||
rootRelative = true;
|
||||
pattern = pattern.slice(1);
|
||||
parsedGitIgnoreLine = parsedGitIgnoreLine.slice(1);
|
||||
}
|
||||
|
||||
// For non-root patterns, prefix with **/ for a recursive match
|
||||
if (
|
||||
!rootRelative &&
|
||||
!pattern.startsWith('*/') &&
|
||||
!pattern.startsWith('**/')
|
||||
!parsedGitIgnoreLine.startsWith('*/') &&
|
||||
!parsedGitIgnoreLine.startsWith('**/')
|
||||
) {
|
||||
pattern = `**/${pattern}`;
|
||||
parsedGitIgnoreLine = `**/${parsedGitIgnoreLine}`;
|
||||
}
|
||||
|
||||
// If the pattern ends with a slash, add '**' for directory contents
|
||||
if (pattern.endsWith('/') && !pattern.endsWith('*/')) {
|
||||
pattern += '**';
|
||||
if (
|
||||
parsedGitIgnoreLine.endsWith('/') &&
|
||||
!parsedGitIgnoreLine.endsWith('*/')
|
||||
) {
|
||||
parsedGitIgnoreLine += '**';
|
||||
}
|
||||
|
||||
return pattern;
|
||||
return parsedGitIgnoreLine;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +1,15 @@
|
|||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class FileChangesNotificationThreshold extends GitChangelogSetting {
|
||||
export class FileChangesNotificationThreshold extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName('Custom threshold for file changes alert')
|
||||
|
||||
.addText((text) => {
|
||||
text.setDisabled(this.disabled).onChange((value) => {
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.contentDeletionsAndMovesWarningThreshold =
|
||||
validateFileChangesNotificationThreshold(value)
|
||||
? value
|
||||
: DEFAULT_SETTINGS.contentDeletionsAndMovesWarningThreshold;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
});
|
||||
// This.restrictToPositiveIntegerInput(text);
|
||||
this.settingTab.bind(text, 'filesChangedWarningThreshold');
|
||||
|
||||
this.setValueIfNonDefaultSetting({
|
||||
settingsProperty: 'filesChangesWarningThreshold',
|
||||
text
|
||||
});
|
||||
text.setDisabled(this.disabled);
|
||||
|
||||
// This.restrictToPositiveIntegerInput(text);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function validateFileChangesNotificationThreshold(
|
||||
fileChangesNotificationThreshold: string
|
||||
): boolean {
|
||||
if (
|
||||
!Number.isInteger(Number(fileChangesNotificationThreshold)) ||
|
||||
Number(fileChangesNotificationThreshold) < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,13 @@
|
|||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { DEFAULT_SETTINGS, MAX_SUPPORTED_INTERVAL } from 'settings/settings.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class FileExplorerStatsInterval extends GitChangelogSetting {
|
||||
export class FileExplorerStatsInterval extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
|
||||
.setName('Interval for file explorer stats (minutes)')
|
||||
|
||||
.addText((text) => {
|
||||
text.setDisabled(this.disabled).onChange((value) => {
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.fileExplorerInterval = validateFileExplorerStatsInterval(
|
||||
value
|
||||
)
|
||||
? value
|
||||
: DEFAULT_SETTINGS.fileExplorerInterval;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
});
|
||||
this.settingTab.bind(text, 'fileExplorerInterval');
|
||||
|
||||
// This.restrictToPositiveIntegerInput(text, 5);
|
||||
this.setValueIfNonDefaultSetting({
|
||||
settingsProperty: 'fileExplorerInterval',
|
||||
text
|
||||
});
|
||||
text.setDisabled(this.disabled);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function validateFileExplorerStatsInterval(
|
||||
fileExplorerStatsInterval: string
|
||||
): boolean {
|
||||
if (
|
||||
!Number.isInteger(Number(fileExplorerStatsInterval)) ||
|
||||
Number(fileExplorerStatsInterval) < 1 ||
|
||||
Number(fileExplorerStatsInterval) > MAX_SUPPORTED_INTERVAL
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ import type { Setting } from 'obsidian';
|
|||
import { compareVersions } from 'compare-versions';
|
||||
import {
|
||||
MAX_TESTED_GIT_PLUGIN_VERSION,
|
||||
MIN_COMPATIBLE_GIT_PLUGIN_VERSION,
|
||||
PLUGIN_NAME
|
||||
MIN_COMPATIBLE_GIT_PLUGIN_VERSION
|
||||
} from 'constants.ts';
|
||||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
import { GitPluginState } from 'types.ts';
|
||||
|
||||
export class GitPluginWarning extends GitChangelogSetting {
|
||||
export class GitPluginWarning extends SettingComponent {
|
||||
public display(): void {
|
||||
let desc: DocumentFragment | string;
|
||||
let setting: Setting;
|
||||
|
|
@ -22,18 +21,21 @@ export class GitPluginWarning extends GitChangelogSetting {
|
|||
break;
|
||||
}
|
||||
case GitPluginState.IncompatibleVersion: {
|
||||
desc = '⚠️ The installed Git plugin version is incompatible.';
|
||||
desc = `⚠️ The installed Git plugin version is incompatible. Oldest compatible version is ${
|
||||
MIN_COMPATIBLE_GIT_PLUGIN_VERSION
|
||||
}`;
|
||||
break;
|
||||
}
|
||||
case GitPluginState.UntestedVersion: {
|
||||
desc =
|
||||
'Compatibility with the installed Git plugin version is not tested.';
|
||||
desc = `Compatibility with the installed Git plugin version is not tested. Latest tested version is ${
|
||||
MAX_TESTED_GIT_PLUGIN_VERSION
|
||||
}`;
|
||||
// A button to continue using at your own risk
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
desc = createFragment((fragment) => {
|
||||
fragment.appendText(`⚠️ ${PLUGIN_NAME} requires the `);
|
||||
fragment.appendText(`⚠️ This plugin requires the `);
|
||||
fragment.createEl('a', {
|
||||
href: 'https://github.com/Vinzent03/obsidian-git',
|
||||
text: 'Obsidian Git'
|
||||
|
|
|
|||
16
src/settings/ui/IgnoreBlankLinesToggle.ts
Normal file
16
src/settings/ui/IgnoreBlankLinesToggle.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class IgnoreBlankLinesToggle extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName(
|
||||
createFragment((fragment) => {
|
||||
fragment.appendText(`Ignore changes whose lines are all blank`);
|
||||
})
|
||||
)
|
||||
.setDesc(
|
||||
"Doesn't show a file as changed if the only changes are blank lines."
|
||||
)
|
||||
.addToggle((toggle) => this.settingTab.bind(toggle, 'ignoreBlankLines'));
|
||||
}
|
||||
}
|
||||
26
src/settings/ui/IncludeItemsToggle.ts
Normal file
26
src/settings/ui/IncludeItemsToggle.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { EXCLUDE_FILES_AND_FOLDERS } from 'constants.ts';
|
||||
import { appendCodeBlock } from 'obsidian-dev-utils/HTMLElement';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class IncludeItemsToggle extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName(
|
||||
createFragment((fragment) => {
|
||||
fragment.appendText(`Include items instead`);
|
||||
})
|
||||
)
|
||||
.setDesc(
|
||||
createFragment((fragment) => {
|
||||
fragment.appendText('Include the items listed in ');
|
||||
appendCodeBlock(fragment, EXCLUDE_FILES_AND_FOLDERS);
|
||||
fragment.appendText(
|
||||
` instead of excluding them, and exclude everything else.`
|
||||
);
|
||||
})
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
this.settingTab.bind(toggle, 'convertToIncludeList')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { Notice } from 'obsidian';
|
||||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { FEEDBACK_URL } from 'constants.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class MiscellaneousButtons extends GitChangelogSetting {
|
||||
export class MiscellaneousButtons extends SettingComponent {
|
||||
public display(): void {
|
||||
const bugReportDiv = this.createSetting();
|
||||
bugReportDiv.addButton((button) => {
|
||||
button.setButtonText('Give feedback');
|
||||
button.onClick(() => {
|
||||
window.open('https://github.com/shumadrid/obsidian-git-changelog');
|
||||
window.open(FEEDBACK_URL);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ export class MiscellaneousButtons extends GitChangelogSetting {
|
|||
4
|
||||
)
|
||||
);
|
||||
new Notice(
|
||||
this.plugin.displayNotice(
|
||||
'Debug information copied to clipboard. May contain sensitive information!'
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,53 +1,24 @@
|
|||
import type { ChangelogGenerationSettings } from 'settings/settings.ts';
|
||||
|
||||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
// https://github.com/git/git/blob/58b5801aa94ad5031978f8e42c1be1230b3d352f/diff.c#L58 - 1000 default
|
||||
export class RenameDetectionFileLimit extends GitChangelogSetting {
|
||||
export class RenameDetectionFileLimit extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
|
||||
.setName('Rename detection file limit')
|
||||
.setDesc(
|
||||
'If number of changed files exceeds the limit, exhaustive rename detection won’t run, though some renames may still be detected. Be aware of potential computation costs when setting higher limits. Set 0 for no limit.'
|
||||
createFragment((fragment) => {
|
||||
fragment.appendText(
|
||||
'If number of changed files exceeds the limit, exhaustive rename detection won’t run, though some renames may still be detected.'
|
||||
);
|
||||
fragment.createEl('br');
|
||||
fragment.appendText(
|
||||
'Be aware of potential computation costs when setting higher limits. Set 0 for no limit.'
|
||||
);
|
||||
})
|
||||
)
|
||||
.addText((text) => {
|
||||
text.inputEl.pattern = '[0-9]*';
|
||||
|
||||
text.inputEl.maxLength = 20;
|
||||
text.inputEl.inputMode = 'numeric';
|
||||
text
|
||||
.setPlaceholder(
|
||||
DEFAULT_SETTINGS.changelogGenerationSettings.renameLimit
|
||||
)
|
||||
.onChange((value) => {
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.changelogGenerationSettings.renameLimit = String(value);
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
});
|
||||
// This.restrictToPositiveIntegerInput(text);
|
||||
|
||||
this.setValueIfNonDefaultSetting({
|
||||
diffSettingsProperty: 'renameLimit',
|
||||
settingsProperty: 'changelogGenerationSettings',
|
||||
text
|
||||
});
|
||||
.addNumber((text) => {
|
||||
this.settingTab.bind(text, 'renameLimit');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getRenameLimit(
|
||||
changelogGenerationSettings: ChangelogGenerationSettings
|
||||
): number {
|
||||
if (!validateRenameLimit(changelogGenerationSettings.renameLimit)) {
|
||||
return Number(DEFAULT_SETTINGS.changelogGenerationSettings.renameLimit);
|
||||
}
|
||||
|
||||
return Number(changelogGenerationSettings.renameLimit);
|
||||
}
|
||||
|
||||
export function validateRenameLimit(renameLimit: string): boolean {
|
||||
return Number(renameLimit) >= 0 && Number.isInteger(renameLimit);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,67 +1,49 @@
|
|||
import type { SliderComponent } from 'obsidian';
|
||||
import type { ChangelogGenerationSettings } from 'settings/settings.ts';
|
||||
|
||||
import {
|
||||
MAX_RENAME_DETECTION_STRICTNESS,
|
||||
MIN_RENAME_DETECTION_STRICTNESS
|
||||
} from 'constants.ts';
|
||||
import { ResetButton } from 'settings/components/resetButton.ts';
|
||||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
const MIN_RENAME_DETECTION_STRICTNESS = 1;
|
||||
const MAX_RENAME_DETECTION_STRICTNESS = 100;
|
||||
|
||||
export class RenameDetectionStrictnessSlider extends GitChangelogSetting {
|
||||
export class RenameDetectionStrictnessSlider extends SettingComponent {
|
||||
public display(): void {
|
||||
let slider: SliderComponent;
|
||||
const setting = this.createSetting()
|
||||
.setName('File move/rename detection strictness')
|
||||
.setDesc(
|
||||
"Flag files as renamed only if more than X% of the file hasn't changed. Adjust this if you notice the plugin missing valid moves/renames or showing false positives."
|
||||
createFragment((fragment) => {
|
||||
fragment.appendText(
|
||||
`Flag files as renamed only if more than X% of the file hasn't changed.`
|
||||
);
|
||||
fragment.createEl('br');
|
||||
fragment.appendText(
|
||||
'Adjust this if you notice the plugin missing valid moves/renames or showing false positives.'
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
new ResetButton(setting.controlEl).onClick(() => {
|
||||
slider.setValue(
|
||||
DEFAULT_SETTINGS.changelogGenerationSettings.renameDetectionStrictness
|
||||
const defaultValue =
|
||||
this.plugin.settingsManager.defaultSettings.renameDetectionStrictness;
|
||||
|
||||
// It saves to file, which we don't want to wait for
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.settingsManager.setProperty(
|
||||
'renameDetectionStrictness',
|
||||
defaultValue
|
||||
);
|
||||
this.refreshDisplayWithDelay(0);
|
||||
});
|
||||
|
||||
setting.addSlider((percent) => {
|
||||
slider = percent;
|
||||
this.settingTab.bind(percent, 'renameDetectionStrictness');
|
||||
|
||||
percent
|
||||
.setValue(
|
||||
getRenameDetectionStrictness(
|
||||
this.plugin.settings.changelogGenerationSettings
|
||||
)
|
||||
)
|
||||
.setLimits(
|
||||
MIN_RENAME_DETECTION_STRICTNESS,
|
||||
MAX_RENAME_DETECTION_STRICTNESS,
|
||||
1
|
||||
)
|
||||
.onChange((value) => {
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.changelogGenerationSettings.renameDetectionStrictness =
|
||||
value;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
})
|
||||
.setDynamicTooltip();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getRenameDetectionStrictness(
|
||||
changelogGenerationSettings: ChangelogGenerationSettings
|
||||
): number {
|
||||
if (
|
||||
!Number.isInteger(changelogGenerationSettings.renameDetectionStrictness) ||
|
||||
changelogGenerationSettings.renameDetectionStrictness <
|
||||
MIN_RENAME_DETECTION_STRICTNESS ||
|
||||
changelogGenerationSettings.renameDetectionStrictness >
|
||||
MAX_RENAME_DETECTION_STRICTNESS
|
||||
) {
|
||||
return DEFAULT_SETTINGS.changelogGenerationSettings
|
||||
.renameDetectionStrictness;
|
||||
}
|
||||
|
||||
return changelogGenerationSettings.renameDetectionStrictness;
|
||||
}
|
||||
|
|
|
|||
139
src/settings/ui/ReviewChangesReminderInterval.ts
Normal file
139
src/settings/ui/ReviewChangesReminderInterval.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
|
||||
import { COMPARE_TO_CHECKPOINT_VIEW_CONFIG } from 'constants.ts';
|
||||
import { Notice } from 'obsidian';
|
||||
import { invokeAsyncSafely } from 'obsidian-dev-utils/Async';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
import { removeCompareVersionsView } from 'utils.ts';
|
||||
|
||||
export class ReviewChangesReminderInterval extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName(
|
||||
createFragment((fragment) => {
|
||||
fragment.appendText('Review changes reminder interval');
|
||||
fragment
|
||||
.createEl('span', {
|
||||
cls: 'nav-file-tag git-changelog-new'
|
||||
})
|
||||
.setText('NEW');
|
||||
})
|
||||
)
|
||||
|
||||
.setDesc(
|
||||
createFragment((fragment) => {
|
||||
fragment.appendText(
|
||||
'How often to remind you to review changes made inside the vault (minutes).'
|
||||
);
|
||||
fragment.createEl('br');
|
||||
fragment.appendText('Set 0 to disable reminders.');
|
||||
})
|
||||
)
|
||||
.addNumber((text) => {
|
||||
this.settingTab.bind(text, 'reviewChangesReminderInterval');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses the checkpoint reminder notification if present.
|
||||
*/
|
||||
export async function dismissCheckpointReminder(
|
||||
plugin: GitChangelogPlugin,
|
||||
hide: boolean
|
||||
): Promise<void> {
|
||||
if (plugin.checkpointReminderNotice) {
|
||||
if (hide) {
|
||||
plugin.checkpointReminderNotice.hide();
|
||||
}
|
||||
plugin.checkpointReminderNotice = undefined;
|
||||
}
|
||||
await resetCheckpointReminderCounter(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the activeMinutesPassedSinceLastCheckpoint counter in settings.
|
||||
*/
|
||||
export async function resetCheckpointReminderCounter(
|
||||
plugin: GitChangelogPlugin
|
||||
): Promise<void> {
|
||||
await plugin.settingsManager.editAndSave((settings) => {
|
||||
settings.activeMinutesPassedSinceLastCheckpoint = 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the minute interval for checkpoint reminders.
|
||||
*/
|
||||
export async function handleReviewChangesReminderInterval(
|
||||
plugin: GitChangelogPlugin
|
||||
): Promise<void> {
|
||||
const { reviewChangesReminderInterval } = plugin.settings;
|
||||
if (!reviewChangesReminderInterval || reviewChangesReminderInterval <= 0) {
|
||||
await dismissCheckpointReminder(plugin, true);
|
||||
return;
|
||||
}
|
||||
// Don't show if already open
|
||||
if (plugin.checkpointReminderNotice) return;
|
||||
|
||||
// Increment counter in settings
|
||||
await plugin.settingsManager.editAndSave((settings) => {
|
||||
settings.activeMinutesPassedSinceLastCheckpoint =
|
||||
(settings.activeMinutesPassedSinceLastCheckpoint || 0) + 1;
|
||||
});
|
||||
|
||||
if (
|
||||
plugin.settings.activeMinutesPassedSinceLastCheckpoint >=
|
||||
reviewChangesReminderInterval
|
||||
) {
|
||||
showCheckpointReminder(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the checkpoint reminder notification if not already shown.
|
||||
*/
|
||||
export function showCheckpointReminder(plugin: GitChangelogPlugin): void {
|
||||
if (plugin.checkpointReminderNotice) return;
|
||||
const frag = createFragment((element) => {
|
||||
element.createEl('span', {
|
||||
text: `Git changelog:\nIt's time to review the changes made to your vault. 🧐`
|
||||
});
|
||||
element.createEl('br');
|
||||
|
||||
const openButton = element.createEl('button', {
|
||||
text: 'Review'
|
||||
});
|
||||
openButton.style.marginTop = '8px';
|
||||
openButton.classList.add('mod-cta');
|
||||
openButton.addEventListener('click', () => {
|
||||
invokeAsyncSafely(() => openCompareToCheckpointView(plugin, false));
|
||||
});
|
||||
const dismissButton = element.createEl('button', {
|
||||
text: 'Remind me later'
|
||||
});
|
||||
dismissButton.style.marginLeft = '8px';
|
||||
dismissButton.addEventListener('click', () => {
|
||||
invokeAsyncSafely(() => dismissCheckpointReminder(plugin, false));
|
||||
});
|
||||
});
|
||||
plugin.checkpointReminderNotice = new Notice(frag, 0);
|
||||
}
|
||||
|
||||
export async function openCompareToCheckpointView(
|
||||
plugin: GitChangelogPlugin,
|
||||
hideCheckpointReminder = true
|
||||
): Promise<void> {
|
||||
// First close any existing COMPARE_REPO_STATES_VIEW views
|
||||
removeCompareVersionsView(plugin);
|
||||
|
||||
await plugin.app.workspace.ensureSideLeaf(
|
||||
COMPARE_TO_CHECKPOINT_VIEW_CONFIG.type,
|
||||
'left',
|
||||
{ reveal: true }
|
||||
);
|
||||
|
||||
invokeAsyncSafely(() =>
|
||||
dismissCheckpointReminder(plugin, hideCheckpointReminder)
|
||||
);
|
||||
}
|
||||
|
|
@ -1,60 +1,16 @@
|
|||
import type { GitChangelogPluginSettings } from 'settings/settings.ts';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { DEFAULT_SETTINGS, MAX_SUPPORTED_INTERVAL } from 'settings/settings.ts';
|
||||
|
||||
export class StatusBarInterval extends GitChangelogSetting {
|
||||
export class StatusBarInterval extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName('Interval for status bar stats (minutes)')
|
||||
.setDesc(
|
||||
'Works by comparing the live file version against the first commit before the interval.'
|
||||
)
|
||||
.addText((text) => {
|
||||
text.inputEl.pattern = '[1-9][0-9]*';
|
||||
text.inputEl.maxLength = MAX_SUPPORTED_INTERVAL.toString().length;
|
||||
text.inputEl.inputMode = 'numeric';
|
||||
text
|
||||
.setDisabled(this.disabled)
|
||||
.setPlaceholder(DEFAULT_SETTINGS.statusBarInterval)
|
||||
.onChange((value) => {
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.statusBarInterval = validateStatusBarInterval(
|
||||
String(value)
|
||||
)
|
||||
? String(value)
|
||||
: DEFAULT_SETTINGS.statusBarInterval;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
});
|
||||
.addNumber((text) => {
|
||||
this.settingTab.bind(text, 'statusBarInterval');
|
||||
|
||||
this.setValueIfNonDefaultSetting({
|
||||
settingsProperty: 'statusBarInterval',
|
||||
text
|
||||
});
|
||||
text.setDisabled(this.disabled);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getStatusBarInterval(
|
||||
settings: ReadonlyDeep<GitChangelogPluginSettings>
|
||||
): number {
|
||||
if (!validateStatusBarInterval(settings.statusBarInterval)) {
|
||||
return Number(DEFAULT_SETTINGS.statusBarInterval);
|
||||
}
|
||||
return Number(settings.statusBarInterval);
|
||||
}
|
||||
|
||||
export function validateStatusBarInterval(
|
||||
statusBarAlternateInterval: string
|
||||
): boolean {
|
||||
if (
|
||||
!Number.isInteger(Number(statusBarAlternateInterval)) ||
|
||||
Number(statusBarAlternateInterval) < 1 ||
|
||||
Number(statusBarAlternateInterval) > MAX_SUPPORTED_INTERVAL
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,15 @@
|
|||
import { GitChangelogSetting } from 'settings/components/setting.ts';
|
||||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class StatusBarStatsToggle extends GitChangelogSetting {
|
||||
export class StatusBarStatsToggle extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName('Active note live status bar stats')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.statusBarStats)
|
||||
.onChange((value) => {
|
||||
this.refreshDisplayDelayed();
|
||||
|
||||
const newSettings = this.plugin.settingsClone;
|
||||
newSettings.statusBarStats = value;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.plugin.saveSettings(newSettings);
|
||||
|
||||
if (value) {
|
||||
this.plugin.initStatusBar();
|
||||
} else {
|
||||
this.plugin.statusBarStats?.destroy?.();
|
||||
this.plugin.statusBarStats = undefined;
|
||||
}
|
||||
})
|
||||
);
|
||||
.addToggle((toggle) => {
|
||||
this.settingTab.bind(toggle, 'showStatusBarStats', {
|
||||
onChanged: () => {
|
||||
this.refreshDisplayWithDelay();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
src/settings/ui/WhitespaceIgnoreMode.ts
Normal file
32
src/settings/ui/WhitespaceIgnoreMode.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
import { WhitespaceIgnoreMode } from 'types.ts';
|
||||
|
||||
export class WhitespaceIgnoreModeOptions extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setName(
|
||||
createFragment((fragment) => {
|
||||
fragment.appendText(`Whitespace changes detection`);
|
||||
})
|
||||
)
|
||||
.addTypedDropdown((dropdown) => {
|
||||
dropdown.addOption(
|
||||
WhitespaceIgnoreMode.None,
|
||||
'Track all whitespace changes'
|
||||
);
|
||||
dropdown.addOption(
|
||||
WhitespaceIgnoreMode.SpaceAtEol,
|
||||
'Ignore whitespace changes at end of line'
|
||||
);
|
||||
dropdown.addOption(
|
||||
WhitespaceIgnoreMode.SpaceChange,
|
||||
'Ignore changes to pre-existing whitespace'
|
||||
);
|
||||
dropdown.addOption(
|
||||
WhitespaceIgnoreMode.AllSpace,
|
||||
'Ignore all whitespace changes per line'
|
||||
);
|
||||
this.settingTab.bind(dropdown, 'whitespaceIgnoreMode');
|
||||
});
|
||||
}
|
||||
}
|
||||
12
src/settings/ui/WhitespaceSettingsHeading.ts
Normal file
12
src/settings/ui/WhitespaceSettingsHeading.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { SettingComponent } from 'settings/components/setting.ts';
|
||||
|
||||
export class WhitespaceSettingsHeading extends SettingComponent {
|
||||
public display(): void {
|
||||
this.createSetting()
|
||||
.setHeading()
|
||||
.setName('Whitespace settings')
|
||||
.setDesc(
|
||||
`Note that adjusting these settings will make the generated stats slightly differ from the diff view, because unlike the diff algorithm, these settings can't be applied globally.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import type GitChangelogPlugin from 'main.ts';
|
||||
|
||||
import { DEFAULT_SETTINGS } from 'settings/settings.ts';
|
||||
import { ChangelogInterval } from 'types.ts';
|
||||
|
||||
// Redundant currently
|
||||
export function getChangelogIntervalFromSettings(
|
||||
plugin: GitChangelogPlugin,
|
||||
fileOrVault: 'file' | 'vault'
|
||||
): ChangelogInterval {
|
||||
const interval =
|
||||
fileOrVault === 'file'
|
||||
? plugin.settings.fileChangelogInterval
|
||||
: plugin.settings.vaultChangelogGenerationSettings.interval;
|
||||
|
||||
if (!validateChangelogInterval(interval)) {
|
||||
return fileOrVault === 'file'
|
||||
? DEFAULT_SETTINGS.fileChangelogInterval
|
||||
: DEFAULT_SETTINGS.vaultChangelogGenerationSettings.interval;
|
||||
}
|
||||
|
||||
return interval;
|
||||
}
|
||||
|
||||
export function validateChangelogInterval(
|
||||
changelogInterval: ChangelogInterval
|
||||
): boolean {
|
||||
if (Object.values(ChangelogInterval).includes(changelogInterval)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1,16 +1,13 @@
|
|||
import type { GitChangelogPluginSettings } from 'settings/settings.ts';
|
||||
import type { GitChangelogPluginTypes } from 'constants.ts';
|
||||
import type { ExtractPluginSettingsWrapper } from 'obsidian-dev-utils/obsidian/Plugin/PluginTypesBase';
|
||||
import type { TaskManager } from 'TaskManager.svelte.ts';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
import { findFirstCommitBefore } from 'core/gitOperations/findFirstCommitBefore.ts';
|
||||
import { findFirstFileCommitBefore } from 'core/findFirstFileCommitBefore.ts';
|
||||
import { runCheckIgnore } from 'core/gitOperations/runCheckIgnore.ts';
|
||||
import { runWorkingDirFileDiff } from 'core/gitOperations/runWorkingDirFileDiff.ts';
|
||||
import { MarkdownView } from 'obsidian';
|
||||
import { invokeAsyncSafely } from 'obsidian-dev-utils/Async';
|
||||
import { clearPendingQueueItems } from 'obsidian-dev-utils/obsidian/Queue';
|
||||
import { getMeasurementUnit } from 'settings/ui/ChangelogMeasurementUnit.ts';
|
||||
import { getStatusBarInterval } from 'settings/ui/StatusBarInterval.ts';
|
||||
import { DiffMeasurementUnit } from 'types.ts';
|
||||
import { AbortError, DiffMeasurementUnit } from 'types.ts';
|
||||
import { getGitRelativeFilePath } from 'Views/helper.ts';
|
||||
|
||||
import type GitChangelogPlugin from './main.ts';
|
||||
|
|
@ -22,6 +19,7 @@ export class StatusBarStats {
|
|||
public taskManager: TaskManager
|
||||
) {
|
||||
// Initialize immediately
|
||||
this.setText('+... -...');
|
||||
this.recompute();
|
||||
|
||||
// It doesn't listen to obsidian-git:head-change event because it always compares the working directory to some past commit anyway.
|
||||
|
|
@ -37,6 +35,9 @@ export class StatusBarStats {
|
|||
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.workspace.on('file-open', () => {
|
||||
// Set loading state
|
||||
this.setText('+... -...');
|
||||
// Then schedule a recompute
|
||||
this.recompute();
|
||||
})
|
||||
);
|
||||
|
|
@ -52,8 +53,7 @@ export class StatusBarStats {
|
|||
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.workspace.on('editor-change', () => {
|
||||
// Previous operations aren't aborted because this will be triggered often, and the results will lag behind if new calls ones keep getting scheduled and resetting the previous calls before they finish.
|
||||
this.recompute(false);
|
||||
this.recompute();
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -67,11 +67,16 @@ export class StatusBarStats {
|
|||
}
|
||||
|
||||
public static generationSettingsChanged(
|
||||
oldSettings: ReadonlyDeep<GitChangelogPluginSettings>,
|
||||
newSettings: GitChangelogPluginSettings
|
||||
oldSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>,
|
||||
newSettings: ReadonlyDeep<
|
||||
ExtractPluginSettingsWrapper<GitChangelogPluginTypes>
|
||||
>
|
||||
): boolean {
|
||||
return (
|
||||
getStatusBarInterval(oldSettings) !== getStatusBarInterval(newSettings)
|
||||
oldSettings.safeSettings.statusBarInterval !==
|
||||
newSettings.safeSettings.statusBarInterval
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +91,10 @@ export class StatusBarStats {
|
|||
|
||||
private async updateStats(abortSignal: AbortSignal): Promise<void> {
|
||||
try {
|
||||
if (this.plugin.settings.statusBarStats) {
|
||||
if (abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
if (this.plugin.settings.showStatusBarStats) {
|
||||
const result = await this.calculateStatsForActiveFile(
|
||||
this.plugin.app.workspace.getActiveViewOfType(MarkdownView),
|
||||
abortSignal
|
||||
|
|
@ -97,24 +105,20 @@ export class StatusBarStats {
|
|||
this.setText('');
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
this.setText('');
|
||||
} catch (error) {
|
||||
// If the error was an AbortError, don't do anything. Otherwise, remove the status bar stats.
|
||||
if (!(error instanceof AbortError)) {
|
||||
this.setText('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private recompute(reset = true): void {
|
||||
// Stop the massive build-up of updateStats() calls when the user is typing
|
||||
clearPendingQueueItems(this.plugin.app);
|
||||
private recompute(): void {
|
||||
const abortSignal = this.taskManager.abortPreviousTasksAndGetSignal();
|
||||
|
||||
const abortSignal = reset
|
||||
? this.taskManager.abortPreviousTasksAndGetSignal()
|
||||
: this.taskManager.getAbortSignal();
|
||||
|
||||
invokeAsyncSafely(() =>
|
||||
this.taskManager.enqueueAndWait(async () => {
|
||||
await this.updateStats(abortSignal);
|
||||
})
|
||||
);
|
||||
this.taskManager.enqueueSafely(async () => {
|
||||
await this.updateStats(abortSignal);
|
||||
});
|
||||
}
|
||||
|
||||
private async calculateStatsForActiveFile(
|
||||
|
|
@ -133,18 +137,26 @@ export class StatusBarStats {
|
|||
let additions = 0;
|
||||
let deletions = 0;
|
||||
|
||||
const oldCommit = await findFirstCommitBefore({
|
||||
const git = await this.plugin.getGit();
|
||||
|
||||
const oldCommit = await findFirstFileCommitBefore({
|
||||
abortSignal,
|
||||
filePath: activeGitFile,
|
||||
minutes: getStatusBarInterval(this.plugin.settings),
|
||||
plugin: this.plugin
|
||||
minutes: this.plugin.settings.statusBarInterval,
|
||||
timeZone: await this.plugin.getEmptyTreeHash(),
|
||||
git,
|
||||
renameDetectionStrictness: this.plugin.settings.renameDetectionStrictness
|
||||
});
|
||||
|
||||
if (oldCommit) {
|
||||
if (oldCommit && oldCommit.fileDeleted !== true) {
|
||||
const baseStats = await runWorkingDirFileDiff({
|
||||
abortSignal,
|
||||
oldCommit,
|
||||
plugin: this.plugin
|
||||
activeGitFile,
|
||||
git,
|
||||
diffAlgorithm: this.plugin.settings.diffAlgorithm,
|
||||
whitespaceIgnoreMode: this.plugin.settings.whitespaceIgnoreMode,
|
||||
ignoreBlankLines: this.plugin.settings.ignoreBlankLines
|
||||
});
|
||||
if (baseStats) {
|
||||
additions = baseStats.additions;
|
||||
|
|
@ -159,15 +171,13 @@ export class StatusBarStats {
|
|||
const fileIsGitIgnored = await runCheckIgnore({
|
||||
abortSignal,
|
||||
activeGitFile,
|
||||
plugin: this.plugin
|
||||
git
|
||||
});
|
||||
|
||||
if (fileIsGitIgnored) {
|
||||
return;
|
||||
return 'In .gitignore';
|
||||
}
|
||||
const measurementUnit = getMeasurementUnit(
|
||||
this.plugin.settings.changelogGenerationSettings
|
||||
);
|
||||
const measurementUnit = this.plugin.settings.diffMeasurementUnit;
|
||||
|
||||
if (measurementUnit === DiffMeasurementUnit.Lines) {
|
||||
additions = activeFileView.editor.lineCount();
|
||||
|
|
|
|||
|
|
@ -17,6 +17,28 @@
|
|||
border-bottom: 1px solid var(--text-warning);
|
||||
}
|
||||
|
||||
.git-changelog-top-margin {
|
||||
margin-top: var(--size-4-1);
|
||||
}
|
||||
|
||||
.git-changelog-on-checkpoint-commit-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.git-changelog-compared-versions-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-1);
|
||||
margin-bottom: var(--size-4-1);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: var(--size-2-1);
|
||||
}
|
||||
|
||||
.git-changelog-force-center {
|
||||
align-self: center !important;
|
||||
}
|
||||
|
||||
.git-changelog-align-file {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
|
@ -97,14 +119,14 @@
|
|||
// Layout components
|
||||
.git-changelog-text-area {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
min-width: 250px;
|
||||
min-height: 5em;
|
||||
max-width: 480px;
|
||||
min-width: 260px;
|
||||
min-height: 6em;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.git-changelog-view {
|
||||
--git-changelog-opacity: 0.84;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
|
@ -112,6 +134,18 @@
|
|||
padding-bottom: calc(2 * var(--size-4-1));
|
||||
}
|
||||
|
||||
.git-changelog-checkpoint-modal {
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.git-changelog-approve-button {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.git-changelog-entry-title {
|
||||
@extend .git-changelog-one-line;
|
||||
color: var(--text-accent);
|
||||
|
|
@ -120,6 +154,16 @@
|
|||
margin-bottom: var(--size-4-1);
|
||||
}
|
||||
|
||||
.git-changelog-compared-version-title {
|
||||
@extend .git-changelog-one-line;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-smaller);
|
||||
}
|
||||
|
||||
.git-changelog-compared-version-title-arrow {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.git-changelog-initial-version-tag {
|
||||
@extend .git-changelog-one-line;
|
||||
font-size: 0.75em;
|
||||
|
|
@ -169,9 +213,33 @@
|
|||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
.git-changelog-left-padding {
|
||||
margin-left: var(--size-4-4);
|
||||
}
|
||||
|
||||
.git-changelog-setting-label {
|
||||
padding: 0 var(--size-4-2);
|
||||
transform: translateY(-1px); // workaround to appear centered
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.git-changelog-new {
|
||||
@extend .git-changelog-setting-label;
|
||||
color: var(--color-green);
|
||||
background-color: rgba(var(--color-green-rgb), 0.15);
|
||||
}
|
||||
|
||||
.git-changelog-experimental {
|
||||
@extend .git-changelog-setting-label;
|
||||
color: var(--color-orange);
|
||||
background-color: rgba(var(--color-orange-rgb), 0.15);
|
||||
}
|
||||
|
||||
/* Sticky view header and padding fixes */
|
||||
.workspace-leaf-content {
|
||||
&[data-type='vault-changelog-view'] .view-content,
|
||||
&[data-type='vault-git-history-comparison-view'] .view-content,
|
||||
&[data-type='compare-to-checkpoint-view'] .view-content,
|
||||
&[data-type='file-changelog-view'] .view-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,32 +3,35 @@ import type { Spacetime } from 'spacetime';
|
|||
import type { LogEntry } from 'types.ts';
|
||||
|
||||
/**
|
||||
* This function produces "fullyAdjusted" dates, which are dates that have the "day start time" setting applied to a "timeAdjustedDate". timezoneAdjustedDate is a date adjusted with the timezone setting specified in the settings tab.
|
||||
* This function produces "fullyAdjusted" dates, which are dates that have the "day start time" setting applied to a "timeAdjustedDate". timeZoneAdjustedDate is a date adjusted with the timeZone setting specified in the settings tab.
|
||||
*/
|
||||
export function applyDayStartTimeSetting({
|
||||
dayStartTime,
|
||||
timezoneAdjustedDate
|
||||
export function applyDayStartHourSetting({
|
||||
dayStartHour,
|
||||
timeZoneAdjustedDate
|
||||
}: {
|
||||
dayStartTime: number;
|
||||
timezoneAdjustedDate: Spacetime;
|
||||
dayStartHour: number;
|
||||
timeZoneAdjustedDate: Spacetime;
|
||||
}): Spacetime {
|
||||
return timezoneAdjustedDate.subtract(dayStartTime, 'hours');
|
||||
return timeZoneAdjustedDate.subtract(dayStartHour, 'hours');
|
||||
}
|
||||
|
||||
export function getDayStartTimeAdjustedLogs(
|
||||
export function getDayStartHourAdjustedLogs(
|
||||
logEntries: LogEntry[],
|
||||
dayStartTime: number
|
||||
dayStartHour: number
|
||||
): LogEntry[] {
|
||||
if (dayStartTime === 0) {
|
||||
if (dayStartHour === 0) {
|
||||
return logEntries;
|
||||
}
|
||||
return logEntries.map((entry) => {
|
||||
return {
|
||||
...entry,
|
||||
fullyAdjustedDate: applyDayStartTimeSetting({
|
||||
dayStartTime,
|
||||
timezoneAdjustedDate: entry.timezoneAdjustedDate
|
||||
fullyAdjustedDate: applyDayStartHourSetting({
|
||||
dayStartHour,
|
||||
timeZoneAdjustedDate: entry.timeZoneAdjustedDate
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
export function formatDateHour(date: Spacetime): string {
|
||||
return date.startOf('hour').format('iso-utc');
|
||||
}
|
||||
|
|
|
|||
63
src/types.ts
63
src/types.ts
|
|
@ -29,6 +29,13 @@ export class AbortError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export class NullValueError extends Error {
|
||||
public constructor() {
|
||||
super('Non-nullable value is null or undefined');
|
||||
this.name = 'NullValueError';
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'obsidian' {
|
||||
interface Workspace {
|
||||
on(
|
||||
|
|
@ -41,8 +48,16 @@ declare module 'obsidian' {
|
|||
callback: () => void,
|
||||
context?: unknown
|
||||
): EventRef;
|
||||
|
||||
// BUG:? Read directly from settings instead of passing as arguments?
|
||||
on(
|
||||
name: 'git-changelog:menu',
|
||||
callback: (
|
||||
menu: Menu,
|
||||
inFileMenu: boolean,
|
||||
gitRelativePath?: string,
|
||||
commitHash?: string
|
||||
) => void,
|
||||
context?: unknown
|
||||
): EventRef;
|
||||
trigger(
|
||||
name:
|
||||
| 'git-changelog:active-git-file-changed'
|
||||
|
|
@ -51,6 +66,21 @@ declare module 'obsidian' {
|
|||
| 'git-changelog:status-bar-settings-changed'
|
||||
| 'git-changelog:vault-changelog-generation-settings-changed'
|
||||
): void;
|
||||
trigger(
|
||||
name: 'git-changelog:menu',
|
||||
menu: Menu,
|
||||
// Is the menu standalone, or are it's items added to the existing file menu?
|
||||
inFileMenu: boolean,
|
||||
gitRelativePath?: string,
|
||||
commitHash?: string
|
||||
): void;
|
||||
trigger(
|
||||
name: 'file-menu',
|
||||
menu: Menu,
|
||||
file: TAbstractFile,
|
||||
source: string,
|
||||
leaf?: WorkspaceLeaf
|
||||
): void;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,10 +91,23 @@ export enum ChangelogInterval {
|
|||
Weekly = 'week'
|
||||
}
|
||||
|
||||
/**
|
||||
* Patience and Histogram algorithms don't make sense for this use case.
|
||||
*/
|
||||
export enum DiffAlgorithm {
|
||||
Default = 'Default', // Myers algorithm
|
||||
Inherit = 'Inherit',
|
||||
Minimal = 'Minimal'
|
||||
Default = 'default', // Myers algorithm
|
||||
Minimal = 'minimal'
|
||||
}
|
||||
|
||||
export enum WhitespaceIgnoreMode {
|
||||
None = 'None',
|
||||
SpaceAtEol = 'SpaceAtEol',
|
||||
SpaceChange = 'SpaceChange', // Superset of SpaceAtEol
|
||||
AllSpace = 'AllSpace' // Superset of SpaceChange
|
||||
// None = 'None',
|
||||
// Git lacks a `--no-ignore-whitespace` flag.
|
||||
// `core.whitespace` is related to fixing whitespace issues, not ignoring whitespace in diffs.
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -92,7 +135,7 @@ export enum FileExplorerStats {
|
|||
FoldersAndNotes = 'FoldersAndNotes'
|
||||
}
|
||||
|
||||
export enum FilesSummariesDisplayMode {
|
||||
export enum FileSummariesDisplayMode {
|
||||
TextAndBinary = 'Text and binary',
|
||||
Total = 'Total'
|
||||
// Binary = "Binary",
|
||||
|
|
@ -125,20 +168,24 @@ export interface FilesSummary {
|
|||
addedFiles: number;
|
||||
deletedFiles: number;
|
||||
modifiedFiles: number;
|
||||
renamedFiles: number;
|
||||
renamedAndMovedFiles: number;
|
||||
}
|
||||
|
||||
export interface CompareRepoCommitsViewState {
|
||||
utcOlderDate: string;
|
||||
utcNewerDate: string;
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
// Can be null for file logs, if the file was deleted in some commit
|
||||
hash: string;
|
||||
timezoneAdjustedDate: Spacetime;
|
||||
timeZoneAdjustedDate: Spacetime;
|
||||
|
||||
// For file git logs only:
|
||||
// To track file renames through history
|
||||
filePath?: string;
|
||||
// Covers a deleted edge-case
|
||||
fileDeleted?: boolean;
|
||||
binary?: boolean;
|
||||
}
|
||||
|
||||
export interface StatEntry {
|
||||
|
|
|
|||
86
src/utils.ts
86
src/utils.ts
|
|
@ -1,10 +1,16 @@
|
|||
/* eslint-disable unicorn/prevent-abbreviations */
|
||||
import type { GitChangelogPlugin } from 'GitChangelogPlugin.svelte.ts';
|
||||
import type { App, WorkspaceLeaf } from 'obsidian';
|
||||
import type { DiffFile, TextDiffStats } from 'types.ts';
|
||||
|
||||
import {
|
||||
COMPARE_REPO_COMMITS_VIEW_CONFIG,
|
||||
COMPARE_TO_CHECKPOINT_VIEW_CONFIG,
|
||||
FEEDBACK_URL
|
||||
} from 'constants.ts';
|
||||
// Import * as cssColorConverter from "css-color-converter";
|
||||
import { Keymap, Menu } from 'obsidian';
|
||||
import { DiffFileStatus } from 'types.ts';
|
||||
import { Keymap, Notice } from 'obsidian';
|
||||
import { DiffFileStatus, NullValueError } from 'types.ts';
|
||||
|
||||
export function getFileNameFromPath({
|
||||
normalizedFilePath
|
||||
|
|
@ -64,41 +70,6 @@ export function isRenamed(file: DiffFile): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
export function mayTriggerFileMenu({
|
||||
app,
|
||||
event,
|
||||
filePath,
|
||||
source,
|
||||
view
|
||||
}: {
|
||||
app: App;
|
||||
event: MouseEvent;
|
||||
filePath: string;
|
||||
source: string;
|
||||
view: WorkspaceLeaf;
|
||||
}): void {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (event.button == 2) {
|
||||
const file = app.vault.getAbstractFileByPath(filePath);
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (file == undefined) {
|
||||
const fileMenu = new Menu();
|
||||
app.workspace.trigger(
|
||||
'obsidian-git:menu',
|
||||
fileMenu,
|
||||
filePath,
|
||||
source,
|
||||
view
|
||||
);
|
||||
fileMenu.showAtPosition({ x: event.pageX, y: event.pageY });
|
||||
} else {
|
||||
const fileMenu = new Menu();
|
||||
app.workspace.trigger('file-menu', fileMenu, file, source, view);
|
||||
fileMenu.showAtPosition({ x: event.pageX, y: event.pageY });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseContentChange({
|
||||
addedStr,
|
||||
deletedStr
|
||||
|
|
@ -124,3 +95,44 @@ export function parseContentChange({
|
|||
|
||||
return textDiffStats;
|
||||
}
|
||||
|
||||
export function assertNotNull<T>(value: null | T | undefined): T {
|
||||
DEV: if (value === null || value === undefined) {
|
||||
const error = new NullValueError();
|
||||
const stack = error.stack ?? '';
|
||||
const message = `Git changelog:\nNon-nullable value is null or undefined`;
|
||||
console.error(`${message}\n${stack}`);
|
||||
const fragment = createFragment((el) => {
|
||||
el.createEl('p', {
|
||||
text: 'Git changelog:\nHey, some null error happened that shouldn`t have.\nCan you please copy the error info and report it to the Github issues page so that it can get fixed.\nJust paste the copied info, nothing more'
|
||||
});
|
||||
|
||||
const button = el.createEl('button', { text: 'Copy & Report Issue' });
|
||||
button.addEventListener('click', () => {
|
||||
window.navigator.clipboard
|
||||
.writeText(stack)
|
||||
.then(() => {
|
||||
window.open(FEEDBACK_URL);
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
.catch(() => {});
|
||||
});
|
||||
});
|
||||
|
||||
new Notice(fragment, 0);
|
||||
|
||||
throw error;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function removeCompareVersionsView(plugin: GitChangelogPlugin): void {
|
||||
// Close any existing temporary views
|
||||
plugin.app.workspace.detachLeavesOfType(
|
||||
COMPARE_TO_CHECKPOINT_VIEW_CONFIG.type
|
||||
);
|
||||
|
||||
plugin.app.workspace.detachLeavesOfType(
|
||||
COMPARE_REPO_COMMITS_VIEW_CONFIG.type
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
{
|
||||
"0.1.0": "1.7.2",
|
||||
"0.2.0": "1.7.2",
|
||||
"0.2.1": "1.7.2"
|
||||
"0.1.0": "1.8.9",
|
||||
"0.2.0": "1.8.9",
|
||||
"0.2.1": "1.8.9",
|
||||
"0.3.0": "1.8.9",
|
||||
"0.4.0": "1.8.9",
|
||||
"0.5.0": "1.8.9",
|
||||
"0.5.1": "1.8.10",
|
||||
"0.6.0": "1.8.10"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue