Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bazaar repository support #15

Open
wants to merge 72 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
fcb281e
mercurial: get client_version correctly even if LANG is set
yuja Mar 22, 2010
cbabb6d
mercurial/redminehelper: helper extension to reduce the number of hg …
yuja Mar 28, 2010
85755de
mercurial: rewrite scm.diff to generate correct changeset diff
yuja Mar 28, 2010
d6dc33e
mercurial: rewrite scm.annotate
yuja Mar 23, 2010
6994ffc
mercurial: rewrite scm.info by using helper ext
yuja Mar 28, 2010
031063a
mercurial: cleaned up fetch_changesets
yuja Mar 28, 2010
f9f93c5
Added Rubymine .idea folder and emacs ~ files to .gitignore
brentsowers1 Sep 28, 2010
5768291
Started making changes to fix Bazaar second/third level revisions not…
brentsowers1 Sep 28, 2010
14b0f58
Updated test bazaar repository to include merges
Sep 28, 2010
caff9b7
Changed all repository links to be safe (no . for Bazaar revisions), …
Sep 28, 2010
ec467fe
Redmine now working successfully with Bazaar. Tests are failing, nee…
Sep 29, 2010
3c88e18
Added ability to clear all changesets and re-fetch, rake redmine:clea…
Sep 29, 2010
e6a925a
Fixed bug from last commit with clear and fetch changesets
Sep 29, 2010
1a4fc3b
More bug fixes for clearing changesets, I'm pretty sure it's working now
Sep 29, 2010
26ea3b3
repository: mercurial: rearrange unit tests.
Oct 10, 2010
97068e5
scm: mercurial: preserve seconds of committed timestamp
Oct 10, 2010
9cd2322
repository: mercurial: sort changesets by revision (fixes #3449, #3567)
yuja Jul 29, 2010
cd746fe
Fixed two general repository bugs that were brought about by bazaar c…
brentsowers1 Oct 12, 2010
c6d46fc
Fixed more repository bugs introduced in bazaar changes. All reposit…
brentsowers1 Oct 12, 2010
62156a3
Fixed annotate in bazaar, all bazaar unit tests now pass
brentsowers1 Oct 12, 2010
d829950
Working on getting bazaar repository controller tests to pass, still …
brentsowers1 Oct 13, 2010
0b1340d
More fixes to the bazaar repository controller test, only one failure…
brentsowers1 Oct 14, 2010
83e731f
Moved . in revision path fix from helper methods and substitutions in…
coordinatecommons Oct 17, 2010
107a754
Fix to last commit for Bazaar revision IDs
coordinatecommons Oct 17, 2010
4282ef0
Bazaar repository controller test now passes. Still need to add more…
coordinatecommons Oct 17, 2010
ee9e1ac
Removed some unused code from Bazaar adapter, started adding unit tes…
brentsowers1 Oct 18, 2010
64432bb
repository: mercurial: accept both of revision and nodeid as changese…
Oct 18, 2010
4e7aeac
changeset, revision: add attrs to identify the changeset (refs #3724)
yuja Mar 23, 2010
0b04f15
changeset, revision: add format_identifier for human-readable revisio…
Oct 18, 2010
d2e7c4b
helpers: accecpt Changeset or Revision obj
Oct 18, 2010
f248cfa
repository: pass object to format_revision and link_to_revision, use …
yuja Mar 23, 2010
0bfd4f3
helper: use scmid for "commit:xxx" link if available
Oct 18, 2010
46c9307
activity: use scmid and format_revision if possible
Oct 18, 2010
e699366
repository: don't truncate revision string of input box
yuja Apr 1, 2010
221fa00
changeset: prefer hash id than revision number for auto-close issue text
Oct 18, 2010
e63463e
issue: show scmid on related revisions
Oct 18, 2010
e66149d
repository: catch CommandFailed during bulk fetch_changesets
Oct 18, 2010
13a0410
mercurial/redminehelper: helper extension to fetch info from hg repo …
Mar 28, 2010
1786639
imported patch for-yuya/for-yuya.diff
Oct 18, 2010
1e40760
mercurial: introduced helper method to run hg command
yuja Mar 28, 2010
d1f18f6
imported patch hg/annotate-test.diff
Oct 18, 2010
d7acbb5
mercurial: rewrite MercurialAdapter.revisions as iterator
yuja Mar 28, 2010
fd24782
Completed Bazaar adapter unit test
brentsowers1 Oct 27, 2010
dc07f33
Merge commit 'upstream/master'
brentsowers1 Oct 27, 2010
b429edf
Merge remote branch 'origin/master' into hg-overhaul-mini-mq-c140cebc
marutosi Oct 27, 2010
75bcaa1
Merge with hg-overhaul-mini-mq-c140cebc and bazaar.
Oct 27, 2010
749622d
fix bazaar adapter revisions().
Oct 27, 2010
45b240f
fix app bazaar.
Oct 27, 2010
88d1b5e
fix bazaar adapter info.
Oct 27, 2010
87534ce
Bazaar diff controller action bug fix
coordinatecommons Oct 27, 2010
ba38fff
Merge remote branch 'origin/master' into hg-overhaul-mini-mq-c140cebc
marutosi Oct 28, 2010
41888cb
Updated Bazaar tests to work with latest changes from Mercurial chang…
brentsowers1 Oct 28, 2010
bd57df0
Merge commit 'upstream/master'
brentsowers1 Oct 28, 2010
ed15aef
Merge.
Oct 29, 2010
c107664
Merge and revert .gitignore and app/controllers/repositories_controll…
Oct 29, 2010
6f71f08
Fix app/models/repository/bazaar.rb comments
Oct 29, 2010
bb5bd08
Bazaar diff controller action bug fix
Oct 29, 2010
1ddc977
Fixed bug in revision display on diff and annotate for Bazaar
brentsowers1 Oct 29, 2010
9aea4cc
Merge commit 'marutosi/bzr-hg-overhaul-mini-mq-c140cebc'
brentsowers1 Oct 29, 2010
5a4f629
Added Rubymine folder to gitignore
brentsowers1 Oct 29, 2010
77da045
Fixed bug in annotate revision links
brentsowers1 Oct 31, 2010
e8dd8c4
Added pyc files (for mercurial redminehelper) to .gitignore
brentsowers1 Oct 31, 2010
a5d7a07
Merge commit 'upstream/master'
brentsowers1 Oct 31, 2010
1125855
Bazaar diff revision for human readable
Nov 1, 2010
d26bda6
Change diff 'rev1:rev2' to 'rev1 rev2'.
Nov 1, 2010
b4e0f3b
use format_revision() for html title.
Nov 1, 2010
710d3f3
Truncate Git revision labels in Activity page/feed
marutosi Dec 13, 2010
ad833c1
Merge remote branch 'origin/master' into git-activity
marutosi Dec 15, 2010
33278b3
Merge with Redmine SVN trunk.
Dec 15, 2010
3c3ba3d
Merge with http://www.redmine.org/issues/6092#note-23
Dec 15, 2010
b5440d1
Fixed bug when viewing all revisions
brentsowers1 Dec 16, 2010
aec3865
Added : in between revision numbers when viewing diff for clarification
brentsowers1 Dec 17, 2010
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@
/tmp/test/*
/vendor/rails
*.rbc
/.idea
*.pyc
2 changes: 2 additions & 0 deletions app/controllers/issues_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class IssuesController < ApplicationController
include AttachmentsHelper
helper :queries
include QueriesHelper
helper :repositories
include RepositoriesHelper
helper :sort
include SortHelper
include IssuesHelper
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/repositories_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ def diff
@diff = @repository.diff(@path, @rev, @rev_to)
show_error_not_found unless @diff
end

@changeset = @repository.find_changeset_by_name(@rev)
@changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
end
end

Expand Down
6 changes: 4 additions & 2 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,10 @@ def link_to_attachment(attachment, options={})
# * :text - Link text (default to the formatted revision)
def link_to_revision(revision, project, options={})
text = options.delete(:text) || format_revision(revision)
rev = revision.respond_to?(:identifier) ? revision.identifier : revision

link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
:title => l(:label_revision_id, format_revision(revision)))
end

# Generates a link to a project if active
Expand Down Expand Up @@ -642,7 +644,7 @@ def parse_redmine_links(text, project, obj, attr, only_path, options)
end
when 'commit'
if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
:class => 'changeset',
:title => truncate_single_line(changeset.comments, :length => 100)
end
Expand Down
14 changes: 9 additions & 5 deletions app/helpers/repositories_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@
require 'iconv'

module RepositoriesHelper
def format_revision(txt)
txt.to_s[0,8]
def format_revision(revision)
if revision.respond_to? :format_identifier
revision.format_identifier
else
revision.to_s
end
end

def truncate_at_line_break(text, length = 255)
Expand Down Expand Up @@ -87,7 +91,7 @@ def render_changes_tree(tree)
:action => 'show',
:id => @project,
:path => path_param,
:rev => @changeset.revision)
:rev => @changeset.identifier)
output << "<li class='#{style}'>#{text}</li>"
output << render_changes_tree(s)
elsif c = tree[file][:c]
Expand All @@ -97,13 +101,13 @@ def render_changes_tree(tree)
:action => 'entry',
:id => @project,
:path => path_param,
:rev => @changeset.revision) unless c.action == 'D'
:rev => @changeset.identifier) unless c.action == 'D'
text << " - #{c.revision}" unless c.revision.blank?
text << ' (' + link_to('diff', :controller => 'repositories',
:action => 'diff',
:id => @project,
:path => path_param,
:rev => @changeset.revision) + ') ' if c.action == 'M'
:rev => @changeset.identifier) + ') ' if c.action == 'M'
text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank?
output << "<li class='#{style}'>#{text}</li>"
end
Expand Down
22 changes: 20 additions & 2 deletions app/models/changeset.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ class Changeset < ActiveRecord::Base
has_many :changes, :dependent => :delete_all
has_and_belongs_to_many :issues

acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
:description => :long_comments,
:datetime => :committed_on,
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}

acts_as_searchable :columns => 'comments',
:include => {:repository => :project},
Expand All @@ -47,6 +47,15 @@ class Changeset < ActiveRecord::Base
def revision=(r)
write_attribute :revision, (r.nil? ? nil : r.to_s)
end

# Returns the identifier of this changeset; depending on repository backends
def identifier
if repository.class.respond_to? :changeset_identifier
repository.class.changeset_identifier self
else
revision.to_s
end
end

def comments=(comment)
write_attribute(:comments, Changeset.normalize_comments(comment))
Expand All @@ -56,6 +65,15 @@ def committed_on=(date)
self.commit_date = date
super
end

# Returns the readable identifier
def format_identifier
if repository.class.respond_to? :format_changeset_identifier
repository.class.format_changeset_identifier self
else
identifier
end
end

def committer=(arg)
write_attribute(:committer, self.class.to_utf8(arg.to_s))
Expand Down
31 changes: 24 additions & 7 deletions app/models/repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,24 @@ def find_committer_user(committer)
def self.fetch_changesets
Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
if project.repository
project.repository.fetch_changesets
begin
project.repository.fetch_changesets
rescue Redmine::Scm::Adapters::CommandFailed => e
logger.error "Repository: error during fetching changesets: #{e.message}"
end
end
end
end

# Wipes out all changesets for this repository and fetches them new. Good for
# VCSes like Bazaar where revision numbers can change.
def self.clear_and_fetch_changesets
Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
if project.repository && project.repository.respond_to?(:clear_changesets)
project.repository.clear_changesets
end
end
self.fetch_changesets
end

# scan changeset comments to find related and fixed issues for all repositories
Expand All @@ -196,6 +211,14 @@ def self.factory(klass_name, *args)
rescue
nil
end

def clear_changesets
cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
end


private

Expand All @@ -206,10 +229,4 @@ def before_save
true
end

def clear_changesets
cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
end
end
98 changes: 63 additions & 35 deletions app/models/repository/bazaar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,17 @@ def scm_adapter
def self.scm_name
'Bazaar'
end


# Returns the identifier for the given bazaar changeset
def self.changeset_identifier(changeset)
changeset.scmid
end

# Returns the readable identifier for the given bazaar changeset
def self.format_changeset_identifier(changeset)
changeset.revision
end

def entries(path=nil, identifier=nil)
entries = scm.entries(path, identifier)
if entries
Expand All @@ -51,41 +61,59 @@ def entries(path=nil, identifier=nil)
end
end
end


# With SCM's that have a sequential commit numbering, redmine is able to be
# clever and only fetch changesets going forward from the most recent one
# it knows about. However, with bazaar, you never know if people have merged
# commits into the middle of the repository history, so we should parse
# the entire log. Since it's way too slow for large repositories, we only
# parse 1 week before the last known commit.
# The repository can still be fully reloaded by calling #clear_changesets
# before fetching changesets (eg. for offline resync), you can set this up
# as an external job with rake redmine:clear_and_fetch_changesets
def fetch_changesets
scm_info = scm.info
if scm_info
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
# latest revision in the repository
scm_revision = scm_info.lastrev.identifier.to_i
if db_revision < scm_revision
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
identifier_from = db_revision + 1
while (identifier_from <= scm_revision)
# loads changesets by batches of 200
identifier_to = [identifier_from + 199, scm_revision].min
revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
transaction do
revisions.reverse_each do |revision|
changeset = Changeset.create(:repository => self,
:revision => revision.identifier,
:committer => revision.author,
:committed_on => revision.time,
:scmid => revision.scmid,
:comments => revision.message)

revision.paths.each do |change|
Change.create(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:revision => change[:revision])
end
end
end unless revisions.nil?
identifier_from = identifier_to + 1
end
end
c = changesets.find(:first, :order => 'committed_on DESC')
since = (c ? c.committed_on - 7.days : nil)

revisions = scm.revisions('', nil, nil, :all => true, :since => since)
return if revisions.nil? || revisions.empty?

recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since])

# Clean out revisions that are no longer in bazaar
recent_changesets.each {|c| c.destroy unless revisions.detect {|r| r.scmid.to_s == c.scmid.to_s }}

# Subtract revisions that redmine already knows about
recent_revisions = recent_changesets.map{|c| c.scmid}
revisions.reject!{|r| recent_revisions.include?(r.scmid)}

# Save the remaining ones to the database
revisions.each{|r| r.save(self)} unless revisions.nil?
end

# Finds and returns a revision with a number or the beginning of a hash
def find_changeset_by_name(name)
if /[^\d\.]/ =~ name
e = changesets.find(:first, :conditions => ['scmid = ?', name.to_s])
else
e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
end
return e if e
changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"]) # last ditch
end

def latest_changesets(path,rev,limit=10)
revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
return [] if revisions.nil? || revisions.empty?

changesets.find(
:all,
:conditions => [
"scmid IN (?)",
revisions.map!{|c| c.scmid}
],
:order => 'committed_on DESC'
)
end

end
10 changes: 10 additions & 0 deletions app/models/repository/git.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ def self.scm_name
'Git'
end

# Returns the identifier for the given git changeset
def self.changeset_identifier(changeset)
changeset.scmid
end

# Returns the readable identifier for the given git changeset
def self.format_changeset_identifier(changeset)
changeset.revision[0, 8]
end

def branches
scm.branches
end
Expand Down
4 changes: 2 additions & 2 deletions app/views/issues/_changesets.rhtml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<% changesets.each do |changeset| %>
<div class="changeset <%= cycle('odd', 'even') %>">
<p><%= link_to("#{l(:label_revision)} #{changeset.revision}",
:controller => 'repositories', :action => 'revision', :id => changeset.project, :rev => changeset.revision) %><br />
<p><%= link_to_revision(changeset, changeset.project,
:text => "#{l(:label_revision)} #{changeset.format_identifier}") %><br />
<span class="author"><%= authoring(changeset.committed_on, changeset.author) %></span></p>
<div class="changeset-changes">
<%= textilizable(changeset, :comments) %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/repositories/_dir_list_content.rhtml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</td>
<td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
<td class="revision"><%= link_to_revision(changeset.revision, @project) if changeset %></td>
<td class="revision"><%= link_to_revision(changeset, @project) if changeset %></td>
<td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
<td class="author"><%= changeset.nil? ? h(entry.lastrev.author.to_s.split('<').first) : changeset.author if entry.lastrev %></td>
<td class="comments"><%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %></td>
Expand Down
6 changes: 3 additions & 3 deletions app/views/repositories/_revisions.rhtml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
<% line_num = 1 %>
<% revisions.each do |changeset| %>
<tr class="changeset <%= cycle 'odd', 'even' %>">
<td class="id"><%= link_to_revision(changeset.revision, project) %></td>
<td class="checkbox"><%= radio_button_tag('rev', changeset.revision, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
<td class="checkbox"><%= radio_button_tag('rev_to', changeset.revision, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
<td class="id"><%= link_to_revision(changeset, project) %></td>
<td class="checkbox"><%= radio_button_tag('rev', changeset.identifier, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
<td class="checkbox"><%= radio_button_tag('rev_to', changeset.identifier, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
<td class="committed_on"><%= format_time(changeset.committed_on) %></td>
<td class="author"><%=h changeset.author %></td>
<td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
Expand Down
2 changes: 1 addition & 1 deletion app/views/repositories/annotate.rhtml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
<th class="line-num" id="L<%= line_num %>"><a href="#L<%= line_num %>"><%= line_num %></a></th>
<td class="revision">
<%= (revision.identifier ? link_to(format_revision(revision.identifier), :action => 'revision', :id => @project, :rev => revision.identifier) : format_revision(revision.revision)) if revision %></td>
<%= (revision.identifier ? link_to_revision(revision, @project) : format_revision(revision)) if revision %></td>
<td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
<td class="line-code"><pre><%= line %></pre></td>
</tr>
Expand Down
2 changes: 1 addition & 1 deletion app/views/repositories/diff.rhtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<h2><%= l(:label_revision) %> <%= format_revision(@rev_to) + ':' if @rev_to %><%= format_revision(@rev) %> <%=h @path %></h2>
<h2><%= l(:label_revision) %> <%= format_revision(@changeset_to) + ' : ' if @changeset_to %><%= format_revision(@changeset) %> <%=h @path %></h2>

<!-- Choose view type -->
<% form_tag({:path => to_path_param(@path)}, :method => 'get') do %>
Expand Down
Loading