qwen-code/packages/web-shell/client/components/dialogs/GitLogDialog.module.css
Shaojin Wen ff350a16a7
feat(web-shell): add git commit history browser (#7204)
* feat(web-shell): add git commit history browser

Add a read-only Git Log dialog to the Web Shell, accessible via /log
command or the History tab in the Changes dialog.

Full-stack implementation across core, daemon, SDK, and web-shell:
- core: fetchGitLog (paginated commit list) and fetchGitCommitDetail
  (message body + per-file numstat) with 12 integration tests
- daemon: GET /workspace/git/log and /workspace/git/log/commit routes
  with bound + qualified dual registration
- SDK: DaemonGitLog/DaemonGitCommitDetail types and client methods
- web-shell: GitLogDialog with commit list, expandable details,
  SHA copy icon, Load more pagination, and Changes/History tab
  switching in both dialogs

* fix(web-shell): address review feedback on git log browser

- Critical: use first-parent diff for merge commits (diff-tree without
  -c or explicit parent outputs nothing for merges)
- Remove dead embedded prop from GitDiffDialog and GitLogDialog
- Replace span role=button with aria-hidden for copy icon (a11y)
- Add loadMore error feedback instead of silent catch
- Refresh relative timestamps every 60s (useState + interval)
- Remove dead branch param from subtitle i18n call

* fix(web-shell): address R2 review feedback on git log browser

- Use bounded split in parseLogFields (first 7 separators) to prevent
  subject containing literal \x1f from shifting the parents field
- Add SHA hex format validation at daemon route layer (400 for invalid)
- Add rendering branch for detail.available === false (error message
  instead of empty content)

* fix(web-shell): count renamed files in commit detail + test git-log route & dialog

Follows the R2 review-feedback commit (which fixed the bounded parse, the
non-hex SHA 400, and the unavailable-detail render). Remaining items:

- Commit detail counts renamed files. diff-tree is plumbing and does not
  honour diff.renames, so a `git mv` split into a delete + add pair (or an
  empty-path entry) instead of one file — understating filesCount /
  linesAdded / linesRemoved. Run diff-tree with -M and give the inline
  numstat parser the same pending-rename state machine as parseGitNumstat,
  so a rename is one file keyed by its new path. Covered by a real-repo
  rename test, plus a merge-commit test that locks the first-parent diff.
- Tests for the two previously-untested modules: the workspace-git-log
  route (list shape, pagination clamping, sha-required + non-hex 400, trust
  gating) and GitLogDialog (all five list state paths, load-more offset +
  error, detail expand + both failure branches incl. available:false, and
  the relative-time render).

* fix(web-shell): address R3 review feedback on git log browser

- Fix timeAgo '0y ago' for commits ~360-364 days old (Math.max(1, ...))
- Add .catch() to clipboard writeText to prevent unhandled rejection
- Reset loadMoreError on initial re-fetch (daemon reconnect)
- Preserve prev.available in loadMore merge instead of overwriting
- Add ARIA tab semantics (role=tablist/tab, aria-selected) to both
  Changes and History tab bars

* fix(web-shell): address R4 review feedback on git log browser

- Fix vacuous limit-clamp test: seed 3 commits, verify limit=2 returns
  2 + hasMore, limit=0 clamps to 1
- Add CSS var fallbacks for --subtle-bg and --success-bg (dialog
  portals outside App.module.css scope)
- Extract GIT_DIALOG_SWITCH_DELAY_MS constant with doc comment
- Make copy-SHA control keyboard accessible (tabIndex, onKeyDown,
  aria-label instead of aria-hidden)

* fix(web-shell): escape apostrophe in worktree welcome string

* fix(web-shell): address git history review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(web-shell): mock git diff content in app tests

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): harden git log metadata parsing

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-20 15:02:48 +00:00

211 lines
3.3 KiB
CSS

.content {
display: flex;
flex-direction: column;
min-height: 50vh;
}
.placeholder {
padding: 24px 12px;
text-align: center;
color: var(--muted-foreground);
}
.commitList {
display: flex;
flex-direction: column;
}
.commitRow {
border-bottom: 1px solid var(--border);
}
.commitHeader {
display: flex;
align-items: stretch;
width: 100%;
}
.commitHeader:hover {
background: var(--subtle-bg, rgba(128, 128, 128, 0.06));
}
.commitToggle {
display: flex;
align-items: baseline;
gap: 8px;
flex: 1 1 auto;
min-width: 0;
padding: 8px 4px 8px 10px;
background: transparent;
border: 0;
cursor: pointer;
font: inherit;
text-align: left;
color: inherit;
}
.commitSha {
flex-shrink: 0;
font-family: var(--font-mono, monospace);
font-size: 12px;
color: var(--muted-foreground);
}
.copyBtn {
flex-shrink: 0;
display: inline-flex;
align-items: center;
align-self: stretch;
padding: 0 10px 0 6px;
border: 0;
border-radius: 3px;
background: transparent;
color: var(--muted-foreground);
cursor: pointer;
opacity: 0;
transition: opacity 0.15s;
}
.commitHeader:hover .copyBtn,
.copyBtn:focus-visible {
opacity: 1;
}
.copyBtn:hover {
background: var(--muted);
}
.commitSubject {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.commitMeta {
flex-shrink: 0;
font-size: 11px;
color: var(--muted-foreground);
white-space: nowrap;
}
.commitRefs {
display: inline-flex;
gap: 4px;
flex-shrink: 0;
}
.refTag {
padding: 0 5px;
border-radius: 4px;
background: var(--muted);
color: var(--muted-foreground);
font-size: 10px;
line-height: 16px;
white-space: nowrap;
}
.refHead {
background: var(--success-bg, rgba(76, 175, 80, 0.12));
color: var(--success-color);
}
.mergeIcon {
flex-shrink: 0;
color: var(--muted-foreground);
font-size: 12px;
}
.commitDetail {
padding: 8px 10px 12px;
border-top: 1px solid var(--border);
background: var(--subtle-bg, rgba(128, 128, 128, 0.06));
}
.commitBody {
margin: 0 0 8px;
font-size: 12px;
white-space: pre-wrap;
word-break: break-word;
color: var(--muted-foreground);
}
.fileStats {
display: flex;
flex-direction: column;
gap: 2px;
}
.fileStatHeader {
font-size: 11px;
color: var(--muted-foreground);
margin-bottom: 4px;
}
.fileStatRow {
display: flex;
align-items: baseline;
gap: 6px;
font-size: 12px;
padding: 1px 0;
}
.statNums {
display: inline-flex;
gap: 6px;
flex-shrink: 0;
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.statAdd {
color: var(--success-color);
}
.statDel {
color: var(--error-color);
}
.fileStatPath {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fileBinary {
color: var(--muted-foreground);
}
.hiddenNote {
padding: 4px 0;
color: var(--muted-foreground);
font-size: 11px;
}
.detailError {
padding: 8px 0;
color: var(--error-color);
font-size: 12px;
}
.loadMore {
display: block;
width: 100%;
padding: 10px;
margin-top: 4px;
background: transparent;
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
font: inherit;
font-size: 12px;
color: var(--muted-foreground);
text-align: center;
}
.loadMore:hover {
background: var(--subtle-bg, rgba(128, 128, 128, 0.06));
}