Skip to content

Proj/git semantics#327

Merged
bao-byterover merged 19 commits intomainfrom
proj/git-semantics
Apr 6, 2026
Merged

Proj/git semantics#327
bao-byterover merged 19 commits intomainfrom
proj/git-semantics

Conversation

@bao-byterover
Copy link
Copy Markdown
Collaborator

@bao-byterover bao-byterover commented Apr 5, 2026

  • Server / Daemon
  • Shared (constants, types, transport events)
  • CLI Commands (oclif)
  • Hub / Connectors
  • Cloud Sync
  • CI/CD / Infra

Linked issues

  • Closes ENG-1905
  • Closes ENG-1929
  • Closes ENG-1948
  • Closes ENG-1951

Root cause (bug fixes only, otherwise write N/A)

  • Root cause (ENG-1948): isDescendent uses a strict proper-ancestor test, so a commit is not considered a descendant of itself. Deleting a branch pointing at the same commit as HEAD failed the merge check.
  • Root cause (ENG-1951): oclif strict mode rejected extra positional args after -m, so commit -m hello world only captured hello. TUI error messages passed server-side brv vc hints through without rewriting for the slash-command context.
  • Why this was not caught earlier: No test covered same-OID ancestry. Multi-word unquoted messages are an edge case of user convenience that wasn't in the original spec.

Test plan

  • Coverage added:
    • Unit test
    • Integration test
    • Manual verification only
  • Test file(s):
    • test/unit/core/domain/entities/space.test.ts (new)
    • test/unit/core/domain/entities/team.test.ts
    • test/unit/infra/git/git-remote-url.test.ts
    • test/unit/infra/space/http-space-service.test.ts
    • test/unit/infra/transport/handlers/vc-handler.test.ts
    • test/unit/tui/utils/error-messages.test.ts
    • test/integration/infra/git/isomorphic-git-service.test.ts
  • Key scenario(s) covered:
    • Space/Team slug deserialization, fallback to name when slug is absent
    • Clone with slug-based URLs, case-insensitive slug matching, slug ≠ name resolution
    • buildCogitRemoteUrl produces /{team}/{space}.git (no /git/ prefix)
    • Removed parseGitPathUrl/stripCredentialsFromUrl/isUuid and their tests
    • brv vc/vc rewriting in TUI error messages

User-visible changes

  • Git remote URLs change from https://host/git/{uuid}/{uuid}.git to https://host/{teamSlug}/{spaceSlug}.git
  • brv init now prints "The init command is not available. Use: brv vc init" and is hidden from help
  • brv vc commit -m hello world (unquoted) now correctly captures "hello world" as the full message
  • TUI error messages show /vc instead of brv vc
  • Clone flow shows errors inline and re-prompts for URL instead of exiting
  • Dev environment default URL changed from dev-beta-app.byterover.dev to dev-beta.byterover.dev
  • BRV_GIT_API_BASE_URL env var removed

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording

Checklist

  • Tests added or updated and passing (npm test)
  • Lint passes (npm run lint)
  • Type check passes (npm run typecheck)
  • Build succeeds (npm run build)
  • Commits follow Conventional Commits format
  • Documentation updated (if applicable)
  • No breaking changes (or clearly documented above)
  • Branch is up to date with main

Risks and mitigations

  • Risk: Existing cloned repos have UUID-based remote URLs stored in .git/config which will no longer match the new URL format.
    • Mitigation: remote set-url flow or re-clone. Existing .brv/config.json slug fields fall back to name for backward compatibility.
  • Risk: Backend API may not yet return slug fields for all teams/spaces.
    • Mitigation: All slug fields fall back to name when absent (json.slug ?? json.name), so the system degrades gracefully.
  • Risk: Removing BRV_GIT_API_BASE_URL is a breaking env config change.
    • Mitigation: All URL construction now uses BRV_GIT_REMOTE_BASE_URL only. .env.example updated. Deployments must drop the old var.

bao-byterover and others added 17 commits April 5, 2026 15:58
…heck

When deleting a branch that points to the same commit as HEAD,
`isAncestor` incorrectly returned false because `isDescendent`
uses a strict proper-ancestor test. A commit is trivially an
ancestor of itself, so short-circuit to true when OIDs match.
fix: [ENG-1948] treat identical OIDs as ancestor in branch -d merge check
- Intercept --help/-h/help in TUI slash command processor to show
  TUI-style help instead of falling through to oclif Parser
- Fix /vc commit -m stripping multi-word messages to first word by
  joining remaining argv with the parsed flag value
