From 55f08a7a681bec3d6cf3fceac3984f5e1f7acb30 Mon Sep 17 00:00:00 2001 From: Marco Descher Date: Wed, 4 Nov 2026 08:44:02 +0100 Subject: [PATCH] Branch-Tag for 7.0 --- app/assets/javascripts/application-legacy.js | 7 +++ app/assets/javascripts/repository_navigation.js | 5 ++ app/assets/stylesheets/application.css | 3 + app/assets/stylesheets/scm.css | 3 + app/helpers/repositories_helper.rb | 76 +++++++++++++++++++++++++ app/views/issues/tabs/_changesets.html.erb | 7 +++ app/views/repositories/_changeset.html.erb | 14 ++++- app/views/settings/_issues.html.erb | 9 +++ config/locales/de.yml | 4 ++ config/locales/en.yml | 4 ++ config/settings.yml | 4 ++ lib/redmine/scm/adapters/git_adapter.rb | 23 ++++++++ 12 files changed, 158 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/application-legacy.js b/app/assets/javascripts/application-legacy.js index e8c84d0b8..8c2293542 100644 --- a/app/assets/javascripts/application-legacy.js +++ b/app/assets/javascripts/application-legacy.js @@ -1568,3 +1568,10 @@ $(document).ready(setupFilePreviewNavigation); $(document).on('focus', '[data-auto-complete=true]', function(event) { inlineAutoComplete(event.target); }); + +// Branch groups are rendered on the revision page and in the issue +// history tab, which is loaded later. Delegation keeps the expander working. +$(document).on('click', 'a.scm-branch-group', function(e) { + e.preventDefault(); + $(this).parent().removeClass('scm-branch-hide'); +}); diff --git a/app/assets/javascripts/repository_navigation.js b/app/assets/javascripts/repository_navigation.js index 27da5eac1..029f5bcf5 100644 --- a/app/assets/javascripts/repository_navigation.js +++ b/app/assets/javascripts/repository_navigation.js @@ -36,4 +36,9 @@ $(document).ready(function() { $('#branch,#tag').removeAttr('disabled'); } }); + + $('a.scm-branch-group').on('click', function(e) { + e.preventDefault(); + $(this).parent().removeClass('scm-branch-hide'); + }); }) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 0170d41b1..12cad1adf 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -2725,3 +2725,6 @@ th[role=columnheader]:not(.no-sort):hover:after { #sidebar .api-key-actions .copy-api-key-link svg { opacity: 1; } + +.scm-branch-group, .scm-branch-hide > .scm-branches { display: none; } +.scm-branch-hide > .scm-branch-group { display: inline; } diff --git a/app/assets/stylesheets/scm.css b/app/assets/stylesheets/scm.css index 90d8f2ce8..e1d9942df 100644 --- a/app/assets/stylesheets/scm.css +++ b/app/assets/stylesheets/scm.css @@ -181,3 +181,6 @@ table.annotate td.line-code { .breadcrumbs>.separator::before, .breadcrumbs>.separator::after { content: " "; } + +.scm-branch-group, .scm-branch-hide > .scm-branches { display: none; } +.scm-branch-hide > .scm-branch-group { display: inline; } diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb index 7cfbcdda0..b3188ab00 100644 --- a/app/helpers/repositories_helper.rb +++ b/app/helpers/repositories_helper.rb @@ -339,4 +339,80 @@ module RepositoriesHelper end max_space end + + def has_branch_detail?(repository = @repository) + repository.present? && repository.scm.respond_to?(:branch_contains) + rescue Redmine::Scm::Adapters::CommandFailed + false + end + + def insert_branches_detail(html) + return html unless has_branch_detail? + + html = html.to_s.dup + substring = '' + index = html.index(substring) + return html.html_safe unless index + + html.insert(index + substring.length, branches_html) + html.html_safe + end + + def branches_html + content_tag(:li) do + safe_join( + [ + content_tag(:strong, l(:label_branch)), + ' ', + "#{@repository.identifier}@ ", + safe_join(links_to_branches, ', ') + ] + ) + end + end + + def links_to_branches(repository = @repository, rev = @rev) + return [] unless has_branch_detail?(repository) + + branch_groups(repository, rev).map do |name, branches| + branches_link(repository, name, branches) + end + rescue Redmine::Scm::Adapters::CommandFailed + [] + end + + def branches_link(repository, name, branches) + return branch_link(repository, branches.first) if branches.length == 1 + + link = link_to("[#{name}...]", 'javascript:;', :class => 'scm-branch-group') + content_tag(:span, :class => 'scm-branch-hide') do + link + content_tag( + :span, + safe_join(branches.map {|branch| branch_link(repository, branch)}, ', '), + :class => 'scm-branches' + ) + end + end + + def branch_link(repository, branch) + link_to( + branch, + { + :controller => 'repositories', + :action => 'show', + :id => repository.project, + :repository_id => repository.identifier_param, + :path => to_path_param(@path), + :rev => branch + } + ) + end + + def branch_groups(repository = @repository, rev = @rev) + return [] if rev.blank? + + repository.scm.branch_contains(rev).group_by do |branch| + branch.downcase.gsub(/^\d+/, '#####').split(/[\-\._]/).first + end.sort_by {|name, branches| [branches.length, name.to_s]} + end end diff --git a/app/views/issues/tabs/_changesets.html.erb b/app/views/issues/tabs/_changesets.html.erb index 2df4d40c2..a154ba22d 100644 --- a/app/views/issues/tabs/_changesets.html.erb +++ b/app/views/issues/tabs/_changesets.html.erb @@ -20,6 +20,13 @@ :path => "", :rev => changeset.identifier) %>) <% end %> + <% if Setting.display_under_associated_revisions? && changeset.scmid.present? && + has_branch_detail?(changeset.repository) %> + <% branch_links = links_to_branches(changeset.repository, changeset.identifier) %> + <% if branch_links.any? %> + (<%= l(:label_branches) %>: <%= safe_join(branch_links, ', ') %>) + <% end %> + <% end %>

