Skip to content

Commit ff78007

Browse files
authored
fix(git): use PAT owner as main authors + add co-authors (#203)
1 parent bd89373 commit ff78007

File tree

3 files changed

+110
-22
lines changed

3 files changed

+110
-22
lines changed

playground/docus/content/9.studio/3.git-providers.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ STUDIO_GITLAB_CLIENT_SECRET=<your_gitlab_secret>
3636

3737
When using **Google OAuth** or **Custom Auth** as your Auth provider, you must provide a Personal Access Token (PAT) with repository write permissions:
3838

39+
::note
40+
The user related to the PAT token will be used as main author of commit while the authenticated editor will be marked as co-author.
41+
::
42+
3943
```bash [.env]
4044
# For GitHub repositories
4145
STUDIO_GITHUB_TOKEN=<your_github_personal_access_token>
@@ -44,8 +48,8 @@ STUDIO_GITHUB_TOKEN=<your_github_personal_access_token>
4448
STUDIO_GITLAB_TOKEN=<your_gitlab_personal_access_token>
4549
```
4650

47-
::warning
48-
The Personal Access Token must have write permissions to the repository. Without it, users authenticated via Google OAuth or Custom Auth won't be able to publish changes.
51+
::tip
52+
Check the section below to have more info about how to create a PAT for GitHub or GitLab provider.
4953
::
5054

5155
## Supported Providers

playground/docus/content/index.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ seo:
66

77
::u-page-hero
88
#title
9-
Write docs with Studio
9+
Write docs with Studio.
1010

1111
#description
1212
Ship fast, flexible, and SEO-optimized documentation with beautiful design out of the box.
@@ -49,7 +49,7 @@ Shipped with many features
4949
---
5050
#title
5151
Built with [Nuxt 4]{.text-primary}
52-
52+
5353
#description
5454
Optimized by the most famous Vue framework. Docus gives you everything you need to build fast, performant, and SEO-friendly websites.
5555
:::
@@ -62,7 +62,7 @@ Shipped with many features
6262
---
6363
#title
6464
Powered by [Nuxt UI Pro]{.text-primary}
65-
65+
6666
#description
6767
Beautiful out of the box, minimal by design but highly customizable. Docus leverages Nuxt UI Pro to give you the best docs writing experience with zero boilerplate, just focus on your content.
6868
:::
@@ -75,7 +75,7 @@ Shipped with many features
7575
---
7676
#title
7777
Enhanced Markdown syntax by [Nuxt Content]{.text-primary}
78-
78+
7979
#description
8080
The only thing you need to take care about is writing your content. Write your pages in Markdown and extend with MDC syntax to embed Nuxt UI or custom Vue components. Structure, routing, and rendering are handled for you.
8181
:::
@@ -88,7 +88,7 @@ Shipped with many features
8888
---
8989
#title
9090
Customize with [Nuxt App Config]{.text-primary}
91-
91+
9292
#description
9393
Update colors, social links, header logos and component styles globally using the `app.config.ts`, no direct code modifications required.
9494
:::
@@ -101,7 +101,7 @@ Shipped with many features
101101
---
102102
#title
103103
Collaborate on [Nuxt Studio]{.text-primary}
104-
104+
105105
#description
106106
Write and manage your content visually, with zero Markdown knowledge required. Let your non technical colleagues collaborate on the documentation and integrate Vue components without code skills.
107107
:::
@@ -114,7 +114,7 @@ Shipped with many features
114114
---
115115
#title
116116
Built-in navigation and [full-text search]{.text-primary}
117-
117+
118118
#description
119119
Only focus on ordering your content, Docus handles the search modal and auto-generates the side navigation for you.
120120
:::

src/app/src/utils/providers/github.ts

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,79 @@ import type { GitOptions, GitProviderAPI, GitFile, RawFile, CommitResult, Commit
44
import { StudioFeature } from '../../types'
55
import { DraftStatus } from '../../types/draft'
66

7+
interface GitHubUser {
8+
login: string
9+
email: string | null
10+
name: string | null
11+
}
12+
13+
const NUXT_STUDIO_COAUTHOR = 'Co-authored-by: Nuxt Studio <[email protected]>'
14+
715
export function createGitHubProvider(options: GitOptions): GitProviderAPI {
816
const { owner, repo, token, branch, rootDir, authorName, authorEmail } = options
917
const gitFiles: Record<string, GitFile> = {}
1018

1119
// Support both token formats: "token {token}" for fine grained PATs, "Bearer {token}" for OAuth PATs
12-
const authHeader = token.startsWith('github_pat_') ? `token ${token}` : `Bearer ${token}`
20+
const isPAT = token.startsWith('github_pat_')
21+
const authHeader = isPAT ? `token ${token}` : `Bearer ${token}`
1322

14-
const $api = ofetch.create({
23+
const $repositoryApi = ofetch.create({
1524
baseURL: `https://api.github.com/repos/${owner}/${repo}`,
1625
headers: {
1726
Authorization: authHeader,
1827
Accept: 'application/vnd.github.v3+json',
1928
},
2029
})
2130

31+
const $userApi = ofetch.create({
32+
baseURL: 'https://api.github.com',
33+
headers: {
34+
Authorization: authHeader,
35+
Accept: 'application/vnd.github.v3+json',
36+
},
37+
})
38+
39+
// Cache for authenticated user info (PAT owner)
40+
let cachedPATUser: GitHubUser | null = null
41+
42+
/**
43+
* Fetch the authenticated user associated with the current token
44+
* Used for PAT tokens to get the token owner's info
45+
*/
46+
async function fetchAuthenticatedUser(): Promise<GitHubUser | null> {
47+
if (cachedPATUser) {
48+
return cachedPATUser
49+
}
50+
51+
try {
52+
const user = await $userApi('/user')
53+
54+
// If email is not public, try to fetch from emails endpoint
55+
let email = user.email
56+
if (!email) {
57+
try {
58+
const emails = await $userApi('/user/emails')
59+
const primaryEmail = emails.find((e: { primary: boolean, verified: boolean }) => e.primary && e.verified)
60+
email = primaryEmail?.email || emails.find((e: { verified: boolean }) => e.verified)?.email || null
61+
}
62+
catch {
63+
return null
64+
}
65+
}
66+
67+
cachedPATUser = {
68+
login: user.login,
69+
email,
70+
name: user.name || user.login,
71+
}
72+
73+
return cachedPATUser
74+
}
75+
catch {
76+
return null
77+
}
78+
}
79+
2280
async function fetchFile(path: string, { cached = false }: { cached?: boolean } = {}): Promise<GitFile | null> {
2381
path = joinURL(rootDir, path)
2482
if (cached) {
@@ -29,7 +87,7 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
2987
}
3088

3189
try {
32-
const ghResponse = await $api(`/contents/${path}?ref=${branch}`)
90+
const ghResponse = await $repositoryApi(`/contents/${path}?ref=${branch}`)
3391
const ghFile: GitFile = {
3492
...ghResponse,
3593
provider: 'github' as const,
@@ -58,7 +116,7 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
58116
}
59117
}
60118

61-
function commitFiles(files: RawFile[], message: string): Promise<CommitResult | null> {
119+
async function commitFiles(files: RawFile[], message: string): Promise<CommitResult | null> {
62120
if (!token) {
63121
return Promise.resolve(null)
64122
}
@@ -67,24 +125,50 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
67125
.filter(file => file.status !== DraftStatus.Pristine)
68126
.map(file => ({ ...file, path: joinURL(rootDir, file.path) }))
69127

128+
const coAuthors: string[] = [NUXT_STUDIO_COAUTHOR]
129+
130+
let commitAuthorName = authorName
131+
let commitAuthorEmail = authorEmail
132+
133+
// For PAT tokens, use the PAT owner's info for the commit author
134+
// This ensures the commit email is associated with a GitHub account
135+
if (isPAT) {
136+
const patUser = await fetchAuthenticatedUser()
137+
if (patUser?.email) {
138+
// Add the original user (who performed the action) as co-author if different from PAT owner
139+
if (authorEmail && authorEmail !== patUser.email) {
140+
coAuthors.push(`Co-authored-by: ${authorName} <${authorEmail}>`)
141+
}
142+
143+
// Use PAT owner as the commit author
144+
commitAuthorName = patUser.name || patUser.login
145+
commitAuthorEmail = patUser.email
146+
}
147+
}
148+
149+
// Build commit message with co-authors
150+
const fullMessage = coAuthors.length > 0
151+
? `${message}\n\n${coAuthors.join('\n')}`
152+
: message
153+
70154
return commitFilesToGitHub({
71155
owner,
72156
repo,
73157
branch,
74158
files,
75-
message,
76-
authorName,
77-
authorEmail,
159+
message: fullMessage,
160+
authorName: commitAuthorName,
161+
authorEmail: commitAuthorEmail,
78162
})
79163
}
80164

81165
async function commitFilesToGitHub({ owner, repo, branch, files, message, authorName, authorEmail }: CommitFilesOptions) {
82166
// Get latest commit SHA
83-
const refData = await $api(`/git/refs/heads/${branch}`)
167+
const refData = await $repositoryApi(`/git/refs/heads/${branch}`)
84168
const latestCommitSha = refData.object.sha
85169

86170
// Get base tree SHA
87-
const commitData = await $api(`/git/commits/${latestCommitSha}`)
171+
const commitData = await $repositoryApi(`/git/commits/${latestCommitSha}`)
88172
const baseTreeSha = commitData.tree.sha
89173

90174
// Create blobs and prepare tree
@@ -101,7 +185,7 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
101185
}
102186
else {
103187
// For new/modified files, create blob and use its sha
104-
const blobData = await $api(`/git/blobs`, {
188+
const blobData = await $repositoryApi(`/git/blobs`, {
105189
method: 'POST',
106190
body: JSON.stringify({
107191
content: file.content,
@@ -118,7 +202,7 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
118202
}
119203

120204
// Create new tree
121-
const treeData = await $api(`/git/trees`, {
205+
const treeData = await $repositoryApi(`/git/trees`, {
122206
method: 'POST',
123207
body: JSON.stringify({
124208
base_tree: baseTreeSha,
@@ -127,7 +211,7 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
127211
})
128212

129213
// Create new commit
130-
const newCommit = await $api(`/git/commits`, {
214+
const newCommit = await $repositoryApi(`/git/commits`, {
131215
method: 'POST',
132216
body: JSON.stringify({
133217
message,
@@ -142,7 +226,7 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
142226
})
143227

144228
// Update branch ref
145-
await $api(`/git/refs/heads/${branch}`, {
229+
await $repositoryApi(`/git/refs/heads/${branch}`, {
146230
method: 'PATCH',
147231
body: JSON.stringify({ sha: newCommit.sha }),
148232
})

0 commit comments

Comments
 (0)