/*
* P4Java - java integration with Perforce SCM
* Copyright (C) 2007-, Mike Wille, Tek42
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* You can contact the author at:
*
* Web: http://tek42.com
* Email: mike@tek42.com
* Mail: 755 W Big Beaver Road
* Suite 1110
* Troy, MI 48084
*/
package com.tek42.perforce.parse;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import com.tek42.perforce.Depot;
import com.tek42.perforce.PerforceException;
import com.tek42.perforce.model.Changelist;
import com.tek42.perforce.model.Workspace;
import hudson.plugins.perforce.PerforceSCMHelper;
import java.io.IOException;
/**
* Base API object for interacting with changelists.
*
* @author Mike Wille
* @author Brian Westrich
*/
public class Changes extends AbstractPerforceTemplate {
private transient List<PerforceSCMHelper.WhereMapping> whereMaps = null;
public Changes(Depot depot) {
super(depot);
}
/**
* Returns a single changelist specified by its number.
*
* @param number
* @param maxFiles
* The maximum number of affected files that will be recorded
* to a changelist. With negative value include all the files.
* @return
* @throws PerforceException
*/
public Changelist getChangelist(int number, int maxFiles) throws PerforceException {
ChangelistBuilder builder = new ChangelistBuilder(maxFiles);
Changelist change = builder.build(getPerforceResponse(builder.getBuildCmd(getP4Exe(), Integer.toString(number))));
if(change == null)
throw new PerforceException("Failed to retrieve changelist " + number);
calculateWorkspacePaths(change);
return change;
}
/**
* Calculates the workspace paths for every file in the changelist.
* @param change
*/
private void calculateWorkspacePaths(Changelist change) throws PerforceException{
if(whereMaps == null){
byte[] bytes = getRawPerforceResponseBytes(new String[]{getP4Exe(),"-G","where","//..."});
whereMaps = PerforceSCMHelper.parseWhereMapping(bytes);
if(whereMaps == null) {
whereMaps = new ArrayList<PerforceSCMHelper.WhereMapping>();
}
}
for(Changelist.FileEntry file :change.getFiles()){
String workspacePath;
workspacePath = getWorkspacePathForFile(file.getFilename());
file.setWorkspacePath(workspacePath);
}
}
private String getWorkspacePathForFile(String file) throws PerforceException {
String workspacePath = PerforceSCMHelper.mapToWorkspace(whereMaps, file);
if(workspacePath!=null){
//trim the head off of it, so it's a workspace-relative path.
return workspacePath.replaceAll("^//\\S+?/", "");
} else {
//We didn't get a workspace path, likely because it's not in the workspace
return "";
}
}
/**
* Returns a list of changelists that match the parameters
*
* @param path
* What point in the depot to show changes for?
* @param lastChange
* The last changelist number to start from
* @param limit
* The maximum changes to return if less than 1, will return everything
* @param maxFiles
* The maximum amount of affected files in a changelist to be recorded
* @return
* @throws PerforceException
*/
public List<Changelist> getChangelists(String path, int lastChange, int limit, int maxFiles) throws PerforceException {
path = normalizePath(path);
if(lastChange > 0)
path += "@" + lastChange;
String cmd[];
if(limit > 0)
cmd = new String[] { getP4Exe(), "changes", "-s", "submitted", "-m", Integer.toString(limit), path };
else
cmd = new String[] { getP4Exe(), "changes", "-s", "submitted", path };
StringBuilder response = getPerforceResponse(cmd);
List<String> ids = parseList(response, 1);
List<Changelist> changes = new ArrayList<Changelist>();
for(String id : ids) {
try{
changes.add(getChangelist(new Integer(id), maxFiles));
} catch(Exception e){
throw new PerforceException("Could not retrieve changelists.\nResponse from perforce was:\n" + response, e);
}
}
return changes;
}
/**
* A lightweight call to return changelist numbers for a given path.
* <p>
* To get the latest change in the depot for the project, you can use:
*
* <pre>
* depot.getChangeNumbers("//project/...", -1, 1)
* </pre>
*
* <p>
* Note: this method follows perforce in that it starts at the highest number and works backwards. So this might not
* be what you want. (It certainly isn't for Hudson)
*
* @param path
* Path to filter on
* @param start
* The number of the change to start from
* @param limit
* The number of changes to return
* @return
* @throws PerforceException
*/
public List<Integer> getChangeNumbers(String path, int start, int limit) throws PerforceException {
path = normalizePath(path);
if(start > 0)
path += "@" + start;
String cmd[];
if(limit > 0)
cmd = new String[] { getP4Exe(), "changes", "-s", "submitted", "-m", Integer.toString(limit), path };
else
cmd = new String[] { getP4Exe(), "changes", "-s", "submitted", path };
StringBuilder response = getPerforceResponse(cmd);
if(hitMax(response)){
throw new PerforceException("Hit perforce server limit while pulling changes: " + response);
}
List<String> ids = parseList(response, 1);
List<Integer> numbers = new ArrayList<Integer>(ids.size());
for(String id : ids) {
try{
numbers.add(new Integer(id));
} catch (Exception e) {
throw new PerforceException("Failed to get change numbers.\nResponse from perforce was:\n" + response, e);
}
}
return numbers;
}
/**
* Returns a list of changenumbers that start with the most recent change and work back to the specified change.
*
* @param path
* @param untilChange
* @return
*/
public List<Integer> getChangeNumbersTo(String path, int untilChange) throws PerforceException {
return getChangeNumbersTo(null, path, untilChange);
}
/**
* Returns a list of changenumbers that start with the most recent change and work back to the specified change.
*
* @param workspace
* @param path
* one or more paths, e.g. "//testproject/... //testfw/...". Paths are assumed to be delimited by a
* single space.
* @param untilChange
* @return
*/
public List<Integer> getChangeNumbersTo(String workspace, String path, int untilChange) throws PerforceException {
String DELIM = " ";
// maximum number of paths per command supported by perforce
// note that command line perforce supports up to three, but p4java only
// supports one.
int MAX_PATHS_SUPPORTED_PER_COMMAND = 1;
// Ccheck our path variable to see if we have multiple paths separated by space. Add those to the list
StringTokenizer allPaths = new StringTokenizer(path, DELIM);
List<String> supportedPaths = new ArrayList<String>();
StringBuilder currentPaths = new StringBuilder("");
int numberOfPathsInCurrentPaths = 0;
while(true) {
if(!allPaths.hasMoreTokens()) {
if(currentPaths.length() > 0) {
supportedPaths.add(currentPaths.toString().trim());
}
break;
}
String nextPath = allPaths.nextToken();
currentPaths.append(nextPath + " ");
numberOfPathsInCurrentPaths++;
if(numberOfPathsInCurrentPaths == MAX_PATHS_SUPPORTED_PER_COMMAND) {
supportedPaths.add(currentPaths.toString().trim());
currentPaths.setLength(0);
numberOfPathsInCurrentPaths = 0;
}
}
// For each of those paths found, load the change list numbers for it. Store them in a set.
Set<Integer> uniqueIds = new HashSet<Integer>();
for(String pathToUse : supportedPaths) {
List<Integer> ids = getChangeNumbersToForSinglePath(workspace, pathToUse, untilChange);
uniqueIds.addAll(ids);
}
// Sort and return
List<Integer> sortedIds = new ArrayList<Integer>(uniqueIds);
Collections.sort(sortedIds, Collections.reverseOrder());
return sortedIds;
}
/**
* Returns a list of changenumbers that start with the most recent change and work back to the specified change.
*
* @param workspace
* @param path
* a single path, e.g. //testproject/...
* @param untilChange
* @return
*/
private List<Integer> getChangeNumbersToForSinglePath(String workspace, String path, int untilChange) throws PerforceException {
List<Integer> numbers = new ArrayList<Integer>();
recurseGetChangeNumbersTo(workspace, path, untilChange, numbers);
return numbers;
}
/**
* Internal method that will handle a Perforce MaxResults when looking for changelists that return too many results. If
* the error is encountered, it will call p4 dirs path/* to find a list of top level directories beneath the desired path.
* It will then iterate over that list and call itself on each directory. This gets beyond the MaxResults
* issue. See: https://hudson.dev.java.net/issues/show_bug.cgi?id=1939
*
* @param workspace
* @param path
* @param untilChange
* @param numbers
* @throws PerforceException
*/
private void recurseGetChangeNumbersTo(String workspace, String path, int untilChange, List<Integer> numbers) throws PerforceException {
path = normalizePath(path);
List<String> cmdList = new ArrayList<String>();
addCommand(cmdList, getP4Exe(), "changes", "-s", "submitted", "-m", "25");
addCommandWorkspace(cmdList, workspace);
addCommand(cmdList, path);
String lastChange;
boolean continueProcessing = true;
while(continueProcessing) {
// System.out.println("Looping: " + counter++);
StringBuilder response;
try {
// getPerforceResponse will throw an exception if a command it executes
// returns nothing from perforce. If we are moving back through a list and have
// less change lists in the history then what was specified, we will hit this
// exception
response = getPerforceResponse(cmdList.toArray(new String[cmdList.size()]));
if(hitMax(response)) {
String newPaths[] = getTopLevelDirectoriesForPath(workspace, path);
for(String newPath : newPaths) {
recurseGetChangeNumbersTo(workspace, newPath, untilChange, numbers);
}
break;
}
} catch(PerforceException e) {
if(e.getMessage().startsWith("No output for"))
break;
throw e;
}
List<String> temp = parseList(response, 1);
if(temp.size() == 0)
break;
for(String num : temp) {
if(new Integer(num) >= untilChange)
{
getLogger().warn("num is " + num + " until is " + untilChange);
numbers.add(new Integer(num));
}
else
{
continueProcessing = false;
break;
}
}
lastChange = temp.get(temp.size() - 1);
int next = 0;
try {
next = new Integer(lastChange) - 1;
}
catch (NumberFormatException nfe)
{
getLogger().warn("Unable to parse perforce message. Expected a number but got " + lastChange);
getLogger().warn("From command " + response.toString());
}
cmdList.clear();
getLogger().warn("running p4 changes for " + next + " until change is " + untilChange);
addCommand(cmdList, getP4Exe(), "changes", "-s", "submitted", "-m", "25");
addCommandWorkspace(cmdList, workspace);
addCommand(cmdList, path + "@" + next);
}
}
/**
* Executes: p4 dirs -C workspacename //depot/path/*
* to find a list of top level directories beneath the path.
*
* @param workspace The optional workspace to limit search to. Null if not used.
* @param path The path to search
* @return A string array of paths.
* @throws PerforceException If there are problems communicating with perforce.
*/
private String[] getTopLevelDirectoriesForPath(String workspace, String path) throws PerforceException {
if(path.endsWith("..."))
path = path.replaceAll("\\.\\.\\.", "\\*");
List<String> cmdList = new ArrayList<String>();
addCommand(cmdList, getP4Exe(), "dirs");
addCommandWorkspace(cmdList, workspace);
addCommand(cmdList, path);
StringBuilder response = getPerforceResponse(cmdList.toArray(new String[cmdList.size()]));
List<String> list = parseList(response, 0);
return list.toArray(new String[list.size()]);
}
/**
* Add workspace to the command.
*
* @param cmdList
* @param workspace
*/
private void addCommandWorkspace(List<String> cmdList, String workspace) {
if(workspace != null) {
addCommand(cmdList, "-c", workspace);
}
}
/**
* translate the path into a p4 acceptable format.
*
* @param path
* the path
* @return the normalized path
*/
private String normalizePath(String path) {
if(path == null || path.equals(""))
path = "//...";
return path;
}
/**
* add one or more parameters to a command
*
* @param list
* the command
* @param args
* the parameters to add
*/
private void addCommand(List<String> list, String... args) {
for(String command : args) {
list.add(command);
}
}
/**
* Converts a list of numbers to a list of changes.
*
* @param numbers
* @param maxFiles
* The maximum number of affected files that will be recorded
* to a changelist. With negative value include all the files.
* @return
* @throws PerforceException
*/
public List<Changelist> getChangelistsFromNumbers(List<Integer> numbers, int maxFiles) throws PerforceException {
List<Changelist> changes = new ArrayList<Changelist>();
for(Integer id : numbers) {
changes.add(getChangelist(id, maxFiles));
}
return changes;
}
/**
* Return the change numbers in the range [first, last] that apply to the
* specified workspace. The change numbers are returned highest (most
* recent) first.
* @param first
* The number of the change to start from
* @param last
* The last change to include (if applies to the workspace)
* @param showIntegChanges
* True if integrated changelists should be counted as well
* @return list of change numbers
* @throws PerforceException
*/
public List<Integer> getChangeNumbersInRange(Workspace workspace, int first, int last, boolean showIntegChanges) throws PerforceException {
StringBuilder sb = new StringBuilder();
sb.append("//");
sb.append(workspace.getName());
sb.append("/...");
String path = sb.toString();
return getChangeNumbersInRangeForSinglePath(workspace, first, last, path, showIntegChanges);
}
public List<Integer> getChangeNumbersInRange(Workspace workspace, int first, int last, String paths, boolean showIntegChanges) throws PerforceException {
if(paths == null){
return getChangeNumbersInRange(workspace, first, last, showIntegChanges);
}
List<Integer> numbers = new ArrayList<Integer>();
for(String path : paths.replaceAll("\r", "").split("\n")){
List<Integer> newNumbers = getChangeNumbersInRangeForSinglePath(workspace, first, last, path, showIntegChanges);
for(Integer num : newNumbers){
if(!numbers.contains(num)){
numbers.add(num);
}
}
}
Collections.sort(numbers);
Collections.reverse(numbers);
return numbers;
}
public Integer getHighestLabelChangeNumber(Workspace workspace, String label, String path) throws PerforceException {
StringBuilder sb = new StringBuilder();
sb.append(path.replaceAll("\"", ""));
sb.append("@");
sb.append(label);
String fullPath = sb.toString();
String[] cmd = new String[] { getP4Exe(), "-s", "changes", "-s", "submitted", "-m", "1", fullPath };
List<String> response = getRawPerforceResponseLines(cmd);
List<Integer> numbers = new ArrayList<Integer>(response.size());
// TODO Handle error cases, and "exit: <exit-code>" (currently just ignored,
// should really be parsing that line, and providing that value to the caller
// by some means).
for (String line : response) {
if (line.startsWith("info: Change ")) {
int offset = line.indexOf(' ', 13);
String s = line.substring(13, offset);
Integer n = Integer.valueOf(s);
numbers.add(n);
continue;
}
}
if(numbers.isEmpty()){
return -1;
} else {
return numbers.get(0);
}
}
public List<Integer> getChangeNumbersInRangeForSinglePath(Workspace workspace, int first, int last, String path, boolean showIntegChanges) throws PerforceException {
StringBuilder sb = new StringBuilder();
sb.append(path.replaceAll("\"", ""));
sb.append("@");
sb.append(first);
sb.append(",@");
sb.append(last);
String fullPath = sb.toString();
String[] cmd;
if (showIntegChanges) {
cmd = new String[] { getP4Exe(), "-s", "changes", "-s", "submitted", "-i", fullPath };
} else {
cmd = new String[] { getP4Exe(), "-s", "changes", "-s", "submitted", fullPath };
}
List<String> response = getRawPerforceResponseLines(cmd);
List<Integer> numbers = new ArrayList<Integer>(response.size());
// TODO Handle error cases, and "exit: <exit-code>" (currently just ignored,
// should really be parsing that line, and providing that value to the caller
// by some means).
for (String line : response) {
if (line.startsWith("info: Change ")) {
int offset = line.indexOf(' ', 13);
String s = line.substring(13, offset);
Integer n = Integer.valueOf(s);
numbers.add(n);
continue;
}
}
return numbers;
}
}