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
20 changes: 13 additions & 7 deletions app/api/github-history/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,18 @@ export async function GET(request: Request) {
params.append("path", path);
}

// Construct historyUrl that will always be returned
const historyUrl = path
? `https://github.com/${owner}/${repo}/commits/${GITHUB_ACTIVE_BRANCH}/${path}`
: `https://github.com/${owner}/${repo}/commits/${GITHUB_ACTIVE_BRANCH}`;

try {
if (path) {
// Fetch complete file history including migrated paths and renames
const { commits: allCommitsWithHistory } = await findCompleteFileHistory(owner, repo, path, GITHUB_ACTIVE_BRANCH, headers);

if (!allCommitsWithHistory.length) {
return NextResponse.json({ error: "No commits found", branch: GITHUB_ACTIVE_BRANCH }, { status: 400 });
return NextResponse.json({ error: "No commits found", branch: GITHUB_ACTIVE_BRANCH, historyUrl }, { status: 400 });
}

// Find the latest non-excluded commit (newest first)
Expand All @@ -50,38 +55,39 @@ export async function GET(request: Request) {
const firstCommit = allCommitsWithHistory[allCommitsWithHistory.length - 1];

if (!latestCommit) {
return NextResponse.json({ error: "No valid commits found" }, { status: 400 });
return NextResponse.json({ error: "No valid commits found", historyUrl }, { status: 400 });
}

return NextResponse.json({
latestCommit,
firstCommit,
historyUrl: `https://github.com/${owner}/${repo}/commits/${GITHUB_ACTIVE_BRANCH}/${path}`,
historyUrl,
otherCoAuthorName: getAlternateAuthorName(latestCommit),
});
} else {
// No path specified, just get latest commit for the branch
const latestCommits = await fetchGitHub<GitHubCommit[]>(`${baseUrl}?${params}`, headers);

if (!latestCommits.length) {
return NextResponse.json({ error: "No commits found" }, { status: 400 });
return NextResponse.json({ error: "No commits found", historyUrl }, { status: 400 });
}

const latestCommit = findLatestNonExcludedCommit(latestCommits);

if (!latestCommit) {
return NextResponse.json({ error: "No valid commits found" }, { status: 400 });
return NextResponse.json({ error: "No valid commits found", historyUrl }, { status: 400 });
}

return NextResponse.json({
latestCommit,
firstCommit: null,
historyUrl: `https://github.com/${owner}/${repo}/commits/${GITHUB_ACTIVE_BRANCH}`,
historyUrl,
otherCoAuthorName: getAlternateAuthorName(latestCommit),
});
}
} catch (error) {
console.error("Error fetching GitHub metadata:", error);
return NextResponse.json({ error: "Failed to fetch GitHub metadata" }, { status: 500 });
const errorMessage = error instanceof Error ? error.message : "Failed to fetch GitHub metadata";
return NextResponse.json({ error: errorMessage, historyUrl }, { status: 500 });
}
}
30 changes: 25 additions & 5 deletions app/api/github-history/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,13 @@ export async function fetchAllCommitsForPath(
if (commits.length < perPage) break;

page++;
} catch {
} catch (error) {
// If this is the first page and we have an error, it's a real API issue - throw it
if (page === 1) {
throw error;
}
// For subsequent pages, if there's an error, stop pagination
// This could be a legitimate end of results or a transient error
break;
}
}
Expand Down Expand Up @@ -162,12 +168,24 @@ export async function findCompleteFileHistory(
visitedPaths.add(path);

// Fetch all commits for the current path
const currentCommits = await fetchAllCommitsForPath(owner, repo, path, branch, headers);
let currentCommits: GitHubCommit[];
try {
currentCommits = await fetchAllCommitsForPath(owner, repo, path, branch, headers);
} catch (error) {
// Re-throw GitHub API errors with more context
throw new Error(`Failed to fetch commits for path "${path}": ${error instanceof Error ? error.message : String(error)}`);
}

// Check if this is a migrated path (public/uploads/rules/... -> rules/...)
const migratedOldPath = constructOldPath(path);
if (migratedOldPath) {
const oldPathCommits = await fetchAllCommitsForPath(owner, repo, migratedOldPath, branch, headers);
let oldPathCommits: GitHubCommit[];
try {
oldPathCommits = await fetchAllCommitsForPath(owner, repo, migratedOldPath, branch, headers);
} catch (error) {
// Re-throw GitHub API errors with more context
throw new Error(`Failed to fetch commits for migrated path "${migratedOldPath}": ${error instanceof Error ? error.message : String(error)}`);
}
const mergedCommits = mergeCommits(currentCommits, oldPathCommits);
return {
commits: mergedCommits,
Expand Down Expand Up @@ -200,8 +218,10 @@ export async function findCompleteFileHistory(
originalPath: oldPathHistory.originalPath,
};
}
} catch {
// If we can't fetch commit details, continue to next commit
} catch (error) {
// If we can't fetch commit details, log it but continue to next commit
// This is less critical than the initial commit fetch, so we're more lenient
console.warn(`Failed to fetch commit details for ${commit.sha}: ${error instanceof Error ? error.message : String(error)}`);
continue;
}
}
Expand Down
129 changes: 80 additions & 49 deletions components/last-updated-by/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,18 @@ export default function GitHubMetadata({ owner = "tinacms", repo = "tina.io", pa
}

const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_PATH}/api/github-history?${params.toString()}`);
const result: GitHubMetadataResponse = await response.json();

if (!response.ok) {
throw new Error(`API error: ${response.status}`);
// Even if there's an error, we might have historyUrl
if (result.historyUrl) {
setData(result);
} else {
throw new Error(result.error || `API error: ${response.status}`);
}
} else {
setData(result);
}

const result: GitHubMetadataResponse = await response.json();
setData(result);
} catch (err) {
console.error("Error fetching GitHub metadata:", err);
setError(err instanceof Error ? err.message : "Failed to fetch commit data");
Expand All @@ -50,54 +55,80 @@ export default function GitHubMetadata({ owner = "tinacms", repo = "tina.io", pa
return <div className={`text-slate-500 text-sm mb-2 ${className}`}>Loading last updated info...</div>;
}

if (error) {
return <div className={`text-slate-400 text-sm mb-2 ${className}`}>Unable to load last updated info</div>;
}
// If we have data with historyUrl, show it even if there's an error
if (data?.historyUrl) {
const { latestCommit, firstCommit, historyUrl, otherCoAuthorName } = data;

if (!data) {
return null;
}
// If we have commit data, show the full info
if (latestCommit) {
const lastUpdatedDate = latestCommit.commit.author.date;
const lastUpdateInRelativeTime = getRelativeTime(lastUpdatedDate);
const lastUpdateInAbsoluteTime = formatDate(lastUpdatedDate, "dd MMM yyyy");
const createdDate = firstCommit?.commit.author.date;
const createdTime = createdDate ? formatDate(createdDate, "d MMM yyyy") : null;
const displayAuthorName = otherCoAuthorName ?? latestCommit.commit.author.name;
const shouldShowLink = !otherCoAuthorName && Boolean(latestCommit.author?.login);

const tooltipContent = createdTime ? `Created ${createdTime}\nLast updated ${lastUpdateInAbsoluteTime}` : `Last updated ${lastUpdateInAbsoluteTime}`;

const { latestCommit, firstCommit, historyUrl, otherCoAuthorName } = data;
const lastUpdatedDate = latestCommit.commit.author.date;
const lastUpdateInRelativeTime = getRelativeTime(lastUpdatedDate);
const lastUpdateInAbsoluteTime = formatDate(lastUpdatedDate, "dd MMM yyyy");
const createdDate = firstCommit?.commit.author.date;
const createdTime = createdDate ? formatDate(createdDate, "d MMM yyyy") : null;
const displayAuthorName = otherCoAuthorName ?? latestCommit.commit.author.name;
const shouldShowLink = !otherCoAuthorName && Boolean(latestCommit.author?.login);

const tooltipContent = createdTime ? `Created ${createdTime}\nLast updated ${lastUpdateInAbsoluteTime}` : `Last updated ${lastUpdateInAbsoluteTime}`;

return (
<div className={`text-slate-500 text-sm ${className}`}>
<div className="flex md:flex-row flex-col md:items-center gap-2">
<span>
Last updated by{" "}
<span className="font-bold text-black">
{shouldShowLink ? (
<a href={`https://github.com/${latestCommit.author?.login}`} target="_blank" rel="noopener noreferrer">
{displayAuthorName}
return (
<div className={`text-slate-500 text-sm ${className}`}>
<div className="flex md:flex-row flex-col md:items-center gap-2">
<span>
Last updated by{" "}
<span className="font-bold text-black">
{shouldShowLink ? (
<a href={`https://github.com/${latestCommit.author?.login}`} target="_blank" rel="noopener noreferrer">
{displayAuthorName}
</a>
) : (
displayAuthorName
)}
</span>
{` ${lastUpdateInRelativeTime}.`}
</span>
<div className="relative group text-black">
<a
href={historyUrl}
target="_blank"
title={tooltipContent}
rel="noopener noreferrer"
className="underline flex flex-row items-center gap-1.5 mb-2 md:mb-0"
>
See history
<FaHistory className="w-3 h-3" />
</a>
) : (
displayAuthorName
)}
</span>
{` ${lastUpdateInRelativeTime}.`}
</span>
<div className="relative group text-black">
<a
href={historyUrl}
target="_blank"
title={tooltipContent}
rel="noopener noreferrer"
className="underline flex flex-row items-center gap-1.5 mb-2 md:mb-0"
>
See history
<FaHistory className="w-3 h-3" />
</a>
</div>
</div>
</div>
);
}