<%= format_changeset_comments changeset %> diff --git a/app/views/repositories/_changeset.html.erb b/app/views/repositories/_changeset.html.erb index 393e03e56..5cdc2c40e 100644 --- a/app/views/repositories/_changeset.html.erb +++ b/app/views/repositories/_changeset.html.erb @@ -5,7 +5,13 @@ <%= avatar(@changeset.user) %> <%= authoring(@changeset.committed_on, @changeset.author) %> - <% if @changeset.scmid.present? || @changeset.parents.present? || @changeset.children.present? %> + <% branch_links = + if Setting.display_under_single_revision? && has_branch_detail? + links_to_branches(@repository, @changeset.scmid.presence || @rev) + else + [] + end %> + <% if @changeset.scmid.present? || @changeset.parents.present? || @changeset.children.present? || branch_links.any? %>
+
+ <%= l(:label_display_revision_branches) %> +
+

<%= setting_check_box :display_under_single_revision %>

+ +

<%= setting_check_box :display_under_associated_revisions %>

+
+
+
<%= l(:label_parent_task_attributes) %>
diff --git a/config/locales/de.yml b/config/locales/de.yml index f91a59563..269ded731 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -462,6 +462,7 @@ de: label_board_sticky: Wichtig (immer oben) label_boolean: Boolean label_branch: Zweig + label_branches: Zweige label_bulk_edit_selected_issues: Alle ausgewählten Tickets bearbeiten label_bulk_edit_selected_time_entries: Ausgewählte Zeitaufwände bearbeiten label_calendar: Kalender @@ -1010,6 +1011,8 @@ de: setting_default_projects_tracker_ids: Standardmäßig aktivierte Tracker für neue Projekte setting_diff_max_lines_displayed: Maximale Anzahl anzuzeigender Diff-Zeilen setting_display_subprojects_issues: Tickets von Unterprojekten im Hauptprojekt anzeigen + setting_display_under_single_revision: Unterschiede bei Revision anzeigen + setting_display_under_associated_revisions: Bei zugehörigen Revisionen anzeigen setting_emails_footer: E-Mail-Fußzeile setting_emails_header: E-Mail-Kopfzeile setting_enabled_scm: Aktivierte Versionskontrollsysteme @@ -1305,6 +1308,7 @@ de: label_ends_with: endet mit label_issue_fixed_version_updated: Zielversion aktualisiert setting_project_list_defaults: Voreinstellungen Projektliste + label_display_revision_branches: Zweige zur Revision anzeigen label_display_type: Ergebnisse anzeigen als label_display_type_list: Liste label_display_type_board: Karte diff --git a/config/locales/en.yml b/config/locales/en.yml index 9d55cad6c..ce1aa105b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -456,6 +456,8 @@ en: setting_time_format: Time format setting_timespan_format: Time span format setting_cross_project_issue_relations: Allow cross-project issue relations + setting_display_under_single_revision: Display revision under Single Revision + setting_display_under_associated_revisions: Display revision under Associated Revisions setting_cross_project_subtasks: Allow cross-project subtasks setting_issue_list_default_columns: Issues list defaults setting_repositories_encodings: Attachments and repositories encodings @@ -855,6 +857,7 @@ en: label_repository_new: New repository label_repository_plural: Repositories label_branch: Branch + label_branches: Branches label_tag: Tag label_revision: Revision label_revision_plural: Revisions @@ -979,6 +982,7 @@ en: label_registration_manual_activation: Account activation by administrator approval label_registration_automatic_activation: Automatic account activation label_display_per_page: "Per page: %{value}" + label_display_revision_branches: Show branches for revision label_age: Age label_change_properties: Change properties label_general: General diff --git a/config/settings.yml b/config/settings.yml index 667a4427f..582d1976d 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -245,6 +245,10 @@ related_issues_default_columns: - done_ratio display_related_issues_table_headers: default: 0 +display_under_single_revision: + default: 1 +display_under_associated_revisions: + default: 1 display_subprojects_issues: default: 1 time_entry_list_defaults: diff --git a/lib/redmine/scm/adapters/git_adapter.rb b/lib/redmine/scm/adapters/git_adapter.rb index d6dec455f..a33ee822e 100644 --- a/lib/redmine/scm/adapters/git_adapter.rb +++ b/lib/redmine/scm/adapters/git_adapter.rb @@ -94,6 +94,29 @@ module Redmine nil end + # Local branch names that contain the given commit. + # Non-word characters are stripped before the value is passed to git. + def branch_contains(hash) + cleaned_hash = hash.to_s.gsub(/[^\w]/, '') + return [] if cleaned_hash.empty? + + cmd_args = %w|branch --no-color --contains| + cmd_args << cleaned_hash + branches = [] + git_cmd(cmd_args) do |io| + branches = + io.readlines.filter_map do |line| + name = line.strip.sub(/\A[*+]\s*/, '') + next if name.empty? + + scm_iconv('UTF-8', @path_encoding, name) + end + end + branches.sort.uniq + rescue ScmCommandAborted + [] + end + def tags return @tags if @tags -- 2.54.0