├── .editorconfig ├── .eleventy.js ├── .eslintrc.js ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── getCollectionNewestGitCommitDate.js └── getGitCommitDateFromPath.js └── tests ├── fixtures ├── another-sample-file.md └── sample.md ├── getCollectionNewestGitCommitDate.js └── getGitCommitDateFromPath.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | insert_final_newline = false 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | 11 | [*.js] 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /.eleventy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const getGitCommitDateFromPath = require("./src/getGitCommitDateFromPath"); 3 | const getCollectionNewestGitCommitDate = require("./src/getCollectionNewestGitCommitDate"); 4 | 5 | module.exports = function (eleventyConfig) { 6 | eleventyConfig.addFilter( 7 | "getGitCommitDateFromPath", 8 | getGitCommitDateFromPath 9 | ); 10 | eleventyConfig.addFilter( 11 | "getCollectionNewestGitCommitDate", 12 | getCollectionNewestGitCommitDate 13 | ); 14 | }; 15 | 16 | module.exports.getGitCommitDateFromPath = getGitCommitDateFromPath; 17 | module.exports.getCollectionNewestGitCommitDate = 18 | getCollectionNewestGitCommitDate; 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ["xo-space", "plugin:prettier/recommended"], 4 | plugins: ["prettier"], 5 | rules: { 6 | "prettier/prettier": "error", 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: saneef 2 | ko_fi: saneef 3 | patreon: saneef 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .nyc_output 4 | .eslintcache 5 | tests/output -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | npx lint-staged -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .eslintcache 3 | .eslintrc.js 4 | .github/ 5 | .husky/pre-commit 6 | .nyc_output 7 | tests 8 | .travis.yml 9 | .gitattributes -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 14 5 | - 16 6 | 7 | os: 8 | - linux 9 | - osx 10 | - windows 11 | 12 | before_script: 13 | - npm install 14 | script: 15 | - npm run lint 16 | - npm run test 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Saneef Ansari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eleventy-plugin-git-commit-date 2 | 3 | This Eleventy plugin provides two [template filters](https://www.11ty.dev/docs/filters/): 4 | 5 | | Filter | Description | 6 | | :--------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | 7 | | `getGitCommitDateFromPath` | Gets Git commit date from path.

Usage: `{{ page.inputPath \| getGitCommitDateFromPath }}` | 8 | | `getCollectionNewestGitCommitDate` | Get Git commit date of the newest committed file from a collection.

Usage: `{{ collections.all \| getCollectionNewestGitCommitDate }}` | 9 | 10 | 🌏 This plugin is made primarily to populate `` fields in an RSS feed. Here is [a blog post on how to use this plugin](https://saneef.com/tutorials/fix-dates-on-eleventy-rss-feeds/) with [`eleventy-plugin-rss`](https://www.11ty.dev/docs/plugins/rss/). 11 | 12 | ⚠️ Getting Git commit date is a bit slow (\~50ms for each path). So, use it sparingly. It's recommended to call this filter within a production flag. 13 | 14 | ## Usage 15 | 16 | ### 1. Install 17 | 18 | ```sh 19 | npm install eleventy-plugin-git-commit-date 20 | ``` 21 | 22 | ### 2. Add to Eleventy config 23 | 24 | ```js 25 | // .eleventy.js 26 | 27 | const pluginGitCommitDate = require("eleventy-plugin-git-commit-date"); 28 | 29 | module.exports = function (eleventyConfig) { 30 | eleventyConfig.addPlugin(pluginGitCommitDate); 31 | }; 32 | ``` 33 | 34 | ### 3. Use in templates 35 | 36 | ```nunjucks 37 | Using {{ page.inputPath | getGitCommitDateFromPath }} will display the git commit date of the file using a local time zone like: 38 | 39 | Sun Dec 31 2017 18:00:00 GMT-0600 (Central Standard Time) 40 | 41 | Using {{ collections.all | getCollectionNewestGitCommitDate }} will display the git commit date of newest file in the collection using a local time zone like: 42 | 43 | Sun Dec 31 2017 18:00:00 GMT-0600 (Central Standard Time) 44 | ``` 45 | 46 | ## FAQs 47 | 48 | ### When used with GitHub Pages, why are the last commit dates incorrect? 49 | 50 | Only the last commit is checked out by [GitHub Action Checkout](https://github.com/actions/checkout#:~:text=Only%20a%20single%20commit%20is%20fetched%20by%20default) by default. The commit dates for files changed in the previous commits will not be available. You can set the `fetch-depth` as `0` to get all the history from the Git repository. 51 | 52 | ```diff 53 | - name: Checkout 54 | uses: actions/checkout@v3 55 | + with: 56 | + fetch-depth: 0 57 | ``` 58 | 59 | ## Credits 60 | 61 | - [@zachleat](https://github.com/11ty/eleventy/issues/142) suggested the use of Git commit dates instead of modified date. 62 | - The code is based on [@vuepress/plugin-last-updated](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/plugin-last-updated). 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eleventy-plugin-git-commit-date", 3 | "version": "0.1.3", 4 | "description": "Eleventy plugin to get Git commit time of a file, or a Eleventy collection.", 5 | "main": ".eleventy.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com:saneef/eleventy-plugin-git-commit-date.git" 9 | }, 10 | "scripts": { 11 | "lint": "eslint src/**.js tests/**.js", 12 | "test": "nyc ava --timeout=1m -v --color", 13 | "prepare": "husky install" 14 | }, 15 | "keywords": [ 16 | "last-updated", 17 | "modified", 18 | "git", 19 | "eleventy", 20 | "11ty", 21 | "eleventy-plugin" 22 | ], 23 | "author": "Saneef Ansari (https://saneef.com/)", 24 | "license": "MIT", 25 | "dependencies": { 26 | "cross-spawn": "^7.0.3" 27 | }, 28 | "devDependencies": { 29 | "ava": "^3.15.0", 30 | "eslint": "^7.32.0", 31 | "eslint-config-prettier": "^8.3.0", 32 | "eslint-config-xo-space": "^0.29.0", 33 | "eslint-plugin-prettier": "^3.4.0", 34 | "husky": "^7.0.1", 35 | "lint-staged": "^11.1.2", 36 | "nyc": "^15.1.0", 37 | "prettier": "^2.3.2", 38 | "rimraf": "^3.0.2" 39 | }, 40 | "lint-staged": { 41 | "*.js": "eslint --cache --fix", 42 | "*.{js,md,json}": "prettier --write" 43 | }, 44 | "ava": { 45 | "files": [ 46 | "tests/**/*", 47 | "!tests/utils.js" 48 | ], 49 | "ignoredByWatcher": [ 50 | "tests/output/**" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/getCollectionNewestGitCommitDate.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const getGitCommitDateFromPath = require("./getGitCommitDateFromPath"); 3 | 4 | /** 5 | * Gets the collection's newest Git commit date. 6 | * 7 | * @param {Array} collection The collection 8 | * 9 | * @return {Date} The collection newest git commit date. 10 | */ 11 | module.exports = function (collection) { 12 | if (!collection || !collection.length) { 13 | return; 14 | } 15 | 16 | const timestamps = collection 17 | .map((item) => getGitCommitDateFromPath(item.inputPath)) 18 | // Timestamps will be undefined for the paths not 19 | // yet commited to Git. So weeding them out. 20 | .filter((ts) => Boolean(ts)) 21 | .map((ts) => ts.getTime()); 22 | 23 | if (timestamps.length) { 24 | return new Date(Math.max(...timestamps)); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/getGitCommitDateFromPath.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const path = require("path"); 3 | const spawn = require("cross-spawn"); 4 | 5 | /** 6 | * Gets the Git commit date from path. 7 | * 8 | * The code is based on @vuepress/plugin-last-updated, 9 | * https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/plugin-last-updated/ 10 | * 11 | * @param {string} filePath The file path 12 | * 13 | * @return {Date} The git commit date if path is commited to Git. 14 | */ 15 | module.exports = function (filePath) { 16 | let output; 17 | 18 | try { 19 | output = spawn.sync( 20 | "git", 21 | ["log", "-1", "--format=%at", path.basename(filePath)], 22 | { cwd: path.dirname(filePath) } 23 | ); 24 | } catch { 25 | throw new Error("Fail to run 'git log'"); 26 | } 27 | 28 | if (output && output.stdout) { 29 | const ts = parseInt(output.stdout.toString("utf-8"), 10) * 1000; 30 | 31 | // Paths not commited to Git returns empty timestamps, resulting in NaN. 32 | // So, convert only valid timestamps. 33 | if (!isNaN(ts)) { 34 | return new Date(ts); 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /tests/fixtures/another-sample-file.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Another sample file 3 | --- 4 | -------------------------------------------------------------------------------- /tests/fixtures/sample.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sample file 3 | --- 4 | -------------------------------------------------------------------------------- /tests/getCollectionNewestGitCommitDate.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const path = require("path"); 3 | const fs = require("fs/promises"); 4 | const { promisify } = require("util"); 5 | const rimraf = promisify(require("rimraf")); 6 | const getCollectionNewestGitCommitDate = require("../src/getCollectionNewestGitCommitDate.js"); 7 | 8 | const outputBase = path.join("tests/output/"); 9 | 10 | test("Get newest commit date of collection", (t) => { 11 | const collection = [ 12 | { inputPath: path.join(__dirname, "./fixtures/sample.md") }, 13 | { inputPath: path.join(__dirname, "./fixtures/another-sample-file.md") }, 14 | ]; 15 | const date = getCollectionNewestGitCommitDate(collection); 16 | t.truthy(date); 17 | t.is(date.toISOString(), "2021-08-19T09:57:47.000Z"); 18 | }); 19 | 20 | test("Shouldn't get commit date from an empty collection", async (t) => { 21 | const collection = []; 22 | 23 | t.is(getCollectionNewestGitCommitDate(collection), undefined); 24 | }); 25 | 26 | test("Shouldn't get commit date from collection of uncommited files", async (t) => { 27 | const collection = [ 28 | { inputPath: path.join(outputBase, "test-01.md") }, 29 | { inputPath: path.join(outputBase, "test-02.md") }, 30 | ]; 31 | 32 | await rimraf(outputBase); 33 | 34 | await fs.mkdir(outputBase, { recursive: true }); 35 | await Promise.all(collection.map((p) => fs.writeFile(p.inputPath, ""))); 36 | 37 | t.is(getCollectionNewestGitCommitDate(collection), undefined); 38 | 39 | await rimraf(outputBase); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/getGitCommitDateFromPath.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const path = require("path"); 3 | const fs = require("fs/promises"); 4 | const { promisify } = require("util"); 5 | const rimraf = promisify(require("rimraf")); 6 | const getGitCommitDateFromPath = require("../src/getGitCommitDateFromPath.js"); 7 | 8 | const outputBase = path.join("tests/output/"); 9 | const tempFileName = "test.md"; 10 | 11 | test("Get commit date of a committed file", (t) => { 12 | const filePath = path.join(__dirname, "./fixtures/sample.md"); 13 | const date = getGitCommitDateFromPath(filePath); 14 | t.truthy(date); 15 | t.is(date.toISOString(), "2021-08-19T09:57:47.000Z"); 16 | }); 17 | 18 | test("Should not get commit date of a uncommitted file", async (t) => { 19 | const filePath = path.join(outputBase, tempFileName); 20 | await rimraf(outputBase); 21 | 22 | await fs.mkdir(outputBase, { recursive: true }); 23 | await fs.writeFile(filePath, ""); 24 | 25 | t.is(getGitCommitDateFromPath(filePath), undefined); 26 | 27 | await rimraf(outputBase); 28 | }); 29 | --------------------------------------------------------------------------------