Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
365ee9e
feat: add commit attribution with per-file AI contribution tracking v…
wenshao Apr 10, 2026
7c62e60
feat: enhance commit attribution with real AI/human ratios and genera…
wenshao Apr 10, 2026
260f56b
feat: add surface tracking, prompt counting, session persistence, and…
wenshao Apr 10, 2026
32fe367
fix: audit fixes — initial commit handling, cron prompt exclusion, fa…
wenshao Apr 10, 2026
57bd3b1
fix: cross-platform and correctness fixes from multi-round audit
wenshao Apr 11, 2026
eef2723
Merge remote-tracking branch 'origin/main' into feat/commit-attribution
wenshao Apr 14, 2026
180daa1
Merge remote-tracking branch 'origin/main' into feat/commit-attribution
wenshao Apr 19, 2026
6683953
fix(attribution): also snapshot on ToolResult turns so resume keeps t…
wenshao Apr 19, 2026
ac836b6
refactor(attribution): merge duplicate retry guard and update stale doc
wenshao Apr 19, 2026
5fd65e7
Merge remote-tracking branch 'origin/main' into feat/commit-attribution
wenshao Apr 24, 2026
eca9a06
feat(attribution): split gitCoAuthor into independent commit and pr t…
wenshao Apr 24, 2026
2e4dbbc
feat(settings): add v3→v4 migration for gitCoAuthor shape change
wenshao Apr 24, 2026
37142c2
test(migration): cover null/array/number and partial object for v3-to-v4
wenshao Apr 24, 2026
30b0039
Merge remote-tracking branch 'origin/main' into feat/commit-attribution
wenshao Apr 30, 2026
76ec048
fix(shell): address PR review for compound commits and PR body escaping
wenshao Apr 30, 2026
6f9a39c
Merge remote-tracking branch 'origin/main' into feat/commit-attribution
wenshao May 1, 2026
9894217
fix(attribution): address Copilot review on shell, schema, and totals
wenshao May 1, 2026
c83479e
fix(attribution): parse binary diffs, source generator from model, sy…
wenshao May 1, 2026
ae68a4f
fix(attribution): repo-root baseDir, escape co-author trailer, switch…
wenshao May 1, 2026
9def2b6
fix(shell): shell-aware git-commit detection and apostrophe-escape ha…
wenshao May 1, 2026
10676dc
fix(attribution): drop magic 100 fallback for empty deletions
wenshao May 1, 2026
31bddbc
fix(shell): broaden git-commit detection, gate background, drop dead …
wenshao May 1, 2026
6e1028d
fix(shell): unified git-commit detection split by intent
wenshao May 1, 2026
2af4e1d
fix(shell): position-independent git subcommand detection + bash-shel…
wenshao May 1, 2026
f97ba20
fix(shell): refuse multi-commit attribution; misc review follow-ups
wenshao May 1, 2026
be81c6f
fix(attribution): partial-commit clear, symlink baseDir, gh/git flag …
wenshao May 1, 2026
89ee3e3
fix(attribution): canonicalize file paths centrally in CommitAttribut…
wenshao May 1, 2026
2dd792c
fix(attribution): canonicalize-from-root cleanup; fix mixed-quote -m …
wenshao May 2, 2026
222b188
fix(shell): scope -m rewrite to commit segment, reject nested matches
wenshao May 2, 2026
84d764b
fix(attribution): cd-leak, numstat partial failure, $() bailout, gh p…
wenshao May 2, 2026
66cef3b
fix(attribution): --amend, --message/-b aliases, .d.ts over-exclusion
wenshao May 2, 2026
80bed8f
fix(attribution): cd-subdir, scope --body, multi-commit count guard, …
wenshao May 2, 2026
1498133
fix(attribution): cd embedded .., env wrapper, Windows ARG_MAX, segme…
wenshao May 2, 2026
f531924
fix(attribution): scope isAmendCommit to attributable segment only
wenshao May 2, 2026
d78d4cf
Merge remote-tracking branch 'origin/main' into feat/commit-attribution
wenshao May 3, 2026
8891b83
fix(attribution): last-match --body, symlink leaf canonicalisation, s…
wenshao May 3, 2026
eecb0cf
fix(attribution): skip values for env -u NAME and -S string
wenshao May 3, 2026
0103af5
Merge remote-tracking branch 'origin/main' into feat/commit-attribution
wenshao May 5, 2026
9e73169
fix(attribution): submodule leak, PR body nesting, shallow-clone bail…
wenshao May 5, 2026
090758c
fix(attribution): drop unsafe full-clear, tag analysis-failure with null
wenshao May 5, 2026
3c0e329
fix(attribution): dedup snapshot writes, cap excludedGenerated, doc c…
wenshao May 5, 2026
e4bb018
fix(attribution): depth-1 shallow detection, snapshot dedup post-rewi…
wenshao May 5, 2026
296fb55
fix(attribution): preHead race, regex apostrophe-escape, surface fail…
wenshao May 5, 2026
325a12c
chore(schema): regenerate settings.schema.json to match gitCoAuthor.c…
wenshao May 5, 2026
1ece874
fix(attribution): preserve unstaged AI edits across cleanup branches
wenshao May 5, 2026
dd45e17
fix(attribution): runGit null-on-failure, versionless v3→v4 migration
wenshao May 6, 2026
d429d90
fix(attribution): toggle-off partial clear, normalizeGitCoAuthor type…
wenshao May 6, 2026
a53c750
fix(attribution): harden restoreFromSnapshot against corrupt payloads
wenshao May 6, 2026
715c258
fix(attribution): roll back snapshot dedup key on sync appendRecord f…
wenshao May 6, 2026
acd06e3
Merge remote-tracking branch 'origin/main' into feat/commit-attribution
wenshao May 6, 2026
ee460de
docs(attribution): align cleanup-branch comments with noteCommitWitho…
wenshao May 6, 2026
8f9c7e4
Merge remote-tracking branch 'origin/main' into feat/commit-attribution
wenshao May 6, 2026
c0ed909
fix(attribution): restore fire-and-forget appendRecord, route rollbac…
wenshao May 6, 2026
a66a21d
fix(attribution): GIT_DIR repo-shift bail, snapshot envelope validati…
wenshao May 6, 2026
cc916b5
docs(attribution): correct legacyTypes / EXCLUDED_DIRECTORY_SEGMENTS …
wenshao May 6, 2026
56ecaea
fix(attribution): SHA-pin git notes, on-disk hash divergence detectio…
wenshao May 6, 2026
6ae266f
fix(attribution): pickBool intent-aware, shouldClear gate, ETIMEDOUT …
wenshao May 6, 2026
b3a06a7
fix(attribution): committed-blob validation, deleted-leaf canonicalis…
wenshao May 6, 2026
3bb4cc7
fix(attribution): scope validateAgainst to committed set, SHA-pin rea…
wenshao May 6, 2026
c557bd5
chore(attribution): extract pickOuterLastMatch, log unrecognised pick…
wenshao May 6, 2026
ca21b6d
fix(attribution): canonicalise BOM and CRLF before hashing
wenshao May 6, 2026
9000b9a
fix(attribution): reset accumulator when re-creating a deleted tracke…
wenshao May 6, 2026
014b250
fix(attribution): treat git -C . as in-cwd, gate preHead on attributable
wenshao May 6, 2026
8eb37ce
fix(core): preserve attribution across renamed files
wenshao May 6, 2026
dd8af23
Merge branch 'main' of github.com:QwenLM/qwen-code into feat/commit-a…
wenshao May 6, 2026
2dbf3ea
Merge branch 'feat/commit-attribution' of github.com:QwenLM/qwen-code…
wenshao May 6, 2026
56b5a06
fix(attribution): preserve env-vars in tokens, exclude empty -C targets
wenshao May 7, 2026
8c33120
fix(attribution): SHA-pin diff/rev-list phase, document aiChars heuri…
wenshao May 7, 2026
b8d242d
Merge branch 'main' of github.com:QwenLM/qwen-code into feat/commit-a…
wenshao May 7, 2026
af5a1d9
fix(attribution): use posix join in applyCommittedRenames for Windows…
wenshao May 7, 2026
9493369
docs(attribution): refresh stale HEAD~1/HEAD@{1} references in comments
wenshao May 7, 2026
b89b655
fix(attribution): catch attached-value forms of env/sudo cwd-shift flags
wenshao May 7, 2026
5e52481
test(attribution): cover attached-form git -C/--git-dir/--work-tree
wenshao May 7, 2026
dc28d67
docs(attribution): document why backtick body doesn't bail like $(
wenshao May 7, 2026
1852ef2
fix(attribution): scope trailer rewrite to before unquoted shell comment
wenshao May 7, 2026
1e7c97f
fix(attribution): warn on gh pr create flows that can't be rewritten …
wenshao May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions docs/users/configuration/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,17 @@ Settings are organized into categories. Most settings should be placed within th

