├── index.js ├── .gitignore ├── .release-it.json ├── renovate.json ├── .babelrc ├── .npmignore ├── .circleci └── config.yml ├── package.json ├── src ├── gatsby-node.js └── __tests__ │ └── gatsby_node.js └── README.md /index.js: -------------------------------------------------------------------------------- 1 | //no-op 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /gatsby-node.js 3 | coverage -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["babel-preset-gatsby-package"] 4 | ] 5 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | *.un~ 29 | yarn.lock 30 | src 31 | flow-typed 32 | coverage 33 | decls 34 | examples 35 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10.21 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~ 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: yarn test 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-transformer-gitinfo", 3 | "description": "Gatsby transformer plugin which add git info to File nodes created by gatsby-source-filesystem", 4 | "version": "1.1.0", 5 | "author": "Kevin Raynel", 6 | "bugs": { 7 | "url": "https://github.com/kraynel/gatsby-transformer-gitinfo/issues" 8 | }, 9 | "dependencies": { 10 | "@babel/runtime": "7.10.4", 11 | "simple-git": "1.132.0" 12 | }, 13 | "devDependencies": { 14 | "@babel/cli": "7.10.4", 15 | "@babel/core": "7.10.4", 16 | "babel-preset-gatsby-package": "0.5.1", 17 | "cross-env": "5.2.1", 18 | "jest": "24.9.0" 19 | }, 20 | "homepage": "https://github.com/kraynel/gatsby-transformer-gitinfo", 21 | "keywords": [ 22 | "gatsby", 23 | "gatsby-plugin", 24 | "git" 25 | ], 26 | "license": "MIT", 27 | "peerDependencies": { 28 | "gatsby": "^2.0.15" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/kraynel/gatsby-transformer-gitinfo.git" 33 | }, 34 | "scripts": { 35 | "build": "babel src --out-dir . --ignore **/__tests__", 36 | "prepare": "cross-env NODE_ENV=production npm run build", 37 | "watch": "babel -w src --out-dir . --ignore **/__tests__", 38 | "test": "jest" 39 | }, 40 | "engines": { 41 | "node": ">=8.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const git = require(`simple-git/promise`); 2 | 3 | async function getLogWithRetry(gitRepo, node, retry = 2) { 4 | // Need retry, see https://github.com/steveukx/git-js/issues/302 5 | // Check again after v2 is released? 6 | 7 | const logOptions = { 8 | file: node.absolutePath, 9 | n: 1, 10 | format: { 11 | date: `%ai`, 12 | authorName: `%an`, 13 | authorEmail: "%ae" 14 | } 15 | }; 16 | const log = await gitRepo.log(logOptions); 17 | if (!log.latest && retry > 0) { 18 | return getLogWithRetry(gitRepo, node, retry - 1); 19 | } 20 | 21 | return log; 22 | } 23 | 24 | async function onCreateNode({ node, actions }, pluginOptions) { 25 | const { createNodeField } = actions; 26 | 27 | if (node.internal.type !== `File`) { 28 | return; 29 | } 30 | 31 | if (pluginOptions.include && !pluginOptions.include.test(node.absolutePath)) { 32 | return; 33 | } 34 | 35 | if (pluginOptions.ignore && pluginOptions.ignore.test(node.absolutePath)) { 36 | return; 37 | } 38 | 39 | const gitRepo = git(pluginOptions.dir); 40 | const log = await getLogWithRetry(gitRepo, node); 41 | 42 | if (!log.latest) { 43 | return; 44 | } 45 | 46 | createNodeField({ 47 | node, 48 | name: `gitLogLatestAuthorName`, 49 | value: log.latest.authorName 50 | }); 51 | createNodeField({ 52 | node, 53 | name: `gitLogLatestAuthorEmail`, 54 | value: log.latest.authorEmail 55 | }); 56 | createNodeField({ 57 | node, 58 | name: `gitLogLatestDate`, 59 | value: log.latest.date 60 | }); 61 | } 62 | 63 | exports.onCreateNode = onCreateNode; 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gatsby-transformer-gitinfo 2 | 3 | Add some git information on `File` fields from latest commit: date, author and email. 4 | 5 | ## Install 6 | 7 | `npm install --save gatsby-transformer-gitinfo` 8 | 9 | **Note:** You also need to have `gatsby-source-filesystem` installed and configured so it 10 | points to your files. 11 | 12 | ## How to use 13 | 14 | In your `gatsby-config.js` 15 | 16 | ```javascript 17 | module.exports = { 18 | plugins: [ 19 | { 20 | resolve: `gatsby-source-filesystem`, 21 | options: { 22 | path: `./src/data/`, 23 | }, 24 | }, 25 | `gatsby-transformer-gitinfo`, 26 | ], 27 | } 28 | ``` 29 | 30 | Where the _source folder_ `./src/data/` is a git versionned directory. 31 | 32 | The plugin will add several fields to `File` nodes: `gitLogLatestAuthorName`, `gitLogLatestAuthorEmail` and `gitLogLatestDate`. These fields are related to the latest commit touching that file. 33 | 34 | If the file is not versionned, these fields will be `null`. 35 | 36 | They are exposed in your graphql schema which you can query: 37 | 38 | ```graphql 39 | query { 40 | allFile { 41 | edges { 42 | node { 43 | fields { 44 | gitLogLatestAuthorName 45 | gitLogLatestAuthorEmail 46 | gitLogLatestDate 47 | } 48 | internal { 49 | type 50 | mediaType 51 | description 52 | owner 53 | } 54 | } 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | Now you have a `File` node to work with: 61 | 62 | ```json 63 | { 64 | "data": { 65 | "allFile": { 66 | "edges": [ 67 | { 68 | "node": { 69 | "fields": { 70 | "gitLogLatestAuthorName":"John Doe", 71 | "gitLogLatestAuthorEmail": "john.doe@github.com", 72 | "gitLogLatestDate": "2019-10-14T12:58:39.000Z" 73 | }, 74 | "internal": { 75 | "contentDigest": "c1644b03f380bc5508456ce91faf0c08", 76 | "type": "File", 77 | "mediaType": "text/yaml", 78 | "description": "File \"src/data/example.yml\"", 79 | "owner": "gatsby-source-filesystem" 80 | } 81 | } 82 | } 83 | ] 84 | } 85 | } 86 | } 87 | ``` 88 | 89 | ## Configuration options 90 | 91 | **`include`** [regex][optional] 92 | 93 | This plugin will try to match the absolute path of the file with the `include` regex. 94 | If it *does not* match, the file will be skipped. 95 | 96 | ```javascript 97 | module.exports = { 98 | plugins: [ 99 | { 100 | resolve: `gatsby-transformer-gitinfo`, 101 | options: { 102 | include: /\.md$/i, // Only .md files 103 | }, 104 | }, 105 | ], 106 | } 107 | ``` 108 | 109 | 110 | **`ignore`** [regex][optional] 111 | 112 | This plugin will try to match the absolute path of the file with the `ignore` regex. 113 | If it *does* match, the file will be skipped. 114 | 115 | ```javascript 116 | module.exports = { 117 | plugins: [ 118 | { 119 | resolve: `gatsby-transformer-gitinfo`, 120 | options: { 121 | ignore: /\.jpeg$/i, // All files except .jpeg 122 | }, 123 | }, 124 | ], 125 | } 126 | ``` 127 | 128 | **`dir`** [string][optional] 129 | 130 | The root of the git repository. Will use current directory if not provided. 131 | 132 | ## Example 133 | 134 | **Note:** the execution order is first `ìnclude`, then `ignore`. 135 | 136 | ```javascript 137 | module.exports = { 138 | plugins: [ 139 | { 140 | resolve: `gatsby-transformer-gitinfo`, 141 | options: { 142 | include: /\.md$/i, 143 | ignore: /README/i, // Will match all .md files, except README.md 144 | }, 145 | }, 146 | ], 147 | } 148 | ``` 149 | -------------------------------------------------------------------------------- /src/__tests__/gatsby_node.js: -------------------------------------------------------------------------------- 1 | const fs = require(`fs`); 2 | const os = require(`os`); 3 | const path = require(`path`); 4 | const git = require("simple-git/promise"); 5 | const { onCreateNode } = require(`../gatsby-node`); 6 | 7 | const tmpDir = `./tmp-test/`; 8 | 9 | let createNodeField; 10 | let actions; 11 | let node; 12 | let createNodeSpec; 13 | let dummyRepoPath; 14 | 15 | beforeEach(() => { 16 | createNodeField = jest.fn(); 17 | actions = { createNodeField }; 18 | 19 | node = { 20 | absolutePath: `/some/path/file.mdx`, 21 | dir: `/some/path`, 22 | id: `whatever`, 23 | parent: null, 24 | children: [], 25 | internal: { 26 | type: "File" 27 | } 28 | }; 29 | 30 | createNodeSpec = { 31 | node, 32 | actions 33 | }; 34 | }); 35 | 36 | describe(`Processing nodes not matching initial filtering`, () => { 37 | it(`should not add any field when internal type is not 'File'`, async () => { 38 | node.internal.type = "Other"; 39 | await onCreateNode(createNodeSpec); 40 | expect(createNodeField).not.toHaveBeenCalled(); 41 | }); 42 | 43 | it(`should not add any field when full path is not in include`, async () => { 44 | await onCreateNode(createNodeSpec, { 45 | include: /notmatching/ 46 | }); 47 | expect(createNodeField).not.toHaveBeenCalled(); 48 | }); 49 | 50 | it(`should not add any field when full path is in ignore`, async () => { 51 | await onCreateNode(createNodeSpec, { 52 | ignore: /some\/path\/file/ 53 | }); 54 | expect(createNodeField).not.toHaveBeenCalled(); 55 | }); 56 | 57 | it(`should not add any field when full path is in include and in ignore`, async () => { 58 | await onCreateNode(createNodeSpec, { 59 | include: /mdx/, 60 | ignore: /some\/path\/file/ 61 | }); 62 | expect(createNodeField).not.toHaveBeenCalled(); 63 | }); 64 | }); 65 | 66 | describe(`Processing File nodes matching filter regex`, () => { 67 | beforeEach(async () => { 68 | dummyRepoPath = fs.mkdtempSync( 69 | path.join(os.tmpdir(), "gatsby-transform-gitinfo-") 70 | ); 71 | 72 | const gitRepo = git(dummyRepoPath); 73 | await gitRepo.init(); 74 | await gitRepo.addConfig("user.name", "Some One"); 75 | await gitRepo.addConfig("user.email", "some@one.com"); 76 | await gitRepo.addRemote("origin", "https://some.git.repo"); 77 | 78 | fs.writeFileSync(`${dummyRepoPath}/README.md`, "Hello"); 79 | await gitRepo.add("README.md"); 80 | await gitRepo.commit("Add README", "README.md", { 81 | "--date": '"Mon 20 Aug 2018 20:19:19 UTC"' 82 | }); 83 | 84 | fs.writeFileSync(`${dummyRepoPath}/unversionned`, "World"); 85 | }); 86 | 87 | it("should add log and remote git info to commited File node", async () => { 88 | node.absolutePath = `${dummyRepoPath}/README.md`; 89 | node.dir = dummyRepoPath; 90 | await onCreateNode(createNodeSpec, { 91 | include: /md/, 92 | dir: dummyRepoPath 93 | }); 94 | expect(createNodeField).toHaveBeenCalledTimes(3); 95 | expect(createNodeField).toHaveBeenCalledWith({ 96 | node, 97 | name: `gitLogLatestAuthorName`, 98 | value: `Some One` 99 | }); 100 | expect(createNodeField).toHaveBeenCalledWith({ 101 | node, 102 | name: `gitLogLatestAuthorEmail`, 103 | value: `some@one.com` 104 | }); 105 | expect(createNodeField).toHaveBeenCalledWith({ 106 | node, 107 | name: `gitLogLatestDate`, 108 | value: `2018-08-20 20:19:19 +0000` 109 | }); 110 | }); 111 | 112 | it("should not add log or remote git info to unversionned File node", async () => { 113 | node.absolutePath = `${dummyRepoPath}/unversionned`; 114 | node.dir = dummyRepoPath; 115 | await onCreateNode(createNodeSpec, { 116 | include: /unversionned/, 117 | dir: dummyRepoPath 118 | }); 119 | expect(createNodeField).not.toHaveBeenCalled(); 120 | }); 121 | }); 122 | --------------------------------------------------------------------------------