- Apply same fix to oclif vc commit command (strict: false + argv join)
- Rewrite server "brv vc" hints to "/vc" in TUI formatTransportError
fix:[ENG-1951] TUI help flow and vc commit message parsing
@bao-byterover bao-byterover marked this pull request as ready for review April 6, 2026 03:28
@bao-byterover bao-byterover requested a review from leehpham April 6, 2026 03:28
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 6, 2026

Claude finished @bao-byterover's task in 4m 49s —— View job


PR Review — Score: 4/5

  • Gather context and understand the PR
  • Read changed source files
  • Read changed test files
  • Post inline review comments
  • Post summary review

Overall Assessment

This is a well-scoped, well-tested PR. The slug-based URL migration is cleanly designed with robust fallbacks, the isAncestor same-commit fix is correct (the commitOid === ancestorOid short-circuit at isomorphic-git-service.ts:427 is the right approach), and the strict: false + argv.join pattern for multi-word commit messages is a pragmatic solution. Test coverage for the new entities and URL helpers is thorough.


Issues Found

Medium — Correctness

  • resolveCloneInput uses data.teamName for slug-based URL (vc-handler.ts:1189): The internal clone path calls buildCogitRemoteUrl(…, data.teamName, data.spaceName) — but the new URL format uses slugs. If a team display name diverges from its slug the constructed URL will be wrong. In practice TUI always passes the slug value in this field, but the naming mismatch is a correctness trap. See inline comment →

  • BrvConfig.withSpace() doesn't persist slugs (brv-config.ts:228-243): withSpace(), fromSpace(), and partialFromSpace() all store space.name / space.teamName but drop space.slug / space.teamSlug. After clone or remote-add, the stored config loses slug info. The slug fallback (slug ?? name) in Space means things degrade gracefully today, but as soon as a real-world name ≠ slug (e.g. "Acme Corp""acme-corp"), any code path that re-constructs a Space from BrvConfig and uses its slug for URL building will produce a broken URL. BrvConfig should grow spaceSlug? / teamSlug? fields.

Low — Security

  • validateRemoteUrlDomain does not check protocol (vc-handler.ts:1357): Host is validated but http:// passes through. Git auth tokens would be sent unencrypted. Simple parsed.protocol !== 'https:' guard should be added. See inline comment →

Low — Standards / Nitpicks

  • as string[] cast in vc/commit.ts:22: Violates the no-as-assertions coding standard. The parsed argv from oclif is already typed as string[]; the cast is unnecessary. See inline comment →

  • Dead code in init.ts suppressed with a file-level /* eslint-disable no-unreachable */ — silences the linter for the whole file. Either delete the old body (preferred, since the command is hidden/retired) or limit the disable to the specific lines. See inline comment →

  • parseUserFacingUrl returns {teamName, spaceName} but these are slugs (cogit-url.ts:14): The returned property names are misleading — they hold URL path segments (slugs), not display names. Renaming to {teamSlug, spaceSlug} prevents future misuse. See inline comment →

  • JSDoc in cogit-url.ts:5 still says {teamName}/{spaceName} instead of {teamSlug}/{spaceSlug}. See inline comment →

  • useEffect missing cloneMutation in deps (vc-clone-flow.tsx:88): Stale-closure risk, low probability due to mutatedRef guard but worth fixing. See inline comment →

  • Broad brv rewrite regex (error-messages.ts:85): .replaceAll(/\bbrv\s+/g, '/') is blanket — could accidentally mangle user-supplied strings embedded in error messages. See inline comment →


What's Well Done

  • isAncestor same-OID fix is correct and the integration test covers it.
  • Slug fallback pattern (slug ?? name) across Space, Team, and HttpSpaceService is consistent and backward-compatible.
  • validateRemoteUrlDomain properly prevents SSRF-style remote substitution by allowlisting the expected host.
  • Credential stripping from stored/displayed URLs (maskCredentialsInUrl) is correct.
  • BrvConfig.partialFromSpace / withSpace path is cleanly used in both clone and remote-add handlers.
  • TUI clone flow error-retry behaviour (re-prompt instead of exit) is a good UX improvement.
  • Test file coverage for new Space entity, slug deserialization, and formatTransportError rewriting is solid.

teamId: data.teamId,
teamName: data.teamName,
url: buildCogitRemoteUrl(this.gitApiBaseUrl, data.teamId, data.spaceId),
url: buildCogitRemoteUrl(this.gitRemoteBaseUrl, data.teamName, data.spaceName),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (correctness): buildCogitRemoteUrl is called with data.teamName / data.spaceName, but the new URL format requires slugs, not display names. If a team has a display name like "Acme Corp" (with a space or special chars), the constructed URL will be invalid. This path is only reached when the TUI passes teamName/spaceName via the clone request, so in practice these values come from space.teamSlug / space.slug — but the parameter naming creates a semantic mismatch that could regress.

