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

[JENKINS-71737] Fix redirect when submitting cloud changes #8310

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
081cadd
Fix redirect when submitting cloud changes
car-roll Jul 28, 2023
cb2a5fd
update comment
car-roll Jul 29, 2023
1741953
test cloud name change
car-roll Aug 11, 2023
a0a3e9c
add separate page for cloud rename
car-roll Aug 15, 2023
3d50865
tests cloud name change
car-roll Aug 15, 2023
29ad035
block using apply in cloud rename
car-roll Aug 15, 2023
ff3cbba
fix comments
car-roll Aug 15, 2023
dbf61fd
Merge branch 'master' into redirect-cloud-name-change
car-roll Aug 15, 2023
108bc22
Remove rename change, block apply when changing name
car-roll Aug 16, 2023
cc7c4e1
Revert "Remove rename change, block apply when changing name"
car-roll Aug 17, 2023
e3ef831
Add Renamable interface, generalize TransientActionFactoryImpl
car-roll Aug 26, 2023
7a96cd6
change isNameEditable default to true
car-roll Aug 30, 2023
b8e7f84
update jelly
car-roll Aug 30, 2023
aec2de1
add check for improper name change
car-roll Aug 31, 2023
d5b6dcd
hide sidepanel link if name not editable
car-roll Aug 31, 2023
a57343c
remove unnecessary sidepanel link
car-roll Sep 1, 2023
73f451e
Merge branch 'master' into redirect-cloud-name-change
car-roll Sep 6, 2023
b7eed6f
update Renamable javadoc
car-roll Sep 7, 2023
6c561f7
silently block renaming in config page
car-roll Sep 8, 2023
10ea3dd
Merge branch 'master' into redirect-cloud-name-change
car-roll Sep 8, 2023
92489f1
update test
car-roll Sep 8, 2023
b6e2fde
pass name field during cloud creation
car-roll Sep 9, 2023
5bb029f
chnage cloudName to name in _new.jelly
car-roll Sep 9, 2023
63c3633
change to invisible text box
car-roll Sep 11, 2023
bf62fc2
remove cloudName from Cloud formdata
car-roll Sep 11, 2023
1024b9e
add cloudName back to formData
car-roll Sep 12, 2023
4b89c49
cleanup
car-roll Sep 12, 2023
8c3d6c6
add name fields to config form data
car-roll Sep 12, 2023
231db94
remove cloudName from new cloud form data
car-roll Sep 13, 2023
13bab6c
save to disk when changing name
car-roll Sep 19, 2023
e933c51
Merge branch 'master' into redirect-cloud-name-change
car-roll Sep 20, 2023
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
5 changes: 4 additions & 1 deletion core/src/main/java/hudson/model/AbstractItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
// Item doesn't necessarily have to be Actionable, but
// Java doesn't let multiple inheritance.
@ExportedBean
public abstract class AbstractItem extends Actionable implements Item, HttpDeletable, AccessControlled, DescriptorByNameOwner, StaplerProxy {
public abstract class AbstractItem extends Actionable implements Item, HttpDeletable, AccessControlled, DescriptorByNameOwner, StaplerProxy, Renamable {

private static final Logger LOGGER = Logger.getLogger(AbstractItem.class.getName());

Expand Down Expand Up @@ -245,6 +245,7 @@ protected void doSetName(String name) {
* @see #renameTo
* @since 2.110
*/
@Override
public boolean isNameEditable() {
return false;
}
Expand All @@ -254,6 +255,7 @@ public boolean isNameEditable() {
*/
@RequirePOST
@Restricted(NoExternalUse.class)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that these restrictions become easily bypassable via casting to Renamable with the new changes. Perhaps the new interface should be marked as @Restricted(ProtectedExternally.class)? Even then there would be some issues though, see jenkinsci/lib-access-modifier#22 and jenkinsci/lib-access-modifier#21.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added @Restricted(ProtectedExternally.class) in b7eed6f

@Override
public HttpResponse doConfirmRename(@QueryParameter String newName) throws IOException {
newName = newName == null ? null : newName.trim();
FormValidation validationError = doCheckNewName(newName);
Expand All @@ -274,6 +276,7 @@ public HttpResponse doConfirmRename(@QueryParameter String newName) throws IOExc
* {@link FormValidation#error} with a message explaining the problem.
*/
@Restricted(NoExternalUse.class)
@Override
public @NonNull FormValidation doCheckNewName(@QueryParameter String newName) {

if (!isNameEditable()) {
Expand Down
63 changes: 63 additions & 0 deletions core/src/main/java/hudson/model/Renamable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* The MIT License
*
* Copyright 2023 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package hudson.model;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.util.FormValidation;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.ProtectedExternally;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;

/**
* Interface used to create a dedicated page that changes the name property of an Object.
* See also {@link jenkins.model.RenameAction}.
*
* @since TODO
*/
@Restricted(ProtectedExternally.class)
public interface Renamable {
dwnusbaum marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a hard time understanding the relationship between this interface and Node/Slave. Should they implement this interface or not? If so, why don't they? If not, why does renaming work without issue there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was to make them implement this interface as well: #8310 (comment)

I had originally tried to do it the same way as Computer, but it always failed testing it in other plugins such as ec2 and azure-vm-agents.
Looking back now, I realize the problem was always the cloudName vs name issue that was finally identified and corrected. I have just pushed up a second, simpler, version of a fix for this bug. See #8505

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timja reminded me that one of the big issues was that apply does not work correctly when changing names and other config parts: #8505 (review)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To fully answer the original comment, I guess it is more a matter of being consistent across our UIs? Changing the name of a Node isn't an issue because there is no Apply button. Should we implement this interface in Node, the apply button could be added back in.
Given how this PR affected a bunch of downstream PRs, it's probably best to implement this interface one component at a time.

Copy link
Member

@basil basil Sep 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the flip side, why not remove the Apply button from Cloud to make it consistent with Node? That's inconsistent with AbstractItem, but so is the status quo where Node is inconsistent with AbstractItem. And at least it fixes the bug.

A request to defer implementing this interface in Node is effectively a request to introduce debt. I would not automatically reject such a request, but I think it needs to be justified, and merely mentioning "how this PR affected a bunch of downstream PRs" does not seem like a valid justification to me. And I think the intention behind the request is to implement in stages (which is fine, if the rest of the stages are actually finished in a timely fashion, something which almost never happens in my experience whenever someone makes such a request), but an unintentional consequence is that the design of the interface might turn out worse.

When defining a new interface, it is important to consume it in at least three places before shipping it to ensure the design is correct. If you implement it only once, it probably won't support a second implementation. If you implement it only twice, it will probably support a third implementation with difficulty. If you implement it three or more times, the interface will probably work fine. Will Tracz calls this "The Rule of Threes" (Confessions of a Used Program Salesman, Addison-Wesley, 1995).

So my perspective is that the third implementation should be in scope for this PR not only in order to make the implementation complete but also to validate the design of the interface.


/**
* Controls whether the default rename action is available for this object.
*
* @return whether the name can be modified by a user
*/
boolean isNameEditable();

/**
* Renames the object
*
*/
HttpResponse doConfirmRename(@QueryParameter String newName) throws Exception;

/**
* Controls whether the default rename action is available.
*
* @return whether object name can be modified by a user
*/
@NonNull
FormValidation doCheckNewName(@QueryParameter String newName);
}
84 changes: 77 additions & 7 deletions core/src/main/java/hudson/slaves/Cloud.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,18 @@
import hudson.DescriptorExtensionList;
import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.Functions;
import hudson.Util;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.model.Actionable;
import hudson.model.Computer;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.Failure;
import hudson.model.Label;
import hudson.model.Node;
import hudson.model.Renamable;
import hudson.model.Slave;
import hudson.security.ACL;
import hudson.security.AccessControlled;
Expand All @@ -47,6 +50,7 @@
import hudson.slaves.NodeProvisioner.PlannedNode;
import hudson.util.DescriptorList;
import hudson.util.FormApply;
import hudson.util.FormValidation;
import java.io.IOException;
import java.util.Collection;
import java.util.Objects;
Expand All @@ -57,9 +61,12 @@
import org.apache.commons.lang.Validate;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.HttpRedirect;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
Expand Down Expand Up @@ -107,7 +114,7 @@
* @see NodeProvisioner
* @see AbstractCloudImpl
*/
public abstract class Cloud extends Actionable implements ExtensionPoint, Describable<Cloud>, AccessControlled {
public abstract class Cloud extends Actionable implements ExtensionPoint, Describable<Cloud>, AccessControlled, Renamable {

/**
* Uniquely identifies this {@link Cloud} instance among other instances in {@link jenkins.model.Jenkins#clouds}.
Expand Down Expand Up @@ -310,8 +317,69 @@ public HttpResponse doDoDelete() throws IOException {
return new HttpRedirect("..");
}


@Override
public boolean isNameEditable() {
return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must be missing something, because I can't see how renaming is exposed in the GUI for, say, the Docker plugin. I have a Docker cloud created against a core before this PR and the current release of Docker plugin, and I have another Docker cloud created against a core after this PR and jenkinsci/docker-plugin#1016. I can visit confirm-rename manually and rename the cloud there, but I can't get the link to show up anywhere in the GUI for either of my two clouds. Please forgive me if I am doing something wrong on my side.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rename page gets exposed via TransientActionFactoryImpl, which is hidden in RenameAction. I had to get pointed in the right direction as well when I was first looking into this.
I was able to create an old docker cloud and then when I changed to jenkinsci/docker-plugin#1016, was able to see the old docker cloud name and then rename it though. But to be fair, I didn't really have any parameters filled in my cloud.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still couldn't find where RenameAction is visible in the UI on Cloud with these changes even after looking on several pages. I must be missing something.

}

@RequirePOST
@Restricted(NoExternalUse.class)
@Override
public HttpResponse doConfirmRename(@QueryParameter String newName) throws IOException, ServletException, Descriptor.FormException {
newName = newName == null ? null : newName.trim();
FormValidation validationError = doCheckNewName(newName);
if (validationError.kind != FormValidation.Kind.OK) {
throw new Failure(validationError.getMessage());
}
this.name = newName;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we be doing something to persist this change to disk, the same way it would be persisted immediately if we changed it via the configure page?

Jenkins.get().save();

// take the user to the renamed cloud top page.
return HttpResponses.redirectTo("../" + Functions.encode(newName));
}

@NonNull
@Restricted(NoExternalUse.class)
@Override
public FormValidation doCheckNewName(String newName) {
if (!isNameEditable()) {
return FormValidation.error("Trying to rename an item that does not support this operation.");
}
checkPermission(Jenkins.ADMINISTER);

newName = newName == null ? null : newName.trim();

try {
Jenkins.checkGoodName(newName);
assert newName != null; // Would have thrown Failure
if (newName.equals(name)) {
return FormValidation.warning(hudson.model.Messages.AbstractItem_NewNameUnchanged());
}
if (Jenkins.get().getCloud(newName) != null) {
return FormValidation.warning(jenkins.agents.Messages.CloudSet_CloudAlreadyExists(newName));
}
checkRename(newName);
} catch (Failure e) {
return FormValidation.error(e.getMessage());
}
return FormValidation.ok();
}

/**
* Allows subclasses to block renames for domain-specific reasons. Generic validation of the new name
* (e.g., null checking, checking for illegal characters, and checking that the name is not in use)
* always happens prior to calling this method.
*
* @param newName the new name for the item
* @throws Failure if the rename should be blocked
*/
protected void checkRename(@NonNull String newName) throws Failure {

}

/**
* Accepts the update to the node configuration.
* Accepts the update to the node configuration. To change node name see {@link #doConfirmRename(String)}.
*/
@POST
public HttpResponse doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, Descriptor.FormException {
Expand All @@ -322,11 +390,13 @@ public HttpResponse doConfigSubmit(StaplerRequest req, StaplerResponse rsp) thro
if (cloud == null) {
throw new ServletException("No such cloud " + this.name);
}
Cloud result = cloud.reconfigure(req, req.getSubmittedForm());
String proposedName = result.name;
timja marked this conversation as resolved.
Show resolved Hide resolved
if (!proposedName.equals(this.name)
&& j.getCloud(proposedName) != null) {
throw new Descriptor.FormException(jenkins.agents.Messages.CloudSet_CloudAlreadyExists(proposedName), "name");
JSONObject formData = req.getSubmittedForm();
// add the cloud name to the submitted form data
formData.put("name", name);
Cloud result = cloud.reconfigure(req, formData);
if (!(result.name).equals(this.name)) {
// Do not rename the cloud in the config page. Use doConfirmRename()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who is the intended audience for this imperative statement? If this is temporary logic to support some sort of migration period, then a warning should be logged that the cloud is using an unsupported mechanism that will be eventually removed in the future. (And if so, that also raises the question, why support this but not creating a new cloud with a plugin that still uses the configure page during the same migration period?)

Copy link
Contributor Author

@car-roll car-roll Sep 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, originally I had it as a form exception and I just wanted to block old/incorrect implementations in the downstream plugins' config.jelly. My rationale for a quiet ignore was that there is a large sidebar link that says rename so if hopefully no one would get too frustrated before noticing that. I couldn't find a nice way to pop up a warning message for someone who clicked save. Apply, was possible, but I couldn't figure out when you clicked saved.

Another attempt I tried to disable the save button if the name was changed, but it looked like i couldn't do that with the submit button, and I would have to do something with the regular button. At that point I stopped since I did not think it was worth the risk doing changes I wasn't very sure of doing in the first place.

result.name = this.name;
}
j.clouds.replace(this, result);
j.save();
Expand Down
1 change: 0 additions & 1 deletion core/src/main/java/jenkins/agents/CloudSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ private void handleNewCloudPage(Descriptor<Cloud> descriptor, String name, Stapl
checkName(name);
JSONObject formData = req.getSubmittedForm();
formData.put("name", name);
formData.put("cloudName", name); // ec2 uses that field name
car-roll marked this conversation as resolved.
Show resolved Hide resolved
formData.remove("mode"); // Cloud descriptors won't have this field.
req.setAttribute("instance", formData);
req.setAttribute("descriptor", descriptor);
Expand Down
10 changes: 5 additions & 5 deletions core/src/main/java/jenkins/model/RenameAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
package jenkins.model;

import hudson.Extension;
import hudson.model.AbstractItem;
import hudson.model.Action;
import hudson.model.Renamable;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
Expand All @@ -52,15 +52,15 @@ public String getUrlName() {
}

@Extension
public static class TransientActionFactoryImpl extends TransientActionFactory<AbstractItem> {
public static class TransientActionFactoryImpl extends TransientActionFactory<Renamable> {

@Override
public Class<AbstractItem> type() {
return AbstractItem.class;
public Class<Renamable> type() {
return Renamable.class;
}

@Override
public Collection<? extends Action> createFor(AbstractItem target) {
public Collection<? extends Action> createFor(Renamable target) {
if (target.isNameEditable()) {
return Set.of(new RenameAction());
} else {
Expand Down
43 changes: 43 additions & 0 deletions core/src/main/resources/hudson/slaves/Cloud/confirm-rename.jelly
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!--
The MIT License

Copyright 2018 CloudBees, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:layout title="${%Rename}">
<st:include page="sidepanel.jelly" />
<l:breadcrumb title="${%Rename}" />
<l:main-panel>
<h1>${%DescribeRename(it.name)}</h1>
<f:form method="post" action="confirmRename" name="config" tableClass="config-table scrollspy">
<f:entry title="${%NewName}">
<f:textbox name="newName" value="${it.name}" autocomplete="on" checkUrl="checkNewName" checkDependsOn="newName"/>
</f:entry>
<f:bottomButtonBar>
<f:submit value="${%Rename}" />
</f:bottomButtonBar>
</f:form>
<st:adjunct includes="lib.form.confirm" />
</l:main-panel>
</l:layout>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# The MIT License
#
# Copyright (c) 2018 CloudBees, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

DescribeRename=Rename {0}
NewName=New Name
Rename=Rename
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DescribeRename={0} umbenennen
NewName=Neuer Name
Rename=Umbenennen
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Rename=Renommer
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# The MIT License
#
# Italian localization plugin for Jenkins
# Copyright © 2020 Alessandro Menti
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

DescribeRename=Rinomina {0}
NewName=Nuovo nome
Rename=Rinomina
Loading