@@ -4,21 +4,79 @@ import type { GitOptions, GitProviderAPI, GitFile, RawFile, CommitResult, Commit
44import { StudioFeature } from '../../types'
55import { 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+
715export 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