#### general

| Setting | Type | Description | Default |
| ------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `general.preferredEditor` | string | The preferred editor to open files in. | `undefined` |
| `general.vimMode` | boolean | Enable Vim keybindings. | `false` |
| `general.enableAutoUpdate` | boolean | Enable automatic update checks and installations on startup. | `true` |
| `general.showSessionRecap` | boolean | Auto-show a one-line "where you left off" recap when returning to the terminal after being away. Off by default. Use `/recap` to trigger manually regardless of this setting. | `false` |
| `general.sessionRecapAwayThresholdMinutes` | number | Minutes the terminal must be blurred before an auto-recap fires on focus-in. Only used when `showSessionRecap` is enabled. | `5` |
| `general.gitCoAuthor` | boolean | Automatically add a Co-authored-by trailer to git commit messages when commits are made through Qwen Code. | `true` |
| `general.checkpointing.enabled` | boolean | Enable session checkpointing for recovery. | `false` |
| `general.defaultFileEncoding` | string | Default encoding for new files. Use `"utf-8"` (default) for UTF-8 without BOM, or `"utf-8-bom"` for UTF-8 with BOM. Only change this if your project specifically requires BOM. | `"utf-8"` |
| Setting | Type | Description | Default |
| ------------------------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `general.preferredEditor` | string | The preferred editor to open files in. | `undefined` |
| `general.vimMode` | boolean | Enable Vim keybindings. | `false` |
| `general.enableAutoUpdate` | boolean | Enable automatic update checks and installations on startup. | `true` |
| `general.showSessionRecap` | boolean | Auto-show a one-line "where you left off" recap when returning to the terminal after being away. Off by default. Use `/recap` to trigger manually regardless of this setting. | `false` |
| `general.sessionRecapAwayThresholdMinutes` | number | Minutes the terminal must be blurred before an auto-recap fires on focus-in. Only used when `showSessionRecap` is enabled. | `5` |
| `general.gitCoAuthor.commit` | boolean | Add a Co-authored-by trailer to git commit messages AND attach a per-file AI-attribution git note (`refs/notes/ai-attribution`) for commits made through Qwen Code. Disabling skips both. | `true` |
| `general.gitCoAuthor.pr` | boolean | Append a Qwen Code attribution line to pull request descriptions when running `gh pr create`. | `true` |
| `general.checkpointing.enabled` | boolean | Enable session checkpointing for recovery. | `false` |
| `general.defaultFileEncoding` | string | Default encoding for new files. Use `"utf-8"` (default) for UTF-8 without BOM, or `"utf-8-bom"` for UTF-8 with BOM. Only change this if your project specifically requires BOM. | `"utf-8"` |