// If we only have historyUrl but no commit data (error case), just show the history link
return (
<div className={`text-slate-500 text-sm ${className}`}>
<div className="flex md:flex-row flex-col md:items-center gap-2">
<div className="relative group text-black">
<a
href={historyUrl}
target="_blank"
title="View commit history on GitHub"
rel="noopener noreferrer"
className="underline flex flex-row items-center gap-1.5 mb-2 md:mb-0"
>
See history
<FaHistory className="w-3 h-3" />
</a>
</div>
</div>
</div>
</div>
);
);
}

// No data at all
if (error) {
return <div className={`text-slate-400 text-sm mb-2 ${className}`}>Unable to load last updated info</div>;
}

return null;
}
5 changes: 3 additions & 2 deletions components/last-updated-by/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ export interface GitHubMetadataProps {
}

export interface GitHubMetadataResponse {
latestCommit: GitHubCommit;
firstCommit: GitHubCommit | null;
latestCommit?: GitHubCommit;
firstCommit?: GitHubCommit | null;
historyUrl: string;
otherCoAuthorName?: string | null;
error?: string;
}
4 changes: 2 additions & 2 deletions components/server/MegaMenuWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export function MegaMenuWrapper(props) {

let adjustedHref = href;
if (basePath && href && typeof href === "string") {
// Only adjust relative URLs (not external URLs)
if (!href.startsWith("http://") && !href.startsWith("https://") && href.startsWith("/")) {
// Only adjust relative URLs (not external URLs, and not the home path "/")
if (!href.startsWith("http://") && !href.startsWith("https://") && href.startsWith("/") && href !== "/") {
// Convert absolute path to relative path that escapes basePath
// /some-path becomes ../some-path
const pathWithoutSlash = href.slice(1);
Expand Down