feat(app): add functionality to generate commit messages with co-authors
This commit is contained in:
@@ -97,6 +97,34 @@ if not new_prompts and not updated_prompts:
|
||||
import sys
|
||||
sys.exit(2) # Exit code 2 = no changes
|
||||
else:
|
||||
# Helper function to parse contributors (supports "user1,user2,user3" format)
|
||||
def parse_contributors(contributor_field):
|
||||
"""Parse contributor field, returns (primary_author, co_authors_list)"""
|
||||
if not contributor_field:
|
||||
return 'anonymous', []
|
||||
|
||||
# Split by comma and clean up
|
||||
contributors = [c.strip() for c in contributor_field.split(',') if c.strip()]
|
||||
|
||||
if not contributors:
|
||||
return 'anonymous', []
|
||||
|
||||
primary = contributors[0]
|
||||
co_authors = contributors[1:] if len(contributors) > 1 else []
|
||||
return primary, co_authors
|
||||
|
||||
def build_commit_message(action, act, co_authors):
|
||||
"""Build commit message with optional co-author trailers"""
|
||||
msg = f'{action} prompt: {act}'
|
||||
|
||||
if co_authors:
|
||||
msg += '\n\n'
|
||||
for co_author in co_authors:
|
||||
co_email = f"{co_author}@users.noreply.github.com"
|
||||
msg += f'Co-authored-by: {co_author} <{co_email}>\n'
|
||||
|
||||
return msg
|
||||
|
||||
# Process updates first (rewrite entire file with updates applied)
|
||||
if updated_prompts:
|
||||
print("\nApplying updates to existing prompts...")
|
||||
@@ -118,42 +146,41 @@ else:
|
||||
|
||||
# Commit updates
|
||||
for i, (remote_row, local_row) in enumerate(updated_prompts, 1):
|
||||
contributor = remote_row.get('contributor', '').strip()
|
||||
contributor_field = remote_row.get('contributor', '').strip()
|
||||
act = remote_row.get('act', 'Unknown')
|
||||
|
||||
if not contributor:
|
||||
contributor = 'anonymous'
|
||||
|
||||
email = f"{contributor}@users.noreply.github.com"
|
||||
primary_author, co_authors = parse_contributors(contributor_field)
|
||||
email = f"{primary_author}@users.noreply.github.com"
|
||||
|
||||
subprocess.run(['git', 'add', csv_file], check=True)
|
||||
|
||||
env = os.environ.copy()
|
||||
env['GIT_AUTHOR_NAME'] = contributor
|
||||
env['GIT_AUTHOR_NAME'] = primary_author
|
||||
env['GIT_AUTHOR_EMAIL'] = email
|
||||
env['GIT_COMMITTER_NAME'] = contributor
|
||||
env['GIT_COMMITTER_NAME'] = primary_author
|
||||
env['GIT_COMMITTER_EMAIL'] = email
|
||||
|
||||
commit_msg = build_commit_message('Update', act, co_authors)
|
||||
|
||||
subprocess.run([
|
||||
'git', 'commit',
|
||||
'-m', f'Update prompt: {act}',
|
||||
f'--author={contributor} <{email}>'
|
||||
'-m', commit_msg,
|
||||
f'--author={primary_author} <{email}>'
|
||||
], env=env, check=True)
|
||||
|
||||
print(f"[UPDATE {i}/{len(updated_prompts)}] {contributor}: {act}")
|
||||
co_authors_str = f" (+ {', '.join(co_authors)})" if co_authors else ""
|
||||
print(f"[UPDATE {i}/{len(updated_prompts)}] {primary_author}{co_authors_str}: {act}")
|
||||
|
||||
# Process new prompts
|
||||
if new_prompts:
|
||||
print("\nCreating commits for new prompts...")
|
||||
|
||||
for i, row in enumerate(new_prompts, 1):
|
||||
contributor = row.get('contributor', '').strip()
|
||||
contributor_field = row.get('contributor', '').strip()
|
||||
act = row.get('act', 'Unknown')
|
||||
|
||||
if not contributor:
|
||||
contributor = 'anonymous'
|
||||
|
||||
email = f"{contributor}@users.noreply.github.com"
|
||||
primary_author, co_authors = parse_contributors(contributor_field)
|
||||
email = f"{primary_author}@users.noreply.github.com"
|
||||
|
||||
# Append this row to the CSV
|
||||
with open(csv_file, 'a', newline='') as f:
|
||||
@@ -164,18 +191,21 @@ else:
|
||||
subprocess.run(['git', 'add', csv_file], check=True)
|
||||
|
||||
env = os.environ.copy()
|
||||
env['GIT_AUTHOR_NAME'] = contributor
|
||||
env['GIT_AUTHOR_NAME'] = primary_author
|
||||
env['GIT_AUTHOR_EMAIL'] = email
|
||||
env['GIT_COMMITTER_NAME'] = contributor
|
||||
env['GIT_COMMITTER_NAME'] = primary_author
|
||||
env['GIT_COMMITTER_EMAIL'] = email
|
||||
|
||||
commit_msg = build_commit_message('Add', act, co_authors)
|
||||
|
||||
subprocess.run([
|
||||
'git', 'commit',
|
||||
'-m', f'Add prompt: {act}',
|
||||
f'--author={contributor} <{email}>'
|
||||
'-m', commit_msg,
|
||||
f'--author={primary_author} <{email}>'
|
||||
], env=env, check=True)
|
||||
|
||||
print(f"[NEW {i}/{len(new_prompts)}] {contributor}: {act}")
|
||||
co_authors_str = f" (+ {', '.join(co_authors)})" if co_authors else ""
|
||||
print(f"[NEW {i}/{len(new_prompts)}] {primary_author}{co_authors_str}: {act}")
|
||||
|
||||
print(f"\nDone! Created {len(new_prompts)} new commits, {len(updated_prompts)} update commits.")
|
||||
|
||||
|
||||
@@ -14,6 +14,15 @@ function escapeCSVField(field: string): string {
|
||||
return field;
|
||||
}
|
||||
|
||||
function getUserIdentifier(user: { email: string; username: string; githubUsername: string | null }): string {
|
||||
// Determine contributor identifier (immutable to prevent impersonation):
|
||||
// 1. githubUsername if set (GitHub OAuth users)
|
||||
// 2. username if email ends with @unclaimed.prompts.chat (imported GitHub contributors)
|
||||
// 3. email for others (Google, credentials)
|
||||
const isUnclaimedAccount = user.email.endsWith('@unclaimed.prompts.chat');
|
||||
return user.githubUsername || (isUnclaimedAccount ? user.username : user.email);
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const prompts = await db.prompt.findMany({
|
||||
@@ -34,6 +43,13 @@ export async function GET() {
|
||||
githubUsername: true,
|
||||
},
|
||||
},
|
||||
contributors: {
|
||||
select: {
|
||||
email: true,
|
||||
username: true,
|
||||
githubUsername: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
@@ -44,16 +60,18 @@ export async function GET() {
|
||||
const promptContent = escapeCSVField(prompt.content);
|
||||
const forDevs = prompt.category?.slug === "coding" ? "TRUE" : "FALSE";
|
||||
const type = prompt.type;
|
||||
// Determine contributor identifier (immutable to prevent impersonation):
|
||||
// 1. githubUsername if set (GitHub OAuth users)
|
||||
// 2. username if email ends with @unclaimed.prompts.chat (imported GitHub contributors)
|
||||
// 3. email for others (Google, credentials)
|
||||
const isUnclaimedAccount = prompt.author.email.endsWith('@unclaimed.prompts.chat');
|
||||
const contributor = escapeCSVField(
|
||||
prompt.author.githubUsername || (isUnclaimedAccount ? prompt.author.username : prompt.author.email)
|
||||
);
|
||||
|
||||
return [act, promptContent, forDevs, type, contributor].join(",");
|
||||
// Build contributor list: author first, then co-contributors
|
||||
// Format: "@author,@contributor1,@contributor2" or just "@author"
|
||||
const authorId = getUserIdentifier(prompt.author);
|
||||
const contributorIds = prompt.contributors
|
||||
.map((c: { email: string; username: string; githubUsername: string | null }) => getUserIdentifier(c))
|
||||
.filter((id: string) => id !== authorId); // Exclude author from contributors list
|
||||
|
||||
const allContributors = [authorId, ...contributorIds];
|
||||
const contributorField = escapeCSVField(allContributors.join(","));
|
||||
|
||||
return [act, promptContent, forDevs, type, contributorField].join(",");
|
||||
});
|
||||
|
||||
const csvContent = [headers.join(","), ...rows].join("\n");
|
||||
|
||||
Reference in New Issue
Block a user