From 7eee167198c8c83151e7da4f1a5f7091c909a6bb Mon Sep 17 00:00:00 2001 From: Djuuu Date: Sun, 25 Feb 2024 17:55:01 +0100 Subject: [PATCH 1/2] Use links to merge request diff view Commit links in merge request descriptions are generated by Gitlab when it detects SHA-1 strings matching existing commits. Ex: 01234567ab -> https://gitlab.com/my/project/-/commit/01234567ab These links have not direct link to a merge request. When commenting on them, comments might be lost at rebase. Linking to the diff view, focused on a given commit, is better : https://gitlab.com/my/project/-/merge_requests/123/diffs?commit_id=0123456789abcdef0123456789abcdef0123456 --- git-mr | 177 ++++++--- test/git-mr.bats | 371 ++++++++++++++---- .../gitlab-mock-mr-description-extended.bash | 6 +- .../gitlab-mock-mr-description-simple.bash | 6 +- 4 files changed, 429 insertions(+), 131 deletions(-) diff --git a/git-mr b/git-mr index e7409ee..32078b2 100755 --- a/git-mr +++ b/git-mr @@ -113,6 +113,13 @@ git_commits() { git log --oneline --reverse --no-decorate "${target_branch}..${source_branch}" } +git_commits_hybrid() { + local source_branch=${1:-$(git_current_branch)} + local target_branch=${2:-${GIT_MR_TARGET:-$(git_base_branch "$source_branch")}} + + git log --reverse --format="%H %h %s" "${target_branch}".."${source_branch}" +} + git_commits_extended() { local source_branch=${1:-$(git_current_branch)} local target_branch=${2:-${GIT_MR_TARGET:-$(git_base_branch "$source_branch")}} @@ -129,7 +136,7 @@ git_commits_extended() { sed -r "s/${end}$//g" # remove trailing body end for bodies without trailing blank line } -git_commit_extended() { +git_commit_extended_console_display() { local revision=${1:-HEAD} local prefix="* ${MD_BOLD}" @@ -371,7 +378,7 @@ jq_build() { # escape regex special characters for literal usage regex_escape() { - echo "${1}" | sed -e 's/[]\/$*.^[]/\\&/g' + echo "${1}" | sed -e 's/[]\/$*.^?[]/\\&/g' } open_in_browser() { @@ -1289,8 +1296,11 @@ mr_print_status() { mr_description_first_commit_line() { local mr_description=$1 - local first_commit_line - first_commit_line=$(echo "$mr_description" | grep -Pn '^[^0-9a-fA-F]*[0-9a-fA-F]{7,}\s' -m 1 | cut -d: -f1) + local first_commit_line; first_commit_line=$( + echo "$mr_description" | + grep -Pn "$_git_mr_commit_regex" -m 1 | + cut -d: -f1 + ) echo "$first_commit_line" } @@ -1298,8 +1308,12 @@ mr_description_first_commit_line() { mr_description_last_commit_line() { local mr_description=$1 - local last_commit_line - last_commit_line=$(echo "$mr_description" | grep -Pn '^[^0-9a-fA-F]*[0-9a-fA-F]{7,}\s' | tail -n1 | cut -d: -f1) + local last_commit_line; last_commit_line=$( + echo "$mr_description" | + grep -Pn "$_git_mr_commit_regex" | + tail -n1 | + cut -d: -f1 + ) local description_after_last_commit description_after_last_commit=$(echo "$mr_description" | tail -n "+$((last_commit_line + 1))") @@ -1328,20 +1342,10 @@ mr_description_insert_new_commits() { [[ -n $last_commit_line ]] || last_commit_line=$(mr_description_last_commit_line "$mr_description") - local description_length; description_length=$(echo "$mr_description" | wc -l) - # Iterate over description lines - local i=1 mr_description_line trailing_br + local i=1 mr_description_line while IFS=$'\n' read -r mr_description_line; do - - # Add trailing markdown newline (that would have been trimmes by Gitlab) to list item on last line. - # (As other commit lines, to prepare for an extended description going on the next line.) - [[ $i -eq $description_length && $i -eq $last_commit_line ]] && - grep -q '^* ' <<< "$mr_description_line" && - trailing_br="$MD_BR" - - echo "${mr_description_line}${trailing_br}" - + echo "${mr_description_line}" # Insert new commits if [[ $i -eq $last_commit_line ]]; then mr_description_print_new_commits "$new_commits" "$for_terminal" @@ -2057,17 +2061,27 @@ mr_update() { # Init commit lists - local commit_messages; commit_messages=$(git_commits "$source_branch" "$target_branch") + local commit_reference_combo; commit_reference_combo=$(git_commits_hybrid "$source_branch" "$target_branch") - local current_commits; current_commits=$(echo "$commit_messages" | cut -d ' ' -f1) - local old_commits; old_commits=$(echo "$mr_description" | - grep -Po '^[^0-9a-fA-F]*[0-9a-fA-F]{7,}\s' | - sed -r 's/^[^0-9a-fA-F]*([0-9a-fA-F]{7,})\s/\1/g') + local current_commits_long; current_commits_long=$(echo "$commit_reference_combo" | cut -d ' ' -f1) + local current_commits_short; current_commits_short=$(echo "$commit_reference_combo" | cut -d ' ' -f2) + local commit_messages; commit_messages=$(echo "$commit_reference_combo" | cut -d ' ' -f3-) - local old_commits_array current_commits_array oIFS + local old_commit_lines; old_commit_lines=$(echo "$mr_description" | grep -P "${_git_mr_commit_regex}") + + local old_commits; old_commits=$(echo "$old_commit_lines" | + sed -r -e "s/^${_git_mr_commit_prefix}(${_git_mr_commit_ref})\s.*/\1/g" \ + -e "s/^${_git_mr_commit_prefix}\[(${_git_mr_commit_ref})\]\(http[^)]+\)\s.*/\1/g") + + local old_commits_array oIFS oIFS=$IFS; IFS=$'\n' # shellcheck disable=SC2206 - current_commits_array=($current_commits) + current_commits_long_array=($current_commits_long) + # shellcheck disable=SC2206 + current_commits_short_array=($current_commits_short) + # shellcheck disable=SC2206 + current_commits_messages_array=($commit_messages) + # shellcheck disable=SC2206 old_commits_array=($old_commits) IFS=$oIFS @@ -2090,7 +2104,7 @@ mr_update() { fi local updated_commit_count=0 - local new_commit= + local upgraded_link_count=0 local new_commit_messages_display=() local new_commit_messages_content=() @@ -2103,43 +2117,94 @@ mr_update() { local wrongColor="lightred" # Iterate over commit lists, compare sha-1 and update description - for i in ${!current_commits_array[*]}; do + for i in ${!current_commits_long_array[*]}; do + + local oldSha=${old_commits_array[$i]} + + local currSha=${current_commits_short_array[$i]} + local currShaaaa=${current_commits_long_array[$i]} + local currMsg=${current_commits_messages_array[$i]} + + local oldCommitUrl="$mr_url/diffs?commit_id=$oldSha" + local currCommitUrl="$mr_url/diffs?commit_id=$currShaaaa" + + local msgSuffix matchOldCommitUrl matchOldLogOrLink matchOldLog matchLink + + msgSuffix="(.*)($(regex_escape "$MD_BOLD"))($(regex_escape "$MD_BR"))?" + matchOldCommitUrl="$(regex_escape "$oldCommitUrl")[0-9a-fA-F]*" + + matchOldLogOrLink="^(${_git_mr_commit_prefix})(${oldSha}|\[${oldSha}\]\(${matchOldCommitUrl}\)) ${msgSuffix}" + matchOldLog="^(${_git_mr_commit_prefix})(${oldSha}) ${msgSuffix}" + matchLink="^(${_git_mr_commit_prefix})(\[${oldSha}\]\(${matchOldCommitUrl}\)) ${msgSuffix}" + + local consoleShaColor + if [[ -n $oldSha ]]; then if [[ $oldSha == "$currSha" ]]; then + consoleShaColor="$sameColor"; else + consoleShaColor="$updatedColor"; fi; else + consoleShaColor="$newColor"; fi + + local forConsole_converted forConsole_link forDescription forDescriptionNew + forConsole_converted="\1$(colorize "$currSha" "$consoleShaColor")🔗✔ \3\4${MD_BR}" + forConsole_link="\1$(colorize "$currSha" "$consoleShaColor")🔗 \3\4${MD_BR}" + forDescription="\1[$currSha]($(regex_escape "$currCommitUrl")) \3\4${MD_BR}" + forDescriptionNew="\1[$currSha]($(regex_escape "$currCommitUrl")) \3" + + if [[ -n $oldSha ]]; then + # Existing commit + # echo "🚧 Existing" + new_description_display_before="$new_description_display" + + # shellcheck disable=SC2001 + new_description_display=$(echo "$new_description_display" | sed -r -e "s/${matchOldLog}/${forConsole_converted}/g") + if [[ "$new_description_display" != "$new_description_display_before" ]]; then + upgraded_link_count=$((upgraded_link_count + 1)) + else + # shellcheck disable=SC2001 + new_description_display=$(echo "$new_description_display" | sed -r -e "s/${matchLink}/${forConsole_link}/g") + fi - local curr=${current_commits_array[$i]} - local old=${old_commits_array[$i]} + # shellcheck disable=SC2001 + new_description_content=$(echo "$new_description_content" | sed -r -e "s/${matchOldLogOrLink}/${forDescription}/g") - if [[ -n $old ]]; then - if [[ $old == "$curr" ]]; then - # same sha-1 - only decorate - new_description_display=${new_description_display//"$old"/"$(colorize "$curr" "$sameColor")"} - new_description_content=${new_description_content//"$old"/"$curr"} - else - # different sha-1 - replace & decorate - new_description_display=${new_description_display//"$old"/"$(colorize "$curr" "$updatedColor")"} - new_description_content=${new_description_content//"$old"/"$curr"} + if [[ $oldSha != "$currSha" ]]; then + # Updated commit updated_commit_count=$((updated_commit_count + 1)) fi else - # new commit - [[ $GIT_MR_EXTENDED -eq 1 ]] && - new_commit="$(git_commit_extended "$curr")" || - new_commit="$(echo "$commit_messages" | grep "$curr")" + # New commit + local new_commit_line_display new_commit_line_content + + if [[ $GIT_MR_EXTENDED -eq 1 ]]; then + # shellcheck disable=SC2001 + new_commit_line_display="$(git_commit_extended_console_display "$currSha")" + # shellcheck disable=SC2001 + new_commit_line_content="$(echo "$new_commit_line_display" | sed -r -e "s/^(\* \*\*)(${currSha}) (.*)/${forDescriptionNew}/")" + # shellcheck disable=SC2001 + new_commit_line_display="$(echo "$new_commit_line_display" | + sed -r -e "s/^(\* \*\*)(${currSha}) (.*)$/\1$(colorize "$currSha" "$consoleShaColor")🔗 \3/" + )" + else + new_commit_line_display="$(colorize "$currSha" "$consoleShaColor")🔗 ${currMsg}" + new_commit_line_content="[$currSha]($currCommitUrl) ${currMsg}" + fi - new_commit_messages_display+=("${new_commit//"$curr"/$(colorize "$curr" "$newColor")}") - new_commit_messages_content+=("$new_commit") + new_commit_messages_display+=("$new_commit_line_display") + new_commit_messages_content+=("$new_commit_line_content") fi done local old_commit_count=${#old_commits_array[@]} - local current_commit_count=${#current_commits_array[@]} + local current_commit_count=${#current_commits_short_array[@]} local new_commit_count=${#new_commit_messages_content[@]} local unknown_commit_count=$((old_commit_count - current_commit_count)) if [[ $unknown_commit_count -gt 0 ]]; then for (( i = current_commit_count; i < old_commit_count; i++ )); do - old="${old_commits_array[$i]}" - new_description_display=${new_description_display//"$old"/"$(colorize "$old" "$wrongColor")"} + oldSha="${old_commits_array[$i]}" + new_description_display=${new_description_display//"$oldSha"/"$(colorize "$oldSha" "$wrongColor")"} done + else + unknown_commit_count=0 fi # implode arrays @@ -2163,8 +2228,15 @@ mr_update() { echo echo "--------------------------------------------------------------------------------" echo - echo " updated commits: $(colorize "$updated_commit_count" "$updatedColor")" - echo " new commits: $(colorize "$new_commit_count" "$newColor")" + if [[ $upgraded_link_count -gt 0 ]]; then + echo " upgraded links: $(colorize "$upgraded_link_count" "$sameColor") 🔗" + fi + if [[ $updated_commit_count -gt 0 ]]; then + echo " updated commits: $(colorize "$updated_commit_count" "$updatedColor")" + fi + if [[ $new_commit_count -gt 0 ]]; then + echo " new commits: $(colorize "$new_commit_count" "$newColor")" + fi if [[ $unknown_commit_count -gt 0 ]]; then echo echo " unknown commits: $(colorize "$unknown_commit_count" "$wrongColor")" @@ -2181,7 +2253,7 @@ mr_update() { echo_error fi - if [[ $((updated_commit_count + new_commit_count)) -gt 0 ]]; then + if [[ $((upgraded_link_count + updated_commit_count + new_commit_count)) -gt 0 ]]; then update_prompt=1 local remote; remote=$(gitlab_remote) @@ -2690,6 +2762,11 @@ GIT_MR_MENU_START=${GIT_MR_MENU_START:-"## Menu"} GIT_MR_MENU_END=${GIT_MR_MENU_END:-"--------------------------------------------------------------------------------"} GIT_MR_MENU_UPDATE_CONTEXT_LINES=${GIT_MR_MENU_UPDATE_CONTEXT_LINES:-15} +_git_mr_commit_prefix='[^0-9a-fA-F]*' +_git_mr_commit_ref='[0-9a-fA-F]{7,}' +_git_mr_commit_href="\[${_git_mr_commit_ref}\]\(http[^)]+\)" +_git_mr_commit_regex="^${_git_mr_commit_prefix}(${_git_mr_commit_ref}|${_git_mr_commit_href})\s" + # Custom file descriptors git_mr_fd_mr=${GIT_MR_FD_MR=} git_mr_fd_th=${GIT_MR_FD_TH=} diff --git a/test/git-mr.bats b/test/git-mr.bats index 6a0b503..ab0f994 100644 --- a/test/git-mr.bats +++ b/test/git-mr.bats @@ -134,6 +134,11 @@ full_sha() { git rev-parse "$(short_sha "$1")" } +sha_link() { + local mr_url="some/project/-/merge_requests/1" + echo "[${1}](https://${GITLAB_DOMAIN}/${mr_url}/diffs?commit_id=$(git rev-parse "${1}"))" +} + ################################################################################ # Git functions @@ -355,7 +360,7 @@ full_sha() { @test "Shows commit with commit body" { sha2=$(short_sha "Feature test - 2") - run git_commit_extended "$sha2" + run git_commit_extended_console_display "$sha2" assert_output "$(cat <<- EOF * **${sha2} Feature test - 2**.. @@ -483,8 +488,8 @@ full_sha() { } @test "Escapes regex literals" { - run regex_escape '[] \/ $ * . ^ []' - assert_output '\[\] \\\/ \$ \* \. \^ \[\]' + run regex_escape '[] \/ $ * . ^ ? []' + assert_output '\[\] \\\/ \$ \* \. \^ \? \[\]' } @test "Has read-only mode" { @@ -856,9 +861,9 @@ full_sha() { @test "Generates MR description from commits" { load "test_helper/jira-mock.bash" - c1sha=$(short_sha "Feature test - 1") - c2sha=$(short_sha "Feature test - 2") - c3sha=$(short_sha "Feature test - 3") + local c1sha=$(short_sha "Feature test - 1") + local c2sha=$(short_sha "Feature test - 2") + local c3sha=$(short_sha "Feature test - 3") GIT_MR_NO_COMMITS=1 GIT_MR_EXTENDED= @@ -889,7 +894,7 @@ full_sha() { GIT_MR_EXTENDED=1 run mr_description - empty="" + local empty="" assert_output "$(cat <<- EOF # [AB-123 This is an issue](https://mycompany.example.net/browse/AB-123) @@ -975,18 +980,29 @@ full_sha() { } @test "Identifies first commit line in description" { + + # Initial format mr_description="XY-1234 Some Feature {1} ## Commits {2} * **431561fff0 XY-1234 Donec id justo ut nisi** {3} * **472c4a8cb9 XY-1234 Pellentesque** {4}" run mr_description_first_commit_line "$mr_description" assert_output "3" + + # Proper merge request commit links format + mr_description="XY-1234 Some Feature {1} +## Commits {2} +* **[a2678c36f3](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=a2678c36f307caedd20a11ecc6b3dc615b2bf7ed) XY-1234 Donec id justo ut nisi** {3} +* **[97278d35aa](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=97278d35aa2e62677bff1adf0a1824f0a0357ee3) XY-1234 Pellentesque** {4}" + run mr_description_first_commit_line "$mr_description" + assert_output "3" } @test "Identifies last commit line in description" { indent=" " # Standard commit list - Last description line is commit + # Initial format mr_description="XY-1234 Some Feature {1} ## Commits {2} * **431561fff0 XY-1234 Donec id justo ut nisi** {3} @@ -994,7 +1010,17 @@ full_sha() { run mr_description_last_commit_line "$mr_description" assert_output "4" + # Proper merge request commit links format + mr_description="XY-1234 Some Feature {1} +## Commits {2} +* **[a2678c36f3](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=a2678c36f307caedd20a11ecc6b3dc615b2bf7ed) XY-1234 Donec id justo ut nisi** {3} +* **[97278d35aa](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=97278d35aa2e62677bff1adf0a1824f0a0357ee3) XY-1234 Pellentesque** {4}" + run mr_description_last_commit_line "$mr_description" + assert_output "4" + + # Standard commit list - Last non-empty description line is commit + # Initial format mr_description="XY-1234 Some Feature {1} ## Commits {2} * **431561fff0 XY-1234 Donec id justo ut nisi** {3} @@ -1003,20 +1029,53 @@ full_sha() { " run mr_description_last_commit_line "$mr_description" assert_output "4" + # Proper merge request commit links format + mr_description="XY-1234 Some Feature {1} +## Commits {2} +* **[a2678c36f3](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=a2678c36f307caedd20a11ecc6b3dc615b2bf7ed) XY-1234 Donec id justo ut nisi** {3} +* **[97278d35aa](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=97278d35aa2e62677bff1adf0a1824f0a0357ee3) XY-1234 Pellentesque** {4} + +" + run mr_description_last_commit_line "$mr_description" + assert_output "4" + # Standard commit list - With additional global description + # Initial format mr_description="XY-1234 Some Feature {1} ## Commits {2} * **431561fff0 XY-1234 Donec id justo ut nisi** {3} * **472c4a8cb9 XY-1234 Pellentesque** {4} +Some Description {7}" + run mr_description_last_commit_line "$mr_description" + assert_output "4" + # Proper merge request commit links format + mr_description="XY-1234 Some Feature {1} +## Commits {2} +* **[a2678c36f3](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=a2678c36f307caedd20a11ecc6b3dc615b2bf7ed) XY-1234 Donec id justo ut nisi** {3} +* **[97278d35aa](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=97278d35aa2e62677bff1adf0a1824f0a0357ee3) XY-1234 Pellentesque** {4} + + Some Description {7}" run mr_description_last_commit_line "$mr_description" assert_output "4" # Commits with extended description - Last description line is commit extended description + # Initial format + mr_description="XY-1234 Some Feature {1} +## Commits {2} +* **431561fff0 XY-1234 Donec id justo ut nisi** {3} +${indent}Curabitur eleifend elit in pellentesque dapibus. {4} +* **472c4a8cb9 XY-1234 Pellentesque** {5} +${indent}Duis bibendum lacus id lacus bibendum gravida. {6} +${indent} +${indent}Pellentesque vulputate risus id posuere malesuada. {8}" + run mr_description_last_commit_line "$mr_description" + assert_output "8" + # Proper merge request commit links format mr_description="XY-1234 Some Feature {1} ## Commits {2} * **431561fff0 XY-1234 Donec id justo ut nisi** {3} @@ -1028,7 +1087,8 @@ ${indent}Pellentesque vulputate risus id posuere malesuada. {8}" run mr_description_last_commit_line "$mr_description" assert_output "8" - # (same with non-indented empty lines in extended description) + # (same with non-indented empty lines in extended description: {7}) + # Initial format mr_description="XY-1234 Some Feature {1} ## Commits {2} * **431561fff0 XY-1234 Donec id justo ut nisi** {3} @@ -1039,8 +1099,21 @@ ${indent}Duis bibendum lacus id lacus bibendum gravida. {6} ${indent}Pellentesque vulputate risus id posuere malesuada. {8}" run mr_description_last_commit_line "$mr_description" assert_output "8" + # Proper merge request commit links format + mr_description="XY-1234 Some Feature {1} +## Commits {2} +* **[a2678c36f3](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=a2678c36f307caedd20a11ecc6b3dc615b2bf7ed) XY-1234 Donec id justo ut nisi** {3} +${indent}Curabitur eleifend elit in pellentesque dapibus. {4} +* **[97278d35aa](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=97278d35aa2e62677bff1adf0a1824f0a0357ee3) XY-1234 Pellentesque** {4} +${indent}Duis bibendum lacus id lacus bibendum gravida. {6} + +${indent}Pellentesque vulputate risus id posuere malesuada. {8}" + run mr_description_last_commit_line "$mr_description" + assert_output "8" + # Commits with extended description - Last non-empty description line is commit extended description + # Initial format mr_description="XY-1234 Some Feature {1} ## Commits {2} * **431561fff0 XY-1234 Donec id justo ut nisi** {3} @@ -1052,10 +1125,25 @@ ${indent}Pellentesque vulputate risus id posuere malesuada. {8} " - # (same with trailing indented line in extended description) + run mr_description_last_commit_line "$mr_description" + assert_output "8" + # Proper merge request commit links format + mr_description="XY-1234 Some Feature {1} +## Commits {2} +* **[a2678c36f3](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=a2678c36f307caedd20a11ecc6b3dc615b2bf7ed) XY-1234 Donec id justo ut nisi** {3} +${indent}Curabitur eleifend elit in pellentesque dapibus. {4} +* **[97278d35aa](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=97278d35aa2e62677bff1adf0a1824f0a0357ee3) XY-1234 Pellentesque** {4} +${indent}Duis bibendum lacus id lacus bibendum gravida. {6} +${indent} +${indent}Pellentesque vulputate risus id posuere malesuada. {8} + + +" run mr_description_last_commit_line "$mr_description" assert_output "8" + # (same with trailing indented line in extended description {9}) + # Initial format mr_description="XY-1234 Some Feature {1} ## Commits {2} * **431561fff0 XY-1234 Donec id justo ut nisi** {3} @@ -1067,11 +1155,27 @@ ${indent}Pellentesque vulputate risus id posuere malesuada. {8} ${indent} +" + run mr_description_last_commit_line "$mr_description" + assert_output "9" + # Proper merge request commit links format + mr_description="XY-1234 Some Feature {1} +## Commits {2} +* **[a2678c36f3](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=a2678c36f307caedd20a11ecc6b3dc615b2bf7ed) XY-1234 Donec id justo ut nisi** {3} +${indent}Curabitur eleifend elit in pellentesque dapibus. {4} +* **[97278d35aa](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=97278d35aa2e62677bff1adf0a1824f0a0357ee3) XY-1234 Pellentesque** {4} +${indent}Duis bibendum lacus id lacus bibendum gravida. {6} +${indent} +${indent}Pellentesque vulputate risus id posuere malesuada. {8} +${indent} + + " run mr_description_last_commit_line "$mr_description" assert_output "9" - # (same with non-indented empty lines in extended description) + # (same with non-indented empty lines in extended description {9-10}) + # Initial format mr_description="XY-1234 Some Feature {1} ## Commits {2} * **431561fff0 XY-1234 Donec id justo ut nisi** {3} @@ -1085,8 +1189,24 @@ ${indent}Pellentesque vulputate risus id posuere malesuada. {8} " run mr_description_last_commit_line "$mr_description" assert_output "8" + # Proper merge request commit links format + mr_description="XY-1234 Some Feature {1} +## Commits {2} +* **[a2678c36f3](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=a2678c36f307caedd20a11ecc6b3dc615b2bf7ed) XY-1234 Donec id justo ut nisi** {3} +${indent}Curabitur eleifend elit in pellentesque dapibus. {4} +* **[97278d35aa](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=97278d35aa2e62677bff1adf0a1824f0a0357ee3) XY-1234 Pellentesque** {4} +${indent}Duis bibendum lacus id lacus bibendum gravida. {6} + +${indent}Pellentesque vulputate risus id posuere malesuada. {8} + + +" + run mr_description_last_commit_line "$mr_description" + assert_output "8" + # Commits with extended description - With additional global description + # Initial format mr_description="XY-1234 Some Feature {1} ## Commits {2} * **431561fff0 XY-1234 Donec id justo ut nisi** {3} @@ -1095,11 +1215,24 @@ ${indent}Curabitur eleifend elit in pellentesque dapibus. {4} ${indent}Duis bibendum lacus id lacus bibendum gravida. {6} ${indent} ${indent}Pellentesque vulputate risus id posuere malesuada. {8} +Some global description. {9}" + run mr_description_last_commit_line "$mr_description" + assert_output "8" + # Proper merge request commit links format + mr_description="XY-1234 Some Feature {1} +## Commits {2} +* **[a2678c36f3](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=a2678c36f307caedd20a11ecc6b3dc615b2bf7ed) XY-1234 Donec id justo ut nisi** {3} +${indent}Curabitur eleifend elit in pellentesque dapibus. {4} +* **[97278d35aa](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=97278d35aa2e62677bff1adf0a1824f0a0357ee3) XY-1234 Pellentesque** {4} +${indent}Duis bibendum lacus id lacus bibendum gravida. {6} +${indent} +${indent}Pellentesque vulputate risus id posuere malesuada. {8} Some global description. {9}" run mr_description_last_commit_line "$mr_description" assert_output "8" # (same with additional blank lines before global description) + # Initial format mr_description="XY-1234 Some Feature {1} ## Commits {2} * **431561fff0 XY-1234 Donec id justo ut nisi** {3} @@ -1110,12 +1243,28 @@ ${indent} ${indent}Pellentesque vulputate risus id posuere malesuada. {8} +Nulla eget sem semper, scelerisque enim nec, pellentesque nisi. {11} +${indent}With unrelated indent {12}" + run mr_description_last_commit_line "$mr_description" + assert_output "8" + # Proper merge request commit links format + mr_description="XY-1234 Some Feature {1} +## Commits {2} +* **[a2678c36f3](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=a2678c36f307caedd20a11ecc6b3dc615b2bf7ed) XY-1234 Donec id justo ut nisi** {3} +${indent}Curabitur eleifend elit in pellentesque dapibus. {4} +* **[97278d35aa](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=97278d35aa2e62677bff1adf0a1824f0a0357ee3) XY-1234 Pellentesque** {4} +${indent}Duis bibendum lacus id lacus bibendum gravida. {6} +${indent} +${indent}Pellentesque vulputate risus id posuere malesuada. {8} + + Nulla eget sem semper, scelerisque enim nec, pellentesque nisi. {11} ${indent}With unrelated indent {12}" run mr_description_last_commit_line "$mr_description" assert_output "8" # (same with non-indented empty lines in extended description, and indented lines global description) + # Initial format mr_description="XY-1234 Some Feature {1} ## Commits {2} * **431561fff0 XY-1234 Donec id justo ut nisi** {3} @@ -1126,6 +1275,23 @@ ${indent}Duis bibendum lacus id lacus bibendum gravida. {6} ${indent}Pellentesque vulputate risus id posuere malesuada. {8} +Nulla eget sem semper, scelerisque enim nec, pellentesque nisi. {11} +* Fusce vitae sem {12} +${indent}non mi egestas dignissim {13} +Nunc vitae {14}" + run mr_description_last_commit_line "$mr_description" + assert_output "8" + # Proper merge request commit links format + mr_description="XY-1234 Some Feature {1} +## Commits {2} +* **[a2678c36f3](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=a2678c36f307caedd20a11ecc6b3dc615b2bf7ed) XY-1234 Donec id justo ut nisi** {3} +${indent}Curabitur eleifend elit in pellentesque dapibus. {4} +* **[97278d35aa](https://${GITLAB_DOMAIN}/my/project/-/merge_requests/22/diffs?commit_id=97278d35aa2e62677bff1adf0a1824f0a0357ee3) XY-1234 Pellentesque** {4} +${indent}Duis bibendum lacus id lacus bibendum gravida. {6} + +${indent}Pellentesque vulputate risus id posuere malesuada. {8} + + Nulla eget sem semper, scelerisque enim nec, pellentesque nisi. {11} * Fusce vitae sem {12} ${indent}non mi egestas dignissim {13} @@ -1162,10 +1328,12 @@ End" assert_output "$(cat <<-EOF # Title ## Commits - * **plop**.. + * **plop** * **new commit**.. EOF )" + # Note: mr_description_insert_new_commits does not handle trailing new line anymore. + # Commit lines are normalized by substition regexes in mr_update } @test "Updates MR description with new commits in new section" { @@ -1174,9 +1342,30 @@ End" GIT_MR_UPDATE_NEW_SECTION=1 - c1sha=$(short_sha "Feature test - 1") - c2sha=$(short_sha "Feature test - 2") - c3sha=$(short_sha "Feature test - 3") + local c1sha=$(short_sha "Feature test - 1") + local c2sha=$(short_sha "Feature test - 2"); local c2href=$(sha_link "$c2sha") + local c3sha=$(short_sha "Feature test - 3"); local c3href=$(sha_link "$c3sha") + + # Ensure remote branch is up-to-date + git-push gitlab feature/AB-123-test-feature --force + + run mr_update <<< 'n' + assert_output "$(cat <<- EOF + + + [AB-123 Test issue](https://mycompany.example.net/browse/AB-123) + + ## Commits + + * **${c1sha}🔗✔ Feature test - descr 1**.. + * **${c2sha}🔗 Feature test - descr 2**.. + * **${c3sha}🔗 Feature test - descr 3**.. + + -------------------------------------------------------------------------------- + + upgraded links: 1 🔗 + EOF + )" # Amend last commit git reset --hard HEAD~1 @@ -1200,17 +1389,18 @@ End" ## Commits - * **${c1sha} Feature test - 1**.. - * **${c2sha} Feature test - 2**.. - * **${c3shaNew} Feature test - 3**.. + * **${c1sha}🔗✔ Feature test - descr 1**.. + * **${c2sha}🔗 Feature test - descr 2**.. + * **${c3shaNew}🔗 Feature test - descr 3**.. ## Update - * **${c4shaNew} Feature test - 4**.. - * **${c5shaNew} Feature test - 5**.. + * **${c4shaNew}🔗 Feature test - 4**.. + * **${c5shaNew}🔗 Feature test - 5**.. -------------------------------------------------------------------------------- + upgraded links: 1 🔗 updated commits: 1 new commits: 2 @@ -1226,17 +1416,18 @@ End" ## Commits - * **${c1sha} Feature test - 1**.. - * **${c2sha} Feature test - 2**.. - * **${c3shaNew} Feature test - 3**.. + * **${c1sha}🔗✔ Feature test - descr 1**.. + * **${c2sha}🔗 Feature test - descr 2**.. + * **${c3shaNew}🔗 Feature test - descr 3**.. ## Cleanup & refactor - * **${c4shaNew} Feature test - 4**.. - * **${c5shaNew} Feature test - 5**.. + * **${c4shaNew}🔗 Feature test - 4**.. + * **${c5shaNew}🔗 Feature test - 5**.. -------------------------------------------------------------------------------- + upgraded links: 1 🔗 updated commits: 1 new commits: 2 @@ -1255,26 +1446,53 @@ End" GIT_MR_EXTENDED=1 GIT_MR_UPDATE_NEW_SECTION= - c1sha=$(short_sha "Feature test - 1") - c2sha=$(short_sha "Feature test - 2") - c3sha=$(short_sha "Feature test - 3") + local c1sha=$(short_sha "Feature test - 1") + local c2sha=$(short_sha "Feature test - 2"); local c2href=$(sha_link "$c2sha") + local c3sha=$(short_sha "Feature test - 3"); local c3href=$(sha_link "$c3sha") + + # Ensure remote branch is up-to-date + git-push gitlab feature/AB-123-test-feature --force + + run mr_update <<< 'n' + local empty="" + assert_output "$(cat <<-EOF + + + [AB-123 Test issue](https://mycompany.example.net/browse/AB-123) + + ## Commits + + * **${c1sha}🔗✔ Feature test - descr 1**.. + * **${c2sha}🔗 Feature test - descr 2**.. + This is my second commit.. + * **${c3sha}🔗 Feature test - descr 3**.. + This is my third commit.. + ${empty} + With an extended description + + -------------------------------------------------------------------------------- + + upgraded links: 1 🔗 + + EOF + )" # Amend last commit git reset --hard HEAD~1 git commit --allow-empty -m "Feature test - 3" -m "Updated" - c3shaNew=$(git rev-parse --short HEAD) + local c3shaNew=$(git rev-parse --short HEAD) # Add new commits git commit --allow-empty -m "Feature test - 4" -m "With extended message too" - c4shaNew=$(git rev-parse --short HEAD) + local c4shaNew=$(git rev-parse --short HEAD) git commit --allow-empty -m "Feature test - 5" - c5shaNew=$(git rev-parse --short HEAD) + local c5shaNew=$(git rev-parse --short HEAD) # Ensure remote branch is up-to-date git-push gitlab feature/AB-123-test-feature --force run mr_update <<< 'n' - empty="" + local empty="" assert_output "$(cat <<-EOF @@ -1282,19 +1500,20 @@ End" ## Commits - * **${c1sha} Feature test - 1**.. - * **${c2sha} Feature test - 2**.. + * **${c1sha}🔗✔ Feature test - descr 1**.. + * **${c2sha}🔗 Feature test - descr 2**.. This is my second commit.. - * **${c3shaNew} Feature test - 3**.. + * **${c3shaNew}🔗 Feature test - descr 3**.. This is my third commit.. ${empty} With an extended description - * **${c4shaNew} Feature test - 4**.. + * **${c4shaNew}🔗 Feature test - 4**.. With extended message too.. - * **${c5shaNew} Feature test - 5**.. + * **${c5shaNew}🔗 Feature test - 5**.. -------------------------------------------------------------------------------- + upgraded links: 1 🔗 updated commits: 1 new commits: 2 @@ -1310,13 +1529,16 @@ End" load "test_helper/gitlab-mock-mr-description-simple.bash" load "test_helper/gitlab-mock-mr-update.bash" - c1sha=$(short_sha "Feature test - 1") - c2sha=$(short_sha "Feature test - 2") - c3sha=$(short_sha "Feature test - 3") + local c1sha=$(short_sha "Feature test - 1") + local c2sha=$(short_sha "Feature test - 2"); local c2href=$(sha_link "$c2sha") + local c3sha=$(short_sha "Feature test - 3"); local c3href=$(sha_link "$c3sha") + + # Ensure remote branch is up-to-date + git-push gitlab feature/AB-123-test-feature --force # Add new commit git commit --allow-empty -m "Feature test - 4" - c4shaNew=$(git rev-parse --short HEAD) + local c4shaNew=$(git rev-parse --short HEAD) run mr_update <<< 'n' assert_output "$(cat <<- EOF @@ -1326,14 +1548,14 @@ End" ## Commits - * **${c1sha} Feature test - 1**.. - * **${c2sha} Feature test - 2**.. - * **${c3sha} Feature test - 3**.. - * **${c4shaNew} Feature test - 4**.. + * **${c1sha}🔗✔ Feature test - descr 1**.. + * **${c2sha}🔗 Feature test - descr 2**.. + * **${c3sha}🔗 Feature test - descr 3**.. + * **${c4shaNew}🔗 Feature test - 4**.. -------------------------------------------------------------------------------- - updated commits: 0 + upgraded links: 1 🔗 new commits: 1 Remote branch on gitlab is not up-to-date with local branch feature/AB-123-test-feature. @@ -1348,14 +1570,14 @@ End" ## Commits - * **${c1sha} Feature test - 1**.. - * **${c2sha} Feature test - 2**.. - * **${c3sha} Feature test - 3**.. - * **${c4shaNew} Feature test - 4**.. + * **${c1sha}🔗✔ Feature test - descr 1**.. + * **${c2sha}🔗 Feature test - descr 2**.. + * **${c3sha}🔗 Feature test - descr 3**.. + * **${c4shaNew}🔗 Feature test - 4**.. -------------------------------------------------------------------------------- - updated commits: 0 + upgraded links: 1 🔗 new commits: 1 Remote branch on gitlab is not up-to-date with local branch feature/AB-123-test-feature. @@ -1371,17 +1593,17 @@ End" load "test_helper/gitlab-mock-mr-description-simple.bash" load "test_helper/gitlab-mock-mr-update.bash" - c1sha=$(short_sha "Feature test - 1") - c2sha=$(short_sha "Feature test - 2") - c3sha=$(short_sha "Feature test - 3") + local c1sha=$(short_sha "Feature test - 1") + local c2sha=$(short_sha "Feature test - 2"); local c2href=$(sha_link "$c2sha") + local c3sha=$(short_sha "Feature test - 3"); local c3href=$(sha_link "$c3sha") # Create new branch which will be detected as base - baseCommit=$(short_sha "Feature base - 3") + local baseCommit=$(short_sha "Feature base - 3") git branch newbase "$baseCommit" # Add new commit git commit --allow-empty -m "Feature test - 4" - c4shaNew=$(git rev-parse --short HEAD) + local c4shaNew=$(git rev-parse --short HEAD) # Ensure remote branch is up-to-date git-push gitlab feature/AB-123-test-feature --force @@ -1390,7 +1612,7 @@ End" assert_output --partial "$(cat <<-EOF -------------------------------------------------------------------------------- - updated commits: 0 + upgraded links: 1 🔗 new commits: 1 Target branch 'newbase' does not exist on remote. @@ -1407,19 +1629,19 @@ End" load "test_helper/gitlab-mock-mr-description-simple.bash" load "test_helper/gitlab-mock-mr-update.bash" - c1sha=$(short_sha "Feature test - 1") - c2sha=$(short_sha "Feature test - 2") - c3sha=$(short_sha "Feature test - 3") + local c1sha=$(short_sha "Feature test - 1") + local c2sha=$(short_sha "Feature test - 2"); local c2href=$(sha_link "$c2sha") + local c3sha=$(short_sha "Feature test - 3"); local c3href=$(sha_link "$c3sha") # simulate commit ref base (when 1st possible merge base is used) - baseCommit=$(short_sha "Feature base - 3") + local baseCommit=$(short_sha "Feature base - 3") git_base_branch() { echo "$baseCommit" } # Add new commit git commit --allow-empty -m "Feature test - 4" - c4shaNew=$(git rev-parse --short HEAD) + local c4shaNew=$(git rev-parse --short HEAD) # Ensure remote branch is up-to-date git-push gitlab feature/AB-123-test-feature --force @@ -1428,7 +1650,7 @@ End" assert_output --partial "$(cat <<-EOF -------------------------------------------------------------------------------- - updated commits: 0 + upgraded links: 1 🔗 new commits: 1 @@ -1447,9 +1669,9 @@ End" load "test_helper/gitlab-mock-mr-description-simple.bash" load "test_helper/gitlab-mock-mr-update.bash" - c1sha=$(short_sha "Feature test - 1") - c2sha=$(short_sha "Feature test - 2") - c3sha=$(short_sha "Feature test - 3") + local c1sha=$(short_sha "Feature test - 1") + local c2sha=$(short_sha "Feature test - 2"); local c2href=$(sha_link "$c2sha") + local c3sha=$(short_sha "Feature test - 3"); local c3href=$(sha_link "$c3sha") # Create new branch which will be detected as base baseCommit=$(short_sha "Feature base - 3") @@ -1459,8 +1681,8 @@ End" assert_output --partial "$(cat <<-EOF -------------------------------------------------------------------------------- + upgraded links: 1 🔗 updated commits: 2 - new commits: 0 unknown commits: 1 @@ -1479,24 +1701,24 @@ End" GIT_MR_EXTENDED=1 GIT_MR_REPLACE_COMMITS=1 - c1sha=$(short_sha "Feature test - 1") - c2sha=$(short_sha "Feature test - 2") - c3sha=$(short_sha "Feature test - 3") + local c1sha=$(short_sha "Feature test - 1") + local c2sha=$(short_sha "Feature test - 2"); local c2href=$(sha_link "$c2sha") + local c3sha=$(short_sha "Feature test - 3"); local c3href=$(sha_link "$c3sha") # Amend first commit git reset --soft "$c1sha" git commit --amend --allow-empty -m "Feature test - new 1" -m "Updated" - c1shaNew=$(git rev-parse --short HEAD) + local c1shaNew=$(git rev-parse --short HEAD) # Add new commit git commit --allow-empty -m "Feature test - new 2" -m "With extended message too" - c2shaNew=$(git rev-parse --short HEAD) + local c2shaNew=$(git rev-parse --short HEAD) # Ensure remote branch is up-to-date git-push gitlab feature/AB-123-test-feature --force run mr_update <<< 'n' - empty="" + local empty="" assert_output "$(cat <<-EOF @@ -1504,14 +1726,13 @@ End" ## Commits - * **${c1shaNew} Feature test - new 1**.. + * **${c1shaNew}🔗 Feature test - new 1**.. Updated.. - * **${c2shaNew} Feature test - new 2**.. + * **${c2shaNew}🔗 Feature test - new 2**.. With extended message too.. -------------------------------------------------------------------------------- - updated commits: 0 new commits: 2 EOF diff --git a/test/test_helper/gitlab-mock-mr-description-extended.bash b/test/test_helper/gitlab-mock-mr-description-extended.bash index 7e3532f..30bfa8a 100644 --- a/test/test_helper/gitlab-mock-mr-description-extended.bash +++ b/test/test_helper/gitlab-mock-mr-description-extended.bash @@ -2,10 +2,10 @@ gitlab_mr_description() { echo -n "[AB-123 Test issue](https://mycompany.example.net/browse/AB-123)" echo -n "\n\n## Commits" - echo -n "\n\n* **${c1sha} Feature test - 1**.." - echo -n "\n* **${c2sha} Feature test - 2**.." + echo -n "\n\n* **${c1sha} Feature test - descr 1**" + echo -n "\n* **${c2href} Feature test - descr 2**.." echo -n "\n This is my second commit.." - echo -n "\n* **${c3sha} Feature test - 3**.." + echo -n "\n* **${c3href} Feature test - descr 3**.." echo -n "\n This is my third commit.." echo -n "\n " echo -n "\n With an extended description" diff --git a/test/test_helper/gitlab-mock-mr-description-simple.bash b/test/test_helper/gitlab-mock-mr-description-simple.bash index cc66cfc..2488d5f 100644 --- a/test/test_helper/gitlab-mock-mr-description-simple.bash +++ b/test/test_helper/gitlab-mock-mr-description-simple.bash @@ -2,7 +2,7 @@ gitlab_mr_description() { echo -n "[AB-123 Test issue](https://mycompany.example.net/browse/AB-123)" echo -n "\n\n## Commits" - echo -n "\n\n* **${c1sha} Feature test - 1**.." - echo -n "\n* **${c2sha} Feature test - 2**.." - echo -n "\n* **${c3sha} Feature test - 3**" + echo -n "\n\n* **${c1sha} Feature test - descr 1**.." + echo -n "\n* **${c2href} Feature test - descr 2**" + echo -n "\n* **${c3href} Feature test - descr 3**" } From 1d98624a97aeb1857fa33091f1b87f1f36216ed0 Mon Sep 17 00:00:00 2001 From: Djuuu Date: Mon, 26 Feb 2024 16:35:21 +0100 Subject: [PATCH 2/2] Update documentation --- README.md | 18 +++++++++++++++ doc/generate-sample-output.sh | 41 ++++++++++++++++++++++++++++++++++ doc/git-mr-update-links.png | Bin 0 -> 17379 bytes 3 files changed, 59 insertions(+) create mode 100644 doc/git-mr-update-links.png diff --git a/README.md b/README.md index 3e5e81c..7f27bab 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,24 @@ You can also update the source branch if it is different from the current one. ![git mr update](doc/git-mr-update.png) +#### Note on commit links + +The initial merge request description lists commit SHA-1 and and message in a simple format. + +When Gitlab recognizes a partial Git SHA-1 in a description, it will automatically create a link to the commit, +but this link has no reference to the current merge request: +> `https://myapp.gitlab.com/my-project/-/commit/0a1b2c3d4e5f` + +If a comment is created from this commit page, it might appear in the merge request at first, +but **it will disappear as soon as the branch is rebased**. + +Once the merge request exists, `git mr update` will convert SHA-1 references to **links to the commit in the merge request diff view**: +> `https://myapp.gitlab.com/my-project/-/merge_requests/123/diffs?commit_id=0a1b2c3d4e5f` + +This view allows navigating through the merge request commits, and comments left on this page will remain attached to the merge request. + +![git mr update links](doc/git-mr-update-links.png) + ---------------------------------------------------------------- ### `git mr menu` diff --git a/doc/generate-sample-output.sh b/doc/generate-sample-output.sh index 4931d4b..5e9d55b 100755 --- a/doc/generate-sample-output.sh +++ b/doc/generate-sample-output.sh @@ -166,6 +166,45 @@ $(mr_print_status "$mr" "$threads") EOF } +sample_mr_update_links() { + fake_prompt "git mr update" + + local mr='{ + "title": "Draft: '"$mr_title"'", "web_url":"'"$mr_url"'", + "labels":["Testing","My Team"], "target_branch": "main", + "upvotes": 2, "downvotes": 0, "merge_status": "can_be_merged", + "head_pipeline": {"status":"running", "web_url":"https://myapp.gitlab.com/my/project/pipelines/6"} + }' + + local threads='1 unresolved:false note_id:2 +2 unresolved:true note_id:2' + + cat << EOF + +$(mr_print_title "$mr_title" "$mr_url") + +$(markdown_title "$ticket_link") + +Vivamus venenatis tortor et neque sollicitudin, eget suscipit est malesuada. +Suspendisse nec odio id arcu sagittis pulvinar ut nec lacus. + +Sed non nulla ac metus congue consectetur et vel magna. + +## Commits + +* **$(c_same "78330c9")🔗✔ In vulputate quam ac ultrices volutpat** +* **$(c_same "0010a6a")🔗✔ Curabitur vel purus sed tortor finibus posuere** +* **$(c_same "aac348f")🔗✔ Aenean sed sem hendrerit ex egestas tincidunt** + +-------------------------------------------------------------------------------- + + upgraded links: $(c_same "3") 🔗 + +$(c_question "Do you want to update the merge request description?") [y/N] + +EOF +} + sample_mr_merge() { fake_prompt "git mr merge" @@ -375,6 +414,7 @@ if [[ "$#" -gt 0 ]]; then ;; update) sample_mr_update + sample_mr_update_links ;; menu) sample_mr_menu @@ -396,6 +436,7 @@ else sample_mr_status sample_mr_update + sample_mr_update_links sample_mr_menu sample_mr_menu_status diff --git a/doc/git-mr-update-links.png b/doc/git-mr-update-links.png new file mode 100644 index 0000000000000000000000000000000000000000..930e6f7b74b3edcdc832bce8470001d90e79a33b GIT binary patch literal 17379 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 literal 0 HcmV?d00001