suggestion: Either rename the IVcCloneRequest fields to teamSlug/spaceSlug, or pass data.teamSlug/data.spaceSlug here and fall back to data.teamName/data.spaceName only when slugs are absent. Fix this →

public async run(): Promise<void> {
this.log('The init command is not available. Use: brv vc init')
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (code quality): Dead code is suppressed with /* eslint-disable no-unreachable */ at the file level, silencing the linter for the entire file. If the intent is to keep the old implementation for future reference, it would be safer to move it to a comment block or a separate archived file. The current approach is fragile — any genuinely unreachable code added later to this file won't be caught.

If the brv init command is being permanently retired (it's now hidden and redirects users), just delete the old body entirely.

const {message} = flags
// Support unquoted multi-word messages: brv vc commit -m hello world
const extra = (argv as string[]).join(' ')
const message = flags.message
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (standards): argv as string[] is a type assertion — the coding standards prohibit as Type casts in favour of type guards or proper typing. In oclif with strict: false, this.parse() already returns argv: string[] in the result type; you likely just need to destructure argv without the cast.

Suggested change
const message = flags.message
const extra = argv.join(' ')

* Format: {baseUrl}/{teamName}/{spaceName}.git
*/
export function buildCogitRemoteUrl(baseUrl: string, teamId: string, spaceId: string): string {
export function buildCogitRemoteUrl(baseUrl: string, teamName: string, spaceName: string): string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (docs): JSDoc says {teamName}/{spaceName} but after this PR the segments are slugs, not display names. Update to {teamSlug}/{spaceSlug} to keep the contract clear.

Suggested change
export function buildCogitRemoteUrl(baseUrl: string, teamName: string, spaceName: string): string {
* Format: {baseUrl}/{teamSlug}/{spaceSlug}.git

* Parse a .git URL to extract team and space names.
* Expected path: /{teamName}/{spaceName}.git
*/
export function parseUserFacingUrl(url: string): null | {spaceName: string; teamName: string} {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (docs): Same issue — the property names in the return type {teamName, spaceName} are misleading because these values are parsed from URL path segments, which are slugs. The caller (vc-handler.ts) passes them straight to resolveTeamSpaceNames(parsed.teamName, parsed.spaceName) which then matches against t.slug. Consider renaming to {teamSlug, spaceSlug} to make the intent obvious and prevent future misuse.

@@ -82,6 +88,8 @@ export function VcCloneFlow({onCancel, onComplete, url}: VcCloneFlowProps): Reac
}, [onComplete, step, cloneUrl, url])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (correctness): cloneMutation is used inside this effect but is absent from the dependency array. React's exhaustive-deps rule would flag this. If cloneMutation reference changes (it's from useExecuteVcClone) between renders while step === 'cloning' and mutatedRef.current is false, the stale mutation object would be invoked. In practice this is unlikely because mutatedRef guards re-entry, but adding cloneMutation to the dep array (or using a ref for it) is the safer pattern.

@@ -1366,7 +1356,7 @@ export class VcHandler {
private validateRemoteUrlDomain(url: string): void {
try {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (security): The domain validation only checks host — protocol is not validated. A URL using http://byterover.dev/team/space.git would pass this check and git credentials would be sent over plain HTTP. Consider adding a protocol check:

if (parsed.protocol !== 'https:') {
  throw new VcError(
    `Invalid remote URL. Use: ${this.gitRemoteBaseUrl}/<team>/<space>.git`,
    VcErrorCode.INVALID_REMOTE_URL,
  )
}


return error.message.replace(/ for event '[^']+'(?: after \d+ms)?$/, '')
// Strip transport suffix and rewrite CLI-style hints (brv vc ...) to TUI slash commands (/vc ...)
return error.message.replace(/ for event '[^']+'(?: after \d+ms)?$/, '').replaceAll(/\bbrv\s+/g, '/')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (correctness): The regex .replaceAll(/\bbrv\s+/g, '/') rewrites all brv occurrences — including any that appear in user-provided data embedded in an error message (e.g. a remote URL path, a commit message, or a file path that contains the string brv ). This is low-risk in practice but worth noting for future error message changes where user content could be interpolated.

A tighter approach would be to only match the specific prefixes that the server layer is known to emit (brv vc /vc , brv login/login, etc.) rather than a blanket word-boundary replace.

Copy link
Copy Markdown
Collaborator

@cuongdo-byterover cuongdo-byterover left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's bring this to next release

@bao-byterover bao-byterover merged commit fd59a85 into main Apr 6, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants