Skip to content

Commit d05b6db

Browse files
authored
feat: tags from config, and in log output (#100)
* tags config * tags from config? * filter out empties * remove duplicate prop * fix: only show tags for given oid Co-authored-by: Misha Kaletsky <[email protected]>
1 parent 4977042 commit d05b6db

File tree

6 files changed

+311
-9
lines changed

6 files changed

+311
-9
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"predocker-apply": "yarn docker-copy-query",
3434
"docker-apply": "yarn psql -f /queries/create-git-functions.sql",
3535
"docker-bash": "docker-compose exec postgres bash",
36+
"docker-logs": "docker-compose logs --follow --tail 100",
3637
"docker-psql": "docker-compose exec postgres psql -h localhost -U postgres postgres",
3738
"predocker-copy-query": "yarn docker-exec mkdir -p /queries",
3839
"docker-copy-query": "docker cp queries/create-git-functions.sql plv8-git_postgres_1:/queries",

readme.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ This query will return:
112112
"author": "pguser ([email protected])",
113113
"timestamp": "2000-12-25T12:00:00.000Z",
114114
"oid": "[oid]",
115+
"tags": [],
115116
"changes": [
116117
{
117118
"field": "text",
@@ -125,6 +126,7 @@ This query will return:
125126
"author": "pguser ([email protected])",
126127
"timestamp": "2000-12-25T12:00:00.000Z",
127128
"oid": "[oid]",
129+
"tags": [],
128130
"changes": [
129131
{
130132
"field": "id",
@@ -262,6 +264,7 @@ where identifier->>'id' = '1'
262264
"author": "pguser ([email protected])",
263265
"timestamp": "2000-12-25T12:00:00.000Z",
264266
"oid": "[oid]",
267+
"tags": [],
265268
"changes": [
266269
{
267270
"field": "text",
@@ -275,6 +278,7 @@ where identifier->>'id' = '1'
275278
"author": "pguser ([email protected])",
276279
"timestamp": "2000-12-25T12:00:00.000Z",
277280
"oid": "[oid]",
281+
"tags": [],
278282
"changes": [
279283
{
280284
"field": "id",
@@ -325,6 +329,7 @@ where id = 2
325329
"author": "Alice ([email protected])",
326330
"timestamp": "2000-12-25T12:00:00.000Z",
327331
"oid": "[oid]",
332+
"tags": [],
328333
"changes": [
329334
{
330335
"field": "id",
@@ -366,6 +371,7 @@ where id = 201
366371
"author": "Bob ([email protected])",
367372
"timestamp": "2000-12-25T12:00:00.000Z",
368373
"oid": "[oid]",
374+
"tags": [],
369375
"changes": [
370376
{
371377
"field": "id",
@@ -408,6 +414,7 @@ where id = 2
408414
"author": "pguser ([email protected])",
409415
"timestamp": "2000-12-25T12:00:00.000Z",
410416
"oid": "[oid]",
417+
"tags": [],
411418
"changes": [
412419
{
413420
"field": "text",
@@ -443,6 +450,16 @@ set
443450
where id = 3;
444451
```
445452

453+
Or, set them in git config as a colon-separated list:
454+
455+
```sql
456+
select git_set_local_config('tags', 'your_app_request_id=1234:your_app_trace_id=5678');
457+
458+
update test_table
459+
set text = 'item 3 yet another value'
460+
where id = 3;
461+
```
462+
446463
### Restoring previous versions
447464

448465
`git_resolve` gives you a json representation of a prior version of a row, which can be used for backup and restore. The first argument is a `git` json value, the second value is a valid git ref string (e.g. a git oid returned by `git_log`, or `HEAD`, or `main`. Note that an issue with [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git/issues/1238) means that you can't currently pass values like `HEAD~1` here).
@@ -488,6 +505,91 @@ returning id, text
488505

489506
If you used `tags` as described above, you can take advantage of them to restore to a known-good state easily:
490507

508+
```sql
509+
select git_log(git)
510+
from test_table
511+
where id = 3
512+
```
513+
514+
```json
515+
[
516+
{
517+
"git_log": [
518+
{
519+
"message": "test_table_git_track_trigger: BEFORE UPDATE ROW on public.test_table",
520+
"author": "pguser ([email protected])",
521+
"timestamp": "2000-12-25T12:00:00.000Z",
522+
"oid": "[oid]",
523+
"tags": [
524+
"your_app_request_id=1234",
525+
"your_app_trace_id=5678"
526+
],
527+
"changes": [
528+
{
529+
"field": "text",
530+
"new": "item 3 yet another value",
531+
"old": "item 3 new year value"
532+
}
533+
]
534+
},
535+
{
536+
"message": "test_table_git_track_trigger: BEFORE UPDATE ROW on public.test_table",
537+
"author": "pguser ([email protected])",
538+
"timestamp": "2000-12-25T12:00:00.000Z",
539+
"oid": "[oid]",
540+
"tags": [
541+
"2001",
542+
"2001-01",
543+
"2001-01-01"
544+
],
545+
"changes": [
546+
{
547+
"field": "text",
548+
"new": "item 3 new year value",
549+
"old": "item 3 boxing day value"
550+
}
551+
]
552+
},
553+
{
554+
"message": "test_table_git_track_trigger: BEFORE UPDATE ROW on public.test_table",
555+
"author": "pguser ([email protected])",
556+
"timestamp": "2000-12-25T12:00:00.000Z",
557+
"oid": "[oid]",
558+
"tags": [
559+
"2000",
560+
"2000-12",
561+
"2000-12-26"
562+
],
563+
"changes": [
564+
{
565+
"field": "text",
566+
"new": "item 3 boxing day value",
567+
"old": "item 3 xmas day value"
568+
}
569+
]
570+
},
571+
{
572+
"message": "test_table_git_track_trigger: BEFORE INSERT ROW on public.test_table",
573+
"author": "pguser ([email protected])",
574+
"timestamp": "2000-12-25T12:00:00.000Z",
575+
"oid": "[oid]",
576+
"tags": [],
577+
"changes": [
578+
{
579+
"field": "id",
580+
"new": 3
581+
},
582+
{
583+
"field": "text",
584+
"new": "item 3 xmas day value"
585+
}
586+
]
587+
}
588+
]
589+
}
590+
]
591+
```
592+
491593
```sql
492594
update test_table set (id, text) =
493595
(
@@ -508,6 +610,26 @@ returning id, text
508610
}
509611
```
510612

613+
```sql
614+
update test_table set (id, text) =
615+
(
616+
select id, text
617+
from json_populate_record(
618+
null::test_table,
619+
git_resolve(git, ref := 'your_app_request_id=1234')
620+
)
621+
)
622+
where id = 3
623+
returning id, text
624+
```
625+
626+
```json
627+
{
628+
"id": 3,
629+
"text": "item 3 yet another value"
630+
}
631+
```
632+
511633
A similar technique can restore a deleted item:
512634

513635
```sql
@@ -571,6 +693,7 @@ where git = 'https://github.com/mmkal/plv8-git.git'
571693
"author": "pguser ([email protected])",
572694
"timestamp": "2000-12-25T12:00:00.000Z",
573695
"oid": "[oid]",
696+
"tags": [],
574697
"changes": [
575698
{
576699
"field": "git",

src/git.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as git from 'isomorphic-git'
44
import * as serializer from './serializer'
55
import {PG_Vars} from './pg-types'
66
import {setupMemfs} from './fs'
7+
import {memoizeAsync} from './memoize'
78

89
function writeGitFiles(gitFiles: any, fs: memfs.IFs) {
910
if (!gitFiles) {
@@ -42,7 +43,7 @@ export const rowToRepo = ({OLD, NEW, ...pg}: PG_Vars) => {
4243

4344
const gitParams = NEW?.[repoColumn] || {}
4445

45-
const commitMessage = `${pg.TG_NAME}: ${pg.TG_WHEN} ${pg.TG_OP} ${pg.TG_LEVEL} on ${pg.TG_TABLE_SCHEMA}.${pg.TG_TABLE_NAME}`.trim()
46+
const defaultCommitMessage = `${pg.TG_NAME}: ${pg.TG_WHEN} ${pg.TG_OP} ${pg.TG_LEVEL} on ${pg.TG_TABLE_SCHEMA}.${pg.TG_TABLE_NAME}`.trim()
4647

4748
return Promise.resolve()
4849
.then(setupGitFolder)
@@ -60,20 +61,31 @@ export const rowToRepo = ({OLD, NEW, ...pg}: PG_Vars) => {
6061
.then(() =>
6162
git.commit({
6263
...repo,
63-
message: [gitParams.commit?.message, commitMessage].filter(Boolean).join('\n\n'),
64+
message: [
65+
gitParams.commit?.message,
66+
getSetting('commit.message'),
67+
defaultCommitMessage,
68+
getSetting('commit.message.signature'),
69+
]
70+
.filter(Boolean)
71+
.join('\n\n'),
6472
author: {
6573
name: gitParams.commit?.author?.name || getSetting('user.name') || 'pguser',
6674
email: gitParams.commit?.author?.email || getSetting('user.email') || '[email protected]',
6775
},
6876
}),
6977
)
70-
.then(commit =>
71-
Promise.all(
72-
(gitParams.tags || []).map((tag: string) => {
78+
.then(commit => {
79+
const allTags: string[] = [
80+
...(getSetting('tags')?.split(':') || []), // colon separated tags from config
81+
...(gitParams.tags || []),
82+
].filter(Boolean)
83+
return Promise.all(
84+
allTags.map((tag: string) => {
7385
return git.tag({...repo, ref: tag, object: commit})
7486
}),
75-
),
76-
)
87+
)
88+
})
7789
})
7890
.then(() => {
7991
const files: Record<string, number[]> = {}
@@ -98,7 +110,7 @@ declare const plv8: {
98110
const getSetting = (name: string) => {
99111
// https://www.postgresql.org/docs/9.4/functions-admin.html
100112
const [{git_get_config}] = plv8.execute('select git_get_config($1)', [name])
101-
return git_get_config
113+
return git_get_config as string | null
102114
}
103115

104116
type TreeInfo = {type: string; content: string; oid: string}
@@ -113,6 +125,10 @@ export const gitLog = (gitRepoJson: object, depth?: number) => {
113125
const {fs} = setupMemfs()
114126
const repo = {fs, dir: '/repo'}
115127

128+
// `listTags` lists all tags for the repo. so we need to use resolveRef to check that each tags is pointing at a given id
129+
// this can mean a lot of repeated calls.
130+
const resolveTagRef = memoizeAsync(git.resolveRef)
131+
116132
return Promise.resolve()
117133
.then(() => writeGitFiles(gitRepoJson, fs))
118134
.then(() => git.log({...repo, depth}))
@@ -130,11 +146,20 @@ export const gitLog = (gitRepoJson: object, depth?: number) => {
130146
)
131147
},
132148
})
133-
.then((results: WalkResult[]) => ({
149+
.then((results: WalkResult[]) => {
150+
return git.listTags({...repo}).then(tags => {
151+
return Promise.all(tags.map(t => resolveTagRef({...repo, ref: t}))).then(resolvedTags => {
152+
const filteredTags = tags.filter((t, i) => resolvedTags[i] === e.oid)
153+
return {results, tags: filteredTags}
154+
})
155+
})
156+
})
157+
.then(({results, tags}) => ({
134158
message: e.commit.message.trim(),
135159
author: `${e.commit.author.name} (${e.commit.author.email})`,
136160
timestamp: new Date(e.commit.author.timestamp * 1000).toISOString(),
137161
oid: e.oid,
162+
tags,
138163
changes: results
139164
.filter(
140165
r => r.ChildInfo?.type === 'blob' && r.filepath !== '.' && r.ChildInfo.oid !== r.ParentInfo?.oid,

src/memoize.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const memoizeAsync = <A extends any[], T>(fn: (...args: A) => Promise<T>): ((...args: A) => Promise<T>) => {
2+
const cache = new Map<string, T>()
3+
return (...args: A) => {
4+
const key = JSON.stringify(args)
5+
if (cache.has(key)) {
6+
return Promise.resolve(cache.get(key)!)
7+
}
8+
9+
return fn(...args).then(result => {
10+
cache.set(key, result)
11+
return result
12+
})
13+
}
14+
}

test/memoize.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {memoizeAsync} from '../src/memoize'
2+
3+
test('memoize', async () => {
4+
const mock = jest.fn(async () => Math.random())
5+
6+
const memoized = memoizeAsync(mock)
7+
8+
const first = await memoized()
9+
const second = await memoized()
10+
11+
expect([first, second]).toEqual([expect.any(Number), expect.any(Number)])
12+
expect(mock).toHaveBeenCalledTimes(1)
13+
expect(second).toEqual(first)
14+
})

0 commit comments

Comments
 (0)