Merge branch 'stable-2.11' into master * origin/stable-2.11: Fix buck test in standalone BUCK build Add standalone BUCK build Change-Id: Id849e37156268a04f125bff3b59c804427bbaa04
diff --git a/BUCK b/BUCK index 26aafae..17cda8a 100644 --- a/BUCK +++ b/BUCK
@@ -18,7 +18,7 @@ resources = glob(['src/main/resources/**/*']), manifest_entries = [ 'Implementation-Title: Ref Protection plugin', - 'Implementation-URL: http://review-plus.sonyericsson.net/#/admin/projects/gerrit/plugins/ref-protection', + 'Implementation-URL: http://gerrit.googlesource.com/plugins/ref-protection', 'Gerrit-PluginName: ref-protection', 'Gerrit-Module: com.googlesource.gerrit.plugins.refprotection.RefProtectionModule' ],
diff --git a/src/main/java/com/googlesource/gerrit/plugins/refprotection/BackupBranch.java b/src/main/java/com/googlesource/gerrit/plugins/refprotection/BackupBranch.java deleted file mode 100644 index dbed47a..0000000 --- a/src/main/java/com/googlesource/gerrit/plugins/refprotection/BackupBranch.java +++ /dev/null
@@ -1,45 +0,0 @@ -/* - * The MIT License - * - * Copyright 2015 Sony Mobile Communications AB. All rights reserved. - * - * 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 com.googlesource.gerrit.plugins.refprotection; - -import static org.eclipse.jgit.lib.Constants.R_HEADS; -import static org.eclipse.jgit.lib.Constants.R_REFS; -import static org.eclipse.jgit.lib.Constants.R_TAGS; - -import java.text.SimpleDateFormat; -import java.util.Date; - -public class BackupBranch { - public static final String R_BACKUPS = R_REFS + "backups/"; - - public static String get(String branchName) { - if (branchName.startsWith(R_HEADS) || branchName.startsWith(R_TAGS)) { - return String.format("%s-%s", - R_BACKUPS + branchName.replaceFirst(R_REFS, ""), - new SimpleDateFormat("YYYYMMdd-HHmmss").format(new Date())); - } - - return branchName; - } -}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/refprotection/BackupRef.java b/src/main/java/com/googlesource/gerrit/plugins/refprotection/BackupRef.java new file mode 100644 index 0000000..af075c9 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/refprotection/BackupRef.java
@@ -0,0 +1,214 @@ +/* + * The MIT License + * + * Copyright 2015 Sony Mobile Communications AB. All rights reserved. + * + * 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 com.googlesource.gerrit.plugins.refprotection; + +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.server.config.PluginConfigFactory; +import com.google.gerrit.server.events.RefUpdatedEvent; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.project.CreateBranch; +import com.google.gerrit.server.project.ProjectResource; +import com.google.inject.Inject; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.TagBuilder; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.eclipse.jgit.lib.Constants.R_HEADS; +import static org.eclipse.jgit.lib.Constants.R_REFS; +import static org.eclipse.jgit.lib.Constants.R_TAGS; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class BackupRef { + public static final String R_BACKUPS = R_REFS + "backups/"; + private static final Logger log = + LoggerFactory.getLogger(BackupRef.class); + private final CreateBranch.Factory createBranchFactory; + @Inject private static PluginConfigFactory cfg; + @Inject private static GitRepositoryManager repoManager; + @Inject @PluginName private static String pluginName; + + @Inject + BackupRef(CreateBranch.Factory createBranchFactory) { + this.createBranchFactory = createBranchFactory; + } + + public void createBackup(RefUpdatedEvent event, ProjectResource project) { + String refName = event.getRefName(); + + try (Repository git = repoManager.openRepository(project.getNameKey())) { + String backupRef = get(project, refName); + + // No-op if the backup branch name is same as the original + if (backupRef.equals(refName)) { + return; + } + + try (RevWalk revWalk = new RevWalk(git)) { + if (cfg.getFromGerritConfig(pluginName).getBoolean("createTag", + false)) { + TagBuilder tag = new TagBuilder(); + tag.setTagger( + new PersonIdent(event.submitter.name, event.submitter.email)); + tag.setObjectId(revWalk + .parseCommit(ObjectId.fromString(event.refUpdate.oldRev))); + String update = "Non-fast-forward update to"; + if (event.refUpdate.newRev.equals(ObjectId.zeroId().getName())) { + update = "Deleted"; + } + String type = "branch"; + String fullMessage = ""; + if (event.refUpdate.refName.startsWith(R_TAGS)) { + type = "tag"; + try { + RevTag origTag = + revWalk.parseTag(ObjectId.fromString(event.refUpdate.oldRev)); + SimpleDateFormat format = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy ZZZZ"); + PersonIdent taggerIdent = origTag.getTaggerIdent(); + String tagger = + String.format("Tagger: %s <%s>\nDate: %s", + taggerIdent.getName(), taggerIdent.getEmailAddress(), + format.format(taggerIdent.getWhen())); + fullMessage = "\n\nOriginal tag:\n" + tagger + "\n\n" + origTag.getFullMessage(); + } catch (MissingObjectException e) { + log.warn("Original tag does not exist", e); + } catch (IncorrectObjectTypeException e) { + log.warn("Original tag was not a tag", e); + } catch (IOException e) { + log.warn("Unable to read original tag details", e); + } + } + tag.setMessage(update + " " + type + " " + event.refUpdate.refName + fullMessage); + tag.setTag(backupRef); + + ObjectInserter inserter = git.newObjectInserter(); + ObjectId tagId = inserter.insert(tag); + inserter.flush(); + RefUpdate tagRef = git.updateRef(tag.getTag()); + tagRef.setNewObjectId(tagId); + tagRef.setRefLogMessage("tagged deleted branch/tag " + tag.getTag(), + false); + tagRef.setForceUpdate(false); + Result result = tagRef.update(); + switch (result) { + case NEW: + case FORCED: + log.debug("Successfully created backup tag"); + break; + + case LOCK_FAILURE: + log.error("Failed to lock repository while creating backup tag"); + break; + + case REJECTED: + log.error("Tag already exists while creating backup tag"); + break; + + default: + log.error("Unknown error while creating backup tag"); + } + } else { + CreateBranch.Input input = new CreateBranch.Input(); + input.ref = backupRef; + // We need to parse the commit to ensure if it's a tag, we get the + // commit the tag points to! + input.revision = ObjectId.toString( + revWalk.parseCommit(ObjectId.fromString(event.refUpdate.oldRev)) + .getId()); + + try { + createBranchFactory.create(backupRef).apply(project, input); + } catch (BadRequestException | AuthException + | ResourceConflictException | IOException e) { + log.error(e.getMessage(), e); + } + } + } + } catch (RepositoryNotFoundException e) { + log.error("Repository does not exist", e); + } catch (IOException e) { + log.error("Could not open repository", e); + } + } + + static String get(ProjectResource project, String refName) { + if (cfg.getFromGerritConfig(pluginName).getBoolean("useTimestamp", true)) { + return getTimestampBranch(refName); + } + else { + return getSequentialBranch(project, refName); + } + } + + private static String getTimestampBranch(String refName) { + if (refName.startsWith(R_HEADS) || refName.startsWith(R_TAGS)) { + return String.format("%s-%s", + R_BACKUPS + refName.replaceFirst(R_REFS, ""), + new SimpleDateFormat("YYYYMMdd-HHmmss").format(new Date())); + } + + return refName; + } + + private static String getSequentialBranch(ProjectResource project, String branchName) { + Integer rev = 1; + String deletedName = branchName.replaceFirst(R_REFS, ""); + try (Repository git = repoManager.openRepository(project.getNameKey())) { + for (Ref ref : git.getAllRefs().values()) { + String name = ref.getName(); + if (name.startsWith(R_BACKUPS + deletedName + "/")) { + Integer thisNum = + Integer.parseInt(name.substring(name.lastIndexOf('/') + 1)); + if (thisNum >= rev) { + rev = thisNum + 1; + } + } + } + } catch (RepositoryNotFoundException e) { + log.error("Repository does not exist", e); + } catch (IOException e) { + log.error("Could not determine latest revision of deleted branch", e); + } + + return R_BACKUPS + deletedName + "/" + rev; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/refprotection/RefProtectionModule.java b/src/main/java/com/googlesource/gerrit/plugins/refprotection/RefProtectionModule.java index 96d250c..b2700c7 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/refprotection/RefProtectionModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/refprotection/RefProtectionModule.java
@@ -23,14 +23,15 @@ */ package com.googlesource.gerrit.plugins.refprotection; -import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; +import com.google.gerrit.common.EventListener; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.inject.AbstractModule; public class RefProtectionModule extends AbstractModule { @Override protected void configure() { - DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to( + DynamicSet.bind(binder(), EventListener.class).to( RefUpdateListener.class); + requestStaticInjection(BackupRef.class); } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/refprotection/RefUpdateListener.java b/src/main/java/com/googlesource/gerrit/plugins/refprotection/RefUpdateListener.java index 09d9c94..0ca08f4 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/refprotection/RefUpdateListener.java +++ b/src/main/java/com/googlesource/gerrit/plugins/refprotection/RefUpdateListener.java
@@ -26,14 +26,14 @@ import static org.eclipse.jgit.lib.Constants.R_HEADS; import static org.eclipse.jgit.lib.Constants.R_TAGS; -import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; -import com.google.gerrit.extensions.restapi.AuthException; -import com.google.gerrit.extensions.restapi.BadRequestException; -import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.common.EventListener; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.config.PluginConfigFactory; +import com.google.gerrit.server.events.Event; +import com.google.gerrit.server.events.RefUpdatedEvent; import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.project.CreateBranch; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectResource; @@ -49,75 +49,62 @@ import java.io.IOException; -class RefUpdateListener implements GitReferenceUpdatedListener { +class RefUpdateListener implements EventListener { private static final Logger log = LoggerFactory.getLogger(RefUpdateListener.class); - private final CreateBranch.Factory createBranchFactory; private final ProjectControl.GenericFactory projectControl; private final CurrentUser user; private final GitRepositoryManager repoManager; + private final BackupRef backupRef; + private final boolean protectDeleted; + private final boolean protectFastForward; @Inject - RefUpdateListener(CreateBranch.Factory createBranchFactory, - ProjectControl.GenericFactory p, + RefUpdateListener(ProjectControl.GenericFactory p, CurrentUser user, - GitRepositoryManager repoManager) { - this.createBranchFactory = createBranchFactory; + GitRepositoryManager repoManager, + BackupRef backupRef, + PluginConfigFactory cfg, + @PluginName String pluginName) { this.projectControl = p; this.user = user; this.repoManager = repoManager; + this.backupRef = backupRef; + this.protectDeleted = + cfg.getFromGerritConfig(pluginName).getBoolean("protectDeleted", true); + this.protectFastForward = + cfg.getFromGerritConfig(pluginName).getBoolean("protectFastForward", true); } @Override - public void onGitReferenceUpdated(final Event event) { - if (isRelevantRef(event)) { - Project.NameKey nameKey = new Project.NameKey(event.getProjectName()); - try { - ProjectResource project = - new ProjectResource(projectControl.controlFor(nameKey, user)); - if (isRefDeleted(event) || isNonFastForwardUpdate(event, project)) { - createBackupBranch(event, project); + public void onEvent(Event event) { + if (event instanceof RefUpdatedEvent) { + RefUpdatedEvent refUpdate = (RefUpdatedEvent)event; + if ((protectDeleted || protectFastForward) && isRelevantRef(refUpdate)) { + Project.NameKey nameKey = refUpdate.getProjectNameKey(); + try { + ProjectResource project = + new ProjectResource(projectControl.controlFor(nameKey, user)); + if ((protectDeleted && isRefDeleted(refUpdate)) + || (protectFastForward && isNonFastForwardUpdate(refUpdate, + project))) { + backupRef.createBackup(refUpdate, project); + } + } catch (NoSuchProjectException | IOException e) { + log.error(e.getMessage(), e); } - } catch (NoSuchProjectException | IOException e) { - log.error(e.getMessage(), e); } } } /** - * Create a backup branch for the given ref. - * - * @param event the Event - */ - private void createBackupBranch(Event event, ProjectResource project) { - String branchName = event.getRefName(); - String backupRef = BackupBranch.get(branchName); - - // No-op if the backup branch name is same as the original - if (backupRef.equals(branchName)) { - return; - } - - CreateBranch.Input input = new CreateBranch.Input(); - input.ref = backupRef; - input.revision = event.getOldObjectId(); - - try { - createBranchFactory.create(backupRef).apply(project, input); - } catch (BadRequestException | AuthException | ResourceConflictException - | IOException e) { - log.error(e.getMessage(), e); - } - } - - /** * Is the event on a relevant ref? * * @param event the Event * @return True if relevant, otherwise False. */ - private boolean isRelevantRef(Event event) { + private boolean isRelevantRef(RefUpdatedEvent event) { return (!isNewRef(event)) && (event.getRefName().startsWith(R_HEADS) || event.getRefName().startsWith(R_TAGS)); @@ -129,8 +116,8 @@ * @param event the Event * @return True if a new ref, otherwise False. */ - private boolean isNewRef(Event event) { - return event.getOldObjectId().equals(ObjectId.zeroId().getName()); + private boolean isNewRef(RefUpdatedEvent event) { + return event.refUpdate.oldRev.equals(ObjectId.zeroId().getName()); } /** @@ -139,11 +126,11 @@ * @param event the Event * @return True if a ref deletion, otherwise False. */ - private boolean isRefDeleted(Event event) { - if (event.getNewObjectId().equals(ObjectId.zeroId().getName())) { + private boolean isRefDeleted(RefUpdatedEvent event) { + if (event.refUpdate.newRev.equals(ObjectId.zeroId().getName())) { log.info(String.format( "Ref Deleted: project [%s] refname [%s] old object id [%s]", - event.getProjectName(), event.getRefName(), event.getOldObjectId())); + event.getProjectNameKey().toString(), event.getRefName(), event.refUpdate.oldRev)); return true; } return false; @@ -155,14 +142,19 @@ * @param event the Event * @return True if a non-fast-forward update, otherwise False. */ - private boolean isNonFastForwardUpdate(Event event, ProjectResource project) + private boolean isNonFastForwardUpdate(RefUpdatedEvent event, ProjectResource project) throws RepositoryNotFoundException, IOException { + if (isRefDeleted(event)) { + // Can't be non-fast-forward if the ref was deleted, and + // attempting a check would cause a MissingObjectException. + return false; + } try (Repository repo = repoManager.openRepository(project.getNameKey())) { try (RevWalk walk = new RevWalk(repo)) { RevCommit oldCommit = - walk.parseCommit(repo.resolve(event.getOldObjectId())); + walk.parseCommit(repo.resolve(event.refUpdate.oldRev)); RevCommit newCommit = - walk.parseCommit(repo.resolve(event.getNewObjectId())); + walk.parseCommit(repo.resolve(event.refUpdate.newRev)); return !walk.isMergedInto(oldCommit, newCommit); } }
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md index c681000..4d2d541 100644 --- a/src/main/resources/Documentation/about.md +++ b/src/main/resources/Documentation/about.md
@@ -1,10 +1,24 @@ Ref protection plugin. -Protects against commits being lost by creating backups of deleted refs under -the `refs/backups/` namespace. +Protects against commits being lost by creating backups of deleted refs +(or non-fast-forward commits) under the `refs/backups/` namespace. + +Branch deletion protection can be disabled by setting +`plugin.ref-protection.protectDeleted false` in `gerrit.config`. +Similarly, non-fast-forward update protection can be disabled with +`plugin.ref-protection.protectFastForward false`. Branches under `refs/heads/` that are deleted or rewritten are backed up -as `refs/backups/heads/branch-name-YYYYMMDD-HHmmss`. +as `refs/backups/heads/branch-name-YYYYMMDD-HHmmss` by default, or as +sequentially increasing numbers under `refs/backups/heads/branch-name/#` +by setting `plugin.ref-protection.useTimestamp false`. Tags under `refs/tags/` that are deleted are backed up (as branches) as -`refs/backups/tags/tag-name-YYYYMMDD-HHmmss`. +`refs/backups/tags/tag-name-YYYYMMDD-HHmmss` or as sequentially +increasing numbers under `refs/backups/tags/branch-name/#` using the same +`plugin.ref-protection.useTimestamp` setting. + +By default, the backups are created as branches. Optionally, they may +be created as tags, containing information about the original ref that +was changed, as well as the user that performed the change. This can +be enabled by setting `plugin.ref-protection.createTag true`.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/refprotection/BackupBranchNameTest.java b/src/test/java/com/googlesource/gerrit/plugins/refprotection/BackupBranchNameTest.java deleted file mode 100644 index b2bc9a2..0000000 --- a/src/test/java/com/googlesource/gerrit/plugins/refprotection/BackupBranchNameTest.java +++ /dev/null
@@ -1,28 +0,0 @@ -package com.googlesource.gerrit.plugins.refprotection; - -import static com.google.common.truth.Truth.assertThat; - -import org.junit.Test; - -public class BackupBranchNameTest { - - @Test - public void backupBranchNameForTag() throws Exception { - String name = BackupBranch.get("refs/tags/v1.0"); - String expected_prefix = BackupBranch.R_BACKUPS + "tags/v1.0-"; - assertThat(name).startsWith(expected_prefix); - } - - @Test - public void backupBranchNameForBranch() throws Exception { - String name = BackupBranch.get("refs/heads/master"); - String expected_prefix = BackupBranch.R_BACKUPS + "heads/master-"; - assertThat(name).startsWith(expected_prefix); - } - - @Test - public void backupBranchNameForUnsupportedNamespace() throws Exception { - String ref = "refs/changes/45/12345/1"; - assertThat(BackupBranch.get(ref)).isEqualTo(ref); - } -}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/refprotection/BackupRefNameTest.java b/src/test/java/com/googlesource/gerrit/plugins/refprotection/BackupRefNameTest.java new file mode 100644 index 0000000..d38f3b5 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/refprotection/BackupRefNameTest.java
@@ -0,0 +1,28 @@ +package com.googlesource.gerrit.plugins.refprotection; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; + +public class BackupRefNameTest { + + @Test + public void backupTimestampRefNameForTag() throws Exception { + String name = BackupRef.get(null, "refs/tags/v1.0"); + String expected_prefix = BackupRef.R_BACKUPS + "tags/v1.0-"; + assertThat(name).startsWith(expected_prefix); + } + + @Test + public void backupTimestampRefNameForBranch() throws Exception { + String name = BackupRef.get(null, "refs/heads/master"); + String expected_prefix = BackupRef.R_BACKUPS + "heads/master-"; + assertThat(name).startsWith(expected_prefix); + } + + @Test + public void backupTimestampRefNameForUnsupportedNamespace() throws Exception { + String ref = "refs/changes/45/12345/1"; + assertThat(BackupRef.get(null, ref)).isEqualTo(ref); + } +}