feat: add commit footers for pull merge #109

closed
opened by dvjn.dev targeting master from dvjn.dev/core: push-rwutqnoxqxlr
Changed files
+376 -1
appview
pulls
patchutil
+86
patchutil/patchutil.go
··· 316 316 317 317 return nd 318 318 } 319 + 320 + func extractTrailersFromCommitMessage(commitMsg string) (string, []string) { 321 + lines := strings.Split(strings.TrimSpace(commitMsg), "\n") 322 + if len(lines) == 0 { 323 + return "", []string{} 324 + } 325 + 326 + trailerStart := len(lines) 327 + trailers := []string{} 328 + 329 + // Loop over commit message lines in reverse order to find trailers 330 + for i := len(lines) - 1; i >= 0; i-- { 331 + line := strings.TrimSpace(lines[i]) 332 + if line == "" { 333 + continue 334 + } 335 + 336 + // Check if line looks like a trailer (has a colon) 337 + if colonIndex := strings.Index(line, ":"); colonIndex <= 0 { 338 + // Not a trailer, stop scanning 339 + break 340 + } 341 + 342 + trailers = append([]string{line}, trailers...) 343 + trailerStart = i 344 + } 345 + 346 + // Extract message body (everything before trailers) 347 + messageBody := "" 348 + if trailerStart > 0 { 349 + bodyLines := lines[:trailerStart] 350 + for len(bodyLines) > 0 && strings.TrimSpace(bodyLines[len(bodyLines)-1]) == "" { 351 + bodyLines = bodyLines[:len(bodyLines)-1] 352 + } 353 + messageBody = strings.Join(bodyLines, "\n") 354 + } 355 + 356 + return messageBody, trailers 357 + } 358 + 359 + func AddCommitMessageTrailers(patch string, trailers []string) string { 360 + re := regexp.MustCompile(`(?s)\n(Subject: )(.*?)(---\n|\ndiff --git )`) 361 + 362 + // Find all matches 363 + allMatches := re.FindAllStringSubmatch(patch, -1) 364 + if len(allMatches) == 0 { 365 + return patch 366 + } 367 + 368 + result := patch 369 + // Process matches in reverse order to avoid offset issues 370 + for i := len(allMatches) - 1; i >= 0; i-- { 371 + matches := allMatches[i] 372 + if len(matches) < 4 { 373 + continue 374 + } 375 + 376 + commitMsg := matches[2] 377 + messageBody, existingTrailers := extractTrailersFromCommitMessage(commitMsg) 378 + 379 + allTrailers := append([]string{}, existingTrailers...) 380 + 381 + for _, trailer := range trailers { 382 + allTrailers = append(allTrailers, strings.TrimSpace(trailer)) 383 + } 384 + 385 + // Reconstruct commit message 386 + newCommitMsg := messageBody 387 + if len(allTrailers) > 0 { 388 + if !strings.HasSuffix(newCommitMsg, "\n\n") { 389 + if strings.HasSuffix(newCommitMsg, "\n") { 390 + newCommitMsg += "\n" 391 + } else { 392 + newCommitMsg += "\n\n" 393 + } 394 + } 395 + newCommitMsg += strings.Join(allTrailers, "\n") 396 + newCommitMsg += "\n" 397 + } 398 + 399 + // Replace in result 400 + result = strings.Replace(result, matches[0], "\n"+matches[1]+newCommitMsg+matches[3], 1) 401 + } 402 + 403 + return result 404 + }
+279
patchutil/patchutil_test.go
··· 322 322 }) 323 323 } 324 324 } 325 + 326 + func TestAddCommmitMessageTrailers(t *testing.T) { 327 + tests := []struct { 328 + name string 329 + input string 330 + trailers []string 331 + expected string 332 + }{ 333 + { 334 + name: "Single patch with existing trailers", 335 + input: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001 336 + From: Author <author@example.com> 337 + Date: Wed, 16 Apr 2025 11:01:00 +0300 338 + Subject: [PATCH] Example patch 339 + 340 + more message body 341 + 342 + Signed-off-by: Author <author@example.com> 343 + Reviewed-by: Reviewer <reviewer@example.com> 344 + --- 345 + file.txt | 2 +- 346 + 1 file changed, 1 insertions(+), 1 deletions(-) 347 + 348 + diff --git a/file.txt b/file.txt 349 + index 123456..789012 100644 350 + --- a/file.txt 351 + +++ b/file.txt 352 + @@ -1 +1 @@ 353 + -old content 354 + +new content 355 + -- 356 + 2.48.1`, 357 + trailers: []string{`Pull-Id: Author <author@example.com>`, `Merge-Request: 123`}, 358 + expected: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001 359 + From: Author <author@example.com> 360 + Date: Wed, 16 Apr 2025 11:01:00 +0300 361 + Subject: [PATCH] Example patch 362 + 363 + more message body 364 + 365 + Signed-off-by: Author <author@example.com> 366 + Reviewed-by: Reviewer <reviewer@example.com> 367 + Pull-Id: Author <author@example.com> 368 + Merge-Request: 123 369 + --- 370 + file.txt | 2 +- 371 + 1 file changed, 1 insertions(+), 1 deletions(-) 372 + 373 + diff --git a/file.txt b/file.txt 374 + index 123456..789012 100644 375 + --- a/file.txt 376 + +++ b/file.txt 377 + @@ -1 +1 @@ 378 + -old content 379 + +new content 380 + -- 381 + 2.48.1`, 382 + }, 383 + { 384 + name: "Single patch with existing trailers and no stat", 385 + input: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001 386 + From: Author <author@example.com> 387 + Date: Wed, 16 Apr 2025 11:01:00 +0300 388 + Subject: [PATCH] Example patch 389 + 390 + Signed-off-by: Author <author@example.com> 391 + Reviewed-by: Reviewer <reviewer@example.com> 392 + 393 + diff --git a/file.txt b/file.txt 394 + index 123456..789012 100644 395 + --- a/file.txt 396 + +++ b/file.txt 397 + @@ -1 +1 @@ 398 + -old content 399 + +new content 400 + -- 401 + 2.48.1`, 402 + trailers: []string{`Pull-Id: Author <author@example.com>`, `Merge-Request: 123`}, 403 + expected: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001 404 + From: Author <author@example.com> 405 + Date: Wed, 16 Apr 2025 11:01:00 +0300 406 + Subject: [PATCH] Example patch 407 + 408 + Signed-off-by: Author <author@example.com> 409 + Reviewed-by: Reviewer <reviewer@example.com> 410 + Pull-Id: Author <author@example.com> 411 + Merge-Request: 123 412 + 413 + diff --git a/file.txt b/file.txt 414 + index 123456..789012 100644 415 + --- a/file.txt 416 + +++ b/file.txt 417 + @@ -1 +1 @@ 418 + -old content 419 + +new content 420 + -- 421 + 2.48.1`, 422 + }, 423 + { 424 + name: "Multiple patches with existing trailers", 425 + input: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001 426 + From: Author <author@example.com> 427 + Date: Wed, 16 Apr 2025 11:01:00 +0300 428 + Subject: [PATCH 1/2] First patch 429 + 430 + more message body 431 + 432 + --- 433 + file.txt | 2 +- 434 + 1 file changed, 1 insertions(+), 1 deletions(-) 435 + 436 + diff --git a/file1.txt b/file1.txt 437 + index 123456..789012 100644 438 + --- a/file1.txt 439 + +++ b/file1.txt 440 + @@ -1 +1 @@ 441 + -old content 442 + +new content 443 + -- 444 + 2.48.1 445 + From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001 446 + From: Author <author@example.com> 447 + Date: Wed, 16 Apr 2025 11:03:11 +0300 448 + Subject: [PATCH 2/2] Second patch 449 + 450 + more message body 451 + 452 + Signed-off-by: Author <author@example.com> 453 + Reviewed-by: Reviewer <reviewer@example.com> 454 + --- 455 + file.txt | 2 +- 456 + 1 file changed, 1 insertions(+), 1 deletions(-) 457 + 458 + diff --git a/file2.txt b/file2.txt 459 + index abcdef..ghijkl 100644 460 + --- a/file2.txt 461 + +++ b/file2.txt 462 + @@ -1 +1 @@ 463 + -foo bar 464 + +baz qux 465 + -- 466 + 2.48.1`, 467 + trailers: []string{`Pull-Id: Author <author@example.com>`, `Merge-Request: 123`}, 468 + expected: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001 469 + From: Author <author@example.com> 470 + Date: Wed, 16 Apr 2025 11:01:00 +0300 471 + Subject: [PATCH 1/2] First patch 472 + 473 + more message body 474 + 475 + Pull-Id: Author <author@example.com> 476 + Merge-Request: 123 477 + --- 478 + file.txt | 2 +- 479 + 1 file changed, 1 insertions(+), 1 deletions(-) 480 + 481 + diff --git a/file1.txt b/file1.txt 482 + index 123456..789012 100644 483 + --- a/file1.txt 484 + +++ b/file1.txt 485 + @@ -1 +1 @@ 486 + -old content 487 + +new content 488 + -- 489 + 2.48.1 490 + From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001 491 + From: Author <author@example.com> 492 + Date: Wed, 16 Apr 2025 11:03:11 +0300 493 + Subject: [PATCH 2/2] Second patch 494 + 495 + more message body 496 + 497 + Signed-off-by: Author <author@example.com> 498 + Reviewed-by: Reviewer <reviewer@example.com> 499 + Pull-Id: Author <author@example.com> 500 + Merge-Request: 123 501 + --- 502 + file.txt | 2 +- 503 + 1 file changed, 1 insertions(+), 1 deletions(-) 504 + 505 + diff --git a/file2.txt b/file2.txt 506 + index abcdef..ghijkl 100644 507 + --- a/file2.txt 508 + +++ b/file2.txt 509 + @@ -1 +1 @@ 510 + -foo bar 511 + +baz qux 512 + -- 513 + 2.48.1`, 514 + }, 515 + { 516 + name: "Multiple patches with existing trailers and no stat", 517 + input: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001 518 + From: Author <author@example.com> 519 + Date: Wed, 16 Apr 2025 11:01:00 +0300 520 + Subject: [PATCH 1/2] First patch 521 + 522 + with long message body 523 + 524 + Signed-off-by: Author <author@example.com> 525 + 526 + diff --git a/file1.txt b/file1.txt 527 + index 123456..789012 100644 528 + --- a/file1.txt 529 + +++ b/file1.txt 530 + @@ -1 +1 @@ 531 + -old content 532 + +new content 533 + -- 534 + 2.48.1 535 + From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001 536 + From: Author <author@example.com> 537 + Date: Wed, 16 Apr 2025 11:03:11 +0300 538 + Subject: [PATCH 2/2] Second patch 539 + 540 + Signed-off-by: Author <author@example.com> 541 + Reviewed-by: Reviewer <reviewer@example.com> 542 + 543 + diff --git a/file2.txt b/file2.txt 544 + index abcdef..ghijkl 100644 545 + --- a/file2.txt 546 + +++ b/file2.txt 547 + @@ -1 +1 @@ 548 + -foo bar 549 + +baz qux 550 + -- 551 + 2.48.1`, 552 + trailers: []string{`Pull-Id: Author <author@example.com>`, `Merge-Request: 123`}, 553 + expected: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001 554 + From: Author <author@example.com> 555 + Date: Wed, 16 Apr 2025 11:01:00 +0300 556 + Subject: [PATCH 1/2] First patch 557 + 558 + with long message body 559 + 560 + Signed-off-by: Author <author@example.com> 561 + Pull-Id: Author <author@example.com> 562 + Merge-Request: 123 563 + 564 + diff --git a/file1.txt b/file1.txt 565 + index 123456..789012 100644 566 + --- a/file1.txt 567 + +++ b/file1.txt 568 + @@ -1 +1 @@ 569 + -old content 570 + +new content 571 + -- 572 + 2.48.1 573 + From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001 574 + From: Author <author@example.com> 575 + Date: Wed, 16 Apr 2025 11:03:11 +0300 576 + Subject: [PATCH 2/2] Second patch 577 + 578 + Signed-off-by: Author <author@example.com> 579 + Reviewed-by: Reviewer <reviewer@example.com> 580 + Pull-Id: Author <author@example.com> 581 + Merge-Request: 123 582 + 583 + diff --git a/file2.txt b/file2.txt 584 + index abcdef..ghijkl 100644 585 + --- a/file2.txt 586 + +++ b/file2.txt 587 + @@ -1 +1 @@ 588 + -foo bar 589 + +baz qux 590 + -- 591 + 2.48.1`, 592 + }, 593 + } 594 + 595 + for _, tt := range tests { 596 + t.Run(tt.name, func(t *testing.T) { 597 + result := AddCommitMessageTrailers(tt.input, tt.trailers) 598 + if result != tt.expected { 599 + t.Errorf("Got:\n========\n%v\n========\nExpected:\n========\n%v\n========\n", result, tt.expected) 600 + } 601 + }) 602 + } 603 + }
+11 -1
appview/pulls/pulls.go
··· 1915 1915 return 1916 1916 } 1917 1917 1918 + actor := s.oauth.GetUser(r) // no need to check for nil as this is an authenticated request 1919 + 1920 + footers := strings.Join([]string{ 1921 + fmt.Sprintf("Pull-id: %s", pull.PullAt()), 1922 + fmt.Sprintf("Merged-by: %s", actor.Did), 1923 + }, "\n") 1924 + 1925 + pullBody := pull.Body + "\n\n" + footers 1926 + patch = patchutil.AddCommitMessageFooters(patch, footers) 1927 + 1918 1928 // Merge the pull request 1919 - resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1929 + resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pullBody, ident.Handle.String(), email.Address) 1920 1930 if err != nil { 1921 1931 log.Printf("failed to merge pull request: %s", err) 1922 1932 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")