/** * This file is part of git-as-svn. It is subject to the license terms * in the LICENSE file found in the top-level directory of this distribution * and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn, * including this file, may be copied, modified, propagated, or distributed * except according to the terms contained in the LICENSE file. */ package svnserver.server.command; import org.jetbrains.annotations.NotNull; import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNErrorMessage; import org.tmatesoft.svn.core.SVNException; import svnserver.parser.SvnServerWriter; import svnserver.repository.VcsCopyFrom; import svnserver.repository.VcsLogEntry; import svnserver.repository.VcsRevision; import svnserver.server.SessionContext; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import java.util.Map; /** * Change current path in repository. * <p> * <pre> * log * params: ( ( target-path:string ... ) [ start-rev:number ] * [ end-rev:number ] changed-paths:bool strict-node:bool * ? limit:number * ? include-merged-revisions:bool * all-revprops | revprops ( revprop:string ... ) ) * Before sending response, server sends log entries, ending with "done". * If a client does not want to specify a limit, it should send 0 as the * limit parameter. rev-props excludes author, date, and log; they are * sent separately for backwards-compatibility. * log-entry: ( ( change:changed-path-entry ... ) rev:number * [ author:string ] [ date:string ] [ message:string ] * ? has-children:bool invalid-revnum:bool * revprop-count:number rev-props:proplist * ? subtractive-merge:bool ) * | done * changed-path-entry: ( path:string A|D|R|M * ? ( ? copy-path:string copy-rev:number ) * ? ( ? node-kind:string ? text-mods:bool prop-mods:bool ) ) * response: ( ) * </pre> * * @author a.navrotskiy */ public final class LogCmd extends BaseCmd<LogCmd.Params> { public static class Params { @NotNull private final String[] targetPath; @NotNull private final int[] startRev; @NotNull private final int[] endRev; private final boolean changedPaths; private final boolean strictNode; private final int limit; /** * TODO: issue #26. */ private final boolean includeMergedRevisions; public Params(@NotNull String[] targetPath, @NotNull int[] startRev, @NotNull int[] endRev, boolean changedPaths, boolean strictNode, int limit, boolean includeMergedRevisions, /** * Broken-minded SVN feature we're unlikely to support EVER. */ @SuppressWarnings("UnusedParameters") @NotNull String revpropsMode, @SuppressWarnings("UnusedParameters") @NotNull String[] revprops) { this.targetPath = targetPath; this.startRev = startRev; this.endRev = endRev; this.changedPaths = changedPaths; this.strictNode = strictNode; this.limit = limit; this.includeMergedRevisions = includeMergedRevisions; } } @NotNull @Override public Class<Params> getArguments() { return Params.class; } @Override protected void processCommand(@NotNull SessionContext context, @NotNull Params args) throws IOException, SVNException { final SvnServerWriter writer = context.getWriter(); final int head = context.getRepository().getLatestRevision().getId(); int endRev = getRevision(args.endRev, head); int startRev = getRevision(args.startRev, 1); if (startRev > head || endRev > head) { writer.word("done"); throw new SVNException(SVNErrorMessage.create(SVNErrorCode.FS_NO_SUCH_REVISION, "No such revision " + Math.max(startRev, endRev))); } final List<VcsRevision> log; if (startRev >= endRev) { log = getLog(context, args, startRev, endRev, args.limit); } else { final List<VcsRevision> logReverse = getLog(context, args, endRev, startRev, -1); final int minIndex = args.limit <= 0 ? 0 : Math.max(0, logReverse.size() - args.limit); log = new ArrayList<>(logReverse.size() - minIndex); for (int i = logReverse.size() - 1; i >= minIndex; i--) { log.add(logReverse.get(i)); } } for (VcsRevision revisionInfo : log) { writer .listBegin() .listBegin(); if (args.changedPaths) { final Map<String, ? extends VcsLogEntry> changes = revisionInfo.getChanges(); writer.separator(); for (Map.Entry<String, ? extends VcsLogEntry> entry : changes.entrySet()) { final VcsLogEntry logEntry = entry.getValue(); final char change = logEntry.getChange(); if (change == 0) continue; writer .listBegin() .string(entry.getKey()) // Path .word(change) .listBegin(); final VcsCopyFrom copyFrom = logEntry.getCopyFrom(); if (copyFrom != null) { writer.string(copyFrom.getPath()); writer.number(copyFrom.getRevision()); } writer.listEnd() .listBegin() .string(logEntry.getKind().toString()) .bool(logEntry.isContentModified()) // text-mods .bool(logEntry.isPropertyModified()) // prop-mods .listEnd() .listEnd() .separator(); } } final Map<String, String> revProps = revisionInfo.getProperties(false); writer.listEnd() .number(revisionInfo.getId()) .listBegin().stringNullable(revisionInfo.getAuthor()).listEnd() .listBegin().stringNullable(revisionInfo.getDateString()).listEnd() .listBegin().stringNullable(revisionInfo.getLog()).listEnd() .bool(false) .bool(false) .number(revProps.size()) .writeMap(revProps) .listEnd() .separator(); } writer .word("done"); writer .listBegin() .word("success") .listBegin() .listEnd() .listEnd(); } private List<VcsRevision> getLog(@NotNull SessionContext context, @NotNull Params args, int endRev, int startRev, int limit) throws IOException, SVNException { final List<VcsCopyFrom> targetPaths = new ArrayList<>(); int revision = -1; for (String target : args.targetPath) { final String fullTargetPath = context.getRepositoryPath(target); final int lastChange = context.getRepository().getLastChange(fullTargetPath, endRev); if (lastChange >= startRev) { targetPaths.add(new VcsCopyFrom(lastChange, fullTargetPath)); revision = Math.max(revision, lastChange); } } final List<VcsRevision> result = new ArrayList<>(); int logLimit = limit; while (revision >= startRev) { final VcsRevision revisionInfo = context.getRepository().getRevisionInfo(revision); result.add(revisionInfo); if (--logLimit == 0) break; int nextRevision = -1; final ListIterator<VcsCopyFrom> iter = targetPaths.listIterator(); while (iter.hasNext()) { final VcsCopyFrom entry = iter.next(); if (revision == entry.getRevision()) { final int lastChange = context.getRepository().getLastChange(entry.getPath(), revision - 1); if (lastChange >= revision) { throw new IllegalStateException(); } if (lastChange < 0) { if (args.strictNode) { iter.remove(); continue; } final VcsCopyFrom copyFrom = revisionInfo.getCopyFrom(entry.getPath()); if (copyFrom != null) { iter.set(copyFrom); nextRevision = Math.max(nextRevision, copyFrom.getRevision()); } else { iter.remove(); } } else { iter.set(new VcsCopyFrom(lastChange, entry.getPath())); nextRevision = Math.max(nextRevision, lastChange); } } } revision = nextRevision; } return result; } }