Skip to content

Commit 02c893f

Browse files
authored
feat(contacts): apply duplicate merge plans (#817)
Closes #815
1 parent f212bc5 commit 02c893f

18 files changed

Lines changed: 1231 additions & 26 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## 0.27.2 - Unreleased
44

5+
### Added
6+
7+
- Contacts: add guarded `contacts dedupe --apply` merging with exact dry-run plans, repeatable `--resource` scoping, confirmation, full updatable-field preservation, etag checks before deletion, and refusal of ambiguous or unmergeable groups. (#815) — thanks @privatenumber.
8+
59
### Changed
610

711
- Gmail: show ordinary message bodies in full by default in text output, retain a generous cap for unusually large messages, and point truncated output to `--full` or `--json`. (#807) — thanks @privatenumber.

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,16 @@ Docs: [contacts dedupe](docs/contacts-dedupe.md),
316316
gog contacts search alice --json
317317
gog contacts export --all --out contacts.vcf
318318

319-
# Preview only: no merge/delete/update call is made.
319+
# Preview by default.
320320
gog contacts dedupe --json
321-
gog contacts dedupe --match email,phone,name --dry-run
321+
gog contacts dedupe --match email,phone,name
322+
323+
# Inspect the mutation plan, then apply with confirmation.
324+
gog contacts dedupe --apply --dry-run --json
325+
gog contacts dedupe --apply
326+
327+
# Scope automation to exact reviewed contact resources.
328+
gog contacts dedupe --resource people/123 --resource people/456 --apply --force --json
322329
```
323330

324331
### Docs

docs/commands.generated.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ Generated from `gog schema --json`.
208208
- [`gog config unset (rm,del,remove) <key>`](commands/gog-config-unset.md) - Unset a config value
209209
- [`gog contacts (contact) <command> [flags]`](commands/gog-contacts.md) - Google Contacts
210210
- [`gog contacts (contact) create (add,new) [flags]`](commands/gog-contacts-create.md) - Create a contact
211-
- [`gog contacts (contact) dedupe [flags]`](commands/gog-contacts-dedupe.md) - Find likely duplicate contacts (preview only)
211+
- [`gog contacts (contact) dedupe [flags]`](commands/gog-contacts-dedupe.md) - Find likely duplicate contacts and optionally merge them
212212
- [`gog contacts (contact) delete (rm,del,remove) <resourceName>`](commands/gog-contacts-delete.md) - Delete a contact
213213
- [`gog contacts (contact) directory <command>`](commands/gog-contacts-directory.md) - Directory contacts
214214
- [`gog contacts (contact) directory list [flags]`](commands/gog-contacts-directory-list.md) - List people from the Workspace directory

docs/commands/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ Generated pages: 646.
259259
- [gog config unset](gog-config-unset.md) - Unset a config value
260260
- [gog contacts](gog-contacts.md) - Google Contacts
261261
- [gog contacts create](gog-contacts-create.md) - Create a contact
262-
- [gog contacts dedupe](gog-contacts-dedupe.md) - Find likely duplicate contacts (preview only)
262+
- [gog contacts dedupe](gog-contacts-dedupe.md) - Find likely duplicate contacts and optionally merge them
263263
- [gog contacts delete](gog-contacts-delete.md) - Delete a contact
264264
- [gog contacts directory](gog-contacts-directory.md) - Directory contacts
265265
- [gog contacts directory list](gog-contacts-directory-list.md) - List people from the Workspace directory

docs/commands/gog-contacts-dedupe.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
44
5-
Find likely duplicate contacts (preview only)
5+
Find likely duplicate contacts and optionally merge them
66

77
## Usage
88

@@ -20,6 +20,7 @@ gog contacts (contact) dedupe [flags]
2020
| --- | --- | --- | --- |
2121
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
2222
| `-a`<br>`--account`<br>`--acct` | `string` | | Account email, alias, or auto for authenticated Google API commands |
23+
| `--apply`<br>`--merge` | `bool` | | Merge duplicate groups and delete redundant contacts (requires confirmation) |
2324
| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
2425
| `--color` | `string` | auto | Color output: auto\|always\|never |
2526
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
@@ -36,6 +37,7 @@ gog contacts (contact) dedupe [flags]
3637
| `--max`<br>`--limit` | `int64` | 0 | Max contacts to scan (0 = all) |
3738
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
3839
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
40+
| `--resource` | `[]string` | | Limit dedupe to exact contact resource names (people/...); repeatable |
3941
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
4042
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
4143
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |

docs/commands/gog-contacts.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ gog contacts (contact) <command> [flags]
1717
## Subcommands
1818

1919
- [gog contacts create](gog-contacts-create.md) - Create a contact
20-
- [gog contacts dedupe](gog-contacts-dedupe.md) - Find likely duplicate contacts (preview only)
20+
- [gog contacts dedupe](gog-contacts-dedupe.md) - Find likely duplicate contacts and optionally merge them
2121
- [gog contacts delete](gog-contacts-delete.md) - Delete a contact
2222
- [gog contacts directory](gog-contacts-directory.md) - Directory contacts
2323
- [gog contacts export](gog-contacts-export.md) - Export contacts as vCard (.vcf)

docs/contacts-dedupe.md

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# Contacts Dedupe Preview
1+
# Contacts Dedupe
22

33
read_when:
44
- Finding duplicate Google Contacts.
55
- Reviewing or changing `gog contacts dedupe`.
66

77
`gog contacts dedupe` finds likely duplicate personal contacts and prints a
8-
merge plan. It is preview-only: it does not merge, update, or delete contacts.
8+
merge plan. Preview is the default; `--apply` performs the reviewed plan.
99

1010
## Command Page
1111

@@ -31,6 +31,49 @@ Name matching is opt-in because it can produce false positives:
3131
gog contacts dedupe --match email,phone,name
3232
```
3333

34+
## Apply A Merge
35+
36+
Review the plan first:
37+
38+
```bash
39+
gog contacts dedupe --json
40+
```
41+
42+
Then inspect the exact mutation plan without changing contacts:
43+
44+
```bash
45+
gog contacts dedupe --apply --dry-run --json
46+
```
47+
48+
For automation, copy the contact resource names from the reviewed preview and
49+
scope both dry-run and apply to that exact set:
50+
51+
```bash
52+
gog contacts dedupe \
53+
--resource people/123 \
54+
--resource people/456 \
55+
--apply \
56+
--dry-run \
57+
--json
58+
```
59+
60+
Apply interactively:
61+
62+
```bash
63+
gog contacts dedupe --apply
64+
```
65+
66+
Non-interactive automation must explicitly skip confirmation:
67+
68+
```bash
69+
gog contacts dedupe \
70+
--resource people/123 \
71+
--resource people/456 \
72+
--apply \
73+
--force \
74+
--json
75+
```
76+
3477
## Output
3578

3679
The command groups contacts that share a matching key. JSON output includes:
@@ -42,16 +85,36 @@ The command groups contacts that share a matching key. JSON output includes:
4285
- `matched_on`: duplicate email/phone/name keys that caused the group
4386
- `members`: all contacts in the group
4487

45-
## Safety
88+
Applied output also includes:
4689

47-
`contacts dedupe` is read-only. There is no apply flag.
90+
- `applied`: whether mutations ran
91+
- `groups_merged`: groups completed
92+
- `contacts_deleted`: redundant contacts deleted
93+
- `update_fields`: People API fields unioned into the primary contact
94+
- `delete`: redundant contacts removed after their data was copied
4895

49-
Use `--dry-run` in automation anyway when you want a uniform safety habit across
50-
commands:
96+
## Safety
5197

52-
```bash
53-
gog contacts dedupe --dry-run --json
54-
```
98+
`contacts dedupe` remains read-only unless `--apply` is present. Apply mode:
99+
100+
- requires confirmation unless `--force` is present
101+
- honors `--dry-run`
102+
- supports repeatable `--resource` scoping so automation can apply an exact
103+
reviewed contact set
104+
- reads contact-source data only
105+
- refreshes each contact before planning
106+
- updates the selected primary before deleting anything
107+
- rechecks each redundant contact's etag immediately before deletion
108+
- refuses groups with conflicting singleton fields (`names`, `birthdays`,
109+
`biographies`, or `genders`)
110+
- refuses secondary contacts with photos or other fields the People API cannot
111+
preserve through `updateContact`
112+
113+
The People API does not expose Google Contacts' native merge operation. gog
114+
therefore unions all API-updatable fields into the selected primary, then
115+
deletes redundant contacts sequentially. If a request fails, the command stops
116+
and reports completed groups/deletions; copied data remains on the primary and
117+
undeleted contacts remain intact.
55118

56119
Use `--fail-empty` in scheduled checks when "no duplicates" should be reported
57120
as a distinct exit code:

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ gog slides create-from-markdown "Weekly update" --content-file slides.md
4040
- **Runtime discovery.** `gog schema --json` exposes command shape, stable exit codes, output modes, and effective safety state.
4141
- **Multi-account, multi-client.** Many Google accounts and OAuth client projects in one config; OAuth, direct access tokens, ADC, and Workspace service accounts all supported.
4242
- **One automation contract.** Humans, scripts, CI, and agents use the same commands, with JSON/TSV output, non-interactive operation, stable exit codes, untrusted-content wrapping, runtime command guards, and baked safety profiles.
43-
- **Read-only audits.** Drive `tree`, `du`, `inventory`; Contacts `dedupe` preview; raw API JSON dumps without ever mutating remote state.
43+
- **Preview-first audits.** Drive `tree`, `du`, `inventory`; Contacts `dedupe` previews by default and requires explicit `--apply` for guarded merges; raw API JSON dumps never mutate remote state.
4444
- **Generated reference.** Every command has a docs page produced from `gog schema --json`.
4545

4646
## Pick your path

docs/live-testing.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ available services. Recent-feature coverage includes:
3434
- Drive shortcuts, revisions, and persisted changes polling.
3535
- Gmail thread-aware drafts, attachment metadata preservation/clearing, and
3636
explicit thread archive semantics.
37+
- Contacts duplicate merge dry-run, apply, merged-field readback, and cleanup.
3738
- CLI schema exit codes, Git-style help, output-mode precedence, and early
3839
validation errors.
3940

docs/spec.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ after the bounded retry window, the command exits with retryable code `8`.
370370
- `gog classroom guardian-invitations get <studentId> <invitationId>`
371371
- `gog classroom guardian-invitations create <studentId> --email EMAIL`
372372
- `gog classroom profile [userId]`
373-
- `gog contacts dedupe [--match email,phone,name] [--max N]`
373+
- `gog contacts dedupe [--match email,phone,name] [--max N] [--resource people/...] [--apply]`
374374
- `gog gmail search <query> [--max N] [--page TOKEN]`
375375
- `gog gmail messages search <query> [--max N] [--page TOKEN] [--include-body] [--body-format text|html] [--full]`
376376
- `gog gmail autoreply <query> [--max N] [--subject S] [--body B|--body-file PATH|--body-html HTML] [--from addr] [--reply-to addr] [--label L] [--archive] [--mark-read] [--skip-bulk] [--allow-self]`

0 commit comments

Comments
 (0)