Skip to content
Open
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
2 changes: 2 additions & 0 deletions sample_settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export default {
conversion: {
useLowerCaseLabels: true,
addIssueInformation: true,
enrichGitLabMetadata: true,
createWeightLabels: true,
},
transfer: {
description: true,
Expand Down
65 changes: 64 additions & 1 deletion src/githubHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);

Expand Down Expand Up @@ -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<void> {
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.
Expand All @@ -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,
Expand All @@ -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 });

//
Expand Down
5 changes: 2 additions & 3 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export default interface Settings {
conversion: {
useLowerCaseLabels: boolean;
addIssueInformation: boolean;
enrichGitLabMetadata: boolean;
createWeightLabels: boolean;
};
transfer: {
description: boolean;
Expand All @@ -40,9 +42,6 @@ export default interface Settings {
logFile: string;
log: boolean;
};
commitMap?: {
[key: string]: string;
};
s3?: S3Settings;
commitMap: {
[key: string]: string;
Expand Down
71 changes: 68 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<void> {
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);
}
}