Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Properly map all hotkeys in UI based on the platform [#784](https://github.com/sourcebot-dev/sourcebot/pull/784)
- Allow parenthesis in query and filter terms [#788](https://github.com/sourcebot-dev/sourcebot/pull/788)
- Fixed issue where Sourcebot would not index the new default branch when changed. [#789](https://github.com/sourcebot-dev/sourcebot/pull/789)

### Changed
- Changed the UI to display the default branch name instead of HEAD where applicable. [#789](https://github.com/sourcebot-dev/sourcebot/pull/789)

## [4.10.16] - 2026-01-22

Expand Down
73 changes: 72 additions & 1 deletion packages/backend/src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ export const fetchRepository = async (
"--prune",
"--progress"
]);

// Update HEAD to match the remote's default branch. This handles the case where the remote's
// default branch changes.
const remoteDefaultBranch = await getRemoteDefaultBranch({
path,
cloneUrl,
});

if (remoteDefaultBranch) {
await git.raw(['symbolic-ref', 'HEAD', `refs/heads/${remoteDefaultBranch}`]);
}
} catch (error: unknown) {
const baseLog = `Failed to fetch repository: ${path}`;
if (env.SOURCEBOT_LOG_LEVEL !== "debug") {
Expand Down Expand Up @@ -297,4 +308,64 @@ export const getCommitHashForRefName = async ({
logger.debug(error);
return undefined;
}
}
}

/**
* Gets the default branch name from the remote repository by querying what
* the remote's HEAD symbolic ref points to.
*
* This is useful for detecting when a remote repository's default branch has
* changed (e.g., from "master" to "main").
*
* @returns The branch name (e.g., "main", "master") or undefined if it cannot be determined
*/
export const getRemoteDefaultBranch = async ({
path,
cloneUrl,
}: {
path: string,
cloneUrl: string,
}) => {
const git = createGitClientForPath(path);
try {
const remoteHead = await git.raw(['ls-remote', '--symref', cloneUrl, 'HEAD']);
const match = remoteHead.match(/^ref: refs\/heads\/(\S+)\s+HEAD/m);
if (match) {
return match[1];
}
} catch (error: unknown) {
// Avoid printing error here since cloneUrl may contain credentials.
console.error(`Failed to get remote default branch for repository: ${path}`);
return undefined;
}
}

/**
* Gets the branch name that the local HEAD symbolic ref points to.
*
* In a git repository, HEAD is typically a symbolic reference that points to
* a branch (e.g., refs/heads/main). This function resolves that symbolic ref
* and returns just the branch name.
*
* @returns The branch name (e.g., "main", "master") or undefined if HEAD is not a symbolic ref
*/
export const getLocalDefaultBranch = async ({
path,
}: {
path: string,
}) => {
const git = createGitClientForPath(path);

try {
const ref = await git.raw(['symbolic-ref', 'HEAD']);
// Returns something like "refs/heads/main\n", so trim and remove prefix
const trimmed = ref.trim();
const match = trimmed.match(/^refs\/heads\/(.+)$/);
if (match) {
return match[1];
}
} catch (error: unknown) {
console.error(`Failed to get local default branch for repository: ${path}`);
return undefined;
}
}
11 changes: 6 additions & 5 deletions packages/backend/src/repoIndexManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Job, Queue, ReservedJob, Worker } from "groupmq";
import { Redis } from 'ioredis';
import micromatch from 'micromatch';
import { GROUPMQ_WORKER_STOP_GRACEFUL_TIMEOUT_MS, INDEX_CACHE_DIR } from './constants.js';
import { cloneRepository, fetchRepository, getBranches, getCommitHashForRefName, getTags, isPathAValidGitRepoRoot, unsetGitConfig, upsertGitConfig } from './git.js';
import { cloneRepository, fetchRepository, getBranches, getCommitHashForRefName, getLocalDefaultBranch, getTags, isPathAValidGitRepoRoot, unsetGitConfig, upsertGitConfig } from './git.js';
import { captureEvent } from './posthog.js';
import { PromClient } from './promClient.js';
import { RepoWithConnections, Settings } from "./types.js";
Expand Down Expand Up @@ -364,7 +364,6 @@ export class RepoIndexManager {

process.stdout.write('\n');
logger.info(`Fetched ${repo.name} (id: ${repo.id}) in ${fetchDuration_s}s`);

} else if (!isReadOnly) {
logger.info(`Cloning ${repo.name} (id: ${repo.id})...`);

Expand Down Expand Up @@ -394,9 +393,11 @@ export class RepoIndexManager {
});
}

let revisions = [
'HEAD'
];
const defaultBranch = await getLocalDefaultBranch({
path: repoPath,
});

let revisions = defaultBranch ? [defaultBranch] : ['HEAD'];

if (metadata.branches) {
const branchGlobs = metadata.branches
Expand Down
4 changes: 3 additions & 1 deletion packages/web/src/app/[domain]/components/pathHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface FileHeaderProps {
displayName?: string;
webUrl?: string;
},
isBranchDisplayNameVisible?: boolean;
branchDisplayName?: string;
branchDisplayTitle?: string;
isCodeHostIconVisible?: boolean;
Expand All @@ -53,6 +54,7 @@ export const PathHeader = ({
path,
pathHighlightRange,
branchDisplayName,
isBranchDisplayNameVisible = !!branchDisplayName,
branchDisplayTitle,
pathType = 'blob',
isCodeHostIconVisible = true,
Expand Down Expand Up @@ -224,7 +226,7 @@ export const PathHeader = ({
>
{info?.displayName}
</Link>
{branchDisplayName && (
{(isBranchDisplayNameVisible && branchDisplayName) && (
<p
className="text-xs font-semibold text-gray-500 dark:text-gray-400 mt-[3px] flex items-center gap-0.5"
title={branchDisplayTitle}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,8 @@ export const SearchResultsPage = ({

// Look for any files that are not on the default branch.
const isBranchFilteringEnabled = useMemo(() => {
return files.some((file) => {
return file.branches?.some((branch) => branch !== 'HEAD') ?? false;
});
}, [files]);
return searchQuery.includes('rev:');
}, [searchQuery]);

useEffect(() => {
if (isStreaming || !stats) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ export const FileMatchContainer = ({
}, [file.branches]);

const branchDisplayName = useMemo(() => {
if (!isBranchFilteringEnabled || branches.length === 0) {
if (branches.length === 0) {
return undefined;
}

return `${branches[0]}${branches.length > 1 ? ` +${branches.length - 1}` : ''}`;
}, [branches, isBranchFilteringEnabled]);
}, [branches]);

const repo = useMemo(() => {
return repoInfo[file.repositoryId];
Expand All @@ -99,6 +99,7 @@ export const FileMatchContainer = ({
}}
path={file.fileName.text}
pathHighlightRange={fileNameRange}
isBranchDisplayNameVisible={isBranchFilteringEnabled}
branchDisplayName={branchDisplayName}
branchDisplayTitle={branches.join(", ")}
/>
Expand Down