#### output

Expand Down
92 changes: 67 additions & 25 deletions integration-tests/cli/settings-migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,21 @@ const {
v2PreexistingEnableSettings,
v3LegacyDisableSettings,
v999FutureVersionSettings,
v3GitCoAuthorBooleanSettings,
} = workspacesSettings;

/**
* Integration tests for settings migration chain (V1 -> V2 -> V3)
* Integration tests for settings migration chain (V1 -> V2 -> V3 -> V4)
*
* These tests verify that:
* 1. V1 settings are automatically migrated to V3 on CLI startup
* 2. V2 settings are automatically migrated to V3 on CLI startup
* 3. V3 settings remain unchanged
* 1. V1 settings are automatically migrated to V4 on CLI startup
* 2. V2 settings are automatically migrated to V4 on CLI startup
* 3. V3 settings are automatically migrated to V4 on CLI startup
* 4. Migration is idempotent (running multiple times produces same result)
*
* The numeric assertions use the literal `4` to match
* `SETTINGS_VERSION`; bump that constant and the literal together
* when adding a future migration.
*/
describe('settings-migration', () => {
let rig: TestRig;
Expand Down Expand Up @@ -77,7 +82,7 @@ describe('settings-migration', () => {
};

describe('V1 settings migration', () => {
it('should migrate V1 settings to V3 on CLI startup', async () => {
it('should migrate V1 settings forward through the chain on CLI startup', async () => {
rig.setup('v1-to-v3-migration');

// Write V1 settings directly (overwrites the one created by setup)
Expand All @@ -94,8 +99,8 @@ describe('settings-migration', () => {
// Read migrated settings
const migratedSettings = readSettingsFile(rig);

// Verify migration to V3
expect(migratedSettings['$version']).toBe(3);
// Verify migration to V4 (current SETTINGS_VERSION)
expect(migratedSettings['$version']).toBe(4);
expect(migratedSettings['ui']).toEqual({
theme: 'dark',
hideTips: false,
Expand Down Expand Up @@ -137,7 +142,7 @@ describe('settings-migration', () => {
const migratedSettings = readSettingsFile(rig);

// Expected output based on stable test output
expect(migratedSettings['$version']).toBe(3);
expect(migratedSettings['$version']).toBe(4);
expect(migratedSettings['tools']).toEqual({ autoAccept: false });
expect(migratedSettings['context']).toEqual({ includeDirectories: [] });
expect(migratedSettings['model']).toEqual({ name: ['gemini', 'claude'] });
Expand All @@ -161,8 +166,8 @@ describe('settings-migration', () => {
// Read migrated settings
const migratedSettings = readSettingsFile(rig);

// Should be migrated to V3
expect(migratedSettings['$version']).toBe(3);
// Should be migrated to V4
expect(migratedSettings['$version']).toBe(4);
// Legacy string values for ui/general should be preserved as-is (user data)
expect(migratedSettings['ui']).toBe('legacy-ui-string');
expect(migratedSettings['general']).toBe('legacy-general-string');
Expand All @@ -189,7 +194,7 @@ describe('settings-migration', () => {
const migratedSettings = readSettingsFile(rig);

// Expected output based on stable test output
expect(migratedSettings['$version']).toBe(3);
expect(migratedSettings['$version']).toBe(4);
expect(migratedSettings['model']).toEqual({ name: 'qwen-plus' });
expect(migratedSettings['ui']).toEqual({
hideWindowTitle: true,
Expand All @@ -209,7 +214,7 @@ describe('settings-migration', () => {
});

describe('V2 settings migration', () => {
it('should migrate V2 settings to V3 on CLI startup', async () => {
it('should migrate V2 settings forward through the chain on CLI startup', async () => {
rig.setup('v2-to-v3-migration');

// Write V2 settings directly (overwrites the one created by setup)
Expand All @@ -225,8 +230,8 @@ describe('settings-migration', () => {
// Read migrated settings
const migratedSettings = readSettingsFile(rig);

// Verify migration to V3
expect(migratedSettings['$version']).toBe(3);
// Verify migration to V4 (current SETTINGS_VERSION)
expect(migratedSettings['$version']).toBe(4);

// Verify disable* -> enable* conversion with inversion
expect(
Expand Down Expand Up @@ -302,8 +307,8 @@ describe('settings-migration', () => {
// Read migrated settings
const migratedSettings = readSettingsFile(rig);

// Should be updated to V3 version
expect(migratedSettings['$version']).toBe(3);
// Should be updated to V4 version
expect(migratedSettings['$version']).toBe(4);
// Other settings should remain unchanged
expect(migratedSettings['ui']).toEqual({ theme: 'dark' });
expect(migratedSettings['model']).toEqual({ name: 'gemini' });
Expand All @@ -330,12 +335,12 @@ describe('settings-migration', () => {
const migratedSettings = readSettingsFile(rig);

// Version metadata should still be normalized to current version
expect(migratedSettings['$version']).toBe(3);
expect(migratedSettings['$version']).toBe(4);
// Existing user content should be preserved
expect(migratedSettings['customOnlyKey']).toBe('value');
});

it('should coerce valid string booleans and remove invalid deprecated keys while bumping V2 to V3', async () => {
it('should coerce valid string booleans and remove invalid deprecated keys while bumping V2 forward through the chain', async () => {
rig.setup('v2-non-boolean-disable-values-migration');

// Cover both coercible string booleans and invalid non-boolean values:
Expand Down Expand Up @@ -372,7 +377,7 @@ describe('settings-migration', () => {
const migratedSettings = readSettingsFile(rig);

// Coercible strings are migrated; invalid disable* values are removed.
expect(migratedSettings['$version']).toBe(3);
expect(migratedSettings['$version']).toBe(4);
expect(migratedSettings['general']).toEqual({
enableAutoUpdate: false,
});
Expand Down Expand Up @@ -437,7 +442,7 @@ describe('settings-migration', () => {
const migratedSettings = readSettingsFile(rig);

// Expected output based on stable test output
expect(migratedSettings['$version']).toBe(3);
expect(migratedSettings['$version']).toBe(4);
// Migration converts disable* to enable* by inverting the value
// disableAutoUpdate: false -> enableAutoUpdate: true (inverted)
// But disableUpdateNag: true may affect the consolidation
Expand Down Expand Up @@ -501,11 +506,10 @@ describe('settings-migration', () => {
// Read settings
const finalSettings = readSettingsFile(rig);

// Should remain V3
expect(finalSettings['$version']).toBe(3);
// Note: V3 settings with legacy disable* keys are left as-is
// Migration only runs when version < current version
// Since this is already V3, no migration logic is applied
// V3 → V4 migration bumps the version; V3→V4 only touches
Comment thread
wenshao marked this conversation as resolved.
// general.gitCoAuthor, so unrelated legacy disable* keys remain as-is
// (V2→V3 ran on original V3 load, not re-applied here).
expect(finalSettings['$version']).toBe(4);
expect(
(finalSettings['general'] as Record<string, unknown>)?.[
'disableAutoUpdate'
Expand Down Expand Up @@ -536,6 +540,44 @@ describe('settings-migration', () => {
note: 'should remain unchanged in v3',
});
});

// V3 used to allow `general.gitCoAuthor: <boolean>`. The V3→V4
// migration must expand that boolean into the new
// `{ commit, pr }` object shape so the user's stored opt-out
// doesn't get silently overwritten by the schema defaults
// (which default both sub-toggles to `true`) on the next save.
// The unit test in `v3-to-v4.test.ts` already pins the
// migration body, but without an end-to-end fixture the real
// CLI load → migrate → write path could regress without
// this suite noticing.
it('should expand legacy boolean general.gitCoAuthor: false through V3 → V4', async () => {
rig.setup('v3-gitcoauthor-boolean');

overwriteSettingsFile(rig, v3GitCoAuthorBooleanSettings);

try {
await rig.runCommand(['mcp', 'list']);
} catch {
// Expected to potentially fail
}

const finalSettings = readSettingsFile(rig);

expect(finalSettings['$version']).toBe(4);
expect(
(finalSettings['general'] as Record<string, unknown>)?.['gitCoAuthor'],
).toEqual({ commit: false, pr: false });
// Sibling general.* keys must survive the migration unchanged.
expect(
(finalSettings['general'] as Record<string, unknown>)?.[
'disableAutoUpdate'
],
).toBe(true);
// And so must unrelated top-level sections.
expect(finalSettings['custom']).toEqual({
note: 'preserve me through v3->v4',
});
});
});

describe('Future version settings handling', () => {
Expand Down
10 changes: 10 additions & 0 deletions integration-tests/fixtures/settings-migration/workspaces.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,5 +184,15 @@
"experimentalFlag": {
"enabled": true
}
},
"v3GitCoAuthorBooleanSettings": {
"$version": 3,
"general": {
"gitCoAuthor": false,
"disableAutoUpdate": true
},
"custom": {
"note": "preserve me through v3->v4"
}
}
}
Loading
Loading