diff --git a/sample_settings.ts b/sample_settings.ts index 67bd8a9..25a5345 100644 --- a/sample_settings.ts +++ b/sample_settings.ts @@ -39,6 +39,8 @@ export default { conversion: { useLowerCaseLabels: true, addIssueInformation: true, + enrichGitLabMetadata: true, + createWeightLabels: true, }, transfer: { description: true, diff --git a/src/githubHelper.ts b/src/githubHelper.ts index f7fb1c8..999cfec 100644 --- a/src/githubHelper.ts +++ b/src/githubHelper.ts @@ -383,6 +383,11 @@ export class GithubHelper { true, ); + // Enrich with GitLab metadata if enabled and not a placeholder + if (!issue.isPlaceholder) { + bodyConverted = this.enrichBodyWithMetadata(issue, bodyConverted); + } + let props: RestEndpointMethodTypes['issues']['create']['parameters'] = { owner: this.githubOwner, repo: this.githubRepo, @@ -392,7 +397,13 @@ export class GithubHelper { props.assignees = this.convertAssignees(issue); props.milestone = this.convertMilestone(issue); - props.labels = this.convertLabels(issue); + + let labels = this.convertLabels(issue); + // Add weight label if enabled and not a placeholder + if (!issue.isPlaceholder) { + await this.addWeightLabel(issue, labels); + } + props.labels = labels; await utils.sleep(this.delayInMs); @@ -487,6 +498,48 @@ export class GithubHelper { return labels; } + /** + * Enriches issue body with GitLab metadata (time tracking, weight, due date, health status) + * if enrichGitLabMetadata is enabled in settings + */ + enrichBodyWithMetadata(issue: GitLabIssue, body: string): string { + if (!settings.conversion.enrichGitLabMetadata) { + return body; + } + + const metadata = utils.extractGitLabMetadata(issue); + const metadataSection = utils.formatGitLabMetadata(metadata); + + if (!metadataSection) { + return body; + } + + // Insert metadata before the footer "Migrated from" if it exists + const footerIndex = body.lastIndexOf('*Migrated from'); + if (footerIndex !== -1) { + return body.slice(0, footerIndex) + metadataSection + body.slice(footerIndex); + } + + return body + metadataSection; + } + + /** + * Adds weight label to labels array if createWeightLabels is enabled + * and ensures the label exists in the repository + */ + async addWeightLabel(issue: GitLabIssue, labels: string[]): Promise { + if (!settings.conversion.createWeightLabels || !issue.weight) { + return; + } + + const weightLabel = `weight::${issue.weight}`; + if (!labels.includes(weightLabel)) { + labels.push(weightLabel); + } + + await utils.ensureWeightLabel(this.githubApi, this.githubOwner, this.githubRepo, issue.weight); + } + /** * Uses the preview issue import API to set creation date on issues and comments. * Also it does not notify assignees. @@ -506,6 +559,11 @@ export class GithubHelper { true, ); + // Enrich with GitLab metadata if enabled and not a placeholder + if (!issue.isPlaceholder) { + bodyConverted = this.enrichBodyWithMetadata(issue, bodyConverted); + } + let props: IssueImport = { title: issue.title ? issue.title.trim() : '', body: bodyConverted, @@ -523,6 +581,11 @@ export class GithubHelper { props.milestone = this.convertMilestone(issue); props.labels = this.convertLabels(issue); + // Add weight label if enabled and not a placeholder + if (!issue.isPlaceholder) { + await this.addWeightLabel(issue, props.labels); + } + if (settings.dryRun) return Promise.resolve({ data: issue }); // diff --git a/src/settings.ts b/src/settings.ts index c8fa531..ca7915c 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -18,6 +18,8 @@ export default interface Settings { conversion: { useLowerCaseLabels: boolean; addIssueInformation: boolean; + enrichGitLabMetadata: boolean; + createWeightLabels: boolean; }; transfer: { description: boolean; @@ -40,9 +42,6 @@ export default interface Settings { logFile: string; log: boolean; }; - commitMap?: { - [key: string]: string; - }; s3?: S3Settings; commitMap: { [key: string]: string; diff --git a/src/utils.ts b/src/utils.ts index 9ed95d5..d95ea3c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -29,14 +29,14 @@ export const readProjectsFromCsv = ( for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); - + if (!line || line.startsWith('#')) { continue; } const values = line.split(',').map(v => v.trim()); const maxColumn = Math.max(idColumn, gitlabPathColumn, githubPathColumn); - + if (maxColumn >= values.length) { console.warn(`Warning: Line ${i + 1} has only ${values.length} column(s), skipping (need column ${maxColumn})`); if (!headerSkipped) { @@ -76,7 +76,7 @@ export const readProjectsFromCsv = ( if (projectMap.size === 0) { throw new Error(`No valid project mappings found in CSV file: ${filePath}`); } - + console.log(`✓ Loaded ${projectMap.size} project mappings from CSV`); return projectMap; } catch (err) { @@ -192,3 +192,68 @@ export const organizationUsersString = (users: string[], prefix: string): string return ''; } + +export interface GitLabMetadata { + timeEstimate?: string; + timeSpent?: string; + weight?: number; + dueDate?: string; + healthStatus?: string; +} + +const WEIGHT_LABEL_COLOR = 'D4C5F9'; // Light purple + +/** + * Extracts GitLab-specific metadata from an issue + */ +export function extractGitLabMetadata(issue: any): GitLabMetadata { + return { + timeEstimate: issue.time_stats?.human_time_estimate, + timeSpent: issue.time_stats?.human_total_time_spent, + weight: issue.weight, + dueDate: issue.due_date, + healthStatus: issue.health_status, + }; +} + +/** + * Formats GitLab metadata as a markdown section + * Returns empty string if no metadata exists + */ +export function formatGitLabMetadata(metadata: GitLabMetadata): string { + const hasAnyMetadata = metadata.timeEstimate || metadata.timeSpent || metadata.weight || metadata.dueDate || metadata.healthStatus; + if (!hasAnyMetadata) return ''; + + let result = '\n---\n**GitLab Metadata**\n\n'; + + if (metadata.timeEstimate) result += `- ⏱️ Time Estimate: ${metadata.timeEstimate}\n`; + if (metadata.timeSpent) result += `- ⏲️ Time Spent: ${metadata.timeSpent}\n`; + if (metadata.weight) result += `- ⚖️ Weight: ${metadata.weight}\n`; + if (metadata.dueDate) result += `- 📅 Due Date: ${metadata.dueDate}\n`; + if (metadata.healthStatus) result += `- 🏥 Health: ${metadata.healthStatus}\n`; + + result += '---\n'; + return result; +} + +/** + * Ensures a weight label exists in the GitHub repository + * Silently ignores if label already exists + */ +export async function ensureWeightLabel(octokit: any, owner: string, repo: string, weight: number): Promise { + try { + await octokit.rest.issues.createLabel({ + owner, + repo, + name: `weight::${weight}`, + color: WEIGHT_LABEL_COLOR, + description: `GitLab issue weight: ${weight}`, + }); + } catch (error) { + if ((error as any).status === 422) { + // Label already exists, this is expected + return; + } + console.warn(`Warning: Could not create label weight::${weight}:`, (error as any).message); + } +}