| // Copyright (C) 2013 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.googlesource.gerrit.plugins.hooks.workflow; |
| |
| |
| import com.google.gerrit.server.config.SitePath; |
| import com.google.gerrit.server.data.ApprovalAttribute; |
| import com.google.gerrit.server.data.ChangeAttribute; |
| import com.google.gerrit.server.events.ChangeAbandonedEvent; |
| import com.google.gerrit.server.events.ChangeMergedEvent; |
| import com.google.gerrit.server.events.ChangeRestoredEvent; |
| import com.google.gerrit.server.events.CommentAddedEvent; |
| import com.google.gerrit.server.events.PatchSetCreatedEvent; |
| import com.google.inject.Inject; |
| |
| import com.googlesource.gerrit.plugins.hooks.its.InvalidTransitionException; |
| import com.googlesource.gerrit.plugins.hooks.its.ItsFacade; |
| import com.googlesource.gerrit.plugins.hooks.util.IssueExtractor; |
| |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.storage.file.FileBasedConfig; |
| import org.eclipse.jgit.util.FS; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.file.Path; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Set; |
| |
| public class GerritHookFilterChangeState extends GerritHookFilter { |
| private static final Logger log = LoggerFactory |
| .getLogger(GerritHookFilterChangeState.class); |
| |
| @Inject |
| private ItsFacade its; |
| |
| @Inject |
| @SitePath |
| private Path sitePath; |
| |
| @Inject |
| private IssueExtractor issueExtractor; |
| |
| @Override |
| public void doFilter(PatchSetCreatedEvent hook) throws IOException { |
| performAction(hook.change, new Condition("change", "created")); |
| } |
| |
| @Override |
| public void doFilter(CommentAddedEvent hook) throws IOException { |
| try { |
| List<Condition> conditions = new ArrayList<>(); |
| conditions.add(new Condition("change", "commented")); |
| |
| if (hook.approvals != null) { |
| for (ApprovalAttribute approval : hook.approvals) { |
| addApprovalCategoryCondition(conditions, approval.type, approval.value); |
| } |
| } |
| |
| performAction(hook.change, |
| conditions.toArray(new Condition[conditions.size()])); |
| } catch (InvalidTransitionException ex) { |
| log.warn(ex.getMessage()); |
| } |
| } |
| |
| @Override |
| public void doFilter(ChangeMergedEvent hook) throws IOException { |
| performAction(hook.change, new Condition("change", "merged")); |
| } |
| |
| @Override |
| public void doFilter(ChangeAbandonedEvent hook) throws IOException { |
| performAction(hook.change, new Condition("change", "abandoned")); |
| } |
| |
| @Override |
| public void doFilter(ChangeRestoredEvent hook) throws IOException { |
| performAction(hook.change, new Condition("change", "restored")); |
| } |
| |
| private void addApprovalCategoryCondition(List<Condition> conditions, |
| String name, String value) { |
| value = toConditionValue(value); |
| if (value == null) return; |
| |
| conditions.add(new Condition(name, value)); |
| } |
| |
| private String toConditionValue(String text) { |
| if (text == null) return null; |
| |
| try { |
| int val = Integer.parseInt(text); |
| if (val > 0) |
| return "+" + val; |
| else |
| return text; |
| } catch (Exception any) { |
| return null; |
| } |
| } |
| |
| private void performAction(ChangeAttribute change, Condition... conditionArgs) |
| throws IOException { |
| |
| List<Condition> conditions = Arrays.asList(conditionArgs); |
| |
| log.debug("Checking suitable transition for: " + conditions); |
| |
| Transition transition = null; |
| List<Transition> transitions = loadTransitions(); |
| for (Transition tx : transitions) { |
| |
| log.debug("Checking transition: " + tx); |
| if (tx.matches(conditions)) { |
| log.debug("Transition FOUND > " + tx.getAction()); |
| transition = tx; |
| break; |
| } |
| } |
| |
| if (transition == null) { |
| log.debug("Nothing to perform, transition not found for conditions " |
| + conditions); |
| return; |
| } |
| |
| String gitComment = change.subject; |
| String[] issues = issueExtractor.getIssueIds(gitComment); |
| |
| for (String issue : issues) { |
| its.performAction(issue, transition.getAction()); |
| } |
| } |
| |
| private List<Transition> loadTransitions() { |
| File configFile = new File(sitePath.toFile(), "etc/issue-state-transition.config"); |
| FileBasedConfig cfg = new FileBasedConfig(configFile, FS.DETECTED); |
| try { |
| cfg.load(); |
| } catch (IOException e) { |
| log.error("Cannot load transitions configuration file " + cfg, e); |
| return Collections.emptyList(); |
| } catch (ConfigInvalidException e) { |
| log.error("Invalid transitions configuration file" + cfg, e); |
| return Collections.emptyList(); |
| } |
| |
| List<Transition> transitions = new ArrayList<>(); |
| Set<String> sections = cfg.getSubsections("action"); |
| for (String section : sections) { |
| List<Condition> conditions = new ArrayList<>(); |
| Set<String> keys = cfg.getNames("action", section); |
| for (String key : keys) { |
| String val = cfg.getString("action", section, key); |
| conditions.add(new Condition(key.trim(), val.trim().split(","))); |
| } |
| transitions.add(new Transition(toAction(section), conditions)); |
| } |
| return transitions; |
| } |
| |
| private String toAction(String name) { |
| name = name.trim(); |
| try { |
| int i = name.lastIndexOf(' '); |
| Integer.parseInt(name.substring(i + 1)); |
| name = name.substring(0, i); |
| } catch (Exception ignore) { |
| } |
| return name; |
| } |
| |
| public class Condition { |
| private String key; |
| private String[] val; |
| |
| public Condition(String key, String[] values) { |
| super(); |
| this.key = key.toLowerCase(); |
| this.val = values; |
| } |
| |
| public Condition(String key, String value) { |
| this(key, new String[] {value}); |
| } |
| |
| public String getKey() { |
| return key; |
| } |
| |
| public String[] getVal() { |
| return val; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| try { |
| Condition other = (Condition) o; |
| if (!(key.equals(other.key))) return false; |
| |
| boolean valMatch = false; |
| List<String> otherVals = Arrays.asList(other.val); |
| for (String value : val) { |
| if (otherVals.contains(value)) valMatch = true; |
| } |
| |
| return valMatch; |
| } catch (Exception any) { |
| return false; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return key + "=" + Arrays.asList(val); |
| } |
| } |
| |
| public class Transition { |
| private String action; |
| private List<Condition> conditions; |
| |
| public Transition(String action, List<Condition> conditions) { |
| super(); |
| this.action = action; |
| this.conditions = conditions; |
| } |
| |
| public String getAction() { |
| return action; |
| } |
| |
| public List<Condition> getCondition() { |
| return conditions; |
| } |
| |
| public boolean matches(List<Condition> eventConditions) { |
| |
| for (Condition condition : conditions) { |
| if (!eventConditions.contains(condition)) return false; |
| } |
| |
| return true; |
| } |
| |
| @Override |
| public String toString() { |
| return "action=\"" + action + "\", conditions=" + conditions; |
| } |
| } |
| } |